diff options
| author | Brad Marshall <brad.marshall@canonical.com> | 2017-07-07 10:38:21 +1000 |
|---|---|---|
| committer | Brad Marshall <brad.marshall@canonical.com> | 2017-07-07 10:38:21 +1000 |
| commit | 87ba401b0eaea6a4532f76e6027cf46ab077d074 (patch) | |
| tree | 93fe526f68774891c54918848d187228761d49bf | |
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 @@ -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 Binary files differnew file mode 100644 index 0000000..82ae159 --- /dev/null +++ b/wheelhouse/Jinja2-2.9.5.tar.gz diff --git a/wheelhouse/Jinja2-2.9.6.tar.gz b/wheelhouse/Jinja2-2.9.6.tar.gz Binary files differnew file mode 100644 index 0000000..53ad902 --- /dev/null +++ b/wheelhouse/Jinja2-2.9.6.tar.gz diff --git a/wheelhouse/MarkupSafe-0.23.tar.gz b/wheelhouse/MarkupSafe-0.23.tar.gz Binary files differnew file mode 100644 index 0000000..6b19006 --- /dev/null +++ b/wheelhouse/MarkupSafe-0.23.tar.gz diff --git a/wheelhouse/MarkupSafe-1.0.tar.gz b/wheelhouse/MarkupSafe-1.0.tar.gz Binary files differnew file mode 100644 index 0000000..606021a --- /dev/null +++ b/wheelhouse/MarkupSafe-1.0.tar.gz diff --git a/wheelhouse/PyYAML-3.12.tar.gz b/wheelhouse/PyYAML-3.12.tar.gz Binary files differnew file mode 100644 index 0000000..aabee39 --- /dev/null +++ b/wheelhouse/PyYAML-3.12.tar.gz diff --git a/wheelhouse/Tempita-0.5.2.tar.gz b/wheelhouse/Tempita-0.5.2.tar.gz Binary files differnew file mode 100644 index 0000000..755befc --- /dev/null +++ b/wheelhouse/Tempita-0.5.2.tar.gz diff --git a/wheelhouse/charmhelpers-0.13.0.tar.gz b/wheelhouse/charmhelpers-0.13.0.tar.gz Binary files differnew file mode 100644 index 0000000..c862e55 --- /dev/null +++ b/wheelhouse/charmhelpers-0.13.0.tar.gz diff --git a/wheelhouse/charmhelpers-0.17.0.tar.gz b/wheelhouse/charmhelpers-0.17.0.tar.gz Binary files differnew file mode 100644 index 0000000..f9ab393 --- /dev/null +++ b/wheelhouse/charmhelpers-0.17.0.tar.gz diff --git a/wheelhouse/charms.reactive-0.4.5.tar.gz b/wheelhouse/charms.reactive-0.4.5.tar.gz Binary files differnew file mode 100644 index 0000000..ff4e308 --- /dev/null +++ b/wheelhouse/charms.reactive-0.4.5.tar.gz diff --git a/wheelhouse/charms.reactive-0.4.7.tar.gz b/wheelhouse/charms.reactive-0.4.7.tar.gz Binary files differnew file mode 100644 index 0000000..fcf8bb9 --- /dev/null +++ b/wheelhouse/charms.reactive-0.4.7.tar.gz diff --git a/wheelhouse/netaddr-0.7.19.tar.gz b/wheelhouse/netaddr-0.7.19.tar.gz Binary files differnew file mode 100644 index 0000000..cc31d9d --- /dev/null +++ b/wheelhouse/netaddr-0.7.19.tar.gz diff --git a/wheelhouse/pip-8.1.2.tar.gz b/wheelhouse/pip-8.1.2.tar.gz Binary files differnew file mode 100644 index 0000000..e7a1a3c --- /dev/null +++ b/wheelhouse/pip-8.1.2.tar.gz diff --git a/wheelhouse/pyaml-16.12.2.tar.gz b/wheelhouse/pyaml-16.12.2.tar.gz Binary files differnew file mode 100644 index 0000000..2ff0b0a --- /dev/null +++ b/wheelhouse/pyaml-16.12.2.tar.gz diff --git a/wheelhouse/six-1.10.0.tar.gz b/wheelhouse/six-1.10.0.tar.gz Binary files differnew file mode 100644 index 0000000..ac8eec5 --- /dev/null +++ b/wheelhouse/six-1.10.0.tar.gz |
