summaryrefslogtreecommitdiff
diff options
authorBrad Marshall <brad.marshall@canonical.com>2017-07-07 10:38:21 +1000
committerBrad Marshall <brad.marshall@canonical.com>2017-07-07 10:38:21 +1000
commit87ba401b0eaea6a4532f76e6027cf46ab077d074 (patch)
tree93fe526f68774891c54918848d187228761d49bf
Initial charm buildHEADmaster
-rw-r--r--.build.manifest266
-rw-r--r--.gitignore3
-rw-r--r--Makefile24
-rw-r--r--README.md28
-rwxr-xr-xbin/layer_option24
-rw-r--r--config.yaml27
-rw-r--r--copyright201
-rwxr-xr-xhooks/ceph-exporter-relation-broken19
-rwxr-xr-xhooks/ceph-exporter-relation-changed19
-rwxr-xr-xhooks/ceph-exporter-relation-departed19
-rwxr-xr-xhooks/ceph-exporter-relation-joined19
-rwxr-xr-xhooks/config-changed19
-rw-r--r--hooks/hook.template19
-rwxr-xr-xhooks/install19
-rwxr-xr-xhooks/leader-elected19
-rwxr-xr-xhooks/leader-settings-changed19
-rw-r--r--hooks/relations/http/README.md68
-rw-r--r--hooks/relations/http/__init__.py0
-rw-r--r--hooks/relations/http/interface.yaml4
-rw-r--r--hooks/relations/http/provides.py28
-rw-r--r--hooks/relations/http/requires.py58
-rwxr-xr-xhooks/start19
-rwxr-xr-xhooks/stop19
-rwxr-xr-xhooks/update-status19
-rwxr-xr-xhooks/upgrade-charm28
-rw-r--r--icon.svg7
-rw-r--r--layer.yaml15
-rw-r--r--lib/charms/layer/__init__.py21
-rw-r--r--lib/charms/layer/basic.py205
-rw-r--r--lib/charms/layer/execd.py138
-rw-r--r--lib/charms/layer/snap.py194
-rw-r--r--metadata.yaml15
-rw-r--r--reactive/__init__.py0
-rw-r--r--reactive/prometheus-ceph-exporter.py105
-rw-r--r--reactive/snap.py133
-rw-r--r--requirements.txt2
-rw-r--r--tox.ini13
-rw-r--r--wheelhouse/Jinja2-2.9.5.tar.gzbin0 -> 437631 bytes
-rw-r--r--wheelhouse/Jinja2-2.9.6.tar.gzbin0 -> 437659 bytes
-rw-r--r--wheelhouse/MarkupSafe-0.23.tar.gzbin0 -> 13416 bytes
-rw-r--r--wheelhouse/MarkupSafe-1.0.tar.gzbin0 -> 14356 bytes
-rw-r--r--wheelhouse/PyYAML-3.12.tar.gzbin0 -> 253011 bytes
-rw-r--r--wheelhouse/Tempita-0.5.2.tar.gzbin0 -> 12648 bytes
-rw-r--r--wheelhouse/charmhelpers-0.13.0.tar.gzbin0 -> 325612 bytes
-rw-r--r--wheelhouse/charmhelpers-0.17.0.tar.gzbin0 -> 345200 bytes
-rw-r--r--wheelhouse/charms.reactive-0.4.5.tar.gzbin0 -> 21028 bytes
-rw-r--r--wheelhouse/charms.reactive-0.4.7.tar.gzbin0 -> 21504 bytes
-rw-r--r--wheelhouse/netaddr-0.7.19.tar.gzbin0 -> 1622835 bytes
-rw-r--r--wheelhouse/pip-8.1.2.tar.gzbin0 -> 1140573 bytes
-rw-r--r--wheelhouse/pyaml-16.12.2.tar.gzbin0 -> 18705 bytes
-rw-r--r--wheelhouse/six-1.10.0.tar.gzbin0 -> 29630 bytes
51 files changed, 1835 insertions, 0 deletions
diff --git a/.build.manifest b/.build.manifest
new file mode 100644
index 0000000..8121baf
--- /dev/null
+++ b/.build.manifest
@@ -0,0 +1,266 @@
+{
+ "layers": [
+ "layer:basic",
+ "layer:snap",
+ "prometheus-ceph-exporter",
+ "interface:http",
+ "build"
+ ],
+ "signatures": {
+ ".build.manifest": [
+ "build",
+ "dynamic",
+ "unchecked"
+ ],
+ ".gitignore": [
+ "layer:snap",
+ "static",
+ "cafb5f0bc3ca8e22ee6736efd09046d3cb0634908d64294dc48fa504c9316126"
+ ],
+ "Makefile": [
+ "layer:basic",
+ "static",
+ "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301"
+ ],
+ "README.md": [
+ "prometheus-ceph-exporter",
+ "static",
+ "5b13361af2e21e7011030dd86e65fd760d9b9f518106b0d9c581628a2365844a"
+ ],
+ "bin/layer_option": [
+ "prometheus-ceph-exporter",
+ "static",
+ "621b556cd208005e131e9f648859294347da9376609745a73ca2e808dd2032f9"
+ ],
+ "config.yaml": [
+ "prometheus-ceph-exporter",
+ "dynamic",
+ "9c7e2e09c057041d541224051ec46ee70a2f81a9b96d9dc14eab6b5c8ceb7a6d"
+ ],
+ "copyright": [
+ "prometheus-ceph-exporter",
+ "static",
+ "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4"
+ ],
+ "hooks/ceph-exporter-relation-broken": [
+ "interface:http",
+ "dynamic",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/ceph-exporter-relation-changed": [
+ "interface:http",
+ "dynamic",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/ceph-exporter-relation-departed": [
+ "interface:http",
+ "dynamic",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/ceph-exporter-relation-joined": [
+ "interface:http",
+ "dynamic",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/config-changed": [
+ "layer:basic",
+ "static",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/hook.template": [
+ "layer:basic",
+ "static",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/install": [
+ "layer:basic",
+ "static",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/leader-elected": [
+ "layer:basic",
+ "static",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/leader-settings-changed": [
+ "layer:basic",
+ "static",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/relations/http/README.md": [
+ "interface:http",
+ "static",
+ "9c95320ad040745374fc03e972077f52c27e07eb0386ec93ae19bd50dca24c0d"
+ ],
+ "hooks/relations/http/__init__.py": [
+ "interface:http",
+ "static",
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ ],
+ "hooks/relations/http/interface.yaml": [
+ "interface:http",
+ "static",
+ "d0b64038b85b7791ee4f3a42d73ffc8c208f206f73f899cbf33a519d12f9ad13"
+ ],
+ "hooks/relations/http/provides.py": [
+ "interface:http",
+ "static",
+ "eb4b01fc022cafbb6e67cb200f497ab3aeee4b954b1b9ff2be1d8e64d1a4be20"
+ ],
+ "hooks/relations/http/requires.py": [
+ "interface:http",
+ "static",
+ "70188554d673bff61578f4e601a78a91efb4736df727507e984be5ca19335f82"
+ ],
+ "hooks/start": [
+ "layer:basic",
+ "static",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/stop": [
+ "layer:basic",
+ "static",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/update-status": [
+ "layer:basic",
+ "static",
+ "f976e7ff3e347d44dac22626507f2a88042ba913aeebca7ffdcd5f6179b83683"
+ ],
+ "hooks/upgrade-charm": [
+ "layer:basic",
+ "static",
+ "8fb6559bf4d4b7cdf1b857564c1d7265df98e863a4746fcbae092f141e59d97e"
+ ],
+ "icon.svg": [
+ "prometheus-ceph-exporter",
+ "static",
+ "ab80faa83df36e0e40fedc44e0cc9dea40cd2ff5ed55b2b1ee714bf9dfd0c3d1"
+ ],
+ "layer.yaml": [
+ "prometheus-ceph-exporter",
+ "dynamic",
+ "89a51360be593c9d74e1366200fb44bae2a5362da511fa58c0255be8486d4c91"
+ ],
+ "lib/charms/layer/__init__.py": [
+ "layer:basic",
+ "static",
+ "71a8b901f7e64a6b1b61959fc25819bafc9c9905e37a918615b9f949295095a4"
+ ],
+ "lib/charms/layer/basic.py": [
+ "layer:basic",
+ "static",
+ "d09706ef3a224eb60d34366a2867c382791aa30157ab5b77eda37f29731df2e2"
+ ],
+ "lib/charms/layer/execd.py": [
+ "layer:basic",
+ "static",
+ "af1f2409869f990cdb33cffc4120fb6713876e8e680ac5f064ea510bbdca1073"
+ ],
+ "lib/charms/layer/snap.py": [
+ "layer:snap",
+ "static",
+ "80dfa8989118c3b56e7d18d3d515021b959b69aaf4f8230d099e62a99137f47f"
+ ],
+ "metadata.yaml": [
+ "prometheus-ceph-exporter",
+ "dynamic",
+ "e280d0b4df91d8c580deb60bd3c93c1fca86a7a471203ad03c415b8b3a319dbf"
+ ],
+ "reactive/__init__.py": [
+ "prometheus-ceph-exporter",
+ "static",
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ ],
+ "reactive/prometheus-ceph-exporter.py": [
+ "prometheus-ceph-exporter",
+ "static",
+ "a4b3be7f6f052948020b71830141d158bd150d303e392cb87b9409368e3420b1"
+ ],
+ "reactive/snap.py": [
+ "prometheus-ceph-exporter",
+ "static",
+ "819cb6ab040fab2ce09c0a5b59a5ff5a09ff3b1e254869051a05aa52fd2f04b9"
+ ],
+ "requirements.txt": [
+ "layer:basic",
+ "static",
+ "0f1c70d27e26005a96d66ad54482877ae20f7737693c833e29dd72bd6ac24892"
+ ],
+ "tox.ini": [
+ "layer:snap",
+ "static",
+ "dc6db1aff7cdbbc459a35cba40ec6110d91ad62a11f54784d11cf7c47fac53ee"
+ ],
+ "wheelhouse/Jinja2-2.9.5.tar.gz": [
+ "prometheus-ceph-exporter",
+ "static",
+ "702a24d992f856fa8d5a7a36db6128198d0c21e1da34448ca236c42e92384825"
+ ],
+ "wheelhouse/Jinja2-2.9.6.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "ddaa01a212cd6d641401cb01b605f4a4d9f37bfc93043d7f760ec70fb99ff9ff"
+ ],
+ "wheelhouse/MarkupSafe-0.23.tar.gz": [
+ "prometheus-ceph-exporter",
+ "static",
+ "a4ec1aff59b95a14b45eb2e23761a0179e98319da5a7eb76b56ea8cdc7b871c3"
+ ],
+ "wheelhouse/MarkupSafe-1.0.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
+ ],
+ "wheelhouse/PyYAML-3.12.tar.gz": [
+ "prometheus-ceph-exporter",
+ "static",
+ "592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab"
+ ],
+ "wheelhouse/Tempita-0.5.2.tar.gz": [
+ "prometheus-ceph-exporter",
+ "static",
+ "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c"
+ ],
+ "wheelhouse/charmhelpers-0.13.0.tar.gz": [
+ "prometheus-ceph-exporter",
+ "static",
+ "3494ff105c793de5941a41bac1499b915e54a2236d1f396b358a2c677f8e5706"
+ ],
+ "wheelhouse/charmhelpers-0.17.0.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "232f9ee252c58e9357aaa74d14d351e786716fa0cec3f496f74c40ad0bc6a0b5"
+ ],
+ "wheelhouse/charms.reactive-0.4.5.tar.gz": [
+ "prometheus-ceph-exporter",
+ "static",
+ "b2a475aff349a1c6532de0f9b4df5ee456b97a1e1755695afd8747dbc51bb710"
+ ],
+ "wheelhouse/charms.reactive-0.4.7.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "f023f5aef267786db1be51209610487171ba56e848f7b1c49877b06337000256"
+ ],
+ "wheelhouse/netaddr-0.7.19.tar.gz": [
+ "prometheus-ceph-exporter",
+ "static",
+ "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd"
+ ],
+ "wheelhouse/pip-8.1.2.tar.gz": [
+ "prometheus-ceph-exporter",
+ "static",
+ "4d24b03ffa67638a3fa931c09fd9e0273ffa904e95ebebe7d4b1a54c93d7b732"
+ ],
+ "wheelhouse/pyaml-16.12.2.tar.gz": [
+ "prometheus-ceph-exporter",
+ "static",
+ "b865e4f53a85f4d8a092e7701f759a3237fb3ee8a928627401914aafadc00907"
+ ],
+ "wheelhouse/six-1.10.0.tar.gz": [
+ "prometheus-ceph-exporter",
+ "static",
+ "105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a"
+ ]
+ }
+} \ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..24e7dad
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*~
+*.pyc
+.tox/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..a1ad3a5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,24 @@
+#!/usr/bin/make
+
+all: lint unit_test
+
+
+.PHONY: clean
+clean:
+ @rm -rf .tox
+
+.PHONY: apt_prereqs
+apt_prereqs:
+ @# Need tox, but don't install the apt version unless we have to (don't want to conflict with pip)
+ @which tox >/dev/null || (sudo apt-get install -y python-pip && sudo pip install tox)
+
+.PHONY: lint
+lint: apt_prereqs
+ @tox --notest
+ @PATH=.tox/py34/bin:.tox/py35/bin flake8 $(wildcard hooks reactive lib unit_tests tests)
+ @charm proof
+
+.PHONY: unit_test
+unit_test: apt_prereqs
+ @echo Starting tests...
+ tox
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2559ee3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+# Juju prometheus Ceph exporter charm
+Based on https://github.com/digitalocean/ceph_exporter
+
+# Introduction and Preparation
+The charm implements ceph-exporter functionality for Prometheus, it consumes the prometheus-ceph-exporter snap package,
+Charm needs to be deployed where Ceph is running, a special read-only account ("exporter") will be created by the charm.
+Since the snap is confined to his own filesystem, ceph config file and "exporter" keyring will be created in ($SNAP_DATA) :
+
+ /var/snap/prometheus-ceph-exporter/current/
+
+# How to Deploy:
+
+From the MAAS host:
+
+ export JUJU_REPOSITORY=$PWD/charms
+ export INTERFACE_PATH=$PWD/interfaces
+
+# Build the charm
+
+ charm build -s xenial
+
+# Deploy the charm
+
+ juju deploy local:xenial/prometheus-ceph-exporter
+
+To change the port, refer to the daemon_arguments provided by the snap package at:
+ /var/snap/prometheus-ceph-exporter/current/daemon_arguments
+
diff --git a/bin/layer_option b/bin/layer_option
new file mode 100755
index 0000000..90dc400
--- /dev/null
+++ b/bin/layer_option
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+import sys
+sys.path.append('lib')
+
+import argparse
+from charms.layer import options
+
+
+parser = argparse.ArgumentParser(description='Access layer options.')
+parser.add_argument('section',
+ help='the section, or layer, the option is from')
+parser.add_argument('option',
+ help='the option to access')
+
+args = parser.parse_args()
+value = options(args.section).get(args.option, '')
+if isinstance(value, bool):
+ sys.exit(0 if value else 1)
+elif isinstance(value, list):
+ for val in value:
+ print(val)
+else:
+ print(value)
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..05b882e
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,27 @@
+# Copyright 2016 Canonical Ltd.
+#
+# This file is part of the Snap layer for Juju.
+#
+# 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.
+"options":
+ "snap_proxy":
+ "description": "HTTP/HTTPS web proxy for Snappy to use when accessing the snap\
+ \ store.\n"
+ "type": "string"
+ "default": ""
+ "snap_channel":
+ "default": "stable"
+ "type": "string"
+ "description": |
+ If install_method is set to "snap" this option controlls channel name.
+ Supported values are: "stable", "candidate", "beta" and "edge"
diff --git a/copyright b/copyright
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/copyright
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/hooks/ceph-exporter-relation-broken b/hooks/ceph-exporter-relation-broken
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/ceph-exporter-relation-broken
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/ceph-exporter-relation-changed b/hooks/ceph-exporter-relation-changed
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/ceph-exporter-relation-changed
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/ceph-exporter-relation-departed b/hooks/ceph-exporter-relation-departed
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/ceph-exporter-relation-departed
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/ceph-exporter-relation-joined b/hooks/ceph-exporter-relation-joined
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/ceph-exporter-relation-joined
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/config-changed b/hooks/config-changed
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/config-changed
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/hook.template b/hooks/hook.template
new file mode 100644
index 0000000..64c8489
--- /dev/null
+++ b/hooks/hook.template
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/install b/hooks/install
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/install
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/leader-elected b/hooks/leader-elected
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/leader-elected
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/leader-settings-changed b/hooks/leader-settings-changed
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/leader-settings-changed
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/relations/http/README.md b/hooks/relations/http/README.md
new file mode 100644
index 0000000..3d7822a
--- /dev/null
+++ b/hooks/relations/http/README.md
@@ -0,0 +1,68 @@
+# Overview
+
+This interface layer implements the basic form of the `http` interface protocol,
+which is used for things such as reverse-proxies, load-balanced servers, REST
+service discovery, et cetera.
+
+# Usage
+
+## Provides
+
+By providing the `http` interface, your charm is providing an HTTP server that
+can be load-balanced, reverse-proxied, used as a REST endpoint, etc.
+
+Your charm need only provide the port on which it is serving its content, as
+soon as the `{relation_name}.available` state is set:
+
+```python
+@when('website.available')
+def configure_website(website):
+ website.configure(port=hookenv.config('port'))
+```
+
+## Requires
+
+By requiring the `http` interface, your charm is consuming one or more HTTP
+servers, as a REST endpoint, to load-balance a set of servers, etc.
+
+Your charm should respond to the `{relation_name}.available` state, which
+indicates that there is at least one HTTP server connected.
+
+The `services()` method returns a list of available HTTP services and their
+associated hosts and ports.
+
+The return value is a list of dicts of the following form:
+
+```python
+[
+ {
+ 'service_name': name_of_service,
+ 'hosts': [
+ {
+ 'hostname': address_of_host,
+ 'port': port_for_host,
+ },
+ # ...
+ ],
+ },
+ # ...
+]
+```
+
+A trivial example of handling this interface would be:
+
+```python
+from charms.reactive.helpers import data_changed
+
+@when('reverseproxy.available')
+def update_reverse_proxy_config(reverseproxy):
+ services = reverseproxy.services()
+ if not data_changed('reverseproxy.services', services):
+ return
+ for service in services:
+ for host in service['hosts']:
+ hookenv.log('{} has a unit {}:{}'.format(
+ services['service_name'],
+ host['hostname'],
+ host['port']))
+```
diff --git a/hooks/relations/http/__init__.py b/hooks/relations/http/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/hooks/relations/http/__init__.py
diff --git a/hooks/relations/http/interface.yaml b/hooks/relations/http/interface.yaml
new file mode 100644
index 0000000..54e7748
--- /dev/null
+++ b/hooks/relations/http/interface.yaml
@@ -0,0 +1,4 @@
+name: http
+summary: Basic HTTP interface
+version: 1
+repo: https://git.launchpad.net/~bcsaller/charms/+source/http
diff --git a/hooks/relations/http/provides.py b/hooks/relations/http/provides.py
new file mode 100644
index 0000000..4c65b1e
--- /dev/null
+++ b/hooks/relations/http/provides.py
@@ -0,0 +1,28 @@
+from charmhelpers.core import hookenv
+from charms.reactive import hook
+from charms.reactive import RelationBase
+from charms.reactive import scopes
+
+
+class HttpProvides(RelationBase):
+ scope = scopes.GLOBAL
+
+ @hook('{provides:http}-relation-{joined,changed}')
+ def changed(self):
+ self.set_state('{relation_name}.available')
+
+ @hook('{provides:http}-relation-{broken,departed}')
+ def broken(self):
+ self.remove_state('{relation_name}.available')
+
+ def configure(self, port, private_address=None, hostname=None):
+ if not hostname:
+ hostname = hookenv.unit_get('private-address')
+ if not private_address:
+ private_address = hookenv.unit_get('private-address')
+ relation_info = {
+ 'hostname': hostname,
+ 'private-address': private_address,
+ 'port': port,
+ }
+ self.set_remote(**relation_info)
diff --git a/hooks/relations/http/requires.py b/hooks/relations/http/requires.py
new file mode 100644
index 0000000..0f57be0
--- /dev/null
+++ b/hooks/relations/http/requires.py
@@ -0,0 +1,58 @@
+from charms.reactive import hook
+from charms.reactive import RelationBase
+from charms.reactive import scopes
+
+
+class HttpRequires(RelationBase):
+ scope = scopes.UNIT
+
+ @hook('{requires:http}-relation-{joined,changed}')
+ def changed(self):
+ conv = self.conversation()
+ if conv.get_remote('port'):
+ # this unit's conversation has a port, so
+ # it is part of the set of available units
+ conv.set_state('{relation_name}.available')
+
+ @hook('{requires:http}-relation-{departed,broken}')
+ def broken(self):
+ conv = self.conversation()
+ conv.remove_state('{relation_name}.available')
+
+ def services(self):
+ """
+ Returns a list of available HTTP services and their associated hosts
+ and ports.
+
+ The return value is a list of dicts of the following form::
+
+ [
+ {
+ 'service_name': name_of_service,
+ 'hosts': [
+ {
+ 'hostname': address_of_host,
+ 'port': port_for_host,
+ },
+ # ...
+ ],
+ },
+ # ...
+ ]
+ """
+ services = {}
+ for conv in self.conversations():
+ service_name = conv.scope.split('/')[0]
+ service = services.setdefault(service_name, {
+ 'service_name': service_name,
+ 'hosts': [],
+ })
+ host = conv.get_remote('hostname') or \
+ conv.get_remote('private-address')
+ port = conv.get_remote('port')
+ if host and port:
+ service['hosts'].append({
+ 'hostname': host,
+ 'port': port,
+ })
+ return [s for s in services.values() if s['hosts']]
diff --git a/hooks/start b/hooks/start
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/start
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/stop b/hooks/stop
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/stop
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/update-status b/hooks/update-status
new file mode 100755
index 0000000..64c8489
--- /dev/null
+++ b/hooks/update-status
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/hooks/upgrade-charm b/hooks/upgrade-charm
new file mode 100755
index 0000000..85817d8
--- /dev/null
+++ b/hooks/upgrade-charm
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+
+# Load modules from $JUJU_CHARM_DIR/lib
+import os
+import sys
+sys.path.append('lib')
+
+# This is an upgrade-charm context, make sure we install latest deps
+if not os.path.exists('wheelhouse/.upgrade'):
+ open('wheelhouse/.upgrade', 'w').close()
+ if os.path.exists('wheelhouse/.bootstrapped'):
+ os.unlink('wheelhouse/.bootstrapped')
+else:
+ os.unlink('wheelhouse/.upgrade')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
+# and $JUJU_CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 0000000..56b42ba
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generated by IcoMoon.io -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 32 32">
+<path d="M12 20l4-2 14-14-2-2-14 14-2 4zM9.041 27.097c-0.989-2.085-2.052-3.149-4.137-4.137l3.097-8.525 4-2.435 12-12h-6l-12 12-6 20 20-6 12-12v-6l-12 12-2.435 4z"></path>
+</svg>
+
diff --git a/layer.yaml b/layer.yaml
new file mode 100644
index 0000000..a3d662a
--- /dev/null
+++ b/layer.yaml
@@ -0,0 +1,15 @@
+"options":
+ "basic":
+ "use_venv": !!bool "true"
+ "packages": []
+ "include_system_packages": !!bool "false"
+ "snap": {}
+ "prometheus-ceph-exporter": {}
+"includes":
+- "layer:basic"
+- "interface:http"
+- "layer:snap"
+"repo": "https://git.launchpad.net/prometheus-ceph-exporter-charm"
+"ignore":
+ "prometheus-ceph-exporter": [".*.swp"]
+"is": "prometheus-ceph-exporter"
diff --git a/lib/charms/layer/__init__.py b/lib/charms/layer/__init__.py
new file mode 100644
index 0000000..9d1048d
--- /dev/null
+++ b/lib/charms/layer/__init__.py
@@ -0,0 +1,21 @@
+import os
+
+
+class LayerOptions(dict):
+ def __init__(self, layer_file, section=None):
+ import yaml # defer, might not be available until bootstrap
+ with open(layer_file) as f:
+ layer = yaml.safe_load(f.read())
+ opts = layer.get('options', {})
+ if section and section in opts:
+ super(LayerOptions, self).__init__(opts.get(section))
+ else:
+ super(LayerOptions, self).__init__(opts)
+
+
+def options(section=None, layer_file=None):
+ if not layer_file:
+ base_dir = os.environ.get('JUJU_CHARM_DIR', os.getcwd())
+ layer_file = os.path.join(base_dir, 'layer.yaml')
+
+ return LayerOptions(layer_file, section)
diff --git a/lib/charms/layer/basic.py b/lib/charms/layer/basic.py
new file mode 100644
index 0000000..f1ec007
--- /dev/null
+++ b/lib/charms/layer/basic.py
@@ -0,0 +1,205 @@
+import os
+import sys
+import shutil
+from glob import glob
+from subprocess import check_call, CalledProcessError
+from time import sleep
+
+from charms.layer.execd import execd_preinstall
+
+
+def lsb_release():
+ """Return /etc/lsb-release in a dict"""
+ d = {}
+ with open('/etc/lsb-release', 'r') as lsb:
+ for l in lsb:
+ k, v = l.split('=')
+ d[k.strip()] = v.strip()
+ return d
+
+
+def bootstrap_charm_deps():
+ """
+ Set up the base charm dependencies so that the reactive system can run.
+ """
+ # execd must happen first, before any attempt to install packages or
+ # access the network, because sites use this hook to do bespoke
+ # configuration and install secrets so the rest of this bootstrap
+ # and the charm itself can actually succeed. This call does nothing
+ # unless the operator has created and populated $JUJU_CHARM_DIR/exec.d.
+ execd_preinstall()
+ # ensure that $JUJU_CHARM_DIR/bin is on the path, for helper scripts
+ charm_dir = os.environ['JUJU_CHARM_DIR']
+ os.environ['PATH'] += ':%s' % os.path.join(charm_dir, 'bin')
+ venv = os.path.abspath('../.venv')
+ vbin = os.path.join(venv, 'bin')
+ vpip = os.path.join(vbin, 'pip')
+ vpy = os.path.join(vbin, 'python')
+ if os.path.exists('wheelhouse/.bootstrapped'):
+ activate_venv()
+ return
+ # bootstrap wheelhouse
+ if os.path.exists('wheelhouse'):
+ with open('/root/.pydistutils.cfg', 'w') as fp:
+ # make sure that easy_install also only uses the wheelhouse
+ # (see https://github.com/pypa/pip/issues/410)
+ fp.writelines([
+ "[easy_install]\n",
+ "allow_hosts = ''\n",
+ "find_links = file://{}/wheelhouse/\n".format(charm_dir),
+ ])
+ apt_install([
+ 'python3-pip',
+ 'python3-setuptools',
+ 'python3-yaml',
+ 'python3-dev',
+ ])
+ from charms import layer
+ cfg = layer.options('basic')
+ # include packages defined in layer.yaml
+ apt_install(cfg.get('packages', []))
+ # if we're using a venv, set it up
+ if cfg.get('use_venv'):
+ if not os.path.exists(venv):
+ series = lsb_release()['DISTRIB_CODENAME']
+ if series in ('precise', 'trusty'):
+ apt_install(['python-virtualenv'])
+ else:
+ apt_install(['virtualenv'])
+ cmd = ['virtualenv', '-ppython3', '--never-download', venv]
+ if cfg.get('include_system_packages'):
+ cmd.append('--system-site-packages')
+ check_call(cmd)
+ os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
+ pip = vpip
+ else:
+ pip = 'pip3'
+ # save a copy of system pip to prevent `pip3 install -U pip`
+ # from changing it
+ if os.path.exists('/usr/bin/pip'):
+ shutil.copy2('/usr/bin/pip', '/usr/bin/pip.save')
+ # need newer pip, to fix spurious Double Requirement error:
+ # https://github.com/pypa/pip/issues/56
+ check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse',
+ 'pip'])
+ # install the rest of the wheelhouse deps
+ check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse'] +
+ glob('wheelhouse/*'))
+ if not cfg.get('use_venv'):
+ # restore system pip to prevent `pip3 install -U pip`
+ # from changing it
+ if os.path.exists('/usr/bin/pip.save'):
+ shutil.copy2('/usr/bin/pip.save', '/usr/bin/pip')
+ os.remove('/usr/bin/pip.save')
+ os.remove('/root/.pydistutils.cfg')
+ # flag us as having already bootstrapped so we don't do it again
+ open('wheelhouse/.bootstrapped', 'w').close()
+ # Ensure that the newly bootstrapped libs are available.
+ # Note: this only seems to be an issue with namespace packages.
+ # Non-namespace-package libs (e.g., charmhelpers) are available
+ # without having to reload the interpreter. :/
+ reload_interpreter(vpy if cfg.get('use_venv') else sys.argv[0])
+
+
+def activate_venv():
+ """
+ Activate the venv if enabled in ``layer.yaml``.
+
+ This is handled automatically for normal hooks, but actions might
+ need to invoke this manually, using something like:
+
+ # Load modules from $JUJU_CHARM_DIR/lib
+ import sys
+ sys.path.append('lib')
+
+ from charms.layer.basic import activate_venv
+ activate_venv()
+
+ This will ensure that modules installed in the charm's
+ virtual environment are available to the action.
+ """
+ venv = os.path.abspath('../.venv')
+ vbin = os.path.join(venv, 'bin')
+ vpy = os.path.join(vbin, 'python')
+ from charms import layer
+ cfg = layer.options('basic')
+ if cfg.get('use_venv') and '.venv' not in sys.executable:
+ # activate the venv
+ os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
+ reload_interpreter(vpy)
+
+
+def reload_interpreter(python):
+ """
+ Reload the python interpreter to ensure that all deps are available.
+
+ Newly installed modules in namespace packages sometimes seemt to
+ not be picked up by Python 3.
+ """
+ os.execve(python, [python] + list(sys.argv), os.environ)
+
+
+def apt_install(packages):
+ """
+ Install apt packages.
+
+ This ensures a consistent set of options that are often missed but
+ should really be set.
+ """
+ if isinstance(packages, (str, bytes)):
+ packages = [packages]
+
+ env = os.environ.copy()
+
+ if 'DEBIAN_FRONTEND' not in env:
+ env['DEBIAN_FRONTEND'] = 'noninteractive'
+
+ cmd = ['apt-get',
+ '--option=Dpkg::Options::=--force-confold',
+ '--assume-yes',
+ 'install']
+ for attempt in range(3):
+ try:
+ check_call(cmd + packages, env=env)
+ except CalledProcessError:
+ if attempt == 2: # third attempt
+ raise
+ sleep(5)
+ else:
+ break
+
+
+def init_config_states():
+ import yaml
+ from charmhelpers.core import hookenv
+ from charms.reactive import set_state
+ from charms.reactive import toggle_state
+ config = hookenv.config()
+ config_defaults = {}
+ config_defs = {}
+ config_yaml = os.path.join(hookenv.charm_dir(), 'config.yaml')
+ if os.path.exists(config_yaml):
+ with open(config_yaml) as fp:
+ config_defs = yaml.safe_load(fp).get('options', {})
+ config_defaults = {key: value.get('default')
+ for key, value in config_defs.items()}
+ for opt in config_defs.keys():
+ if config.changed(opt):
+ set_state('config.changed')
+ set_state('config.changed.{}'.format(opt))
+ toggle_state('config.set.{}'.format(opt), config.get(opt))
+ toggle_state('config.default.{}'.format(opt),
+ config.get(opt) == config_defaults[opt])
+ hookenv.atexit(clear_config_states)
+
+
+def clear_config_states():
+ from charmhelpers.core import hookenv, unitdata
+ from charms.reactive import remove_state
+ config = hookenv.config()
+ remove_state('config.changed')
+ for opt in config.keys():
+ remove_state('config.changed.{}'.format(opt))
+ remove_state('config.set.{}'.format(opt))
+ remove_state('config.default.{}'.format(opt))
+ unitdata.kv().flush()
diff --git a/lib/charms/layer/execd.py b/lib/charms/layer/execd.py
new file mode 100644
index 0000000..8eb5b76
--- /dev/null
+++ b/lib/charms/layer/execd.py
@@ -0,0 +1,138 @@
+# Copyright 2014-2016 Canonical Limited.
+#
+# This file is part of layer-basic, the reactive base layer for Juju.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
+
+# This module may only import from the Python standard library.
+import os
+import sys
+import subprocess
+import time
+
+'''
+execd/preinstall
+
+It is often necessary to configure and reconfigure machines
+after provisioning, but before attempting to run the charm.
+Common examples are specialized network configuration, enabling
+of custom hardware, non-standard disk partitioning and filesystems,
+adding secrets and keys required for using a secured network.
+
+The reactive framework's base layer invokes this mechanism as
+early as possible, before any network access is made or dependencies
+unpacked or non-standard modules imported (including the charms.reactive
+framework itself).
+
+Operators needing to use this functionality may branch a charm and
+create an exec.d directory in it. The exec.d directory in turn contains
+one or more subdirectories, each of which contains an executable called
+charm-pre-install and any other required resources. The charm-pre-install
+executables are run, and if successful, state saved so they will not be
+run again.
+
+ $JUJU_CHARM_DIR/exec.d/mynamespace/charm-pre-install
+
+An alternative to branching a charm is to compose a new charm that contains
+the exec.d directory, using the original charm as a layer,
+
+A charm author could also abuse this mechanism to modify the charm
+environment in unusual ways, but for most purposes it is saner to use
+charmhelpers.core.hookenv.atstart().
+'''
+
+
+def default_execd_dir():
+ return os.path.join(os.environ['JUJU_CHARM_DIR'], 'exec.d')
+
+
+def execd_module_paths(execd_dir=None):
+ """Generate a list of full paths to modules within execd_dir."""
+ if not execd_dir:
+ execd_dir = default_execd_dir()
+
+ if not os.path.exists(execd_dir):
+ return
+
+ for subpath in os.listdir(execd_dir):
+ module = os.path.join(execd_dir, subpath)
+ if os.path.isdir(module):
+ yield module
+
+
+def execd_submodule_paths(command, execd_dir=None):
+ """Generate a list of full paths to the specified command within exec_dir.
+ """
+ for module_path in execd_module_paths(execd_dir):
+ path = os.path.join(module_path, command)
+ if os.access(path, os.X_OK) and os.path.isfile(path):
+ yield path
+
+
+def execd_sentinel_path(submodule_path):
+ module_path = os.path.dirname(submodule_path)
+ execd_path = os.path.dirname(module_path)
+ module_name = os.path.basename(module_path)
+ submodule_name = os.path.basename(submodule_path)
+ return os.path.join(execd_path,
+ '.{}_{}.done'.format(module_name, submodule_name))
+
+
+def execd_run(command, execd_dir=None, stop_on_error=True, stderr=None):
+ """Run command for each module within execd_dir which defines it."""
+ if stderr is None:
+ stderr = sys.stdout
+ for submodule_path in execd_submodule_paths(command, execd_dir):
+ # Only run each execd once. We cannot simply run them in the
+ # install hook, as potentially storage hooks are run before that.
+ # We cannot rely on them being idempotent.
+ sentinel = execd_sentinel_path(submodule_path)
+ if os.path.exists(sentinel):
+ continue
+
+ try:
+ subprocess.check_call([submodule_path], stderr=stderr,
+ universal_newlines=True)
+ with open(sentinel, 'w') as f:
+ f.write('{} ran successfully {}\n'.format(submodule_path,
+ time.ctime()))
+ f.write('Removing this file will cause it to be run again\n')
+ except subprocess.CalledProcessError as e:
+ # Logs get the details. We can't use juju-log, as the
+ # output may be substantial and exceed command line
+ # length limits.
+ print("ERROR ({}) running {}".format(e.returncode, e.cmd),
+ file=stderr)
+ print("STDOUT<<EOM", file=stderr)
+ print(e.output, file=stderr)
+ print("EOM", file=stderr)
+
+ # Unit workload status gets a shorter fail message.
+ short_path = os.path.relpath(submodule_path)
+ block_msg = "Error ({}) running {}".format(e.returncode,
+ short_path)
+ try:
+ subprocess.check_call(['status-set', 'blocked', block_msg],
+ universal_newlines=True)
+ if stop_on_error:
+ sys.exit(0) # Leave unit in blocked state.
+ except Exception:
+ pass # We care about the exec.d/* failure, not status-set.
+
+ if stop_on_error:
+ sys.exit(e.returncode or 1) # Error state for pre-1.24 Juju
+
+
+def execd_preinstall(execd_dir=None):
+ """Run charm-pre-install for each module within execd_dir."""
+ execd_run('charm-pre-install', execd_dir=execd_dir)
diff --git a/lib/charms/layer/snap.py b/lib/charms/layer/snap.py
new file mode 100644
index 0000000..b9b70fa
--- /dev/null
+++ b/lib/charms/layer/snap.py
@@ -0,0 +1,194 @@
+# Copyright 2016-2017 Canonical Ltd.
+#
+# This file is part of the Snap layer for Juju.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import subprocess
+
+from charmhelpers.core import hookenv
+from charms import layer
+from charms import reactive
+from charms.reactive.helpers import any_file_changed, data_changed
+from time import sleep
+
+
+def install(snapname, **kw):
+ '''Install a snap.
+
+ Snap will be installed from the coresponding resource if available,
+ otherwise from the Snap Store.
+
+ Sets the snap.installed.{snapname} state.
+
+ If the snap.installed.{snapname} state is already set then the refresh()
+ function is called.
+ '''
+ installed_state = 'snap.installed.{}'.format(snapname)
+ if reactive.is_state(installed_state):
+ refresh(snapname, **kw)
+ else:
+ if hookenv.has_juju_version('2.0'):
+ res_path = _resource_get(snapname)
+ if res_path is False:
+ _install_store(snapname, **kw)
+ else:
+ _install_local(res_path, **kw)
+ else:
+ _install_store(snapname, **kw)
+ reactive.set_state(installed_state)
+
+
+def refresh(snapname, **kw):
+ '''Update a snap.
+
+ Snap will be pulled from the coresponding resource if available
+ and reinstalled if it has changed. Otherwise a 'snap refresh' is
+ run updating the snap from the Snap Store, potentially switching
+ channel and changing confinement options.
+ '''
+ # Note that once you upload a resource, you can't remove it.
+ # This means we don't need to cope with an operator switching
+ # from a resource provided to a store provided snap, because there
+ # is no way for them to do that. Well, actually the operator could
+ # upload a zero byte resource, but then we would need to uninstall
+ # the snap before reinstalling from the store and that has the
+ # potential for data loss.
+ if hookenv.has_juju_version('2.0'):
+ res_path = _resource_get(snapname)
+ if res_path is False:
+ _refresh_store(snapname, **kw)
+ else:
+ _install_local(res_path, **kw)
+ else:
+ _refresh_store(snapname, **kw)
+
+
+def remove(snapname):
+ hookenv.log('Removing snap {}'.format(snapname))
+ subprocess.check_call(['snap', 'remove', snapname],
+ universal_newlines=True)
+ reactive.remove_state('snap.installed.{}'.format(snapname))
+
+
+def connect(plug, slot):
+ '''Connect or reconnect a snap plug with a slot.
+
+ Each argument must be a two element tuple, corresponding to
+ the two arguments to the 'snap connect' command.
+ '''
+ hookenv.log('Connecting {} to {}'.format(plug, slot), hookenv.DEBUG)
+ subprocess.check_call(['snap', 'connect', plug, slot],
+ universal_newlines=True)
+
+
+def connect_all():
+ '''Connect or reconnect all interface connections defined in layer.yaml.
+
+ This method will fail if called before all referenced snaps have been
+ installed.
+ '''
+ opts = layer.options('snap')
+ for snapname, snap_opts in opts.items():
+ for plug, slot in snap_opts.get('connect', []):
+ connect(plug, slot)
+
+
+def _snap_args(channel='stable', devmode=False, jailmode=False,
+ dangerous=False, force_dangerous=False, connect=None,
+ classic=False, revision=None):
+ if channel != 'stable':
+ yield '--channel={}'.format(channel)
+ if devmode is True:
+ yield '--devmode'
+ if jailmode is True:
+ yield '--jailmode'
+ if force_dangerous is True or dangerous is True:
+ yield '--dangerous'
+ if classic is True:
+ yield '--classic'
+ if revision is not None:
+ yield '--revision={}'.format(revision)
+
+
+def _install_local(path, **kw):
+ key = 'snap.local.{}'.format(path)
+ if (data_changed(key, kw) or any_file_changed([path])):
+ cmd = ['snap', 'install']
+ cmd.extend(_snap_args(**kw))
+ cmd.append('--dangerous')
+ cmd.append(path)
+ hookenv.log('Installing {} from local resource'.format(path))
+ subprocess.check_call(cmd, universal_newlines=True)
+
+
+def _install_store(snapname, **kw):
+ cmd = ['snap', 'install']
+ cmd.extend(_snap_args(**kw))
+ cmd.append(snapname)
+ hookenv.log('Installing {} from store'.format(snapname))
+ # Attempting the snap install 3 times to resolve unexpected EOF.
+ # This is a work around to lp:1677557. Stop doing this once it
+ # is resolved everywhere.
+ for attempt in range(3):
+ try:
+ out = subprocess.check_output(cmd, universal_newlines=True,
+ stderr=subprocess.STDOUT)
+ print(out)
+ break
+ except subprocess.CalledProcessError as x:
+ print(x.output)
+ # Per https://bugs.launchpad.net/bugs/1622782, we don't
+ # get a useful error code out of 'snap install', much like
+ # 'snap refresh' below. Remove this when we can rely on
+ # snap installs everywhere returning 0 for 'already insatlled'
+ if "already installed" in x.output:
+ break
+ if attempt == 2:
+ raise
+ sleep(5)
+
+
+def _refresh_store(snapname, **kw):
+ if not data_changed('snap.opts.{}'.format(snapname), kw):
+ return
+
+ cmd = ['snap', 'refresh']
+ cmd.extend(_snap_args(**kw))
+ cmd.append(snapname)
+ hookenv.log('Refreshing {} from store'.format(snapname))
+ # Per https://bugs.launchpad.net/layer-snap/+bug/1588322 we don't get
+ # a useful error code out of 'snap refresh'. We are forced to parse
+ # the output to see if it is a non-fatal error.
+ # subprocess.check_call(cmd, universal_newlines=True)
+ try:
+ out = subprocess.check_output(cmd, universal_newlines=True,
+ stderr=subprocess.STDOUT)
+ print(out)
+ except subprocess.CalledProcessError as x:
+ print(x.output)
+ if "has no updates available" not in x.output:
+ raise
+
+
+def _resource_get(snapname):
+ '''Used to fetch the resource path of the given name.
+
+ This wrapper obtains a resource path and adds an additional
+ check to return False if the resource is zero length.
+ '''
+ res_path = hookenv.resource_get(snapname)
+ if res_path and os.stat(res_path).st_size != 0:
+ return res_path
+ return False
diff --git a/metadata.yaml b/metadata.yaml
new file mode 100644
index 0000000..b80bbc2
--- /dev/null
+++ b/metadata.yaml
@@ -0,0 +1,15 @@
+"name": "prometheus-ceph-exporter"
+"summary": "Ceph exporter for Prometheus"
+"maintainer": "Giorgio Di Guardia <giorgio.diguardia@canonical.com>"
+"description": |
+ This is an exporter that exposes information gathered from Ceph
+ for use by the Prometheus monitoring system.
+"tags":
+- "monitoring"
+- "prometheus"
+"series":
+- "xenial"
+"provides":
+ "ceph-exporter":
+ "interface": "http"
+"subordinate": !!bool "false"
diff --git a/reactive/__init__.py b/reactive/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/reactive/__init__.py
diff --git a/reactive/prometheus-ceph-exporter.py b/reactive/prometheus-ceph-exporter.py
new file mode 100644
index 0000000..c6ba0c9
--- /dev/null
+++ b/reactive/prometheus-ceph-exporter.py
@@ -0,0 +1,105 @@
+# Copyright 2017 Canonical Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import yaml
+import subprocess
+
+from charmhelpers.core import host, hookenv
+from charmhelpers.core.templating import render
+from charms.reactive import (
+ when, when_not, set_state, remove_state
+)
+from charms.reactive.helpers import any_file_changed, data_changed
+from charms.layer import snap
+
+
+SNAP_NAME = 'prometheus-ceph-exporter'
+SVC_NAME = 'snap.prometheus-ceph-exporter.ceph-exporter'
+SNAP_DATA = '/var/snap/' + SNAP_NAME + '/current/'
+PORT_DEF = 9128
+
+def templates_changed(tmpl_list):
+ return any_file_changed(['templates/{}'.format(x) for x in tmpl_list])
+
+
+@when_not('ceph-exporter.installed')
+def install_packages():
+ hookenv.status_set('maintenance', 'Installing software')
+ config = hookenv.config()
+ channel = config.get('snap_channel', 'stable')
+ snap.install(SNAP_NAME, channel=channel, force_dangerous=False)
+ set_state('ceph-exporter.do-auth-config')
+ set_state('ceph-exporter.installed')
+ set_state('ceph-exporter.do-check-reconfig')
+
+
+def validate_config(filename):
+ return yaml.safe_load(open(filename))
+
+
+@when('ceph-exporter.installed')
+@when('ceph-exporter.do-reconfig-yaml')
+def write_ceph_exporter_config_yaml():
+ config = hookenv.config()
+ hookenv.open_port(PORT_DEF)
+ set_state('ceph-exporter.do-restart')
+ remove_state('ceph-exporter.do-reconfig-yaml')
+
+
+@when('ceph-exporter.started')
+def check_config():
+ set_state('ceph-exporter.do-check-reconfig')
+
+
+@when('ceph-exporter.do-check-reconfig')
+def check_reconfig_ceph_exporter():
+ config = hookenv.config()
+ if data_changed('ceph-exporter.config', config):
+ set_state('ceph-exporter.do-reconfig-yaml')
+
+ remove_state('ceph-exporter.do-check-reconfig')
+
+
+@when('ceph-exporter.do-auth-config')
+def ceph_auth_config():
+ # Working around snap confinement, creating ceph user, moving conf to snap confined environment ($SNAP_DATA)
+ hookenv.status_set('maintenance', 'Creating ceph user')
+ hookenv.log('Creating exporter ceph user')
+ subprocess.check_call(['ceph', 'auth', 'add', 'client.exporter', 'mon', "allow r"])
+ hookenv.log('Creating exporter keyring file onto {}'.format(SNAP_DATA))
+ subprocess.check_call(['ceph', 'auth', 'get', 'client.exporter', '-o', SNAP_DATA + 'ceph.client.exporter.keyring'])
+ hookenv.log('Copying ceph.conf onto {}'.format(SNAP_DATA))
+ subprocess.check_call(['cp', '/etc/ceph/ceph.conf', SNAP_DATA + 'ceph.conf'])
+ hookenv.log('Modifying snap ceph.conf to point to $SNAP_DATA')
+ subprocess.check_call(['sed', '-i', 's=/etc/ceph/=' + SNAP_DATA + '=g', SNAP_DATA + 'ceph.conf'])
+ remove_state('ceph-exporter.do-auth-config')
+
+@when('ceph-exporter.do-restart')
+def restart_ceph_exporter():
+ if not host.service_running(SVC_NAME):
+ hookenv.log('Starting {}...'.format(SVC_NAME))
+ host.service_start(SVC_NAME)
+ else:
+ hookenv.log('Restarting {}, config file changed...'.format(SVC_NAME))
+ host.service_restart(SVC_NAME)
+ hookenv.status_set('active', 'Ready')
+ set_state('ceph-exporter.started')
+ remove_state('ceph-exporter.do-restart')
+
+
+# Relations
+@when('ceph-exporter.started')
+@when('ceph-exporter.available') # Relation name is "ceph-exporter"
+def configure_ceph_exporter_relation(target):
+ target.configure(PORT_DEF)
diff --git a/reactive/snap.py b/reactive/snap.py
new file mode 100644
index 0000000..d357e05
--- /dev/null
+++ b/reactive/snap.py
@@ -0,0 +1,133 @@
+# Copyright 2016 Canonical Ltd.
+#
+# This file is part of the Snap layer for Juju.
+#
+# 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.
+'''
+charms.reactive helpers for dealing with Snap packages.
+'''
+import os.path
+import shutil
+import subprocess
+from textwrap import dedent
+import time
+
+from charmhelpers.core import hookenv, host
+from charms import layer
+from charms import reactive
+from charms.layer import snap
+from charms.reactive import hook
+from charms.reactive.helpers import data_changed
+
+
+def install():
+ opts = layer.options('snap')
+ for snapname, snap_opts in opts.items():
+ snap.install(snapname, **snap_opts)
+ if data_changed('snap.install.opts', opts):
+ snap.connect_all()
+
+
+def refresh():
+ opts = layer.options('snap')
+ for snapname, snap_opts in opts.items():
+ snap.refresh(snapname, **snap_opts)
+ snap.connect_all()
+
+
+@hook('upgrade-charm')
+def upgrade_charm():
+ refresh()
+
+
+def get_series():
+ return subprocess.check_output(['lsb_release', '-sc'],
+ universal_newlines=True).strip()
+
+
+def ensure_snapd():
+ # I don't use the apt layer, because that would tie this layer
+ # too closely to apt packaging. Perhaps this is a snap-only system.
+ if not shutil.which('snap'):
+ cmd = ['apt', 'install', '-y', 'snapd']
+ subprocess.check_call(cmd, universal_newlines=True)
+ # Work around lp:1628289. Remove this stanza once snapd depends
+ # on the necessary package and snaps work in lxd xenial containers
+ # without the workaround.
+ if get_series() == 'xenial' and not shutil.which('squashfuse'):
+ cmd = ['apt', 'install', '-y', 'squashfuse']
+ subprocess.check_call(cmd, universal_newlines=True)
+
+
+def update_snap_proxy():
+ # This is a hack based on
+ # https://bugs.launchpad.net/layer-snap/+bug/1533899/comments/1
+ # Do it properly when Bug #1533899 is addressed.
+ # Note we can't do this in a standard reactive handler as we need
+ # to ensure proxies are configured before attempting installs or
+ # updates.
+ proxy = hookenv.config()['snap_proxy']
+ if not data_changed('snap.proxy', proxy):
+ return # Short circuit avoids unnecessary restarts.
+
+ path = '/etc/systemd/system/snapd.service.d/snap_layer_proxy.conf'
+ if proxy:
+ create_snap_proxy_conf(path, proxy)
+ else:
+ remove_snap_proxy_conf(path)
+ subprocess.check_call(['systemctl', 'daemon-reload'],
+ universal_newlines=True)
+ time.sleep(2)
+ subprocess.check_call(['systemctl', 'restart', 'snapd.service'],
+ universal_newlines=True)
+
+
+def create_snap_proxy_conf(path, proxy):
+ host.mkdir(os.path.dirname(path))
+ content = dedent('''\
+ # Managed by Juju
+ [Service]
+ Environment=http_proxy={}
+ Environment=https_proxy={}
+ ''').format(proxy, proxy)
+ host.write_file(path, content.encode())
+
+
+def remove_snap_proxy_conf(path):
+ if os.path.exists(path):
+ os.remove(path)
+
+
+def ensure_path():
+ # Per Bug #1662856, /snap/bin may be missing from $PATH. Fix this.
+ if '/snap/bin' not in os.environ['PATH'].split(':'):
+ os.environ['PATH'] += ':/snap/bin'
+
+
+# Per https://github.com/juju-solutions/charms.reactive/issues/33,
+# this module may be imported multiple times so ensure the
+# initialization hook is only registered once. I have to piggy back
+# onto the namespace of a module imported before reactive discovery
+# to do this.
+if not hasattr(reactive, '_snap_registered'):
+ # We need to register this to run every hook, not just during install
+ # and config-changed, to protect against race conditions. If we don't
+ # do this, then the config in the hook environment may show updates
+ # to running hooks well before the config-changed hook has been invoked
+ # and the intialization provided an opertunity to be run.
+ hookenv.atstart(hookenv.log, 'Initializing Snap Layer')
+ hookenv.atstart(ensure_snapd)
+ hookenv.atstart(ensure_path)
+ hookenv.atstart(update_snap_proxy)
+ hookenv.atstart(install)
+ reactive._snap_registered = True
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..28ecaca
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+flake8
+pytest
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..447547d
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,13 @@
+[tox]
+skipsdist = True
+envlist=lint
+
+[flake8]
+max-complexity=10
+
+[testenv:lint]
+basepython=python3
+sitepackages=False
+deps=flake8
+commands=
+ flake8 {posargs:lib/ reactive/}
diff --git a/wheelhouse/Jinja2-2.9.5.tar.gz b/wheelhouse/Jinja2-2.9.5.tar.gz
new file mode 100644
index 0000000..82ae159
--- /dev/null
+++ b/wheelhouse/Jinja2-2.9.5.tar.gz
Binary files differ
diff --git a/wheelhouse/Jinja2-2.9.6.tar.gz b/wheelhouse/Jinja2-2.9.6.tar.gz
new file mode 100644
index 0000000..53ad902
--- /dev/null
+++ b/wheelhouse/Jinja2-2.9.6.tar.gz
Binary files differ
diff --git a/wheelhouse/MarkupSafe-0.23.tar.gz b/wheelhouse/MarkupSafe-0.23.tar.gz
new file mode 100644
index 0000000..6b19006
--- /dev/null
+++ b/wheelhouse/MarkupSafe-0.23.tar.gz
Binary files differ
diff --git a/wheelhouse/MarkupSafe-1.0.tar.gz b/wheelhouse/MarkupSafe-1.0.tar.gz
new file mode 100644
index 0000000..606021a
--- /dev/null
+++ b/wheelhouse/MarkupSafe-1.0.tar.gz
Binary files differ
diff --git a/wheelhouse/PyYAML-3.12.tar.gz b/wheelhouse/PyYAML-3.12.tar.gz
new file mode 100644
index 0000000..aabee39
--- /dev/null
+++ b/wheelhouse/PyYAML-3.12.tar.gz
Binary files differ
diff --git a/wheelhouse/Tempita-0.5.2.tar.gz b/wheelhouse/Tempita-0.5.2.tar.gz
new file mode 100644
index 0000000..755befc
--- /dev/null
+++ b/wheelhouse/Tempita-0.5.2.tar.gz
Binary files differ
diff --git a/wheelhouse/charmhelpers-0.13.0.tar.gz b/wheelhouse/charmhelpers-0.13.0.tar.gz
new file mode 100644
index 0000000..c862e55
--- /dev/null
+++ b/wheelhouse/charmhelpers-0.13.0.tar.gz
Binary files differ
diff --git a/wheelhouse/charmhelpers-0.17.0.tar.gz b/wheelhouse/charmhelpers-0.17.0.tar.gz
new file mode 100644
index 0000000..f9ab393
--- /dev/null
+++ b/wheelhouse/charmhelpers-0.17.0.tar.gz
Binary files differ
diff --git a/wheelhouse/charms.reactive-0.4.5.tar.gz b/wheelhouse/charms.reactive-0.4.5.tar.gz
new file mode 100644
index 0000000..ff4e308
--- /dev/null
+++ b/wheelhouse/charms.reactive-0.4.5.tar.gz
Binary files differ
diff --git a/wheelhouse/charms.reactive-0.4.7.tar.gz b/wheelhouse/charms.reactive-0.4.7.tar.gz
new file mode 100644
index 0000000..fcf8bb9
--- /dev/null
+++ b/wheelhouse/charms.reactive-0.4.7.tar.gz
Binary files differ
diff --git a/wheelhouse/netaddr-0.7.19.tar.gz b/wheelhouse/netaddr-0.7.19.tar.gz
new file mode 100644
index 0000000..cc31d9d
--- /dev/null
+++ b/wheelhouse/netaddr-0.7.19.tar.gz
Binary files differ
diff --git a/wheelhouse/pip-8.1.2.tar.gz b/wheelhouse/pip-8.1.2.tar.gz
new file mode 100644
index 0000000..e7a1a3c
--- /dev/null
+++ b/wheelhouse/pip-8.1.2.tar.gz
Binary files differ
diff --git a/wheelhouse/pyaml-16.12.2.tar.gz b/wheelhouse/pyaml-16.12.2.tar.gz
new file mode 100644
index 0000000..2ff0b0a
--- /dev/null
+++ b/wheelhouse/pyaml-16.12.2.tar.gz
Binary files differ
diff --git a/wheelhouse/six-1.10.0.tar.gz b/wheelhouse/six-1.10.0.tar.gz
new file mode 100644
index 0000000..ac8eec5
--- /dev/null
+++ b/wheelhouse/six-1.10.0.tar.gz
Binary files differ