diff --git a/calico/.build.manifest b/calico/.build.manifest new file mode 100644 index 0000000..8fdefc8 --- /dev/null +++ b/calico/.build.manifest @@ -0,0 +1,601 @@ +{ + "layers": [ + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "fcdcea4e5de3e1556c24e6704607862d0ba00a56", + "url": "layer:options" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "a3ff62c32c993d80417f6e093e3ef95e42f62083", + "url": "layer:basic" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "cc5bd3f49b2fa5e6c3ab2336763c313ec8bf083f", + "url": "layer:leadership" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "a7d7b6423db37a47611310039e6ed1929c0a2eab", + "url": "layer:status" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "bbeabfee52c4442cdaf3a34e5e35530a3bd71156", + "url": "layer:kubernetes-common" + }, + { + "branch": "refs/heads/stable", + "rev": "96b4e06d5d35fec30cdf2cc25076dd25c51b893c", + "url": "calico" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "44f244cbd08b86bf2b68bd71c3fb34c7c070c382", + "url": "interface:etcd" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "88b1e8fad78d06efdbf512cd75eaa0bb308eb1c1", + "url": "interface:kubernetes-cni" + } + ], + "signatures": { + ".build.manifest": [ + "build", + "dynamic", + "unchecked" + ], + ".github/workflows/main.yml": [ + "layer:kubernetes-common", + "static", + "d4f8fec0456cb2fc05993253a995983488a76fbbef10c2ee40649e83d6c9e078" + ], + ".github/workflows/tox.yaml": [ + "calico", + "static", + "8b7dba2bd100fc3dfce764499b0eba1799b58469701b032b238cb1d0055c44bb" + ], + ".gitignore": [ + "calico", + "static", + "3437c2cd90de443f44766939172b82e750e19fd474df499ffe003bb807e8cef4" + ], + "CONTRIBUTING.md": [ + "calico", + "static", + "fa04ec96762f4edc071c7b0097223c121e33fd6769226562681646577d7b1146" + ], + "DEVELOPING.md": [ + "calico", + "static", + "ccb2d8ad4b5c328d810c53fa43b41f6641af0f002a45d548f6ed9d9f546d3dbe" + ], + "LICENSE": [ + "calico", + "static", + "58d1e17ffe5109a7ae296caafcadfdbe6a7d176f0bc4ab01e12a689b0499d8bd" + ], + "Makefile": [ + "layer:basic", + "static", + "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301" + ], + "README.md": [ + "calico", + "static", + "d2d26569f5a63b1be2e23835346ed2e8b0b13cdd74a6efb161221d2462a58dc5" + ], + "bin/charm-env": [ + "layer:basic", + "static", + "fb6a20fac4102a6a4b6ffe903fcf666998f9a95a3647e6f9af7a1eeb44e58fd5" + ], + "bin/layer_option": [ + "layer:options", + "static", + "e959bf29da4c5edff28b2602c24113c4df9e25cdc9f2aa3b5d46c8577b2a40cc" + ], + "build-calico-resource.sh": [ + "calico", + "static", + "1c98f05945166e17cf9c530a6ee064092a323e5529639474b07f380210959acb" + ], + "config.yaml": [ + "calico", + "dynamic", + "d75dd7b4ddd803d88c5d86b14826fa7f047b8e6907885cafe37cda29afb3c13d" + ], + "copyright": [ + "layer:status", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "copyright.layer-basic": [ + "layer:basic", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-leadership": [ + "layer:leadership", + "static", + "8ce407829378fc0f72ce44c7f624e4951c7ccb3db1cfb949bee026b701728cc9" + ], + "copyright.layer-options": [ + "layer:options", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "docs/status.md": [ + "layer:status", + "static", + "975dec9f8c938196e102e954a80226bda293407c4e5ae857c118bf692154702a" + ], + "exec.d/docker-compose/charm-pre-install": [ + "calico", + "static", + "2760db1047cdc4beeb974923c693bf824d45a9ee88fb50496efada92461aceb8" + ], + "hooks/cni-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/config-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/etcd-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/etcd-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/etcd-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/etcd-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/etcd-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/hook.template": [ + "layer:basic", + "static", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/install": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-elected": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-settings-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/post-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/pre-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/relations/etcd/.gitignore": [ + "interface:etcd", + "static", + "cf237c7aff44efbe6e502e645c3e06da03a69d7bdeb43392108ef3348143417e" + ], + "hooks/relations/etcd/README.md": [ + "interface:etcd", + "static", + "93873d073f5f5302d352e09321aaf87458556e9730f89e1c682699c1d0db2386" + ], + "hooks/relations/etcd/__init__.py": [ + "interface:etcd", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/etcd/interface.yaml": [ + "interface:etcd", + "static", + "ba9f723b57a434f7efb2c06abec4167cd412c16da5f496a477dd7691e9a715be" + ], + "hooks/relations/etcd/peers.py": [ + "interface:etcd", + "static", + "99419c3d139fb5bb90021e0482f9e7ac2cfb776fb7af79b46209c6a75b36e834" + ], + "hooks/relations/etcd/provides.py": [ + "interface:etcd", + "static", + "3db1f644ab669e2dec59d59b61de63b721bc05b38fe646e525fff8f0d60982f9" + ], + "hooks/relations/etcd/requires.py": [ + "interface:etcd", + "static", + "8ffc1a094807fd36a1d1428b0a07b2428074134d46086066ecd6c0acd9fcd13e" + ], + "hooks/relations/kubernetes-cni/.github/workflows/tests.yaml": [ + "interface:kubernetes-cni", + "static", + "d0015cd49675976ff87832f5ef7ea20ffca961786379c72bb6acdbdeddd9137c" + ], + "hooks/relations/kubernetes-cni/.gitignore": [ + "interface:kubernetes-cni", + "static", + "0594213ebf9c6ef87827b30405ee67d847f73f4185a865e0e5e9c0be9d29eabe" + ], + "hooks/relations/kubernetes-cni/README.md": [ + "interface:kubernetes-cni", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/kubernetes-cni/__init__.py": [ + "interface:kubernetes-cni", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/kubernetes-cni/interface.yaml": [ + "interface:kubernetes-cni", + "static", + "03affdaf7e879adfdf8c434aa31d40faa6d2872faa7dfd93a5d3a1ebae02487d" + ], + "hooks/relations/kubernetes-cni/provides.py": [ + "interface:kubernetes-cni", + "static", + "e436e187f2bab6e73add2b897cd43a2f000fde4726e40b772b66f27786c85dee" + ], + "hooks/relations/kubernetes-cni/requires.py": [ + "interface:kubernetes-cni", + "static", + "45398af27246eaf2005115bd3f270b78fc830d4345b02cc0c4d438711b7cd9fe" + ], + "hooks/relations/kubernetes-cni/tox.ini": [ + "interface:kubernetes-cni", + "static", + "f08626c9b65362031edb07f96f15f101bc3dda075abc64f54d1c83efd2c05e39" + ], + "hooks/start": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/stop": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/update-status": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/upgrade-charm": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "icon.svg": [ + "calico", + "static", + "49b68e61506d639d3c859e9477338469d1d44f7b76ad381ff152c728c71c43d9" + ], + "layer.yaml": [ + "calico", + "dynamic", + "3a95aaa6fd50027d9a98ad9322568cfb0c228135df7cbff79953a86d01ec533f" + ], + "lib/calico_common.py": [ + "calico", + "static", + "ec886f86a4505148016a540652c51afd7bf8ee4ef3b21db368e10ded2b9569be" + ], + "lib/calico_upgrade.py": [ + "calico", + "static", + "1200e9016b1db2f2a853033d04126adff1d4d43ccb29c48a613232e06f33a8c4" + ], + "lib/charms/layer/__init__.py": [ + "layer:basic", + "static", + "dfe0d26c6bf409767de6e2546bc648f150e1b396243619bad3aa0553ab7e0e6f" + ], + "lib/charms/layer/basic.py": [ + "layer:basic", + "static", + "98b47134770ed6e4c0b2d4aad73cd5bc200bec84aa9c1c4e075fd70c3222a0c9" + ], + "lib/charms/layer/execd.py": [ + "layer:basic", + "static", + "fda8bd491032db1db8ddaf4e99e7cc878c6fb5432efe1f91cadb5b34765d076d" + ], + "lib/charms/layer/kubernetes_common.py": [ + "layer:kubernetes-common", + "static", + "29cedffd490e6295273d195a7c9bace2fcdf149826e7427f2af9698f7f75055b" + ], + "lib/charms/layer/options.py": [ + "layer:options", + "static", + "8ae7a07d22542fc964f2d2bee8219d1c78a68dace70a1b38d36d4aea47b1c3b2" + ], + "lib/charms/layer/status.py": [ + "layer:status", + "static", + "d560a5e07b2e5f2b0f25f30e1f0278b06f3f90c01e4dbad5c83d71efc79018c6" + ], + "lib/charms/leadership.py": [ + "layer:leadership", + "static", + "20ffcbbc08147506759726ad51567420659ffb8a2e0121079240b8706658e332" + ], + "make_docs": [ + "layer:status", + "static", + "c990f55c8e879793a62ed8464ee3d7e0d7d2225fdecaf17af24b0df0e2daa8c1" + ], + "metadata.yaml": [ + "calico", + "dynamic", + "b1a1e252fb9eac35a8b1a10564b400a07d5c810d8ceed1a1e3460bea314886bb" + ], + "pydocmd.yml": [ + "layer:status", + "static", + "11d9293901f32f75f4256ae4ac2073b92ce1d7ef7b6c892ba9fbb98690a0b330" + ], + "reactive/__init__.py": [ + "layer:leadership", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "reactive/calico.py": [ + "calico", + "static", + "6b8bef93b474c95bab4d9df09b74b8082f230fa5e906b469fae66baa319472ad" + ], + "reactive/leadership.py": [ + "layer:leadership", + "static", + "e2b233cf861adc3b2d9e9c062134ce2f104953f03283cdddd88f49efee652e8f" + ], + "reactive/status.py": [ + "layer:status", + "static", + "30207fc206f24e91def5252f1c7f7c8e23c0aed0e93076babf5e03c05296d207" + ], + "requirements.txt": [ + "layer:basic", + "static", + "a00f75d80849e5b4fc5ad2e7536f947c25b1a4044b341caa8ee87a92d3a4c804" + ], + "templates/10-calico.conflist": [ + "calico", + "static", + "9332e14d9422781530cd13fef5748e3d06fcce7f4221123f625c3a7e09238abb" + ], + "templates/calico-node.service": [ + "calico", + "static", + "cc80a397a77f7d80740c697fcdaffd373790492b01959649587345bdcfe44fe3" + ], + "templates/calicoctl": [ + "calico", + "static", + "b913dfdce8de51aa9a13950817e4101f8f4229052927a272fff5b37a4370537f" + ], + "templates/cdk.auth-webhook-secret.yaml": [ + "layer:kubernetes-common", + "static", + "efaf34c12a5c961fa7843199070945ba05717b3656a0f3acc3327f45334bcaec" + ], + "templates/policy-controller.yaml": [ + "calico", + "static", + "427820ac4957c60306b3084c4426ecc84af34dc2b2a8f7c0d70242e53c27957c" + ], + "tests/data/bird-operator/config.yaml": [ + "calico", + "static", + "4786605f043192ab2970b7abd55c434620463248e2840a7d25a9ca31d913b416" + ], + "tests/data/bird-operator/metadata.yaml": [ + "calico", + "static", + "aff75a91343249cb86978512609d0e00c9d6271664b18eeed9e4ef415bd22937" + ], + "tests/data/bird-operator/requirements.txt": [ + "calico", + "static", + "7a70b4e7059a7d283c883288be3de0bed02d10fda4602c8de4699debcf6afbf2" + ], + "tests/data/bird-operator/src/charm.py": [ + "calico", + "static", + "8e0374bf6e887604e3ede4ba33d37cf0e43202b653cb3945cefff0d2a33e7a0c" + ], + "tests/data/bundle.yaml": [ + "calico", + "static", + "0bfb15407e4ac33c87718e20493c0eec3979d8658db85d4f38620b9fca4408bd" + ], + "tests/functional/conftest.py": [ + "layer:kubernetes-common", + "static", + "fd53e0c38b4dda0c18096167889cd0d85b98b0a13225f9f8853261241e94078c" + ], + "tests/functional/test_k8s_common.py": [ + "layer:kubernetes-common", + "static", + "680a53724154771dd78422bbaf24b151788d86dd07960712c5d9e0d758499b50" + ], + "tests/integration/conftest.py": [ + "calico", + "static", + "9069857cdd09798df7813cec38c1147938e9affb777d971c6b8ff3405fa726ff" + ], + "tests/integration/test_calico_integration.py": [ + "calico", + "static", + "65ce21b694059e6cf6b39d28cba16252a34322b77b5b33050fd46482f60d07fd" + ], + "tests/unit/conftest.py": [ + "calico", + "static", + "2c58cb11bf276805f586c05c20bf4ba15a7431b5c23ea3323dc4256ddc34c4d2" + ], + "tests/unit/test_calico.py": [ + "calico", + "static", + "2de748d396d66f5c581ade110a3f8a709e6aabe50f97502e1d0ac0ec817c223d" + ], + "tests/unit/test_k8s_common.py": [ + "layer:kubernetes-common", + "static", + "da9bcea8e75160311a4055c1cbf577b497ddd45dc00223c5f1667598f94d9be4" + ], + "tests/validate-wheelhouse.sh": [ + "calico", + "static", + "cdfd66832b110243b6fd165a75562d9b958f9741b334be2d3a7a1d05adfa6fe7" + ], + "tox.ini": [ + "calico", + "static", + "a96563719d29a96d41a0e91ef08da35b5e1de5aee2d5884c74d85dca7f43f2d2" + ], + "version": [ + "calico", + "dynamic", + "d42cce56c73a1877421efe5be4d1e7e914a99ce4e1e4b0143bd97ea895c7c629" + ], + "wheelhouse.txt": [ + "calico", + "dynamic", + "cb5ab8b42ebef8ae5adc80de0d7c39f84aeaa97207298aa453142bff87c39f8c" + ], + "wheelhouse/Jinja2-2.10.1.tar.gz": [ + "layer:basic", + "dynamic", + "065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013" + ], + "wheelhouse/MarkupSafe-1.1.1.tar.gz": [ + "layer:basic", + "dynamic", + "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b" + ], + "wheelhouse/PyYAML-5.2.tar.gz": [ + "layer:basic", + "dynamic", + "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c" + ], + "wheelhouse/Tempita-0.5.2.tar.gz": [ + "__pip__", + "dynamic", + "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c" + ], + "wheelhouse/charmhelpers-0.20.23.tar.gz": [ + "layer:basic", + "dynamic", + "59a9776594e91cd3e3e000043f8668b4d7b279422dbb17e320f01dc16385b80e" + ], + "wheelhouse/charms.reactive-1.4.1.tar.gz": [ + "layer:basic", + "dynamic", + "bba21b4fd40b26c240c9ef2aa10c6fdf73592031c68591da4e7ccc46ca9cb616" + ], + "wheelhouse/click-7.1.2.tar.gz": [ + "calico", + "dynamic", + "d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a" + ], + "wheelhouse/conctl-py35-0.1.2.tar.gz": [ + "__pip__", + "dynamic", + "fad07dd70e04338f2df7fa5a38448223613b87b09a571ea5d2b3c780bb1eca0b" + ], + "wheelhouse/netaddr-0.7.19.tar.gz": [ + "layer:basic", + "dynamic", + "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd" + ], + "wheelhouse/pbr-5.6.0.tar.gz": [ + "__pip__", + "dynamic", + "42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd" + ], + "wheelhouse/pip-18.1.tar.gz": [ + "layer:basic", + "dynamic", + "c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1" + ], + "wheelhouse/pyaml-21.10.1.tar.gz": [ + "__pip__", + "dynamic", + "c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383" + ], + "wheelhouse/setuptools-41.6.0.zip": [ + "layer:basic", + "dynamic", + "6afa61b391dcd16cb8890ec9f66cc4015a8a31a6e1c2b4e0c464514be1a3d722" + ], + "wheelhouse/setuptools_scm-1.17.0.tar.gz": [ + "layer:basic", + "dynamic", + "70a4cf5584e966ae92f54a764e6437af992ba42ac4bca7eb37cc5d02b98ec40a" + ], + "wheelhouse/six-1.16.0.tar.gz": [ + "__pip__", + "dynamic", + "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926" + ], + "wheelhouse/wheel-0.33.6.tar.gz": [ + "layer:basic", + "dynamic", + "10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646" + ] + } +} \ No newline at end of file diff --git a/calico/.github/workflows/main.yml b/calico/.github/workflows/main.yml new file mode 100644 index 0000000..6768aef --- /dev/null +++ b/calico/.github/workflows/main.yml @@ -0,0 +1,22 @@ +name: Test Suite +on: [pull_request] + +jobs: + tests: + name: Lint, Unit, & Func Tests + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + - name: Run lint + run: tox diff --git a/calico/.github/workflows/tox.yaml b/calico/.github/workflows/tox.yaml new file mode 100644 index 0000000..4ecc315 --- /dev/null +++ b/calico/.github/workflows/tox.yaml @@ -0,0 +1,52 @@ +name: Run tests with Tox + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + lint-unit-wheelhouse: + name: Lint, Unit, Wheelhouse + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + sudo snap install charm --classic + - name: Lint + run: tox -vve lint + - name: Unit Tests + run: tox -vve unit + - name: Validate Wheelhouse + run: tox -vve validate-wheelhouse + integration-test: + name: Integration test with VMWare + runs-on: self-hosted + timeout-minutes: 360 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@master + with: + provider: vsphere + credentials-yaml: ${{ secrets.CREDENTIALS_YAML }} + clouds-yaml: ${{ secrets.CLOUDS_YAML }} + bootstrap-options: "--model-default datastore=vsanDatastore --model-default primary-network=VLAN_2764" + - name: Run test + run: tox -e integration diff --git a/calico/.gitignore b/calico/.gitignore new file mode 100644 index 0000000..cc02691 --- /dev/null +++ b/calico/.gitignore @@ -0,0 +1,3 @@ +.tox/ +__pycache__/ +*.pyc diff --git a/calico/CONTRIBUTING.md b/calico/CONTRIBUTING.md new file mode 100644 index 0000000..158b811 --- /dev/null +++ b/calico/CONTRIBUTING.md @@ -0,0 +1,38 @@ + +# Contributor Guide + +This Juju charm is open source ([Apache License 2.0](./LICENSE)) and we actively seek any community contibutions +for code, suggestions and documentation. +This page details a few notes, workflows and suggestions for how to make contributions most effective and help us +all build a better charm - please give them a read before working on any contributions. + +## Licensing + +This charm has been created under the [Apache License 2.0](./LICENSE), which will cover any contributions you may +make to this project. Please familiarise yourself with the terms of the license. + +Additionally, this charm uses the Harmony CLA agreement. It’s the easiest way for you to give us permission to +use your contributions. +In effect, you’re giving us a license, but you still own the copyright — so you retain the right to modify your +code and use it in other projects. Please [sign the CLA here](https://ubuntu.com/legal/contributors/agreement) before +making any contributions. + +## Code of conduct +We have adopted the Ubuntu code of Conduct. You can read this in full [here](https://ubuntu.com/community/code-of-conduct). + +## Contributing code + +The [DEVELOPING.md](./DEVELOPING.md) page has some useful information regarding building and testing. To contribute code +to this project, the workflow is as follows: + +1. [Submit a bug](https://bugs.launchpad.net/charm-calico/+filebug) to explain the need for and track the change. +2. Create a branch on your fork of the repo with your changes, including a unit test covering the new or modified code. +3. Submit a PR. The PR description should include a link to the bug on Launchpad. +4. Update the Launchpad bug to include a link to the PR and the `review-needed` tag. +5. Once reviewed and merged, the change will become available on the edge channel and assigned to an appropriate milestone + for further release according to priority. + +## Documentation + +Documentation for this charm is currently maintained as part of the Charmed Kubernetes docs. +See [this page](https://github.com/charmed-kubernetes/kubernetes-docs/blob/master/pages/k8s/charm-calico.md) \ No newline at end of file diff --git a/calico/DEVELOPING.md b/calico/DEVELOPING.md new file mode 100644 index 0000000..391c15b --- /dev/null +++ b/calico/DEVELOPING.md @@ -0,0 +1,62 @@ +# Developing layer-calico + +## Installing build dependencies + +To install build dependencies: + +``` +sudo snap install charm --classic +sudo apt install docker.io +sudo usermod -aG docker $USER +``` + +After running these commands, terminate your shell session and start a new one +to pick up the modified user groups. + +## Building the charm + +To build the charm: +``` +charm build +``` + +By default, this will build the charm and place it in +`/tmp/charm-builds/calico`. + +## Building resources + +To build resources: +``` +./build-calico-resources.sh +``` + +This will produce several .tar.gz files that you will need to attach to the +charm when you deploy it. + +## Testing + +You can test a locally built calico charm by deploying it with Charmed +Kubernetes. + +Create a file named `local-calico.yaml` that contains the following (with paths +adjusted to fit your environment): +``` +applications: + calico: + charm: /tmp/charm-builds/calico + resources: + calico: /path/to/layer-calico/calico-amd64.tar.gz + calico-upgrade: /path/to/layer-calico/calico-upgrade-amd64.tar.gz +``` + +Then deploy Charmed Kubernetes with your locally built calico charm: + +``` +juju deploy cs:~containers/kubernetes-calico --overlay local-calico.yaml +``` + +## Helpful links + +* [Getting Started with charm development](https://jaas.ai/docs/getting-started-with-charm-development) +* [Charm tools documentation](https://jaas.ai/docs/charm-tools) +* [Charmed Kubernetes Calico documentation](https://ubuntu.com/kubernetes/docs/cni-calico) diff --git a/calico/LICENSE b/calico/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/calico/LICENSE @@ -0,0 +1,202 @@ + + 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. \ No newline at end of file diff --git a/calico/Makefile b/calico/Makefile new file mode 100644 index 0000000..a1ad3a5 --- /dev/null +++ b/calico/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/calico/README.md b/calico/README.md new file mode 100644 index 0000000..2bf4541 --- /dev/null +++ b/calico/README.md @@ -0,0 +1,22 @@ +# Calico Charm + +Calico is a new approach to virtual networking and network security for containers, +VMs, and bare metal services, that provides a rich set of security enforcement +capabilities running on top of a highly scalable and efficient virtual network fabric. + +This charm will deploy calico as a background service, and configure CNI for +use with calico, on any principal charm that implements the [kubernetes-cni][] +interface. + +This charm is a component of Charmed Kubernetes. For full information, +please visit the [official Charmed Kubernetes docs](https://www.ubuntu.com/kubernetes/docs/charm-calico). + +[kubernetes-cni]: https://github.com/juju-solutions/interface-kubernetes-cni + +# Developers + +## Build charm + +``` +make charm +``` diff --git a/calico/bin/charm-env b/calico/bin/charm-env new file mode 100755 index 0000000..d211ce9 --- /dev/null +++ b/calico/bin/charm-env @@ -0,0 +1,107 @@ +#!/bin/bash + +VERSION="1.0.0" + + +find_charm_dirs() { + # Hopefully, $JUJU_CHARM_DIR is set so which venv to use in unambiguous. + if [[ -n "$JUJU_CHARM_DIR" || -n "$CHARM_DIR" ]]; then + if [[ -z "$JUJU_CHARM_DIR" ]]; then + # accept $CHARM_DIR to be more forgiving + export JUJU_CHARM_DIR="$CHARM_DIR" + fi + if [[ -z "$CHARM_DIR" ]]; then + # set CHARM_DIR as well to help with backwards compatibility + export CHARM_DIR="$JUJU_CHARM_DIR" + fi + return + fi + # Try to guess the value for JUJU_CHARM_DIR by looking for a non-subordinate + # (because there's got to be at least one principle) charm directory; + # if there are several, pick the first by alpha order. + agents_dir="/var/lib/juju/agents" + if [[ -d "$agents_dir" ]]; then + desired_charm="$1" + found_charm_dir="" + if [[ -n "$desired_charm" ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + charm_name="$(grep -o '^['\''"]\?name['\''"]\?:.*' $charm_dir/metadata.yaml 2> /dev/null | sed -e 's/.*: *//' -e 's/['\''"]//g')" + if [[ "$charm_name" == "$desired_charm" ]]; then + if [[ -n "$found_charm_dir" ]]; then + >&2 echo "Ambiguous possibilities for JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + found_charm_dir="$charm_dir" + fi + done + if [[ -z "$found_charm_dir" ]]; then + >&2 echo "Unable to determine JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + export JUJU_CHARM_DIR="$found_charm_dir" + export CHARM_DIR="$found_charm_dir" + return + fi + # shellcheck disable=SC2126 + non_subordinates="$(grep -L 'subordinate"\?:.*true' "$agents_dir"/unit-*/charm/metadata.yaml | wc -l)" + if [[ "$non_subordinates" -gt 1 ]]; then + >&2 echo 'Ambiguous possibilities for JUJU_CHARM_DIR; please use --charm or run within a Juju hook context' + exit 1 + elif [[ "$non_subordinates" -eq 1 ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + if grep -q 'subordinate"\?:.*true' "$charm_dir/metadata.yaml"; then + continue + fi + export JUJU_CHARM_DIR="$charm_dir" + export CHARM_DIR="$charm_dir" + return + done + fi + fi + >&2 echo 'Unable to determine JUJU_CHARM_DIR; please run within a Juju hook context' + exit 1 +} + +try_activate_venv() { + if [[ -d "$JUJU_CHARM_DIR/../.venv" ]]; then + . "$JUJU_CHARM_DIR/../.venv/bin/activate" + fi +} + +find_wrapped() { + PATH="${PATH/\/usr\/local\/sbin:}" which "$(basename "$0")" +} + + +if [[ "$1" == "--version" || "$1" == "-v" ]]; then + echo "$VERSION" + exit 0 +fi + + +# allow --charm option to hint which JUJU_CHARM_DIR to choose when ambiguous +# NB: --charm option must come first +# NB: option must be processed outside find_charm_dirs to modify $@ +charm_name="" +if [[ "$1" == "--charm" ]]; then + charm_name="$2" + shift; shift +fi + +find_charm_dirs "$charm_name" +try_activate_venv +export PYTHONPATH="$JUJU_CHARM_DIR/lib:$PYTHONPATH" + +if [[ "$(basename "$0")" == "charm-env" ]]; then + # being used as a shebang + exec "$@" +elif [[ "$0" == "$BASH_SOURCE" ]]; then + # being invoked as a symlink wrapping something to find in the venv + exec "$(find_wrapped)" "$@" +elif [[ "$(basename "$BASH_SOURCE")" == "charm-env" ]]; then + # being sourced directly; do nothing + /bin/true +else + # being sourced for wrapped bash helpers + . "$(find_wrapped)" +fi diff --git a/calico/bin/layer_option b/calico/bin/layer_option new file mode 100755 index 0000000..3253ef8 --- /dev/null +++ b/calico/bin/layer_option @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from charms import layer + + +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 = layer.options.get(args.section, 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/calico/build-calico-resource.sh b/calico/build-calico-resource.sh new file mode 100755 index 0000000..7dfe533 --- /dev/null +++ b/calico/build-calico-resource.sh @@ -0,0 +1,112 @@ +#!/bin/bash +set -eux + +# This script will fetch binaries and create resource tarballs for use by +# charm-[push|release]. The arm64 binaries are not available upsteram for +# v2.6, so we must build them and host them somewhere ourselves. The steps +# for doing that are documented here: +# +# https://gist.github.com/kwmonroe/9b5f8dac2c17f93629a1a3868b22d671 + +# Supported calico architectures +arches="amd64 arm64" +calicoctl_version="v3.10.1" +calico_cni_version="v3.10.1" + +function fetch_and_validate() { + # fetch a binary and make sure it's what we expect (executable > 20MB) + min_bytes=20000000 + location="${1-}" + if [ -z ${location} ]; then + echo "$0: Missing location parameter for fetch_and_validate" + exit 1 + fi + + # remove everything up until the last slash to get the filename + filename=$(echo "${location##*/}") + case ${location} in + http*) + fetch_cmd="wget ${location} -O ./${filename}" + ;; + *) + fetch_cmd="scp ${location} ./${filename}" + ;; + esac + ${fetch_cmd} + + # Make sure we fetched something big enough + actual_bytes=$(wc -c < ${filename}) + if [ $actual_bytes -le $min_bytes ]; then + echo "$0: ${filename} should be at least ${min_bytes} bytes" + exit 1 + fi + + # Make sure we fetched a binary + if ! file ${filename} 2>&1 | grep -q 'executable'; then + echo "$0: ${filename} is not an executable" + exit 1 + fi +} + +for arch in ${arches}; do + rm -rf resource-build-$arch + mkdir resource-build-$arch + pushd resource-build-$arch + fetch_and_validate \ + https://github.com/projectcalico/calicoctl/releases/download/$calicoctl_version/calicoctl-linux-$arch + fetch_and_validate \ + https://github.com/projectcalico/cni-plugin/releases/download/$calico_cni_version/calico-$arch + fetch_and_validate \ + https://github.com/projectcalico/cni-plugin/releases/download/$calico_cni_version/calico-ipam-$arch + mv calicoctl-linux-$arch calicoctl + mv calico-$arch calico + mv calico-ipam-$arch calico-ipam + + chmod +x calicoctl calico calico-ipam + tar -zcvf ../calico-$arch.tar.gz . + + popd + rm -rf resource-build-$arch +done + +# calico-upgrade resource +for arch in ${arches}; do + rm -rf resource-build-upgrade + mkdir resource-build-upgrade + pushd resource-build-upgrade + if [ $arch = amd64 ]; then + fetch_and_validate \ + https://github.com/projectcalico/calico-upgrade/releases/download/v1.0.5/calico-upgrade + chmod +x calico-upgrade + elif [ $arch = arm64 ]; then + # git clone https://github.com/projectcalico/calico-upgrade repo + # pushd repo + # git checkout 2de2f7a0f26ef3bb1c2cabf06b2dcbcc2bba1d35 # known good commit + # make build ARCH=arm64 + # popd + # mv repo/dist/calico-upgrade-linux-$arch ./calico-upgrade + + # arm64 builds are failing due to an upstream issue: + # https://github.com/projectcalico/calico-upgrade/issues/42 + # For now, we will pull a previously built binary from the charm store. + wget https://api.jujucharms.com/charmstore/v5/~containers/calico-698/resource/calico-upgrade-arm64/462 \ + -O calico-upgrade-arm64.tar.gz + tar -xf calico-upgrade-arm64.tar.gz + checksum="$(sha256sum calico-upgrade)" + if [ "$checksum" != "7a07816c26ad19f526ab2f57353043dabd708a48185268b41493e458c59b797d calico-upgrade" ]; then + echo 'ERROR: checksum does not match, aborting' + exit 1 + fi + else + echo "Unsupported architecture for calico-upgrade: $arch" + exit 1 + fi + tar -zcvf ../calico-upgrade-$arch.tar.gz ./calico-upgrade + popd + rm -rf resource-build-upgrade +done + +# calico-upgrade arm64 +rm -rf resource-build-upgrade-arm64 + +touch calico-node-image.tar.gz diff --git a/calico/config.yaml b/calico/config.yaml new file mode 100644 index 0000000..ba4f479 --- /dev/null +++ b/calico/config.yaml @@ -0,0 +1,163 @@ +"options": + "bgp-service-cluster-ips": + "type": "string" + "description": | + Space-separated list of service cluster CIDRs to advertise over BGP. + These will be passed to the .spec.serviceClusterIPs field of the default + BGPConfiguration in Calico. + + Example value: ”10.0.0.0/24 10.0.1.0/24” + "default": "" + "bgp-service-external-ips": + "type": "string" + "description": | + Space-separated list of service external CIDRs to advertise over BGP. + These will be passed to the .spec.serviceExternalIPs field of the default + BGPConfiguration in Calico. + + Example value: ”10.0.0.0/24 10.0.1.0/24” + "default": "" + "calico-node-image": + "type": "string" + # Please refer to layer-canal/versioning.md before changing the version below. + "default": "rocks.canonical.com:443/cdk/calico/node:v3.10.1" + "description": | + The image id to use for calico/node. + "calico-policy-image": + "type": "string" + "default": "rocks.canonical.com:443/cdk/calico/kube-controllers:v3.10.1" + "description": | + The image id to use for calico/kube-controllers. + "ipip": + "type": "string" + "default": "Never" + "description": | + IPIP encapsulation mode. Must be one of "Always", "CrossSubnet", or "Never". + This is incompatible with VXLAN encapsulation. If VXLAN encapsulation is + enabled, then this must be set to "Never". + "vxlan": + "type": "string" + "default": "Never" + "description": | + VXLAN encapsulation mode. Must be one of "Always", "CrossSubnet", or "Never". + This is incompatible with IPIP encapsulation. If IPIP encapsulation is + enabled, then this must be set to "Never". + "veth-mtu": + "type": "int" + "default": !!null "" + "description": | + Set veth MTU size. This should be set to the MTU size of the base network. + + If VXLAN is enabled, then the charm will automatically subtract 50 from the + specified MTU size. + + If IPIP is enabled, then the charm will automatically subtract 20 from the + specified MTU size. + "nat-outgoing": + "type": "boolean" + "default": !!bool "true" + "description": | + NAT outgoing traffic + "cidr": + "type": "string" + "default": "192.168.0.0/16" + "description": | + Network CIDR assigned to Calico. This is applied to the default Calico + pool, and is also communicated to the Kubernetes charms for use in + kube-proxy configuration. + "manage-pools": + "type": "boolean" + "default": !!bool "true" + "description": | + If true, a default pool is created using the cidr and ipip charm + configuration values. + + Warning: When manage-pools is enabled, the charm will delete any pools + that are unrecognized. + "global-as-number": + "type": "int" + "default": !!int "64512" + "description": | + Global AS number. + "subnet-as-numbers": + "type": "string" + "default": "{}" + "description": | + Mapping of subnets to AS numbers, specified as YAML. Each Calico node + will be assigned an AS number based on the entries in this mapping. + + Example value: "{10.0.0.0/24: 64512, 10.0.1.0/24: 64513}" + + If a node's IP matches any of the specified subnets, then the + corresponding AS number is used instead of the global one. + + If a node's IP matches no subnets, then the global AS number will be + used instead. + + If a node's IP matches multiple subnets, then the most specific subnet + will be used, e.g. a /24 subnet will take precedence over a /16. + "unit-as-numbers": + "type": "string" + "default": "{}" + "description": | + Mapping of unit IDs to AS numbers, specified as YAML. Each Calico node + will be assigned an AS number based on the entries in this mapping. + + Example value: "{0: 64512, 1: 64513}" + + This takes precedence over global-as-number and subnet-as-numbers. + "node-to-node-mesh": + "type": "boolean" + "default": !!bool "true" + "description": | + When enabled, each Calico node will peer with every other Calico node in + the cluster. + "global-bgp-peers": + "type": "string" + "default": "[]" + "description": | + List of global BGP peers. Each BGP peer is specified with an address and + an as-number. + + Example value: "[{address: 10.0.0.1, as-number: 65000}, {address: 10.0.0.2, as-number: 65001}]" + "subnet-bgp-peers": + "type": "string" + "default": "{}" + "description": | + Mapping of subnets to lists of BGP peers. Each BGP peer is specified with + an address and an as-number. + + Example value: "{10.0.0.0/24: [{address: 10.0.0.1, as-number: 65000}, {address: 10.0.0.2, as-number: 65001}], 10.0.1.0/24: [{address: 10.0.1.1, as-number: 65002}]}" + + If a node's IP matches multiple subnets, then peerings will be added for + each matched subnet. + "unit-bgp-peers": + "type": "string" + "default": "{}" + "description": | + Mapping of unit IDs to lists of BGP peers. Each BGP peer is specified + with an address and an as-number. + + Example value: "{0: [{address: 10.0.0.1, as-number: 65000}, {address: 10.0.0.2, as-number: 65001}], 1: [{address: 10.0.1.1, as-number: 65002}]}" + "route-reflector-cluster-ids": + "type": "string" + "default": "{}" + "description": | + Mapping of unit IDs to route reflector cluster IDs. Assigning a route + reflector cluster ID allows the node to function as a route reflector. + + Example value: "{0: 224.0.0.1, 2: 224.0.0.1}" + "ignore-loose-rpf": + "type": "boolean" + "default": !!bool "false" + "description": | + Enable or disable IgnoreLooseRPF for Calico Felix. This is only used + when rp_filter is set to a value of 2. + "disable-vxlan-tx-checksumming": + "type": "boolean" + "default": !!bool "true" + "description": | + When set to true, if VXLAN encapsulation is in use, then the charm will + disable TX checksumming on the vxlan.calico network interface. This works + around an upstream issue in Calico: + https://github.com/projectcalico/calico/issues/3145 diff --git a/calico/copyright b/calico/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/calico/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/calico/copyright.layer-basic b/calico/copyright.layer-basic new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/calico/copyright.layer-basic @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/calico/copyright.layer-leadership b/calico/copyright.layer-leadership new file mode 100644 index 0000000..08b983f --- /dev/null +++ b/calico/copyright.layer-leadership @@ -0,0 +1,15 @@ +Copyright 2015-2016 Canonical Ltd. + +This file is part of the Leadership Layer for Juju. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/calico/copyright.layer-options b/calico/copyright.layer-options new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/calico/copyright.layer-options @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/calico/docs/status.md b/calico/docs/status.md new file mode 100644 index 0000000..c6cceab --- /dev/null +++ b/calico/docs/status.md @@ -0,0 +1,91 @@ +

WorkloadState

+ +```python +WorkloadState(self, /, *args, **kwargs) +``` + +Enum of the valid workload states. + +Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + +

maintenance

+ +```python +maintenance(message) +``` + +Set the status to the `MAINTENANCE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

maint

+ +```python +maint(message) +``` + +Shorthand alias for +[maintenance](status.md#charms.layer.status.maintenance). + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

blocked

+ +```python +blocked(message) +``` + +Set the status to the `BLOCKED` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

waiting

+ +```python +waiting(message) +``` + +Set the status to the `WAITING` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

active

+ +```python +active(message) +``` + +Set the status to the `ACTIVE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

status_set

+ +```python +status_set(workload_state, message) +``` + +Set the status to the given workload state with a message. + +__Parameters__ + +- __`workload_state` (WorkloadState or str)__: State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. +- __`message` (str)__: Message to convey to the operator. + diff --git a/calico/exec.d/docker-compose/charm-pre-install b/calico/exec.d/docker-compose/charm-pre-install new file mode 100644 index 0000000..2c724c7 --- /dev/null +++ b/calico/exec.d/docker-compose/charm-pre-install @@ -0,0 +1,4 @@ +# This stubs out charm-pre-install coming from layer-docker as a workaround for +# offline installs until https://github.com/juju/charm-tools/issues/301 is fixed. + + diff --git a/calico/hooks/cni-relation-broken b/calico/hooks/cni-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/cni-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/cni-relation-changed b/calico/hooks/cni-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/cni-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/cni-relation-created b/calico/hooks/cni-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/cni-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/cni-relation-departed b/calico/hooks/cni-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/cni-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/cni-relation-joined b/calico/hooks/cni-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/cni-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/config-changed b/calico/hooks/config-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/config-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/etcd-relation-broken b/calico/hooks/etcd-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/etcd-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/etcd-relation-changed b/calico/hooks/etcd-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/etcd-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/etcd-relation-created b/calico/hooks/etcd-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/etcd-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/etcd-relation-departed b/calico/hooks/etcd-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/etcd-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/etcd-relation-joined b/calico/hooks/etcd-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/etcd-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/hook.template b/calico/hooks/hook.template new file mode 100644 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/hook.template @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/install b/calico/hooks/install new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/install @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/leader-elected b/calico/hooks/leader-elected new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/leader-elected @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/leader-settings-changed b/calico/hooks/leader-settings-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/leader-settings-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/post-series-upgrade b/calico/hooks/post-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/post-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/pre-series-upgrade b/calico/hooks/pre-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/pre-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/relations/etcd/.gitignore b/calico/hooks/relations/etcd/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/calico/hooks/relations/etcd/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/calico/hooks/relations/etcd/README.md b/calico/hooks/relations/etcd/README.md new file mode 100644 index 0000000..9ed51dd --- /dev/null +++ b/calico/hooks/relations/etcd/README.md @@ -0,0 +1,89 @@ +# Overview + +This interface layer handles the communication with Etcd via the `etcd` +interface. + +# Usage + +## Requires + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.connected` The relation is established, but Etcd may not + yet have provided any connection or service information. + + * `{relation_name}.available` Etcd has provided its connection string + information, and is ready to serve as a KV store. + The provided information can be accessed via the following methods: + * `etcd.get_connection_string()` + * `etcd.get_version()` + * `{relation_name}.tls.available` Etcd has provided the connection string + information, and the tls client credentials to communicate with it. + The client credentials can be accessed via: + * `{relation_name}.get_client_credentials()` returning a dictionary of + the clinet certificate, key and CA. + * `{relation_name}.save_client_credentials(key, cert, ca)` is a convenience + method to save the client certificate, key and CA to files of your + choosing. + + +For example, a common application for this is configuring an applications +backend key/value storage, like Docker. + +```python +@when('etcd.available', 'docker.available') +def swarm_etcd_cluster_setup(etcd): + con_string = etcd.connection_string().replace('http', 'etcd') + opts = {} + opts['connection_string'] = con_string + render('docker-compose.yml', 'files/swarm/docker-compose.yml', opts) + +``` + + +## Provides + +A charm providing this interface is providing the Etcd rest api service. + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.connected` One or more clients of any type have + been related. The charm should call the following methods to provide the + appropriate information to the clients: + + * `{relation_name}.set_connection_string(string, version)` + * `{relation_name}.set_client_credentials(key, cert, ca)` + +Example: + +```python +@when('db.connected') +def send_connection_details(db): + cert = leader_get('client_certificate') + key = leader_get('client_key') + ca = leader_get('certificate_authority') + # Set the key, cert, and ca on the db relation + db.set_client_credentials(key, cert, ca) + + port = hookenv.config().get('port') + # Get all the peers participating in the cluster relation. + addresses = cluster.get_peer_addresses() + connections = [] + for address in addresses: + connections.append('http://{0}:{1}'.format(address, port)) + # Set the connection string on the db relation. + db.set_connection_string(','.join(conections)) +``` + + +# Contact Information + +### Maintainer +- Charles Butler + + +# Etcd + +- [Etcd](https://coreos.com/etcd/) home page +- [Etcd bug trackers](https://github.com/coreos/etcd/issues) +- [Etcd Juju Charm](http://jujucharms.com/?text=etcd) diff --git a/calico/hooks/relations/etcd/__init__.py b/calico/hooks/relations/etcd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/calico/hooks/relations/etcd/interface.yaml b/calico/hooks/relations/etcd/interface.yaml new file mode 100644 index 0000000..929b1d5 --- /dev/null +++ b/calico/hooks/relations/etcd/interface.yaml @@ -0,0 +1,4 @@ +name: etcd +summary: Interface for relating to ETCD +version: 2 +maintainer: "Charles Butler " diff --git a/calico/hooks/relations/etcd/peers.py b/calico/hooks/relations/etcd/peers.py new file mode 100644 index 0000000..90980d1 --- /dev/null +++ b/calico/hooks/relations/etcd/peers.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdPeer(RelationBase): + '''This class handles peer relation communication by setting states that + the reactive code can respond to. ''' + + scope = scopes.UNIT + + @hook('{peers:etcd}-relation-joined') + def peer_joined(self): + '''A new peer has joined, set the state on the unit so we can track + when they are departed. ''' + conv = self.conversation() + conv.set_state('{relation_name}.joined') + + @hook('{peers:etcd}-relation-departed') + def peers_going_away(self): + '''Trigger a state on the unit that it is leaving. We can use this + state in conjunction with the joined state to determine which unit to + unregister from the etcd cluster. ''' + conv = self.conversation() + conv.remove_state('{relation_name}.joined') + conv.set_state('{relation_name}.departing') + + def dismiss(self): + '''Remove the departing state from all other units in the conversation, + and we can resume normal operation. + ''' + for conv in self.conversations(): + conv.remove_state('{relation_name}.departing') + + def get_peers(self): + '''Return a list of names for the peers participating in this + conversation scope. ''' + peers = [] + # Iterate over all the conversations of this type. + for conversation in self.conversations(): + peers.append(conversation.scope) + return peers + + def set_db_ingress_address(self, address): + '''Set the ingress address belonging to the db relation.''' + for conversation in self.conversations(): + conversation.set_remote('db-ingress-address', address) + + def get_db_ingress_addresses(self): + '''Return a list of db ingress addresses''' + addresses = [] + # Iterate over all the conversations of this type. + for conversation in self.conversations(): + address = conversation.get_remote('db-ingress-address') + if address: + addresses.append(address) + return addresses diff --git a/calico/hooks/relations/etcd/provides.py b/calico/hooks/relations/etcd/provides.py new file mode 100644 index 0000000..3cfc174 --- /dev/null +++ b/calico/hooks/relations/etcd/provides.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdProvider(RelationBase): + scope = scopes.GLOBAL + + @hook('{provides:etcd}-relation-{joined,changed}') + def joined_or_changed(self): + ''' Set the connected state from the provides side of the relation. ''' + self.set_state('{relation_name}.connected') + + @hook('{provides:etcd}-relation-{broken,departed}') + def broken_or_departed(self): + '''Remove connected state from the provides side of the relation. ''' + conv = self.conversation() + if len(conv.units) == 1: + conv.remove_state('{relation_name}.connected') + + def set_client_credentials(self, key, cert, ca): + ''' Set the client credentials on the global conversation for this + relation. ''' + self.set_remote('client_key', key) + self.set_remote('client_ca', ca) + self.set_remote('client_cert', cert) + + def set_connection_string(self, connection_string, version=''): + ''' Set the connection string on the global conversation for this + relation. ''' + # Note: Version added as a late-dependency for 2 => 3 migration + # If no version is specified, consumers should presume etcd 2.x + self.set_remote('connection_string', connection_string) + self.set_remote('version', version) diff --git a/calico/hooks/relations/etcd/requires.py b/calico/hooks/relations/etcd/requires.py new file mode 100644 index 0000000..435532f --- /dev/null +++ b/calico/hooks/relations/etcd/requires.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# 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 + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdClient(RelationBase): + scope = scopes.GLOBAL + + @hook('{requires:etcd}-relation-{joined,changed}') + def changed(self): + ''' Indicate the relation is connected, and if the relation data is + set it is also available. ''' + self.set_state('{relation_name}.connected') + + if self.get_connection_string(): + self.set_state('{relation_name}.available') + # Get the ca, key, cert from the relation data. + cert = self.get_client_credentials() + # The tls state depends on the existance of the ca, key and cert. + if cert['client_cert'] and cert['client_key'] and cert['client_ca']: # noqa + self.set_state('{relation_name}.tls.available') + + @hook('{requires:etcd}-relation-{broken, departed}') + def broken(self): + ''' Indicate the relation is no longer available and not connected. ''' + self.remove_state('{relation_name}.available') + self.remove_state('{relation_name}.connected') + self.remove_state('{relation_name}.tls.available') + + def connection_string(self): + ''' This method is depreciated but ensures backward compatibility + @see get_connection_string(self). ''' + return self.get_connection_string() + + def get_connection_string(self): + ''' Return the connection string, if available, or None. ''' + return self.get_remote('connection_string') + + def get_version(self): + ''' Return the version of the etd protocol being used, or None. ''' + return self.get_remote('version') + + def get_client_credentials(self): + ''' Return a dict with the client certificate, ca and key to + communicate with etcd using tls. ''' + return {'client_cert': self.get_remote('client_cert'), + 'client_key': self.get_remote('client_key'), + 'client_ca': self.get_remote('client_ca')} + + def save_client_credentials(self, key, cert, ca): + ''' Save all the client certificates for etcd to local files. ''' + self._save_remote_data('client_cert', cert) + self._save_remote_data('client_key', key) + self._save_remote_data('client_ca', ca) + + def _save_remote_data(self, key, path): + ''' Save the remote data to a file indicated by path creating the + parent directory if needed.''' + value = self.get_remote(key) + if value: + parent = os.path.dirname(path) + if not os.path.isdir(parent): + os.makedirs(parent) + with open(path, 'w') as stream: + stream.write(value) diff --git a/calico/hooks/relations/kubernetes-cni/.github/workflows/tests.yaml b/calico/hooks/relations/kubernetes-cni/.github/workflows/tests.yaml new file mode 100644 index 0000000..9801450 --- /dev/null +++ b/calico/hooks/relations/kubernetes-cni/.github/workflows/tests.yaml @@ -0,0 +1,24 @@ +name: Test Suite for K8s Service Interface + +on: + - pull_request + +jobs: + lint-and-unit-tests: + name: Lint & Unit tests + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Tox + run: pip install tox + - name: Run lint & unit tests + run: tox + diff --git a/calico/hooks/relations/kubernetes-cni/.gitignore b/calico/hooks/relations/kubernetes-cni/.gitignore new file mode 100644 index 0000000..8d150f3 --- /dev/null +++ b/calico/hooks/relations/kubernetes-cni/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.tox +__pycache__ +*.pyc diff --git a/calico/hooks/relations/kubernetes-cni/README.md b/calico/hooks/relations/kubernetes-cni/README.md new file mode 100644 index 0000000..e69de29 diff --git a/calico/hooks/relations/kubernetes-cni/__init__.py b/calico/hooks/relations/kubernetes-cni/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/calico/hooks/relations/kubernetes-cni/interface.yaml b/calico/hooks/relations/kubernetes-cni/interface.yaml new file mode 100644 index 0000000..7e3c123 --- /dev/null +++ b/calico/hooks/relations/kubernetes-cni/interface.yaml @@ -0,0 +1,6 @@ +name: kubernetes-cni +summary: Interface for relating various CNI implementations +version: 0 +maintainer: "George Kraft " +ignore: +- tests diff --git a/calico/hooks/relations/kubernetes-cni/provides.py b/calico/hooks/relations/kubernetes-cni/provides.py new file mode 100644 index 0000000..9095c19 --- /dev/null +++ b/calico/hooks/relations/kubernetes-cni/provides.py @@ -0,0 +1,89 @@ +#!/usr/bin/python + +from charmhelpers.core import hookenv +from charmhelpers.core.host import file_hash +from charms.layer.kubernetes_common import kubeclientconfig_path +from charms.reactive import Endpoint +from charms.reactive import toggle_flag, is_flag_set, clear_flag, set_flag + + +class CNIPluginProvider(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name("{endpoint_name}.connected"), self.is_joined) + toggle_flag( + self.expand_name("{endpoint_name}.available"), self.config_available() + ) + if is_flag_set(self.expand_name("endpoint.{endpoint_name}.changed")): + clear_flag(self.expand_name("{endpoint_name}.configured")) + clear_flag(self.expand_name("endpoint.{endpoint_name}.changed")) + + def set_config(self, is_master): + """Relays a dict of kubernetes configuration information.""" + for relation in self.relations: + relation.to_publish_raw.update({"is_master": is_master}) + set_flag(self.expand_name("{endpoint_name}.configured")) + + def config_available(self): + """Ensures all config from the CNI plugin is available.""" + goal_state = hookenv.goal_state() + related_apps = [ + app + for app in goal_state.get("relations", {}).get(self.endpoint_name, "") + if "/" not in app + ] + if not related_apps: + return False + configs = self.get_configs() + return all( + "cidr" in config and "cni-conf-file" in config + for config in [configs.get(related_app, {}) for related_app in related_apps] + ) + + def get_config(self, default=None): + """Get CNI config for one related application. + + If default is specified, and there is a related application with a + matching name, then that application is chosen. Otherwise, the + application is chosen alphabetically. + + Whichever application is chosen, that application's CNI config is + returned. + """ + configs = self.get_configs() + if not configs: + return {} + elif default and default not in configs: + msg = "relation not found for default CNI %s, ignoring" % default + hookenv.log(msg, level="WARN") + return self.get_config() + elif default: + return configs.get(default, {}) + else: + return configs.get(sorted(configs)[0], {}) + + def get_configs(self): + """Get CNI configs for all related applications. + + This returns a mapping of application names to CNI configs. Here's an + example return value: + { + 'flannel': { + 'cidr': '10.1.0.0/16', + 'cni-conf-file': '10-flannel.conflist' + }, + 'calico': { + 'cidr': '192.168.0.0/16', + 'cni-conf-file': '10-calico.conflist' + } + } + """ + return { + relation.application_name: relation.joined_units.received_raw + for relation in self.relations + if relation.application_name + } + + def notify_kubeconfig_changed(self): + kubeconfig_hash = file_hash(kubeclientconfig_path) + for relation in self.relations: + relation.to_publish_raw.update({"kubeconfig-hash": kubeconfig_hash}) diff --git a/calico/hooks/relations/kubernetes-cni/requires.py b/calico/hooks/relations/kubernetes-cni/requires.py new file mode 100644 index 0000000..2067826 --- /dev/null +++ b/calico/hooks/relations/kubernetes-cni/requires.py @@ -0,0 +1,54 @@ +#!/usr/bin/python + +from charmhelpers.core import unitdata +from charms.reactive import Endpoint +from charms.reactive import when_any, when_not +from charms.reactive import set_state, remove_state + +db = unitdata.kv() + + +class CNIPluginClient(Endpoint): + def manage_flags(self): + kubeconfig_hash = self.get_config().get("kubeconfig-hash") + kubeconfig_hash_key = self.expand_name("{endpoint_name}.kubeconfig-hash") + if kubeconfig_hash: + set_state(self.expand_name("{endpoint_name}.kubeconfig.available")) + if kubeconfig_hash != db.get(kubeconfig_hash_key): + set_state(self.expand_name("{endpoint_name}.kubeconfig.changed")) + db.set(kubeconfig_hash_key, kubeconfig_hash) + + @when_any("endpoint.{endpoint_name}.joined", "endpoint.{endpoint_name}.changed") + def changed(self): + """Indicate the relation is connected, and if the relation data is + set it is also available.""" + set_state(self.expand_name("{endpoint_name}.connected")) + config = self.get_config() + if config["is_master"] == "True": + set_state(self.expand_name("{endpoint_name}.is-master")) + set_state(self.expand_name("{endpoint_name}.configured")) + elif config["is_master"] == "False": + set_state(self.expand_name("{endpoint_name}.is-worker")) + set_state(self.expand_name("{endpoint_name}.configured")) + else: + remove_state(self.expand_name("{endpoint_name}.configured")) + remove_state(self.expand_name("endpoint.{endpoint_name}.changed")) + + @when_not("endpoint.{endpoint_name}.joined") + def broken(self): + """Indicate the relation is no longer available and not connected.""" + remove_state(self.expand_name("{endpoint_name}.connected")) + remove_state(self.expand_name("{endpoint_name}.is-master")) + remove_state(self.expand_name("{endpoint_name}.is-worker")) + remove_state(self.expand_name("{endpoint_name}.configured")) + + def get_config(self): + """Get the kubernetes configuration information.""" + return self.all_joined_units.received_raw + + def set_config(self, cidr, cni_conf_file): + """Sets the CNI configuration information.""" + for relation in self.relations: + relation.to_publish_raw.update( + {"cidr": cidr, "cni-conf-file": cni_conf_file} + ) diff --git a/calico/hooks/relations/kubernetes-cni/tox.ini b/calico/hooks/relations/kubernetes-cni/tox.ini new file mode 100644 index 0000000..69ab91a --- /dev/null +++ b/calico/hooks/relations/kubernetes-cni/tox.ini @@ -0,0 +1,27 @@ +[tox] +skipsdist = True +envlist = lint,py3 + +[testenv] +basepython = python3 +setenv = + PYTHONPATH={toxinidir}:{toxinidir}/lib + PYTHONBREAKPOINT=ipdb.set_trace +deps = + pyyaml + pytest + flake8 + black + ipdb + charms.unit_test +commands = pytest --tb native -s {posargs} + +[testenv:lint] +envdir = {toxworkdir}/py3 +commands = + flake8 {toxinidir} + black --check {toxinidir} + +[flake8] +exclude=.tox +max-line-length = 88 diff --git a/calico/hooks/start b/calico/hooks/start new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/start @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/stop b/calico/hooks/stop new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/stop @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/update-status b/calico/hooks/update-status new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/update-status @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/hooks/upgrade-charm b/calico/hooks/upgrade-charm new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/calico/hooks/upgrade-charm @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/calico/icon.svg b/calico/icon.svg new file mode 100644 index 0000000..96ce38e --- /dev/null +++ b/calico/icon.svg @@ -0,0 +1,1378 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/calico/layer.yaml b/calico/layer.yaml new file mode 100644 index 0000000..db8d894 --- /dev/null +++ b/calico/layer.yaml @@ -0,0 +1,22 @@ +"includes": +- "layer:options" +- "interface:etcd" +- "interface:kubernetes-cni" +- "layer:basic" +- "layer:leadership" +- "layer:status" +- "layer:kubernetes-common" +"exclude": [".travis.yml", "tests", "tox.ini", "test-requirements.txt", "unit_tests"] +"options": + "basic": + "packages": [] + "python_packages": [] + "use_venv": !!bool "true" + "include_system_packages": !!bool "false" + "leadership": {} + "status": + "patch-hookenv": !!bool "true" + "kubernetes-common": {} + "calico": {} +"repo": "https://github.com/juju-solutions/layer-calico.git" +"is": "calico" diff --git a/calico/lib/calico_common.py b/calico/lib/calico_common.py new file mode 100644 index 0000000..ffe2165 --- /dev/null +++ b/calico/lib/calico_common.py @@ -0,0 +1,10 @@ +from subprocess import check_output + + +def arch(): + '''Return the package architecture as a string.''' + # Get the package architecture for this system. + architecture = check_output(['dpkg', '--print-architecture']).rstrip() + # Convert the binary result into a string. + architecture = architecture.decode('utf-8') + return architecture diff --git a/calico/lib/calico_upgrade.py b/calico/lib/calico_upgrade.py new file mode 100644 index 0000000..0daaf56 --- /dev/null +++ b/calico/lib/calico_upgrade.py @@ -0,0 +1,108 @@ +import os +import shutil +import yaml +from subprocess import check_call, check_output, CalledProcessError +from calico_common import arch +from charms.reactive import endpoint_from_flag +from charmhelpers.core.hookenv import resource_get, status_set, log + +CALICOCTL_PATH = '/opt/calicoctl' +ETCD_KEY_PATH = os.path.join(CALICOCTL_PATH, 'etcd-key') +ETCD_CERT_PATH = os.path.join(CALICOCTL_PATH, 'etcd-cert') +ETCD_CA_PATH = os.path.join(CALICOCTL_PATH, 'etcd-ca') +CALICO_UPGRADE_DIR = '/opt/calico-upgrade' +ETCD2_DATA_PATH = CALICO_UPGRADE_DIR + '/etcd2.yaml' +ETCD3_DATA_PATH = CALICO_UPGRADE_DIR + '/etcd3.yaml' + + +class ResourceMissing(Exception): + pass + + +class DryRunFailed(Exception): + pass + + +def cleanup(): + shutil.rmtree(CALICO_UPGRADE_DIR, ignore_errors=True) + + +def configure(): + cleanup() + os.makedirs(CALICO_UPGRADE_DIR) + + # Extract calico-upgrade resource + architecture = arch() + if architecture == 'amd64': + resource_name = 'calico-upgrade' + else: + resource_name = 'calico-upgrade-' + architecture + archive = resource_get(resource_name) + + if not archive: + message = 'Missing calico-upgrade resource' + status_set('blocked', message) + raise ResourceMissing(message) + + check_call(['tar', '-xvf', archive, '-C', CALICO_UPGRADE_DIR]) + + # Configure calico-upgrade, etcd2 (data source) + etcd = endpoint_from_flag('etcd.available') + etcd_endpoints = etcd.get_connection_string() + etcd2_data = { + 'apiVersion': 'v1', + 'kind': 'calicoApiConfig', + 'metadata': None, + 'spec': { + 'datastoreType': 'etcdv2', + 'etcdEndpoints': etcd_endpoints, + 'etcdKeyFile': ETCD_KEY_PATH, + 'etcdCertFile': ETCD_CERT_PATH, + 'etcdCACertFile': ETCD_CA_PATH + } + } + with open(ETCD2_DATA_PATH, 'w') as f: + yaml.dump(etcd2_data, f) + + # Configure calico-upgrade, etcd3 (data destination) + etcd3_data = { + 'apiVersion': 'projectcalico.org/v3', + 'kind': 'CalicoAPIConfig', + 'metadata': None, + 'spec': { + 'datastoreType': 'etcdv3', + 'etcdEndpoints': etcd_endpoints, + 'etcdKeyFile': ETCD_KEY_PATH, + 'etcdCertFile': ETCD_CERT_PATH, + 'etcdCACertFile': ETCD_CA_PATH + } + } + with open(ETCD3_DATA_PATH, 'w') as f: + yaml.dump(etcd3_data, f) + + +def invoke(*args): + cmd = [CALICO_UPGRADE_DIR + '/calico-upgrade'] + list(args) + cmd += [ + '--apiconfigv1', ETCD2_DATA_PATH, + '--apiconfigv3', ETCD3_DATA_PATH + ] + try: + return check_output(cmd) + except CalledProcessError as e: + log(e.output) + raise + + +def dry_run(): + output = invoke('dry-run', '--output-dir', CALICO_UPGRADE_DIR) + if b'Successfully validated v1 to v3 conversion' not in output: + raise DryRunFailed() + + +def start(): + invoke('start', '--no-prompts', '--output-dir', CALICO_UPGRADE_DIR) + + +def complete(): + invoke('complete', '--no-prompts') diff --git a/calico/lib/charms/layer/__init__.py b/calico/lib/charms/layer/__init__.py new file mode 100644 index 0000000..a8e0c64 --- /dev/null +++ b/calico/lib/charms/layer/__init__.py @@ -0,0 +1,60 @@ +import sys +from importlib import import_module +from pathlib import Path + + +def import_layer_libs(): + """ + Ensure that all layer libraries are imported. + + This makes it possible to do the following: + + from charms import layer + + layer.foo.do_foo_thing() + + Note: This function must be called after bootstrap. + """ + for module_file in Path('lib/charms/layer').glob('*'): + module_name = module_file.stem + if module_name in ('__init__', 'basic', 'execd') or not ( + module_file.suffix == '.py' or module_file.is_dir() + ): + continue + import_module('charms.layer.{}'.format(module_name)) + + +# Terrible hack to support the old terrible interface. +# Try to get people to call layer.options.get() instead so +# that we can remove this garbage. +# Cribbed from https://stackoverfLow.com/a/48100440/4941864 +class OptionsBackwardsCompatibilityHack(sys.modules[__name__].__class__): + def __call__(self, section=None, layer_file=None): + if layer_file is None: + return self.get(section=section) + else: + return self.get(section=section, + layer_file=Path(layer_file)) + + +def patch_options_interface(): + from charms.layer import options + if sys.version_info.minor >= 5: + options.__class__ = OptionsBackwardsCompatibilityHack + else: + # Py 3.4 doesn't support changing the __class__, so we have to do it + # another way. The last line is needed because we already have a + # reference that doesn't get updated with sys.modules. + name = options.__name__ + hack = OptionsBackwardsCompatibilityHack(name) + hack.get = options.get + sys.modules[name] = hack + sys.modules[__name__].options = hack + + +try: + patch_options_interface() +except ImportError: + # This may fail if pyyaml hasn't been installed yet. But in that + # case, the bootstrap logic will try it again once it has. + pass diff --git a/calico/lib/charms/layer/basic.py b/calico/lib/charms/layer/basic.py new file mode 100644 index 0000000..bbdd074 --- /dev/null +++ b/calico/lib/charms/layer/basic.py @@ -0,0 +1,501 @@ +import os +import sys +import re +import shutil +from distutils.version import LooseVersion +from pkg_resources import Requirement +from glob import glob +from subprocess import check_call, check_output, CalledProcessError +from time import sleep + +from charms import layer +from charms.layer.execd import execd_preinstall + + +def _get_subprocess_env(): + env = os.environ.copy() + env['LANG'] = env.get('LANG', 'C.UTF-8') + return env + + +def get_series(): + """ + Return series for a few known OS:es. + Tested as of 2019 november: + * centos6, centos7, rhel6. + * bionic + """ + series = "" + + # Looking for content in /etc/os-release + # works for ubuntu + some centos + if os.path.isfile('/etc/os-release'): + d = {} + with open('/etc/os-release', 'r') as rel: + for l in rel: + if not re.match(r'^\s*$', l): + k, v = l.split('=') + d[k.strip()] = v.strip().replace('"', '') + series = "{ID}{VERSION_ID}".format(**d) + + # Looking for content in /etc/redhat-release + # works for redhat enterprise systems + elif os.path.isfile('/etc/redhat-release'): + with open('/etc/redhat-release', 'r') as redhatlsb: + # CentOS Linux release 7.7.1908 (Core) + line = redhatlsb.readline() + release = int(line.split("release")[1].split()[0][0]) + series = "centos" + str(release) + + # Looking for content in /etc/lsb-release + # works for ubuntu + elif os.path.isfile('/etc/lsb-release'): + d = {} + with open('/etc/lsb-release', 'r') as lsb: + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + series = d['DISTRIB_CODENAME'] + + # This is what happens if we cant figure out the OS. + else: + series = "unknown" + return series + + +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 + + series = get_series() + + # OMG?! is build-essentials needed? + ubuntu_packages = ['python3-pip', + 'python3-setuptools', + 'python3-yaml', + 'python3-dev', + 'python3-wheel', + 'build-essential'] + + # I'm not going to "yum group info "Development Tools" + # omitting above madness + centos_packages = ['python3-pip', + 'python3-setuptools', + 'python3-devel', + 'python3-wheel'] + + packages_needed = [] + if 'centos' in series: + packages_needed = centos_packages + else: + packages_needed = ubuntu_packages + + 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') + hook_name = os.path.basename(sys.argv[0]) + is_bootstrapped = os.path.exists('wheelhouse/.bootstrapped') + is_charm_upgrade = hook_name == 'upgrade-charm' + is_series_upgrade = hook_name == 'post-series-upgrade' + is_post_upgrade = os.path.exists('wheelhouse/.upgraded') + is_upgrade = (not is_post_upgrade and + (is_charm_upgrade or is_series_upgrade)) + if is_bootstrapped and not is_upgrade: + # older subordinates might have downgraded charm-env, so we should + # restore it if necessary + install_or_update_charm_env() + activate_venv() + # the .upgrade file prevents us from getting stuck in a loop + # when re-execing to activate the venv; at this point, we've + # activated the venv, so it's safe to clear it + if is_post_upgrade: + os.unlink('wheelhouse/.upgraded') + return + if os.path.exists(venv): + try: + # focal installs or upgrades prior to PR 160 could leave the venv + # in a broken state which would prevent subsequent charm upgrades + _load_installed_versions(vpip) + except CalledProcessError: + is_broken_venv = True + else: + is_broken_venv = False + if is_upgrade or is_broken_venv: + # All upgrades should do a full clear of the venv, rather than + # just updating it, to bring in updates to Python itself + shutil.rmtree(venv) + if is_upgrade: + if os.path.exists('wheelhouse/.bootstrapped'): + os.unlink('wheelhouse/.bootstrapped') + # bootstrap wheelhouse + if os.path.exists('wheelhouse'): + pre_eoan = series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty', + 'ubuntu16.04', 'xenial', + 'ubuntu18.04', 'bionic') + pydistutils_lines = [ + "[easy_install]\n", + "find_links = file://{}/wheelhouse/\n".format(charm_dir), + "no_index=True\n", + "index_url=\n", # deliberately nothing here; disables it. + ] + if pre_eoan: + pydistutils_lines.append("allow_hosts = ''\n") + 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(pydistutils_lines) + if 'centos' in series: + yum_install(packages_needed) + else: + apt_install(packages_needed) + from charms.layer import options + cfg = options.get('basic') + # include packages defined in layer.yaml + if 'centos' in series: + yum_install(cfg.get('packages', [])) + else: + 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 = get_series() + if series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty'): + apt_install(['python-virtualenv']) + elif 'centos' in series: + yum_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, env=_get_subprocess_env()) + 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') + pre_install_pkgs = ['pip', 'setuptools', 'setuptools-scm'] + # we bundle these packages to work around bugs in older versions (such + # as https://github.com/pypa/pip/issues/56), but if the system already + # provided a newer version, downgrading it can cause other problems + _update_if_newer(pip, pre_install_pkgs) + # install the rest of the wheelhouse deps (extract the pkg names into + # a set so that we can ignore the pre-install packages and let pip + # choose the best version in case there are multiple from layer + # conflicts) + _versions = _load_wheelhouse_versions() + _pkgs = _versions.keys() - set(pre_install_pkgs) + # add back the versions such that each package in pkgs is + # ==. + # This ensures that pip 20.3.4+ will install the packages from the + # wheelhouse without (erroneously) flagging an error. + pkgs = _add_back_versions(_pkgs, _versions) + reinstall_flag = '--force-reinstall' + if not cfg.get('use_venv', True) and pre_eoan: + reinstall_flag = '--ignore-installed' + check_call([pip, 'install', '-U', reinstall_flag, '--no-index', + '--no-cache-dir', '-f', 'wheelhouse'] + list(pkgs), + env=_get_subprocess_env()) + # re-enable installation from pypi + os.remove('/root/.pydistutils.cfg') + + # install pyyaml for centos7, since, unlike the ubuntu image, the + # default image for centos doesn't include pyyaml; see the discussion: + # https://discourse.jujucharms.com/t/charms-for-centos-lets-begin + if 'centos' in series: + check_call([pip, 'install', '-U', 'pyyaml'], + env=_get_subprocess_env()) + + # install python packages from layer options + if cfg.get('python_packages'): + check_call([pip, 'install', '-U'] + cfg.get('python_packages'), + env=_get_subprocess_env()) + 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') + # setup wrappers to ensure envs are used for scripts + install_or_update_charm_env() + for wrapper in ('charms.reactive', 'charms.reactive.sh', + 'chlp', 'layer_option'): + src = os.path.join('/usr/local/sbin', 'charm-env') + dst = os.path.join('/usr/local/sbin', wrapper) + if not os.path.exists(dst): + os.symlink(src, dst) + if cfg.get('use_venv'): + shutil.copy2('bin/layer_option', vbin) + else: + shutil.copy2('bin/layer_option', '/usr/local/bin/') + # re-link the charm copy to the wrapper in case charms + # call bin/layer_option directly (as was the old pattern) + os.remove('bin/layer_option') + os.symlink('/usr/local/sbin/layer_option', 'bin/layer_option') + # flag us as having already bootstrapped so we don't do it again + open('wheelhouse/.bootstrapped', 'w').close() + if is_upgrade: + # flag us as having already upgraded so we don't do it again + open('wheelhouse/.upgraded', '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 _load_installed_versions(pip): + pip_freeze = check_output([pip, 'freeze']).decode('utf8') + versions = {} + for pkg_ver in pip_freeze.splitlines(): + try: + req = Requirement.parse(pkg_ver) + except ValueError: + continue + versions.update({ + req.project_name: LooseVersion(ver) + for op, ver in req.specs if op == '==' + }) + return versions + + +def _load_wheelhouse_versions(): + versions = {} + for wheel in glob('wheelhouse/*'): + pkg, ver = os.path.basename(wheel).rsplit('-', 1) + # nb: LooseVersion ignores the file extension + versions[pkg.replace('_', '-')] = LooseVersion(ver) + return versions + + +def _add_back_versions(pkgs, versions): + """Add back the version strings to each of the packages. + + The versions are LooseVersion() from _load_wheelhouse_versions(). This + function strips the ".zip" or ".tar.gz" from the end of the version string + and adds it back to the package in the form of == + + If a package name is not a key in the versions dictionary, then it is + returned in the list unchanged. + + :param pkgs: A list of package names + :type pkgs: List[str] + :param versions: A map of package to LooseVersion + :type versions: Dict[str, LooseVersion] + :returns: A list of (maybe) versioned packages + :rtype: List[str] + """ + def _strip_ext(s): + """Strip an extension (if it exists) from the string + + :param s: the string to strip an extension off if it exists + :type s: str + :returns: string without an extension of .zip or .tar.gz + :rtype: str + """ + for ending in [".zip", ".tar.gz"]: + if s.endswith(ending): + return s[:-len(ending)] + return s + + def _maybe_add_version(pkg): + """Maybe add back the version number to a package if it exists. + + Adds the version number, if the package exists in the lexically + captured `versions` dictionary, in the form ==. Strips + the extension if it exists. + + :param pkg: the package name to (maybe) add the version number to. + :type pkg: str + """ + try: + return "{}=={}".format(pkg, _strip_ext(str(versions[pkg]))) + except KeyError: + pass + return pkg + + return [_maybe_add_version(pkg) for pkg in pkgs] + + +def _update_if_newer(pip, pkgs): + installed = _load_installed_versions(pip) + wheelhouse = _load_wheelhouse_versions() + for pkg in pkgs: + if pkg not in installed or wheelhouse[pkg] > installed[pkg]: + check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', + pkg], env=_get_subprocess_env()) + + +def install_or_update_charm_env(): + # On Trusty python3-pkg-resources is not installed + try: + from pkg_resources import parse_version + except ImportError: + apt_install(['python3-pkg-resources']) + from pkg_resources import parse_version + + try: + installed_version = parse_version( + check_output(['/usr/local/sbin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + installed_version = parse_version('0.0.0') + try: + bundled_version = parse_version( + check_output(['bin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + bundled_version = parse_version('0.0.0') + if installed_version < bundled_version: + shutil.copy2('bin/charm-env', '/usr/local/sbin/') + + +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. + """ + from charms.layer import options + venv = os.path.abspath('../.venv') + vbin = os.path.join(venv, 'bin') + vpy = os.path.join(vbin, 'python') + use_venv = options.get('basic', 'use_venv') + if use_venv and '.venv' not in sys.executable: + # activate the venv + os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) + reload_interpreter(vpy) + layer.patch_options_interface() + layer.import_layer_libs() + + +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 = _get_subprocess_env() + + 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 + try: + # sometimes apt-get update needs to be run + check_call(['apt-get', 'update'], env=env) + except CalledProcessError: + # sometimes it's a dpkg lock issue + pass + sleep(5) + else: + break + + +def yum_install(packages): + """ Installs packages with yum. + This function largely mimics the apt_install function for consistency. + """ + if packages: + env = os.environ.copy() + cmd = ['yum', '-y', 'install'] + for attempt in range(3): + try: + check_call(cmd + packages, env=env) + except CalledProcessError: + if attempt == 2: + raise + try: + check_call(['yum', 'update'], env=env) + except CalledProcessError: + pass + sleep(5) + else: + break + else: + pass + + +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]) + + +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/calico/lib/charms/layer/execd.py b/calico/lib/charms/layer/execd.py new file mode 100644 index 0000000..438d9a1 --- /dev/null +++ b/calico/lib/charms/layer/execd.py @@ -0,0 +1,114 @@ +# 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 . + +# This module may only import from the Python standard library. +import os +import sys +import subprocess +import time + +''' +execd/preinstall + +Read the layer-basic docs for more info on how to use this feature. +https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#exec-d-support +''' + + +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<>> `get_version('kubelet') + (1, 6, 0) + + """ + cmd = "{} --version".format(bin_name).split() + version_string = subprocess.check_output(cmd).decode("utf-8") + return tuple(int(q) for q in re.findall("[0-9]+", version_string)[:3]) + + +def retry(times, delay_secs): + """Decorator for retrying a method call. + + Args: + times: How many times should we retry before giving up + delay_secs: Delay in secs + + Returns: A callable that would return the last call outcome + """ + + def retry_decorator(func): + """Decorator to wrap the function provided. + + Args: + func: Provided function should return either True od False + + Returns: A callable that would return the last call outcome + + """ + + def _wrapped(*args, **kwargs): + res = func(*args, **kwargs) + attempt = 0 + while not res and attempt < times: + sleep(delay_secs) + res = func(*args, **kwargs) + if res: + break + attempt += 1 + return res + + return _wrapped + + return retry_decorator + + +def calculate_resource_checksum(resource): + """Calculate a checksum for a resource""" + md5 = hashlib.md5() + path = hookenv.resource_get(resource) + if path: + with open(path, "rb") as f: + data = f.read() + md5.update(data) + return md5.hexdigest() + + +def get_resource_checksum_db_key(checksum_prefix, resource): + """Convert a resource name to a resource checksum database key.""" + return checksum_prefix + resource + + +def migrate_resource_checksums(checksum_prefix, snap_resources): + """Migrate resource checksums from the old schema to the new one""" + for resource in snap_resources: + new_key = get_resource_checksum_db_key(checksum_prefix, resource) + if not db.get(new_key): + path = hookenv.resource_get(resource) + if path: + # old key from charms.reactive.helpers.any_file_changed + old_key = "reactive.files_changed." + path + old_checksum = db.get(old_key) + db.set(new_key, old_checksum) + else: + # No resource is attached. Previously, this meant no checksum + # would be calculated and stored. But now we calculate it as if + # it is a 0-byte resource, so let's go ahead and do that. + zero_checksum = hashlib.md5().hexdigest() + db.set(new_key, zero_checksum) + + +def check_resources_for_upgrade_needed(checksum_prefix, snap_resources): + hookenv.status_set("maintenance", "Checking resources") + for resource in snap_resources: + key = get_resource_checksum_db_key(checksum_prefix, resource) + old_checksum = db.get(key) + new_checksum = calculate_resource_checksum(resource) + if new_checksum != old_checksum: + return True + return False + + +def calculate_and_store_resource_checksums(checksum_prefix, snap_resources): + for resource in snap_resources: + key = get_resource_checksum_db_key(checksum_prefix, resource) + checksum = calculate_resource_checksum(resource) + db.set(key, checksum) + + +def get_ingress_address(endpoint_name, ignore_addresses=None): + try: + network_info = hookenv.network_get(endpoint_name) + except NotImplementedError: + network_info = {} + + if not network_info or "ingress-addresses" not in network_info: + # if they don't have ingress-addresses they are running a juju that + # doesn't support spaces, so just return the private address + return hookenv.unit_get("private-address") + + addresses = network_info["ingress-addresses"] + + if ignore_addresses: + hookenv.log("ingress-addresses before filtering: {}".format(addresses)) + iter_filter = filter(lambda item: item not in ignore_addresses, addresses) + addresses = list(iter_filter) + hookenv.log("ingress-addresses after filtering: {}".format(addresses)) + + # Need to prefer non-fan IP addresses due to various issues, e.g. + # https://bugs.launchpad.net/charm-gcp-integrator/+bug/1822997 + # Fan typically likes to use IPs in the 240.0.0.0/4 block, so we'll + # prioritize those last. Not technically correct, but good enough. + try: + sort_key = lambda a: int(a.partition(".")[0]) >= 240 # noqa: E731 + addresses = sorted(addresses, key=sort_key) + except Exception: + hookenv.log(traceback.format_exc()) + + return addresses[0] + + +def get_ingress_address6(endpoint_name): + try: + network_info = hookenv.network_get(endpoint_name) + except NotImplementedError: + network_info = {} + + if not network_info or "ingress-addresses" not in network_info: + return None + + addresses = network_info["ingress-addresses"] + + for addr in addresses: + ip_addr = ipaddress.ip_interface(addr).ip + if ip_addr.version == 6: + return str(ip_addr) + else: + return None + + +def service_restart(service_name): + hookenv.status_set("maintenance", "Restarting {0} service".format(service_name)) + host.service_restart(service_name) + + +def service_start(service_name): + hookenv.log("Starting {0} service.".format(service_name)) + host.service_stop(service_name) + + +def service_stop(service_name): + hookenv.log("Stopping {0} service.".format(service_name)) + host.service_stop(service_name) + + +def arch(): + """Return the package architecture as a string. Raise an exception if the + architecture is not supported by kubernetes.""" + # Get the package architecture for this system. + architecture = check_output(["dpkg", "--print-architecture"]).rstrip() + # Convert the binary result into a string. + architecture = architecture.decode("utf-8") + return architecture + + +def get_service_ip(service, namespace="kube-system", errors_fatal=True): + try: + output = kubectl( + "get", "service", "--namespace", namespace, service, "--output", "json" + ) + except CalledProcessError: + if errors_fatal: + raise + else: + return None + else: + svc = json.loads(output.decode()) + return svc["spec"]["clusterIP"] + + +def kubectl(*args): + """Run a kubectl cli command with a config file. Returns stdout and throws + an error if the command fails.""" + command = ["kubectl", "--kubeconfig=" + kubeclientconfig_path] + list(args) + hookenv.log("Executing {}".format(command)) + return check_output(command) + + +def kubectl_success(*args): + """Runs kubectl with the given args. Returns True if successful, False if + not.""" + try: + kubectl(*args) + return True + except CalledProcessError: + return False + + +def kubectl_manifest(operation, manifest): + """Wrap the kubectl creation command when using filepath resources + :param operation - one of get, create, delete, replace + :param manifest - filepath to the manifest + """ + # Deletions are a special case + if operation == "delete": + # Ensure we immediately remove requested resources with --now + return kubectl_success(operation, "-f", manifest, "--now") + else: + # Guard against an error re-creating the same manifest multiple times + if operation == "create": + # If we already have the definition, its probably safe to assume + # creation was true. + if kubectl_success("get", "-f", manifest): + hookenv.log("Skipping definition for {}".format(manifest)) + return True + # Execute the requested command that did not match any of the special + # cases above + return kubectl_success(operation, "-f", manifest) + + +def get_node_name(): + kubelet_extra_args = parse_extra_args("kubelet-extra-args") + cloud_provider = kubelet_extra_args.get("cloud-provider", "") + if is_state("endpoint.aws.ready"): + cloud_provider = "aws" + elif is_state("endpoint.gcp.ready"): + cloud_provider = "gce" + elif is_state("endpoint.openstack.ready"): + cloud_provider = "openstack" + elif is_state("endpoint.vsphere.ready"): + cloud_provider = "vsphere" + elif is_state("endpoint.azure.ready"): + cloud_provider = "azure" + if cloud_provider == "aws": + return getfqdn().lower() + else: + return gethostname().lower() + + +def create_kubeconfig( + kubeconfig, + server, + ca, + key=None, + certificate=None, + user="ubuntu", + context="juju-context", + cluster="juju-cluster", + password=None, + token=None, + keystone=False, + aws_iam_cluster_id=None, +): + """Create a configuration for Kubernetes based on path using the supplied + arguments for values of the Kubernetes server, CA, key, certificate, user + context and cluster.""" + if not key and not certificate and not password and not token: + raise ValueError("Missing authentication mechanism.") + elif key and not certificate: + raise ValueError("Missing certificate.") + elif not key and certificate: + raise ValueError("Missing key.") + elif token and password: + # token and password are mutually exclusive. Error early if both are + # present. The developer has requested an impossible situation. + # see: kubectl config set-credentials --help + raise ValueError("Token and Password are mutually exclusive.") + + old_kubeconfig = Path(kubeconfig) + new_kubeconfig = Path(str(kubeconfig) + ".new") + + # Create the config file with the address of the master server. + cmd = ( + "kubectl config --kubeconfig={0} set-cluster {1} " + "--server={2} --certificate-authority={3} --embed-certs=true" + ) + check_call(split(cmd.format(new_kubeconfig, cluster, server, ca))) + # Delete old users + cmd = "kubectl config --kubeconfig={0} unset users" + check_call(split(cmd.format(new_kubeconfig))) + # Create the credentials using the client flags. + cmd = "kubectl config --kubeconfig={0} " "set-credentials {1} ".format( + new_kubeconfig, user + ) + + if key and certificate: + cmd = ( + "{0} --client-key={1} --client-certificate={2} " + "--embed-certs=true".format(cmd, key, certificate) + ) + if password: + cmd = "{0} --username={1} --password={2}".format(cmd, user, password) + # This is mutually exclusive from password. They will not work together. + if token: + cmd = "{0} --token={1}".format(cmd, token) + check_call(split(cmd)) + # Create a default context with the cluster. + cmd = "kubectl config --kubeconfig={0} set-context {1} " "--cluster={2} --user={3}" + check_call(split(cmd.format(new_kubeconfig, context, cluster, user))) + # Make the config use this new context. + cmd = "kubectl config --kubeconfig={0} use-context {1}" + check_call(split(cmd.format(new_kubeconfig, context))) + if keystone: + # create keystone user + cmd = "kubectl config --kubeconfig={0} " "set-credentials keystone-user".format( + new_kubeconfig + ) + check_call(split(cmd)) + # create keystone context + cmd = ( + "kubectl config --kubeconfig={0} " + "set-context --cluster={1} " + "--user=keystone-user keystone".format(new_kubeconfig, cluster) + ) + check_call(split(cmd)) + # use keystone context + cmd = "kubectl config --kubeconfig={0} " "use-context keystone".format( + new_kubeconfig + ) + check_call(split(cmd)) + # manually add exec command until kubectl can do it for us + with open(new_kubeconfig, "r") as f: + content = f.read() + content = content.replace( + """- name: keystone-user + user: {}""", + """- name: keystone-user + user: + exec: + command: "/snap/bin/client-keystone-auth" + apiVersion: "client.authentication.k8s.io/v1beta1" +""", + ) + with open(new_kubeconfig, "w") as f: + f.write(content) + if aws_iam_cluster_id: + # create aws-iam context + cmd = ( + "kubectl config --kubeconfig={0} " + "set-context --cluster={1} " + "--user=aws-iam-user aws-iam-authenticator" + ) + check_call(split(cmd.format(new_kubeconfig, cluster))) + + # append a user for aws-iam + cmd = ( + "kubectl --kubeconfig={0} config set-credentials " + "aws-iam-user --exec-command=aws-iam-authenticator " + '--exec-arg="token" --exec-arg="-i" --exec-arg="{1}" ' + '--exec-arg="-r" --exec-arg="<>" ' + "--exec-api-version=client.authentication.k8s.io/v1alpha1" + ) + check_call(split(cmd.format(new_kubeconfig, aws_iam_cluster_id))) + + # not going to use aws-iam context by default since we don't have + # the desired arn. This will make the config not usable if copied. + + # cmd = 'kubectl config --kubeconfig={0} ' \ + # 'use-context aws-iam-authenticator'.format(new_kubeconfig) + # check_call(split(cmd)) + if old_kubeconfig.exists(): + changed = new_kubeconfig.read_text() != old_kubeconfig.read_text() + else: + changed = True + if changed: + new_kubeconfig.rename(old_kubeconfig) + + +def parse_extra_args(config_key): + elements = hookenv.config().get(config_key, "").split() + args = {} + + for element in elements: + if "=" in element: + key, _, value = element.partition("=") + args[key] = value + else: + args[element] = "true" + + return args + + +def configure_kubernetes_service(key, service, base_args, extra_args_key): + db = unitdata.kv() + + prev_args_key = key + service + prev_snap_args = db.get(prev_args_key) or {} + + extra_args = parse_extra_args(extra_args_key) + + args = {} + args.update(base_args) + args.update(extra_args) + + # CIS benchmark action may inject kv config to pass failing tests. Merge + # these after the func args as they should take precedence. + cis_args_key = "cis-" + service + cis_args = db.get(cis_args_key) or {} + args.update(cis_args) + + # Remove any args with 'None' values (all k8s args are 'k=v') and + # construct an arg string for use by 'snap set'. + args = {k: v for k, v in args.items() if v is not None} + args = ['--%s="%s"' % arg for arg in args.items()] + args = " ".join(args) + + snap_opts = {} + for arg in prev_snap_args: + # remove previous args by setting to null + snap_opts[arg] = "null" + snap_opts["args"] = args + snap_opts = ["%s=%s" % opt for opt in snap_opts.items()] + + cmd = ["snap", "set", service] + snap_opts + check_call(cmd) + + # Now that we've started doing snap configuration through the "args" + # option, we should never need to clear previous args again. + db.set(prev_args_key, {}) + + +def _snap_common_path(component): + return Path("/var/snap/{}/common".format(component)) + + +def cloud_config_path(component): + return _snap_common_path(component) / "cloud-config.conf" + + +def _gcp_creds_path(component): + return _snap_common_path(component) / "gcp-creds.json" + + +def _daemon_env_path(component): + return _snap_common_path(component) / "environment" + + +def _cloud_endpoint_ca_path(component): + return _snap_common_path(component) / "cloud-endpoint-ca.crt" + + +def encryption_config_path(): + apiserver_snap_common_path = _snap_common_path("kube-apiserver") + encryption_conf_dir = apiserver_snap_common_path / "encryption" + return encryption_conf_dir / "encryption_config.yaml" + + +def write_gcp_snap_config(component): + # gcp requires additional credentials setup + gcp = endpoint_from_flag("endpoint.gcp.ready") + creds_path = _gcp_creds_path(component) + with creds_path.open("w") as fp: + os.fchmod(fp.fileno(), 0o600) + fp.write(gcp.credentials) + + # create a cloud-config file that sets token-url to nil to make the + # services use the creds env var instead of the metadata server, as + # well as making the cluster multizone + comp_cloud_config_path = cloud_config_path(component) + comp_cloud_config_path.write_text( + "[Global]\n" "token-url = nil\n" "multizone = true\n" + ) + + daemon_env_path = _daemon_env_path(component) + if daemon_env_path.exists(): + daemon_env = daemon_env_path.read_text() + if not daemon_env.endswith("\n"): + daemon_env += "\n" + else: + daemon_env = "" + if gcp_creds_env_key not in daemon_env: + daemon_env += "{}={}\n".format(gcp_creds_env_key, creds_path) + daemon_env_path.parent.mkdir(parents=True, exist_ok=True) + daemon_env_path.write_text(daemon_env) + + +def generate_openstack_cloud_config(): + # openstack requires additional credentials setup + openstack = endpoint_from_flag("endpoint.openstack.ready") + + lines = [ + "[Global]", + "auth-url = {}".format(openstack.auth_url), + "region = {}".format(openstack.region), + "username = {}".format(openstack.username), + "password = {}".format(openstack.password), + "tenant-name = {}".format(openstack.project_name), + "domain-name = {}".format(openstack.user_domain_name), + "tenant-domain-name = {}".format(openstack.project_domain_name), + ] + if openstack.endpoint_tls_ca: + lines.append("ca-file = /etc/config/endpoint-ca.cert") + + lines.extend( + [ + "", + "[LoadBalancer]", + ] + ) + + if openstack.has_octavia in (True, None): + # Newer integrator charm will detect whether underlying OpenStack has + # Octavia enabled so we can set this intelligently. If we're still + # related to an older integrator, though, default to assuming Octavia + # is available. + lines.append("use-octavia = true") + else: + lines.append("use-octavia = false") + lines.append("lb-provider = haproxy") + if openstack.subnet_id: + lines.append("subnet-id = {}".format(openstack.subnet_id)) + if openstack.floating_network_id: + lines.append("floating-network-id = {}".format(openstack.floating_network_id)) + if openstack.lb_method: + lines.append("lb-method = {}".format(openstack.lb_method)) + if openstack.manage_security_groups: + lines.append( + "manage-security-groups = {}".format(openstack.manage_security_groups) + ) + if any( + [openstack.bs_version, openstack.trust_device_path, openstack.ignore_volume_az] + ): + lines.append("") + lines.append("[BlockStorage]") + if openstack.bs_version is not None: + lines.append("bs-version = {}".format(openstack.bs_version)) + if openstack.trust_device_path is not None: + lines.append("trust-device-path = {}".format(openstack.trust_device_path)) + if openstack.ignore_volume_az is not None: + lines.append("ignore-volume-az = {}".format(openstack.ignore_volume_az)) + return "\n".join(lines) + "\n" + + +def write_azure_snap_config(component): + azure = endpoint_from_flag("endpoint.azure.ready") + comp_cloud_config_path = cloud_config_path(component) + comp_cloud_config_path.write_text( + json.dumps( + { + "useInstanceMetadata": True, + "useManagedIdentityExtension": azure.managed_identity, + "subscriptionId": azure.subscription_id, + "resourceGroup": azure.resource_group, + "location": azure.resource_group_location, + "vnetName": azure.vnet_name, + "vnetResourceGroup": azure.vnet_resource_group, + "subnetName": azure.subnet_name, + "securityGroupName": azure.security_group_name, + "loadBalancerSku": "standard", + "securityGroupResourceGroup": azure.security_group_resource_group, + "aadClientId": azure.aad_client_id, + "aadClientSecret": azure.aad_client_secret, + "tenantId": azure.tenant_id, + } + ) + ) + + +def configure_kube_proxy( + configure_prefix, api_servers, cluster_cidr, bind_address=None +): + kube_proxy_opts = {} + kube_proxy_opts["cluster-cidr"] = cluster_cidr + kube_proxy_opts["kubeconfig"] = kubeproxyconfig_path + kube_proxy_opts["logtostderr"] = "true" + kube_proxy_opts["v"] = "0" + num_apis = len(api_servers) + kube_proxy_opts["master"] = api_servers[get_unit_number() % num_apis] + kube_proxy_opts["hostname-override"] = get_node_name() + if bind_address: + kube_proxy_opts["bind-address"] = bind_address + elif is_ipv6(cluster_cidr): + kube_proxy_opts["bind-address"] = "::" + + if host.is_container(): + kube_proxy_opts["conntrack-max-per-core"] = "0" + + if is_dual_stack(cluster_cidr): + kube_proxy_opts["feature-gates"] = "IPv6DualStack=true" + + configure_kubernetes_service( + configure_prefix, "kube-proxy", kube_proxy_opts, "proxy-extra-args" + ) + + +def get_unit_number(): + return int(hookenv.local_unit().split("/")[1]) + + +def cluster_cidr(): + """Return the cluster CIDR provided by the CNI""" + cni = endpoint_from_flag("cni.available") + if not cni: + return None + config = hookenv.config() + if "default-cni" in config: + # master + default_cni = config["default-cni"] + else: + # worker + kube_control = endpoint_from_flag("kube-control.dns.available") + if not kube_control: + return None + default_cni = kube_control.get_default_cni() + return cni.get_config(default=default_cni)["cidr"] + + +def is_dual_stack(cidrs): + """Detect IPv4/IPv6 dual stack from CIDRs""" + return {net.version for net in get_networks(cidrs)} == {4, 6} + + +def is_ipv4(cidrs): + """Detect IPv6 from CIDRs""" + return get_ipv4_network(cidrs) is not None + + +def is_ipv6(cidrs): + """Detect IPv6 from CIDRs""" + return get_ipv6_network(cidrs) is not None + + +def is_ipv6_preferred(cidrs): + """Detect if IPv6 is preffered from CIDRs""" + return get_networks(cidrs)[0].version == 6 + + +def get_networks(cidrs): + """Convert a comma-separated list of CIDRs to a list of networks.""" + if not cidrs: + return [] + return [ipaddress.ip_interface(cidr).network for cidr in cidrs.split(",")] + + +def get_ipv4_network(cidrs): + """Get the IPv4 network from the given CIDRs or None""" + return {net.version: net for net in get_networks(cidrs)}.get(4) + + +def get_ipv6_network(cidrs): + """Get the IPv6 network from the given CIDRs or None""" + return {net.version: net for net in get_networks(cidrs)}.get(6) + + +def enable_ipv6_forwarding(): + """Enable net.ipv6.conf.all.forwarding in sysctl if it is not already.""" + check_call(["sysctl", "net.ipv6.conf.all.forwarding=1"]) + + +def get_bind_addrs(ipv4=True, ipv6=True): + """Get all global-scoped addresses that we might bind to.""" + try: + output = check_output(["ip", "-br", "addr", "show", "scope", "global"]) + except CalledProcessError: + # stderr will have any details, and go to the log + hookenv.log("Unable to determine global addresses", hookenv.ERROR) + return [] + + ignore_interfaces = ("lxdbr", "flannel", "cni", "virbr", "docker") + accept_versions = set() + if ipv4: + accept_versions.add(4) + if ipv6: + accept_versions.add(6) + + addrs = [] + for line in output.decode("utf8").splitlines(): + intf, state, *intf_addrs = line.split() + if state != "UP" or any( + intf.startswith(prefix) for prefix in ignore_interfaces + ): + continue + for addr in intf_addrs: + ip_addr = ipaddress.ip_interface(addr).ip + if ip_addr.version in accept_versions: + addrs.append(str(ip_addr)) + return addrs + + +class InvalidVMwareHost(Exception): + pass + + +def _get_vmware_uuid(): + serial_id_file = "/sys/class/dmi/id/product_serial" + # The serial id from VMWare VMs comes in following format: + # VMware-42 28 13 f5 d4 20 71 61-5d b0 7b 96 44 0c cf 54 + try: + with open(serial_id_file, "r") as f: + serial_string = f.read().strip() + if "VMware-" not in serial_string: + hookenv.log( + "Unable to find VMware ID in " + "product_serial: {}".format(serial_string) + ) + raise InvalidVMwareHost + serial_string = ( + serial_string.split("VMware-")[1].replace(" ", "").replace("-", "") + ) + uuid = "%s-%s-%s-%s-%s" % ( + serial_string[0:8], + serial_string[8:12], + serial_string[12:16], + serial_string[16:20], + serial_string[20:32], + ) + except IOError as err: + hookenv.log("Unable to read UUID from sysfs: {}".format(err)) + uuid = "UNKNOWN" + + return uuid + + +def token_generator(length=32): + """Generate a random token for use in account tokens. + + param: length - the length of the token to generate + """ + alpha = string.ascii_letters + string.digits + token = "".join(random.SystemRandom().choice(alpha) for _ in range(length)) + return token + + +def get_secret_names(): + """Return a dict of 'username: secret_id' for Charmed Kubernetes users.""" + try: + output = kubectl( + "get", + "secrets", + "-n", + AUTH_SECRET_NS, + "--field-selector", + "type={}".format(AUTH_SECRET_TYPE), + "-o", + "json", + ).decode("UTF-8") + except (CalledProcessError, FileNotFoundError): + # The api server may not be up, or we may be trying to run kubelet before + # the snap is installed. Send back an empty dict. + hookenv.log("Unable to get existing secrets", level=hookenv.WARNING) + return {} + + secrets = json.loads(output) + secret_names = {} + if "items" in secrets: + for secret in secrets["items"]: + try: + secret_id = secret["metadata"]["name"] + username_b64 = secret["data"]["username"].encode("UTF-8") + except (KeyError, TypeError): + # CK secrets will have populated 'data', but not all secrets do + continue + secret_names[b64decode(username_b64).decode("UTF-8")] = secret_id + return secret_names + + +def generate_rfc1123(length=10): + """Generate a random string compliant with RFC 1123. + + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names + + param: length - the length of the string to generate + """ + length = 253 if length > 253 else length + valid_chars = string.ascii_lowercase + string.digits + rand_str = "".join(random.SystemRandom().choice(valid_chars) for _ in range(length)) + return rand_str + + +def create_secret(token, username, user, groups=None): + secrets = get_secret_names() + if username in secrets: + # Use existing secret ID if one exists for our username + secret_id = secrets[username] + else: + # secret IDs must be unique and rfc1123 compliant + sani_name = re.sub("[^0-9a-z.-]+", "-", user.lower()) + secret_id = "auth-{}-{}".format(sani_name, generate_rfc1123(10)) + + # The authenticator expects tokens to be in the form user::token + token_delim = "::" + if token_delim not in token: + token = "{}::{}".format(user, token) + + context = { + "type": AUTH_SECRET_TYPE, + "secret_name": secret_id, + "secret_namespace": AUTH_SECRET_NS, + "user": b64encode(user.encode("UTF-8")).decode("utf-8"), + "username": b64encode(username.encode("UTF-8")).decode("utf-8"), + "password": b64encode(token.encode("UTF-8")).decode("utf-8"), + "groups": b64encode(groups.encode("UTF-8")).decode("utf-8") if groups else "", + } + with tempfile.NamedTemporaryFile() as tmp_manifest: + render("cdk.auth-webhook-secret.yaml", tmp_manifest.name, context=context) + + if kubectl_manifest("apply", tmp_manifest.name): + hookenv.log("Created secret for {}".format(username)) + return True + else: + hookenv.log("WARN: Unable to create secret for {}".format(username)) + return False + + +def get_secret_password(username): + """Get the password for the given user from the secret that CK created.""" + try: + output = kubectl( + "get", + "secrets", + "-n", + AUTH_SECRET_NS, + "--field-selector", + "type={}".format(AUTH_SECRET_TYPE), + "-o", + "json", + ).decode("UTF-8") + except CalledProcessError: + # NB: apiserver probably isn't up. This can happen on boostrap or upgrade + # while trying to build kubeconfig files. If we need the 'admin' token during + # this time, pull it directly out of the kubeconfig file if possible. + token = None + if username == "admin": + admin_kubeconfig = Path("/root/.kube/config") + if admin_kubeconfig.exists(): + data = yaml.safe_load(admin_kubeconfig.read_text()) + try: + token = data["users"][0]["user"]["token"] + except (KeyError, IndexError, TypeError): + pass + return token + except FileNotFoundError: + # New deployments may ask for a token before the kubectl snap is installed. + # Give them nothing! + return None + + secrets = json.loads(output) + if "items" in secrets: + for secret in secrets["items"]: + try: + data_b64 = secret["data"] + password_b64 = data_b64["password"].encode("UTF-8") + username_b64 = data_b64["username"].encode("UTF-8") + except (KeyError, TypeError): + # CK authn secrets will have populated 'data', but not all secrets do + continue + + password = b64decode(password_b64).decode("UTF-8") + secret_user = b64decode(username_b64).decode("UTF-8") + if username == secret_user: + return password + return None diff --git a/calico/lib/charms/layer/options.py b/calico/lib/charms/layer/options.py new file mode 100644 index 0000000..d3f273f --- /dev/null +++ b/calico/lib/charms/layer/options.py @@ -0,0 +1,26 @@ +import os +from pathlib import Path + +import yaml + + +_CHARM_PATH = Path(os.environ.get('JUJU_CHARM_DIR', '.')) +_DEFAULT_FILE = _CHARM_PATH / 'layer.yaml' +_CACHE = {} + + +def get(section=None, option=None, layer_file=_DEFAULT_FILE): + if option and not section: + raise ValueError('Cannot specify option without section') + + layer_file = (_CHARM_PATH / layer_file).resolve() + if layer_file not in _CACHE: + with layer_file.open() as fp: + _CACHE[layer_file] = yaml.safe_load(fp.read()) + + data = _CACHE[layer_file].get('options', {}) + if section: + data = data.get(section, {}) + if option: + data = data.get(option) + return data diff --git a/calico/lib/charms/layer/status.py b/calico/lib/charms/layer/status.py new file mode 100644 index 0000000..95b2997 --- /dev/null +++ b/calico/lib/charms/layer/status.py @@ -0,0 +1,189 @@ +import inspect +import errno +import subprocess +import yaml +from enum import Enum +from functools import wraps +from pathlib import Path + +from charmhelpers.core import hookenv +from charms import layer + + +_orig_call = subprocess.call +_statuses = {'_initialized': False, + '_finalized': False} + + +class WorkloadState(Enum): + """ + Enum of the valid workload states. + + Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + """ + # note: order here determines precedence of state + MAINTENANCE = 'maintenance' + BLOCKED = 'blocked' + WAITING = 'waiting' + ACTIVE = 'active' + + +def maintenance(message): + """ + Set the status to the `MAINTENANCE` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.MAINTENANCE, message) + + +def maint(message): + """ + Shorthand alias for + [maintenance](status.md#charms.layer.status.maintenance). + + # Parameters + `message` (str): Message to convey to the operator. + """ + maintenance(message) + + +def blocked(message): + """ + Set the status to the `BLOCKED` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.BLOCKED, message) + + +def waiting(message): + """ + Set the status to the `WAITING` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.WAITING, message) + + +def active(message): + """ + Set the status to the `ACTIVE` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.ACTIVE, message) + + +def status_set(workload_state, message): + """ + Set the status to the given workload state with a message. + + # Parameters + `workload_state` (WorkloadState or str): State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. + `message` (str): Message to convey to the operator. + """ + if not isinstance(workload_state, WorkloadState): + workload_state = WorkloadState(workload_state) + if workload_state is WorkloadState.MAINTENANCE: + _status_set_immediate(workload_state, message) + return + layer = _find_calling_layer() + _statuses.setdefault(workload_state, []).append((layer, message)) + if not _statuses['_initialized'] or _statuses['_finalized']: + # We either aren't initialized, so the finalizer may never be run, + # or the finalizer has already run, so it won't run again. In either + # case, we need to manually invoke it to ensure the status gets set. + _finalize() + + +def _find_calling_layer(): + for frame in inspect.stack(): + # switch to .filename when trusty (Python 3.4) is EOL + fn = Path(frame[1]) + if fn.parent.stem not in ('reactive', 'layer', 'charms'): + continue + layer_name = fn.stem + if layer_name == 'status': + continue # skip our own frames + return layer_name + return None + + +def _initialize(): + if not _statuses['_initialized']: + if layer.options.get('status', 'patch-hookenv'): + _patch_hookenv() + hookenv.atexit(_finalize) + _statuses['_initialized'] = True + + +def _finalize(): + if _statuses['_initialized']: + # If we haven't been initialized, we can't truly be finalized. + # This makes things more efficient if an action sets a status + # but subsequently starts the reactive bus. + _statuses['_finalized'] = True + charm_name = hookenv.charm_name() + charm_dir = Path(hookenv.charm_dir()) + with charm_dir.joinpath('layer.yaml').open() as fp: + includes = yaml.safe_load(fp.read()).get('includes', []) + layer_order = includes + [charm_name] + + for workload_state in WorkloadState: + if workload_state not in _statuses: + continue + if not _statuses[workload_state]: + continue + + def _get_key(record): + layer_name, message = record + if layer_name in layer_order: + return layer_order.index(layer_name) + else: + return 0 + + sorted_statuses = sorted(_statuses[workload_state], key=_get_key) + layer_name, message = sorted_statuses[-1] + _status_set_immediate(workload_state, message) + break + + +def _status_set_immediate(workload_state, message): + workload_state = workload_state.value + try: + hookenv.log('status-set: {}: {}'.format(workload_state, message), + hookenv.INFO) + ret = _orig_call(['status-set', workload_state, message]) + if ret == 0: + return + except OSError as e: + # ignore status-set not available on older controllers + if e.errno != errno.ENOENT: + raise + + +def _patch_hookenv(): + # we can't patch hookenv.status_set directly because other layers may have + # already imported it into their namespace, so we have to patch sp.call + subprocess.call = _patched_call + + +@wraps(_orig_call) +def _patched_call(cmd, *args, **kwargs): + if not isinstance(cmd, list) or cmd[0] != 'status-set': + return _orig_call(cmd, *args, **kwargs) + _, workload_state, message = cmd + status_set(workload_state, message) + return 0 # make hookenv.status_set not emit spurious failure logs diff --git a/calico/lib/charms/leadership.py b/calico/lib/charms/leadership.py new file mode 100644 index 0000000..d2a95fa --- /dev/null +++ b/calico/lib/charms/leadership.py @@ -0,0 +1,68 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Leadership Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.reactive import not_unless + + +__all__ = ['leader_get', 'leader_set'] + + +@not_unless('leadership.is_leader') +def leader_set(*args, **kw): + '''Change leadership settings, per charmhelpers.core.hookenv.leader_set. + + Settings may either be passed in as a single dictionary, or using + keyword arguments. All values must be strings. + + The leadership.set.{key} reactive state will be set while the + leadership hook environment setting remains set. + + Changed leadership settings will set the leadership.changed.{key} + and leadership.changed states. These states will remain set until + the following hook. + + These state changes take effect immediately on the leader, and + in future hooks run on non-leaders. In this way both leaders and + non-leaders can share handlers, waiting on these states. + ''' + if args: + if len(args) > 1: + raise TypeError('leader_set() takes 1 positional argument but ' + '{} were given'.format(len(args))) + else: + settings = dict(args[0]) + else: + settings = {} + settings.update(kw) + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + + for key, value in settings.items(): + if value != previous.get(key): + reactive.set_state('leadership.changed.{}'.format(key)) + reactive.set_state('leadership.changed') + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + hookenv.leader_set(settings) + unitdata.kv().update(settings, prefix='leadership.settings.') + + +def leader_get(attribute=None): + '''Return leadership settings, per charmhelpers.core.hookenv.leader_get.''' + return hookenv.leader_get(attribute) diff --git a/calico/make_docs b/calico/make_docs new file mode 100644 index 0000000..dcd4c1f --- /dev/null +++ b/calico/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import os +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + sys.path.insert(0, 'lib') + sys.path.insert(1, 'reactive') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') + if os.path.exists('.unit-state.db'): + os.remove('.unit-state.db') diff --git a/calico/metadata.yaml b/calico/metadata.yaml new file mode 100644 index 0000000..1dd2061 --- /dev/null +++ b/calico/metadata.yaml @@ -0,0 +1,46 @@ +"name": "calico" +"summary": "A robust Software Defined Network from Project Calico" +"maintainers": +- "Tim Van Steenburgh " +- "George Kraft " +- "Konstantinos Tsakalozos " +- "Mike Wilson " +- "Kevin Monroe " +- "Joe Borg " +"description": | + Deploys Calico as a background service and configures CNI for use with + calico on any principal charm that implements the kubernetes-cni interface. +"tags": +- "networking" +"series": +- "focal" +- "bionic" +- "xenial" +"requires": + "etcd": + "interface": "etcd" + "cni": + "interface": "kubernetes-cni" + "scope": "container" +"resources": + "calico": + "type": "file" + "filename": "calico.tar.gz" + "description": "Calico resource tarball for amd64" + "calico-arm64": + "type": "file" + "filename": "calico.tar.gz" + "description": "Calico resource tarball for arm64" + "calico-upgrade": + "type": "file" + "filename": "calico-upgrade.tar.gz" + "description": "calico-upgrade tool for amd64" + "calico-upgrade-arm64": + "type": "file" + "filename": "calico-upgrade.tar.gz" + "description": "calico-upgrade tool for arm64" + "calico-node-image": + "type": "file" + "filename": "calico-node-image.tar.gz" + "description": "calico-node container image" +"subordinate": !!bool "true" diff --git a/calico/pydocmd.yml b/calico/pydocmd.yml new file mode 100644 index 0000000..ab3b2ef --- /dev/null +++ b/calico/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'Status Management Layer' + +generate: + - status.md: + - charms.layer.status.WorkloadState + - charms.layer.status.maintenance + - charms.layer.status.maint + - charms.layer.status.blocked + - charms.layer.status.waiting + - charms.layer.status.active + - charms.layer.status.status_set + +pages: + - Status Management Layer: status.md + +gens_dir: docs diff --git a/calico/reactive/__init__.py b/calico/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/calico/reactive/calico.py b/calico/reactive/calico.py new file mode 100644 index 0000000..1410181 --- /dev/null +++ b/calico/reactive/calico.py @@ -0,0 +1,831 @@ +import os +import yaml +import gzip +import traceback +import ipaddress +import calico_upgrade + +from conctl import getContainerRuntimeCtl +from socket import gethostname +from subprocess import check_call, check_output, CalledProcessError, STDOUT + +from charms.leadership import leader_get, leader_set +from charms.reactive import when, when_not, when_any, set_state, remove_state +from charms.reactive import hook, is_state +from charms.reactive import endpoint_from_flag, endpoint_from_name +from charms.reactive import data_changed, any_file_changed +from charms.reactive import register_trigger +from charmhelpers.core.hookenv import ( + log, + resource_get, + network_get, + unit_private_ip, + is_leader, + local_unit, + config as charm_config, + atexit, + env_proxy_settings +) +from charmhelpers.core.host import ( + arch, + service, + service_restart, + service_running +) +from charmhelpers.core.templating import render +from charms.layer import kubernetes_common, status +from charms.layer.kubernetes_common import kubectl + +# TODO: +# - Handle the 'stop' hook by stopping and uninstalling all the things. + +os.environ['PATH'] += os.pathsep + os.path.join(os.sep, 'snap', 'bin') + +try: + CTL = getContainerRuntimeCtl() + set_state('calico.ctl.ready') +except RuntimeError: + log(traceback.format_exc()) + remove_state('calico.ctl.ready') + +CALICOCTL_PATH = '/opt/calicoctl' +ETCD_KEY_PATH = os.path.join(CALICOCTL_PATH, 'etcd-key') +ETCD_CERT_PATH = os.path.join(CALICOCTL_PATH, 'etcd-cert') +ETCD_CA_PATH = os.path.join(CALICOCTL_PATH, 'etcd-ca') +CALICO_UPGRADE_DIR = '/opt/calico-upgrade' + +register_trigger( + when="cni.kubeconfig.changed", clear_flag="calico.service.installed" +) + + +@hook('upgrade-charm') +def upgrade_charm(): + remove_state('calico.binaries.installed') + remove_state('calico.cni.configured') + remove_state('calico.service.installed') + remove_state('calico.pool.configured') + remove_state('calico.npc.deployed') + remove_state('calico.image.pulled') + remove_state('calico.bgp.globals.configured') + remove_state('calico.node.configured') + remove_state('calico.bgp.peers.configured') + try: + log('Deleting /etc/cni/net.d/10-calico.conf') + os.remove('/etc/cni/net.d/10-calico.conf') + except FileNotFoundError as e: + log(e) + if is_leader() and not leader_get('calico-v3-data-ready'): + leader_set({ + 'calico-v3-data-migration-needed': True, + 'calico-v3-npc-cleanup-needed': True, + 'calico-v3-completion-needed': True + }) + cni = endpoint_from_name('cni') + cni.manage_flags() + + +@when('leadership.is_leader', 'leadership.set.calico-v3-data-migration-needed', + 'etcd.available', 'calico.etcd-credentials.installed') +def upgrade_v3_migrate_data(): + status.maintenance('Migrating data to Calico 3') + try: + calico_upgrade.configure() + calico_upgrade.dry_run() + calico_upgrade.start() + except Exception: + log(traceback.format_exc()) + message = 'Calico upgrade failed, see debug log' + status.blocked(message) + return + leader_set({'calico-v3-data-migration-needed': None}) + + +@when('leadership.is_leader') +@when_not('leadership.set.calico-v3-data-migration-needed') +def v3_data_ready(): + leader_set({'calico-v3-data-ready': True}) + + +@when('leadership.is_leader', 'leadership.set.calico-v3-data-ready', + 'leadership.set.calico-v3-npc-cleanup-needed') +def upgrade_v3_npc_cleanup(): + status.maintenance('Cleaning up Calico 2 policy controller') + + resources = [ + ('Deployment', 'kube-system', 'calico-policy-controller'), + ('ClusterRoleBinding', None, 'calico-policy-controller'), + ('ClusterRole', None, 'calico-policy-controller'), + ('ServiceAccount', 'kube-system', 'calico-policy-controller') + ] + + for kind, namespace, name in resources: + args = ['delete', '--ignore-not-found', kind, name] + if namespace: + args += ['-n', namespace] + try: + kubectl(*args) + except CalledProcessError: + log('Failed to cleanup %s %s %s' % (kind, namespace, name)) + return + + leader_set({'calico-v3-npc-cleanup-needed': None}) + + +@when('leadership.is_leader', 'leadership.set.calico-v3-completion-needed', + 'leadership.set.calico-v3-data-ready', 'calico.binaries.installed', + 'calico.service.installed', 'calico.npc.deployed') +@when_not('leadership.set.calico-v3-npc-cleanup-needed') +def upgrade_v3_complete(): + status.maintenance('Completing Calico 3 upgrade') + try: + calico_upgrade.configure() + calico_upgrade.complete() + calico_upgrade.cleanup() + except Exception: + log(traceback.format_exc()) + message = 'Calico upgrade failed, see debug log' + status.blocked(message) + return + leader_set({'calico-v3-completion-needed': None}) + + +@when('leadership.set.calico-v3-data-ready') +@when_not('calico.binaries.installed') +def install_calico_binaries(): + ''' Unpack the Calico binaries. ''' + # on intel, the resource is called 'calico'; other arches have a suffix + architecture = arch() + if architecture == "amd64": + resource_name = 'calico' + else: + resource_name = 'calico-{}'.format(architecture) + + try: + archive = resource_get(resource_name) + except Exception: + message = 'Error fetching the calico resource.' + log(message) + status.blocked(message) + return + + if not archive: + message = 'Missing calico resource.' + log(message) + status.blocked(message) + return + + filesize = os.stat(archive).st_size + if filesize < 1000000: + message = 'Incomplete calico resource' + log(message) + status.blocked(message) + return + + status.maintenance('Unpacking calico resource.') + + charm_dir = os.getenv('CHARM_DIR') + unpack_path = os.path.join(charm_dir, 'files', 'calico') + os.makedirs(unpack_path, exist_ok=True) + cmd = ['tar', 'xfz', archive, '-C', unpack_path] + log(cmd) + check_call(cmd) + + apps = [ + {'name': 'calicoctl', 'path': CALICOCTL_PATH}, + {'name': 'calico', 'path': '/opt/cni/bin'}, + {'name': 'calico-ipam', 'path': '/opt/cni/bin'}, + ] + + for app in apps: + unpacked = os.path.join(unpack_path, app['name']) + app_path = os.path.join(app['path'], app['name']) + install = ['install', '-v', '-D', unpacked, app_path] + check_call(install) + + calicoctl_path = '/usr/local/bin/calicoctl' + render('calicoctl', calicoctl_path, {}) + os.chmod(calicoctl_path, 0o775) + + set_state('calico.binaries.installed') + + +@when('calico.binaries.installed', 'etcd.available') +def update_calicoctl_env(): + env = get_calicoctl_env() + lines = ['export %s=%s' % item for item in sorted(env.items())] + output = '\n'.join(lines) + with open('/opt/calicoctl/calicoctl.env', 'w') as f: + f.write(output) + + +@when('calico.binaries.installed') +@when_not('etcd.connected') +def blocked_without_etcd(): + status.blocked('Waiting for relation to etcd') + + +@when('etcd.tls.available') +@when_not('calico.etcd-credentials.installed') +def install_etcd_credentials(): + etcd = endpoint_from_flag('etcd.available') + etcd.save_client_credentials(ETCD_KEY_PATH, ETCD_CERT_PATH, ETCD_CA_PATH) + # register initial etcd data so that we can detect changes + data_changed('calico.etcd.data', (etcd.get_connection_string(), + etcd.get_client_credentials())) + set_state('calico.etcd-credentials.installed') + + +@when('etcd.tls.available', 'calico.service.installed') +def check_etcd_changes(): + etcd = endpoint_from_flag('etcd.available') + if data_changed('calico.etcd.data', (etcd.get_connection_string(), + etcd.get_client_credentials())): + etcd.save_client_credentials(ETCD_KEY_PATH, + ETCD_CERT_PATH, + ETCD_CA_PATH) + remove_state('calico.service.installed') + remove_state('calico.npc.deployed') + remove_state('calico.cni.configured') + + +def get_mtu(): + ''' Get user-specified MTU size, adjusted to make room for encapsulation + headers. https://docs.projectcalico.org/networking/mtu + ''' + mtu = charm_config('veth-mtu') + if not mtu: + return None + + if charm_config('vxlan') != 'Never': + return mtu - 50 + elif charm_config('ipip') != 'Never': + return mtu - 20 + return mtu + + +def get_bind_address(): + ''' Returns a non-fan bind address for the cni endpoint ''' + try: + data = network_get('cni') + except NotImplementedError: + # Juju < 2.1 + return unit_private_ip() + + if 'bind-addresses' not in data: + # Juju < 2.3 + return unit_private_ip() + + for bind_address in data['bind-addresses']: + if bind_address['interfacename'].startswith('fan-'): + continue + return bind_address['addresses'][0]['address'] + + # If we made it here, we didn't find a non-fan CNI bind-address, which is + # unexpected. Let's log a message and play it safe. + log('Could not find a non-fan bind-address. Using private-address.') + return unit_private_ip() + + +@when('leadership.is_leader', 'leadership.set.calico-v3-data-ready') +@when_not('leadership.set.calico-node-token') +def create_calico_node_token(): + ''' Create the system:calico-node user token ''' + status.maintenance('Creating system:calico-node user token') + token = kubernetes_common.token_generator() + user = 'system:calico-node' + success = kubernetes_common.create_secret( + token=token, + username=user, + user=user + ) + if not success: + log('Failed to create system:calico-node user token, will retry') + status.waiting('Waiting to retry creating calico-node token') + return + # create_secret may have added the :: prefix. Get the new token. + token = kubernetes_common.get_secret_password(user) + if not token: + log('Failed to get system:calico-node user token, will retry') + status.waiting('Waiting to retry creating calico-node token') + return + leader_set({'calico-node-token': token}) + + +@when('calico.binaries.installed', 'etcd.available', + 'calico.etcd-credentials.installed', 'cni.kubeconfig.available', + 'leadership.set.calico-node-token', 'leadership.set.calico-v3-data-ready') +@when_not('calico.service.installed') +def install_calico_service(): + ''' Install the calico-node systemd service. ''' + status.maintenance('Installing calico-node service.') + + with open(kubernetes_common.kubeclientconfig_path) as f: + kubeconfig = yaml.safe_load(f) + any_file_changed([kubernetes_common.kubeclientconfig_path]) + kubeconfig['users'] = [{ + 'name': 'calico-node', + 'user': { + 'token': leader_get('calico-node-token') + } + }] + kubeconfig['contexts'][0]['context']['user'] = 'calico-node' + with open('/opt/calicoctl/kubeconfig', 'w') as f: + yaml.dump(kubeconfig, f) + + etcd = endpoint_from_flag('etcd.available') + service_path = os.path.join(os.sep, 'lib', 'systemd', 'system', + 'calico-node.service') + ip_versions = {net.version for net in get_networks(charm_config('cidr'))} + ip4 = get_bind_address() if 4 in ip_versions else "none" + ip6 = "autodetect" if 6 in ip_versions else "none" + render('calico-node.service', service_path, { + 'connection_string': etcd.get_connection_string(), + 'etcd_key_path': ETCD_KEY_PATH, + 'etcd_ca_path': ETCD_CA_PATH, + 'etcd_cert_path': ETCD_CERT_PATH, + 'nodename': gethostname(), + # specify IP so calico doesn't grab a silly one from, say, lxdbr0 + 'ip': ip4, + 'ip6': ip6, + 'mtu': get_mtu(), + 'calico_node_image': charm_config('calico-node-image'), + 'ignore_loose_rpf': charm_config('ignore-loose-rpf'), + 'lc_all': os.environ.get('LC_ALL', 'C.UTF-8'), + 'lang': os.environ.get('LANG', 'C.UTF-8') + }) + check_call(['systemctl', 'daemon-reload']) + service_restart('calico-node') + service('enable', 'calico-node') + remove_state('cni.kubeconfig.changed') + set_state('calico.service.installed') + + +@when('config.changed.veth-mtu') +def configure_mtu(): + remove_state('calico.service.installed') + remove_state('calico.cni.configured') + + +@when('config.changed.ignore-loose-rpf') +def ignore_loose_rpf_changed(): + remove_state('calico.service.installed') + + +@when('calico.binaries.installed', 'etcd.available', + 'calico.etcd-credentials.installed', + 'leadership.set.calico-v3-data-ready') +@when_not('calico.pool.configured') +def configure_calico_pool(): + ''' Configure Calico IP pool. ''' + config = charm_config() + if not config['manage-pools']: + log('Skipping pool configuration') + set_state('calico.pool.configured') + return + + status.maintenance('Configuring Calico IP pool') + + try: + # remove unrecognized pools, and default pool if CIDR doesn't match + pools = calicoctl_get('pool')['items'] + + cidrs = tuple(cidr.strip() for cidr in config['cidr'].split(',')) + names = tuple('ipv{}'.format(get_network(cidr).version) + for cidr in cidrs) + pool_names_to_delete = [ + pool['metadata']['name'] for pool in pools + if pool['metadata']['name'] not in names + or pool['spec']['cidr'] not in cidrs + ] + + for pool_name in pool_names_to_delete: + log('Deleting pool: %s' % pool_name) + calicoctl('delete', 'pool', pool_name, '--skip-not-exists') + + for cidr, name in zip(cidrs, names): + # configure the default pool + pool = { + 'apiVersion': 'projectcalico.org/v3', + 'kind': 'IPPool', + 'metadata': { + 'name': name, + }, + 'spec': { + 'cidr': cidr, + 'ipipMode': config['ipip'], + 'vxlanMode': config['vxlan'], + 'natOutgoing': config['nat-outgoing'], + } + } + + calicoctl_apply(pool) + except CalledProcessError: + log(traceback.format_exc()) + if config['ipip'] != 'Never' and config['vxlan'] != 'Never': + status.blocked('ipip and vxlan configs are in conflict') + else: + status.waiting('Waiting to retry calico pool configuration') + return + + set_state('calico.pool.configured') + + +@when_any('config.changed.ipip', 'config.changed.nat-outgoing', + 'config.changed.cidr', 'config.changed.manage-pools', + 'config.changed.vxlan') +def reconfigure_calico_pool(): + ''' Reconfigure the Calico IP pool ''' + remove_state('calico.pool.configured') + + +@when('etcd.available', 'cni.is-worker', 'leadership.set.calico-v3-data-ready') +@when_not('calico.cni.configured') +def configure_cni(): + ''' Configure Calico CNI. ''' + status.maintenance('Configuring Calico CNI') + cni = endpoint_from_flag('cni.is-worker') + etcd = endpoint_from_flag('etcd.available') + os.makedirs('/etc/cni/net.d', exist_ok=True) + ip_versions = {net.version for net in get_networks(charm_config('cidr'))} + context = { + 'connection_string': etcd.get_connection_string(), + 'etcd_key_path': ETCD_KEY_PATH, + 'etcd_cert_path': ETCD_CERT_PATH, + 'etcd_ca_path': ETCD_CA_PATH, + 'kubeconfig_path': '/opt/calicoctl/kubeconfig', + 'mtu': get_mtu(), + 'assign_ipv4': 'true' if 4 in ip_versions else 'false', + 'assign_ipv6': 'true' if 6 in ip_versions else 'false', + } + render('10-calico.conflist', '/etc/cni/net.d/10-calico.conflist', context) + config = charm_config() + cni.set_config(cidr=config['cidr'], cni_conf_file='10-calico.conflist') + set_state('calico.cni.configured') + + +@when('etcd.available', 'cni.is-master') +@when_not('calico.cni.configured') +def configure_master_cni(): + status.maintenance('Configuring Calico CNI') + cni = endpoint_from_flag('cni.is-master') + config = charm_config() + cni.set_config(cidr=config['cidr'], cni_conf_file='10-calico.conflist') + set_state('calico.cni.configured') + + +@when_any('config.changed.cidr') +def reconfigure_cni(): + remove_state('calico.cni.configured') + + +@when('etcd.available', 'calico.cni.configured', + 'calico.service.installed', 'leadership.is_leader', + 'leadership.set.calico-v3-data-ready') +@when_not('calico.npc.deployed') +def deploy_network_policy_controller(): + ''' Deploy the Calico network policy controller. ''' + status.maintenance('Deploying network policy controller.') + etcd = endpoint_from_flag('etcd.available') + context = { + 'connection_string': etcd.get_connection_string(), + 'etcd_key_path': ETCD_KEY_PATH, + 'etcd_cert_path': ETCD_CERT_PATH, + 'etcd_ca_path': ETCD_CA_PATH, + 'calico_policy_image': charm_config('calico-policy-image'), + 'etcd_cert_last_modified': os.path.getmtime(ETCD_CERT_PATH) + } + render('policy-controller.yaml', '/tmp/policy-controller.yaml', context) + try: + kubectl('apply', '-f', '/tmp/policy-controller.yaml') + set_state('calico.npc.deployed') + except CalledProcessError as e: + status.waiting('Waiting for kubernetes') + log(str(e)) + + +@when('calico.binaries.installed', 'etcd.available', + 'leadership.set.calico-v3-data-ready') +@when_not('calico.bgp.globals.configured') +def configure_bgp_globals(): + status.maintenance('Configuring BGP globals') + config = charm_config() + + try: + try: + bgp_config = calicoctl_get('bgpconfig', 'default') + except CalledProcessError as e: + if b'resource does not exist' in e.output: + log('default BGPConfiguration does not exist') + bgp_config = { + 'apiVersion': 'projectcalico.org/v3', + 'kind': 'BGPConfiguration', + 'metadata': { + 'name': 'default' + }, + 'spec': {} + } + else: + raise + + spec = bgp_config['spec'] + spec['asNumber'] = config['global-as-number'] + spec['nodeToNodeMeshEnabled'] = config['node-to-node-mesh'] + spec['serviceClusterIPs'] = [ + {'cidr': cidr} + for cidr in config['bgp-service-cluster-ips'].split() + ] + spec['serviceExternalIPs'] = [ + {'cidr': cidr} + for cidr in config['bgp-service-external-ips'].split() + ] + calicoctl_apply(bgp_config) + except CalledProcessError: + log(traceback.format_exc()) + status.waiting('Waiting to retry BGP global configuration') + return + + set_state('calico.bgp.globals.configured') + + +@when_any('config.changed.global-as-number', + 'config.changed.node-to-node-mesh', + 'config.changed.bgp-service-cluster-ips', + 'config.changed.bgp-service-external-ips') +def reconfigure_bgp_globals(): + remove_state('calico.bgp.globals.configured') + + +@when('calico.binaries.installed', 'etcd.available', + 'leadership.set.calico-v3-data-ready') +@when_not('calico.node.configured') +def configure_node(): + status.maintenance('Configuring Calico node') + + node_name = gethostname() + as_number = get_unit_as_number() + route_reflector_cluster_id = get_route_reflector_cluster_id() + + try: + node = calicoctl_get('node', node_name) + node['spec']['bgp']['asNumber'] = as_number + node['spec']['bgp']['routeReflectorClusterID'] = \ + route_reflector_cluster_id + calicoctl_apply(node) + except CalledProcessError: + log(traceback.format_exc()) + status.waiting('Waiting to retry Calico node configuration') + return + + set_state('calico.node.configured') + + +@when_any('config.changed.subnet-as-numbers', 'config.changed.unit-as-numbers', + 'config.changed.route-reflector-cluster-ids') +def reconfigure_node(): + remove_state('calico.node.configured') + + +@when('calico.binaries.installed', 'etcd.available', + 'leadership.set.calico-v3-data-ready') +@when_not('calico.bgp.peers.configured') +def configure_bgp_peers(): + status.maintenance('Configuring BGP peers') + + peers = [] + + # Global BGP peers + config = charm_config() + peers += yaml.safe_load(config['global-bgp-peers']) + + # Subnet-scoped BGP peers + subnet_bgp_peers = yaml.safe_load(config['subnet-bgp-peers']) + subnets = filter_local_subnets(subnet_bgp_peers) + for subnet in subnets: + peers += subnet_bgp_peers[str(subnet)] + + # Unit-scoped BGP peers + unit_id = get_unit_id() + unit_bgp_peers = yaml.safe_load(config['unit-bgp-peers']) + if unit_id in unit_bgp_peers: + peers += unit_bgp_peers[unit_id] + + # Give names to peers + safe_unit_name = local_unit().replace('/', '-') + named_peers = { + # name must consist of lower case alphanumeric characters, '-' or '.' + '%s-%s-%s' % (safe_unit_name, peer['address'].replace(':', '-'), + peer['as-number']): peer + for peer in peers + } + + try: + node_name = gethostname() + for peer_name, peer in named_peers.items(): + peer_def = { + 'apiVersion': 'projectcalico.org/v3', + 'kind': 'BGPPeer', + 'metadata': { + 'name': peer_name, + }, + 'spec': { + 'node': node_name, + 'peerIP': peer['address'], + 'asNumber': peer['as-number'] + } + } + calicoctl_apply(peer_def) + + # Delete unrecognized peers + existing_peers = calicoctl_get('bgppeers')['items'] + existing_peers = [peer['metadata']['name'] for peer in existing_peers] + peers_to_delete = [ + peer for peer in existing_peers + if peer.startswith(safe_unit_name + '-') + and peer not in named_peers + ] + + for peer in peers_to_delete: + calicoctl('delete', 'bgppeer', peer) + except CalledProcessError: + log(traceback.format_exc()) + status.waiting('Waiting to retry BGP peer configuration') + return + + set_state('calico.bgp.peers.configured') + + +@when_any('config.changed.global-bgp-peers', 'config.changed.subnet-bgp-peers', + 'config.changed.unit-bgp-peers') +def reconfigure_bgp_peers(): + remove_state('calico.bgp.peers.configured') + + +@atexit +def ready(): + preconditions = [ + 'calico.service.installed', 'calico.pool.configured', + 'calico.cni.configured', 'calico.bgp.globals.configured', + 'calico.node.configured', 'calico.bgp.peers.configured' + ] + if is_state('upgrade.series.in-progress'): + status.blocked('Series upgrade in progress') + return + for precondition in preconditions: + if not is_state(precondition): + return + if is_leader() and not is_state('calico.npc.deployed'): + status.waiting('Waiting to retry deploying policy controller') + return + if not service_running('calico-node'): + status.waiting('Waiting for service: calico-node') + return + status.active('Calico is active') + + +def calicoctl(*args): + cmd = ['/opt/calicoctl/calicoctl'] + list(args) + env = os.environ.copy() + env.update(get_calicoctl_env()) + try: + return check_output(cmd, env=env, stderr=STDOUT) + except CalledProcessError as e: + log(e.output) + raise + + +def set_http_proxy(): + """ + Check if we have any values for + juju_http*_proxy and apply them. + """ + juju_environment = env_proxy_settings() + if juju_environment and not juju_environment.get('disable-juju-proxy'): + upper = ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY'] + lower = list(map(str.lower, upper)) + keys = upper + lower + for key in keys: + from_juju = juju_environment.get(key, None) + if from_juju: + os.environ[key] = from_juju + + +@when_not('calico.image.pulled') +@when('calico.ctl.ready') +def pull_calico_node_image(): + image = resource_get('calico-node-image') + + if not image or os.path.getsize(image) == 0: + status.maintenance('Pulling calico-node image') + image = charm_config('calico-node-image') + set_http_proxy() + CTL.pull(image) + else: + status.maintenance('Loading calico-node image') + unzipped = '/tmp/calico-node-image.tar' + with gzip.open(image, 'rb') as f_in: + with open(unzipped, 'wb') as f_out: + f_out.write(f_in.read()) + CTL.load(unzipped) + + set_state('calico.image.pulled') + + +@when_any('config.changed.calico-node-image') +def repull_calico_node_image(): + remove_state('calico.image.pulled') + remove_state('calico.service.installed') + + +@when('calico.service.installed', 'calico.pool.configured') +def disable_vxlan_tx_checksumming(): + '''Workaround for https://github.com/projectcalico/calico/issues/3145''' + config = charm_config() + + if config['disable-vxlan-tx-checksumming'] and config['vxlan'] != 'Never': + cmd = ['ethtool', '-K', 'vxlan.calico', 'tx-checksum-ip-generic', + 'off'] + try: + check_call(cmd) + except CalledProcessError: + msg = 'Waiting to retry disabling VXLAN TX checksumming' + log(msg) + status.waiting(msg) + + +def calicoctl_get(*args): + args = ['get', '-o', 'yaml', '--export'] + list(args) + output = calicoctl(*args) + result = yaml.safe_load(output) + return result + + +def calicoctl_apply(data): + path = '/tmp/calicoctl-apply.yaml' + with open(path, 'w') as f: + yaml.dump(data, f) + calicoctl('apply', '-f', path) + + +def get_calicoctl_env(): + etcd = endpoint_from_flag('etcd.available') + env = {} + env['ETCD_ENDPOINTS'] = etcd.get_connection_string() + env['ETCD_KEY_FILE'] = ETCD_KEY_PATH + env['ETCD_CERT_FILE'] = ETCD_CERT_PATH + env['ETCD_CA_CERT_FILE'] = ETCD_CA_PATH + return env + + +def get_unit_as_number(): + config = charm_config() + + # Check for matching unit rule + unit_id = get_unit_id() + unit_as_numbers = yaml.safe_load(config['unit-as-numbers']) + if unit_id in unit_as_numbers: + as_number = unit_as_numbers[unit_id] + return as_number + + # Check for matching subnet rule + subnet_as_numbers = yaml.safe_load(config['subnet-as-numbers']) + subnets = filter_local_subnets(subnet_as_numbers) + if subnets: + subnets.sort(key=lambda subnet: -subnet.prefixlen) + subnet = subnets[0] + as_number = subnet_as_numbers[str(subnet)] + return as_number + + # No AS number specified for this unit. + return None + + +def filter_local_subnets(subnets): + ip_address = get_bind_address() + ip_address = ipaddress.ip_address(ip_address) # IP address + subnets = [ipaddress.ip_network(subnet) for subnet in subnets] + subnets = [subnet for subnet in subnets if ip_address in subnet] + return subnets + + +def get_unit_id(): + return int(local_unit().split('/')[1]) + + +def get_route_reflector_cluster_id(): + config = charm_config() + route_reflector_cluster_ids = yaml.safe_load( + config['route-reflector-cluster-ids'] + ) + unit_id = get_unit_id() + return route_reflector_cluster_ids.get(unit_id) + + +def get_network(cidr): + '''Convert a CIDR to a network instance.''' + return ipaddress.ip_interface(cidr.strip()).network + + +def get_networks(cidrs): + '''Convert a comma-separated list of CIDRs to a list of networks.''' + return [get_network(cidr) for cidr in cidrs.split(',')] diff --git a/calico/reactive/leadership.py b/calico/reactive/leadership.py new file mode 100644 index 0000000..29c6f3a --- /dev/null +++ b/calico/reactive/leadership.py @@ -0,0 +1,68 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Leadership Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.leadership import leader_get, leader_set + + +__all__ = ['leader_get', 'leader_set'] # Backwards compatibility + + +def initialize_leadership_state(): + '''Initialize leadership.* states from the hook environment. + + Invoked by hookenv.atstart() so states are available in + @hook decorated handlers. + ''' + is_leader = hookenv.is_leader() + if is_leader: + hookenv.log('Initializing Leadership Layer (is leader)') + else: + hookenv.log('Initializing Leadership Layer (is follower)') + + reactive.helpers.toggle_state('leadership.is_leader', is_leader) + + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + current = hookenv.leader_get() + + # Handle deletions. + for key in set(previous.keys()) - set(current.keys()): + current[key] = None + + any_changed = False + for key, value in current.items(): + reactive.helpers.toggle_state('leadership.changed.{}'.format(key), + value != previous.get(key)) + if value != previous.get(key): + any_changed = True + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + reactive.helpers.toggle_state('leadership.changed', any_changed) + + unitdata.kv().update(current, prefix='leadership.settings.') + + +# 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, '_leadership_registered'): + hookenv.atstart(initialize_leadership_state) + reactive._leadership_registered = True diff --git a/calico/reactive/status.py b/calico/reactive/status.py new file mode 100644 index 0000000..2f33f3f --- /dev/null +++ b/calico/reactive/status.py @@ -0,0 +1,4 @@ +from charms import layer + + +layer.status._initialize() diff --git a/calico/requirements.txt b/calico/requirements.txt new file mode 100644 index 0000000..55543d9 --- /dev/null +++ b/calico/requirements.txt @@ -0,0 +1,3 @@ +mock +flake8 +pytest diff --git a/calico/revision b/calico/revision new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/calico/revision @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/calico/templates/10-calico.conflist b/calico/templates/10-calico.conflist new file mode 100644 index 0000000..5d60e08 --- /dev/null +++ b/calico/templates/10-calico.conflist @@ -0,0 +1,33 @@ +{ + "name": "calico-k8s-network", + "cniVersion": "0.3.1", + "plugins": [ + { + "type": "calico", + "etcd_endpoints": "{{ connection_string }}", + "etcd_key_file": "{{ etcd_key_path }}", + "etcd_cert_file": "{{ etcd_cert_path }}", + "etcd_ca_cert_file": "{{ etcd_ca_path }}", + "log_level": "info", + {% if mtu -%} + "mtu": {{ mtu }}, + {%- endif %} + "ipam": { + "type": "calico-ipam", + "assign_ipv4": "{{ assign_ipv4 }}", + "assign_ipv6": "{{ assign_ipv6 }}" + }, + "policy": { + "type": "k8s" + }, + "kubernetes": { + "kubeconfig": "{{ kubeconfig_path }}" + } + }, + { + "type": "portmap", + "capabilities": {"portMappings": true}, + "snat": true + } + ] +} diff --git a/calico/templates/calico-node.service b/calico/templates/calico-node.service new file mode 100644 index 0000000..bbac1c0 --- /dev/null +++ b/calico/templates/calico-node.service @@ -0,0 +1,54 @@ +[Unit] +Description=calico node + +[Service] +User=root +Environment=ETCD_ENDPOINTS={{ connection_string }} +# Setting LC_ALL and LANG works around a bug that only occurs on Xenial +# https://bugs.launchpad.net/bugs/1911220 +Environment=LC_ALL={{ lc_all }} +Environment=LANG={{ lang }} +PermissionsStartOnly=true +ExecStartPre=-/usr/local/sbin/charm-env --charm calico conctl delete calico-node +ExecStartPre=/bin/mkdir -p /var/run/calico /var/log/calico /var/lib/calico +ExecStart=/usr/local/sbin/charm-env --charm calico conctl run \ + --rm \ + --net-host \ + --privileged \ + --env ETCD_ENDPOINTS={{ connection_string }} \ + --env ETCD_CA_CERT_FILE={{ etcd_ca_path }} \ + --env ETCD_CERT_FILE={{ etcd_cert_path }} \ + --env ETCD_KEY_FILE={{ etcd_key_path }} \ + --env NODENAME={{ nodename }} \ + --env IP={{ ip }} \ + --env KUBECONFIG=/opt/calicoctl/kubeconfig \ + {% if ipv4 == "none" -%} + --env CALICO_ROUTER_ID="hash" \ + {% endif -%} + --env IP6={{ ip6 }} \ + {% if ip6 != "none" -%} + --env FELIX_IPV6SUPPORT=true \ + {% endif -%} + --env NO_DEFAULT_POOLS=true \ + --env AS= \ + --env CALICO_LIBNETWORK_ENABLED=true \ + --env CALICO_NETWORKING_BACKEND=bird \ + --env FELIX_DEFAULTENDPOINTTOHOSTACTION=ACCEPT \ + --env FELIX_IGNORELOOSERPF={{ ignore_loose_rpf | string | lower }} \ + {% if mtu -%} + --env FELIX_IPINIPMTU={{ mtu }} \ + --env FELIX_VXLANMTU={{ mtu }} \ + {% endif -%} + --mount /lib/modules:/lib/modules \ + --mount /var/run/calico:/var/run/calico \ + --mount /var/log/calico:/var/log/calico \ + --mount /var/lib/calico:/var/lib/calico \ + --mount /opt/calicoctl:/opt/calicoctl \ + --name calico-node \ + {{ calico_node_image }} +ExecStop=-/usr/local/sbin/charm-env --charm calico conctl delete calico-node +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/calico/templates/calicoctl b/calico/templates/calicoctl new file mode 100644 index 0000000..98fb522 --- /dev/null +++ b/calico/templates/calicoctl @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu +. /opt/calicoctl/calicoctl.env +exec /opt/calicoctl/calicoctl "$@" diff --git a/calico/templates/cdk.auth-webhook-secret.yaml b/calico/templates/cdk.auth-webhook-secret.yaml new file mode 100644 index 0000000..a12c402 --- /dev/null +++ b/calico/templates/cdk.auth-webhook-secret.yaml @@ -0,0 +1,13 @@ +# Manifest for CK secrets that auth-webhook expects +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ secret_name }} + namespace: {{ secret_namespace }} +type: {{ type }} +data: + uid: {{ user }} + username: {{ username }} + password: {{ password }} + groups: '{{ groups }}' diff --git a/calico/templates/policy-controller.yaml b/calico/templates/policy-controller.yaml new file mode 100644 index 0000000..c064f94 --- /dev/null +++ b/calico/templates/policy-controller.yaml @@ -0,0 +1,265 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: calico-kube-controllers + namespace: kube-system +--- +# Include a clusterrole for the kube-controllers component, +# and bind it to the calico-kube-controllers serviceaccount. +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: calico-kube-controllers +rules: + # Pods are monitored for changing labels. + # The node controller monitors Kubernetes nodes. + # Namespace and serviceaccount labels are used for policy. + - apiGroups: + - "" + - extensions + resources: + - pods + - nodes + - namespaces + - serviceaccounts + - networkpolicies + verbs: + - watch + - list + - apiGroups: + - networking.k8s.io + resources: + - networkpolicies + verbs: + - watch + - list +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: calico-kube-controllers +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: calico-kube-controllers +subjects: +- kind: ServiceAccount + name: calico-kube-controllers + namespace: kube-system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: calico-kube-controllers + namespace: kube-system + labels: + k8s-app: calico-kube-controllers + cdk-restart-on-ca-change: "true" +spec: + # Only a single instance of the this pod should be + # active at a time. Since this pod is run as a Deployment, + # Kubernetes will ensure the pod is recreated in case of failure, + # removing the need for passive backups. + selector: + matchLabels: + k8s-app: calico-kube-controllers + replicas: 1 + strategy: + type: Recreate + template: + metadata: + name: calico-kube-controllers + namespace: kube-system + labels: + k8s-app: calico-kube-controllers + annotations: + # annotate etcd cert modification time, so that when it changes, k8s + # will restart the pod + cdk-etcd-cert-last-modified: "{{ etcd_cert_last_modified }}" + spec: + hostNetwork: true + serviceAccountName: calico-kube-controllers + containers: + - name: calico-kube-controllers + image: {{ calico_policy_image }} + env: + - name: ETCD_ENDPOINTS + value: {{ connection_string }} + - name: ETCD_CA_CERT_FILE + value: {{ etcd_ca_path }} + - name: ETCD_CERT_FILE + value: {{ etcd_cert_path }} + - name: ETCD_KEY_FILE + value: {{ etcd_key_path }} + volumeMounts: + - name: calicoctl + mountPath: /opt/calicoctl + volumes: + - name: calicoctl + hostPath: + path: /opt/calicoctl +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: calico-node +rules: + # The CNI plugin needs to get pods, nodes, and namespaces. + - apiGroups: [""] + resources: + - pods + - nodes + - namespaces + verbs: + - get + - apiGroups: [""] + resources: + - endpoints + - services + verbs: + # Used to discover service IPs for advertisement. + - watch + - list + # Used to discover Typhas. + - get + - apiGroups: [""] + resources: + - nodes/status + verbs: + # Needed for clearing NodeNetworkUnavailable flag. + - patch + # Calico stores some configuration information in node annotations. + - update + # Watch for changes to Kubernetes NetworkPolicies. + - apiGroups: ["networking.k8s.io"] + resources: + - networkpolicies + verbs: + - watch + - list + # Used by Calico for policy information. + - apiGroups: [""] + resources: + - pods + - namespaces + - serviceaccounts + verbs: + - list + - watch + # The CNI plugin patches pods/status. + - apiGroups: [""] + resources: + - pods/status + verbs: + - patch + # Calico monitors various CRDs for config. + - apiGroups: ["crd.projectcalico.org"] + resources: + - globalfelixconfigs + - felixconfigurations + - bgppeers + - globalbgpconfigs + - bgpconfigurations + - ippools + - ipamblocks + - globalnetworkpolicies + - globalnetworksets + - networkpolicies + - networksets + - clusterinformations + - hostendpoints + - blockaffinities + verbs: + - get + - list + - watch + # Calico must create and update some CRDs on startup. + - apiGroups: ["crd.projectcalico.org"] + resources: + - ippools + - felixconfigurations + - clusterinformations + verbs: + - create + - update + # Calico stores some configuration information on the node. + - apiGroups: [""] + resources: + - nodes + verbs: + - get + - list + - watch + # These permissions are only requried for upgrade from v2.6, and can + # be removed after upgrade or on fresh installations. + - apiGroups: ["crd.projectcalico.org"] + resources: + - bgpconfigurations + - bgppeers + verbs: + - create + - update + # These permissions are required for Calico CNI to perform IPAM allocations. + - apiGroups: ["crd.projectcalico.org"] + resources: + - blockaffinities + - ipamblocks + - ipamhandles + verbs: + - get + - list + - create + - update + - delete + - apiGroups: ["crd.projectcalico.org"] + resources: + - ipamconfigs + verbs: + - get + # Block affinities must also be watchable by confd for route aggregation. + - apiGroups: ["crd.projectcalico.org"] + resources: + - blockaffinities + verbs: + - watch + # The Calico IPAM migration needs to get daemonsets. These permissions can be + # removed if not upgrading from an installation using host-local IPAM. + - apiGroups: ["apps"] + resources: + - daemonsets + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: calico-node +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: calico-node +subjects: +- kind: User + name: system:calico-node +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: namespace-reader +rules: +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: nodes-namespace-reader +subjects: +- apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:nodes +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: namespace-reader diff --git a/calico/tests/data/bird-operator/config.yaml b/calico/tests/data/bird-operator/config.yaml new file mode 100644 index 0000000..45e04fc --- /dev/null +++ b/calico/tests/data/bird-operator/config.yaml @@ -0,0 +1,9 @@ +options: + as-number: + type: int + description: AS Number + default: 64512 + bgp-peers: + type: string + description: BGP peers + default: "[]" diff --git a/calico/tests/data/bird-operator/metadata.yaml b/calico/tests/data/bird-operator/metadata.yaml new file mode 100644 index 0000000..9bacbcc --- /dev/null +++ b/calico/tests/data/bird-operator/metadata.yaml @@ -0,0 +1,7 @@ +name: bird +description: | + Test charm running BIRD +summary: | + Test charm running BIRD +series: +- focal diff --git a/calico/tests/data/bird-operator/requirements.txt b/calico/tests/data/bird-operator/requirements.txt new file mode 100644 index 0000000..2d81d3b --- /dev/null +++ b/calico/tests/data/bird-operator/requirements.txt @@ -0,0 +1 @@ +ops diff --git a/calico/tests/data/bird-operator/src/charm.py b/calico/tests/data/bird-operator/src/charm.py new file mode 100755 index 0000000..89d936c --- /dev/null +++ b/calico/tests/data/bird-operator/src/charm.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import logging + +from ops.charm import CharmBase +from ops.main import main +from ops.model import ActiveStatus, MaintenanceStatus +from subprocess import check_call +import yaml + +log = logging.getLogger(__name__) +bird_config_base = """ +log syslog all; +debug protocols all; + +protocol kernel { + persist; + scan time 20; + export all; +} + +protocol device { + scan time 10; +} +""" +bird_config_peer = """ +protocol bgp { + import all; + local as %s; + neighbor %s as %s; + direct; +} +""" + + +class BirdCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.framework.observe(self.on.install, self.install) + self.framework.observe(self.on.config_changed, self.config_changed) + + def install(self, event): + self.unit.status = MaintenanceStatus("Installing BIRD") + check_call(['apt-get', 'update']) + check_call(['apt-get', 'install', '-y', 'bird']) + + def config_changed(self, event): + self.unit.status = MaintenanceStatus("Configuring BIRD") + as_number = self.config['as-number'] + bird_config = "\n".join([bird_config_base] + [ + bird_config_peer % (as_number, peer['address'], peer['as-number']) + for peer in yaml.safe_load(self.config['bgp-peers']) + ]) + with open('/etc/bird/bird.conf', 'w') as f: + f.write(bird_config) + check_call(['systemctl', 'reload', 'bird']) + self.unit.status = ActiveStatus() + + +if __name__ == "__main__": + main(BirdCharm) diff --git a/calico/tests/data/bundle.yaml b/calico/tests/data/bundle.yaml new file mode 100644 index 0000000..1ae35f9 --- /dev/null +++ b/calico/tests/data/bundle.yaml @@ -0,0 +1,80 @@ +description: A minimal two-machine Kubernetes cluster, appropriate for development. +series: focal +machines: + '0': + constraints: cores=2 mem=4G root-disk=16G + series: focal + '1': + constraints: cores=4 mem=4G root-disk=16G + series: focal +services: + containerd: + charm: cs:~containers/containerd + channel: edge + easyrsa: + charm: cs:~containers/easyrsa + channel: edge + num_units: 1 + to: + - '1' + etcd: + charm: cs:~containers/etcd + channel: edge + num_units: 1 + options: + channel: 3.4/stable + to: + - '0' + calico: + charm: {{calico_charm}} + resources: + calico: {{resource_path}}/calico-amd64.tar.gz + calico-arm64: {{resource_path}}/calico-arm64.tar.gz + calico-upgrade: {{resource_path}}/calico-upgrade-amd64.tar.gz + calico-upgrade-arm64: {{resource_path}}/calico-upgrade-arm64.tar.gz + calico-node-image: {{resource_path}}/calico-node-image.tar.gz + options: + ignore-loose-rpf: true + vxlan: Always + kubernetes-master: + charm: cs:~containers/kubernetes-master + channel: edge + constraints: cores=2 mem=4G root-disk=16G + expose: true + num_units: 1 + options: + channel: 1.22/edge + to: + - '0' + kubernetes-worker: + charm: cs:~containers/kubernetes-worker + channel: edge + constraints: cores=4 mem=4G root-disk=16G + expose: true + num_units: 1 + options: + channel: 1.22/edge + to: + - '1' +relations: +- - kubernetes-master:kube-control + - kubernetes-worker:kube-control +- - kubernetes-master:certificates + - easyrsa:client +- - kubernetes-master:etcd + - etcd:db +- - kubernetes-worker:certificates + - easyrsa:client +- - etcd:certificates + - easyrsa:client +- - calico:etcd + - etcd:db +- - calico:cni + - kubernetes-master:cni +- - calico:cni + - kubernetes-worker:cni +- - containerd:containerd + - kubernetes-worker:container-runtime +- - containerd:containerd + - kubernetes-master:container-runtime + diff --git a/calico/tests/functional/conftest.py b/calico/tests/functional/conftest.py new file mode 100644 index 0000000..a92e249 --- /dev/null +++ b/calico/tests/functional/conftest.py @@ -0,0 +1,4 @@ +import charms.unit_test + + +charms.unit_test.patch_reactive() diff --git a/calico/tests/functional/test_k8s_common.py b/calico/tests/functional/test_k8s_common.py new file mode 100644 index 0000000..4b867e6 --- /dev/null +++ b/calico/tests/functional/test_k8s_common.py @@ -0,0 +1,90 @@ +from functools import partial + +import pytest +from unittest import mock +from charms.layer import kubernetes_common + + +class TestCreateKubeConfig: + @pytest.fixture(autouse=True) + def _files(self, tmp_path): + self.cfg_file = tmp_path / "config" + self.ca_file = tmp_path / "ca.crt" + self.ca_file.write_text("foo") + self.ckc = partial( + kubernetes_common.create_kubeconfig, + self.cfg_file, + "server", + self.ca_file, + ) + + def test_guard_clauses(self): + with pytest.raises(ValueError): + self.ckc() + assert not self.cfg_file.exists() + with pytest.raises(ValueError): + self.ckc(token="token", password="password") + assert not self.cfg_file.exists() + with pytest.raises(ValueError): + self.ckc(key="key") + assert not self.cfg_file.exists() + + def test_file_creation(self): + self.ckc(password="password") + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert cfg_data_1 + + def test_idempotency(self): + self.ckc(password="password") + cfg_data_1 = self.cfg_file.read_text() + self.ckc(password="password") + cfg_data_2 = self.cfg_file.read_text() + # Verify that calling w/ the same data keeps the same file contents. + assert cfg_data_2 == cfg_data_1 + + def test_efficient_updates(self): + self.ckc(password="old_password") + cfg_stat_1 = self.cfg_file.stat() + self.ckc(password="old_password") + cfg_stat_2 = self.cfg_file.stat() + self.ckc(password="new_password") + cfg_stat_3 = self.cfg_file.stat() + # Verify that calling with the same data doesn't + # modify the file at all, but that new data does + assert cfg_stat_1.st_mtime == cfg_stat_2.st_mtime < cfg_stat_3.st_mtime + + def test_aws_iam(self): + self.ckc(password="password", aws_iam_cluster_id="aws-cluster") + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert "aws-cluster" in cfg_data_1 + + def test_keystone(self): + self.ckc(password="password", keystone=True) + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert "keystone-user" in cfg_data_1 + assert "exec" in cfg_data_1 + + def test_atomic_updates(self): + self.ckc(password="old_password") + with self.cfg_file.open("rt") as f: + # Perform a write in the middle of reading + self.ckc(password="new_password") + # Read data from existing FH after new data was written + cfg_data_1 = f.read() + # Read updated data + cfg_data_2 = self.cfg_file.read_text() + # Verify that the in-progress read didn't get any of the new data + assert cfg_data_1 != cfg_data_2 + assert "old_password" in cfg_data_1 + assert "new_password" in cfg_data_2 + + @mock.patch("charmhelpers.core.hookenv.network_get", autospec=True) + def test_get_ingress_address(self, network_get): + network_get.return_value = {"ingress-addresses": ["1.2.3.4", "5.6.7.8"]} + ingress = kubernetes_common.get_ingress_address("endpoint-name") + assert ingress == "1.2.3.4" + ingress = kubernetes_common.get_ingress_address("endpoint-name", ["1.2.3.4"]) + assert ingress == "5.6.7.8" diff --git a/calico/tests/integration/conftest.py b/calico/tests/integration/conftest.py new file mode 100644 index 0000000..6bd4784 --- /dev/null +++ b/calico/tests/integration/conftest.py @@ -0,0 +1,36 @@ +from kubernetes_wrapper import Kubernetes +import logging +import pytest +import random +import string + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +@pytest.mark.asyncio +async def kubernetes(ops_test): + kubeconfig_path = ops_test.tmp_path / "kubeconfig" + retcode, stdout, stderr = await ops_test.run( + "juju", "scp", "kubernetes-master/leader:config", kubeconfig_path + ) + if retcode != 0: + log.error(f"retcode: {retcode}") + log.error(f"stdout:\n{stdout.strip()}") + log.error(f"stderr:\n{stderr.strip()}") + pytest.fail("Failed to copy kubeconfig from kubernetes-master") + namespace = "test-calico-integration-" + "".join( + random.choice(string.ascii_lowercase + string.digits) + for _ in range(5) + ) + kubernetes = Kubernetes(namespace, kubeconfig=str(kubeconfig_path)) + namespace_object = { + 'apiVersion': 'v1', + 'kind': 'Namespace', + 'metadata': { + 'name': namespace + } + } + kubernetes.apply_object(namespace_object) + yield kubernetes + kubernetes.delete_object(namespace_object) diff --git a/calico/tests/integration/test_calico_integration.py b/calico/tests/integration/test_calico_integration.py new file mode 100644 index 0000000..03bd3d8 --- /dev/null +++ b/calico/tests/integration/test_calico_integration.py @@ -0,0 +1,139 @@ +import logging +import os +import pytest +import time +import yaml + +log = logging.getLogger(__name__) + + +@pytest.mark.abort_on_fail +async def test_build_and_deploy(ops_test): + resource_path = ops_test.tmp_path / "charm-resources" + resource_path.mkdir() + resource_build_script = os.path.abspath("./build-calico-resource.sh") + log.info("Building charm resources") + retcode, stdout, stderr = await ops_test.run( + resource_build_script, + cwd=resource_path + ) + if retcode != 0: + log.error(f"retcode: {retcode}") + log.error(f"stdout:\n{stdout.strip()}") + log.error(f"stderr:\n{stderr.strip()}") + pytest.fail("Failed to build charm resources") + bundle = ops_test.render_bundle( + "tests/data/bundle.yaml", + calico_charm=await ops_test.build_charm("."), + resource_path=resource_path + ) + # deploy with Juju CLI because libjuju does not support local resource + # paths in bundles + log.info("Deploying bundle") + retcode, stdout, stderr = await ops_test.run( + "juju", "deploy", "-m", ops_test.model_full_name, bundle + ) + if retcode != 0: + log.error(f"retcode: {retcode}") + log.error(f"stdout:\n{stdout.strip()}") + log.error(f"stderr:\n{stderr.strip()}") + pytest.fail("Failed to deploy bundle") + await ops_test.model.wait_for_idle(wait_for_active=True, timeout=60 * 60) + + +async def test_bgp_service_ip_advertisement(ops_test, kubernetes): + # deploy a test service in k8s (nginx) + deployment = { + 'apiVersion': 'apps/v1', + 'kind': 'Deployment', + 'metadata': { + 'name': 'nginx' + }, + 'spec': { + 'selector': { + 'matchLabels': { + 'app': 'nginx' + } + }, + 'template': { + 'metadata': { + 'labels': { + 'app': 'nginx' + } + }, + 'spec': { + 'containers': [{ + 'name': 'nginx', + 'image': 'rocks.canonical.com/cdk/nginx:1.18', + 'ports': [{ + 'containerPort': 80 + }] + }] + } + } + } + } + service = { + 'apiVersion': 'v1', + 'kind': 'Service', + 'metadata': { + 'name': 'nginx' + }, + 'spec': { + 'selector': { + 'app': 'nginx' + }, + 'ports': [{ + 'protocol': 'TCP', + 'port': 80 + }] + } + + } + kubernetes.apply_object(deployment) + kubernetes.apply_object(service) + service_ip = kubernetes.read_object(service).spec.cluster_ip + + # build and deploy bird charm + bird_charm = await ops_test.build_charm("tests/data/bird-operator") + await ops_test.model.deploy(bird_charm) + await ops_test.model.wait_for_idle(wait_for_active=True, timeout=60 * 10) + + # configure calico to peer with bird + master_config = await ops_test.model.applications['kubernetes-master'].get_config() + bird_app = ops_test.model.applications['bird'] + calico_app = ops_test.model.applications['calico'] + await calico_app.set_config({ + 'bgp-service-cluster-ips': master_config['service-cidr']['value'], + 'global-bgp-peers': yaml.dump([ + {'address': unit.public_address, 'as-number': 64512} + for unit in bird_app.units + ]) + }) + + # configure bird to peer with calico + await bird_app.set_config({ + 'bgp-peers': yaml.dump([ + {'address': unit.public_address, 'as-number': 64512} + for unit in calico_app.units + ]) + }) + + # verify test service is reachable from bird + deadline = time.time() + 60 * 10 + while time.time() < deadline: + retcode, stdout, stderr = await ops_test.run( + 'juju', 'ssh', '-m', ops_test.model_full_name, 'bird/leader', + 'curl', '--connect-timeout', '10', service_ip + ) + if retcode == 0: + break + else: + pytest.fail("Failed service connection test after BGP config") + + # clean up + await calico_app.set_config({ + 'bgp-service-cluster-ips': '', + 'global-bgp-peers': '[]' + }) + await bird_app.destroy() diff --git a/calico/tests/unit/conftest.py b/calico/tests/unit/conftest.py new file mode 100644 index 0000000..462d852 --- /dev/null +++ b/calico/tests/unit/conftest.py @@ -0,0 +1,6 @@ +import charms.unit_test + + +charms.unit_test.patch_reactive() +charms.unit_test.patch_module('conctl') +charms.unit_test.patch_module('charms.leadership') diff --git a/calico/tests/unit/test_calico.py b/calico/tests/unit/test_calico.py new file mode 100644 index 0000000..1923e1f --- /dev/null +++ b/calico/tests/unit/test_calico.py @@ -0,0 +1,16 @@ +from charmhelpers.core.hookenv import is_leader # patched +from charmhelpers.core.host import service_running # patched +from reactive import calico + + +def test_series_upgrade(): + calico.set_state('upgrade.series.in-progress') + is_leader.return_value = False + service_running.return_value = True + assert calico.status.blocked.call_count == 0 + assert calico.status.waiting.call_count == 0 + assert calico.status.active.call_count == 0 + calico.ready() + assert calico.status.blocked.call_count == 1 + assert calico.status.waiting.call_count == 0 + assert calico.status.active.call_count == 0 diff --git a/calico/tests/unit/test_k8s_common.py b/calico/tests/unit/test_k8s_common.py new file mode 100644 index 0000000..0dcad31 --- /dev/null +++ b/calico/tests/unit/test_k8s_common.py @@ -0,0 +1,122 @@ +import json +import string +from subprocess import CalledProcessError +from unittest.mock import Mock + +from charms.layer import kubernetes_common as kc + + +def test_token_generator(): + alphanum = string.ascii_letters + string.digits + token = kc.token_generator(10) + assert len(token) == 10 + unknown_chars = set(token) - set(alphanum) + assert not unknown_chars + + +def test_get_secret_names(monkeypatch): + monkeypatch.setattr(kc, "kubectl", Mock()) + kc.kubectl.side_effect = [ + CalledProcessError(1, "none"), + FileNotFoundError, + "{}".encode("utf8"), + json.dumps( + { + "items": [ + { + "metadata": {"name": "secret-id"}, + "data": {"username": "dXNlcg=="}, + }, + ], + } + ).encode("utf8"), + ] + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {"user": "secret-id"} + + +def test_generate_rfc1123(): + alphanum = string.ascii_letters + string.digits + token = kc.generate_rfc1123(1000) + assert len(token) == 253 + unknown_chars = set(token) - set(alphanum) + assert not unknown_chars + + +def test_create_secret(monkeypatch): + monkeypatch.setattr(kc, "render", Mock()) + monkeypatch.setattr(kc, "kubectl_manifest", Mock()) + monkeypatch.setattr(kc, "get_secret_names", Mock()) + monkeypatch.setattr(kc, "generate_rfc1123", Mock()) + kc.kubectl_manifest.side_effect = [True, False] + kc.get_secret_names.side_effect = [{"username": "secret-id"}, {}] + kc.generate_rfc1123.return_value = "foo" + assert kc.create_secret("token", "username", "user", "groups") + assert kc.render.call_args[1]["context"] == { + "groups": "Z3JvdXBz", + "password": "dXNlcjo6dG9rZW4=", + "secret_name": "secret-id", + "secret_namespace": "kube-system", + "type": "juju.is/token-auth", + "user": "dXNlcg==", + "username": "dXNlcm5hbWU=", + } + assert not kc.create_secret("token", "username", "user", "groups") + assert kc.render.call_args[1]["context"] == { + "groups": "Z3JvdXBz", + "password": "dXNlcjo6dG9rZW4=", + "secret_name": "auth-user-foo", + "secret_namespace": "kube-system", + "type": "juju.is/token-auth", + "user": "dXNlcg==", + "username": "dXNlcm5hbWU=", + } + + +def test_get_secret_password(monkeypatch): + monkeypatch.setattr(kc, "kubectl", Mock()) + monkeypatch.setattr(kc, "Path", Mock()) + monkeypatch.setattr(kc, "yaml", Mock()) + kc.kubectl.side_effect = [ + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + FileNotFoundError, + json.dumps({}).encode("utf8"), + json.dumps({"items": []}).encode("utf8"), + json.dumps({"items": []}).encode("utf8"), + json.dumps({"items": [{}]}).encode("utf8"), + json.dumps({"items": [{"data": {}}]}).encode("utf8"), + json.dumps( + {"items": [{"data": {"username": "Ym9i", "password": "c2VjcmV0"}}]} + ).encode("utf8"), + json.dumps( + {"items": [{"data": {"username": "dXNlcm5hbWU=", "password": "c2VjcmV0"}}]} + ).encode("utf8"), + ] + kc.yaml.safe_load.side_effect = [ + {}, + {"users": None}, + {"users": []}, + {"users": [{"user": {}}]}, + {"users": [{"user": {"token": "secret"}}]}, + ] + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") == "secret" + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") == "secret" diff --git a/calico/tests/validate-wheelhouse.sh b/calico/tests/validate-wheelhouse.sh new file mode 100755 index 0000000..72f7131 --- /dev/null +++ b/calico/tests/validate-wheelhouse.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +build_dir="$(mktemp -d)" +function cleanup { rm -rf "$build_dir"; } +trap cleanup EXIT + +charm build . --build-dir "$build_dir" +pip install -f "$build_dir/calico/wheelhouse" --no-index --no-cache-dir "$build_dir"/calico/wheelhouse/* diff --git a/calico/tox.ini b/calico/tox.ini new file mode 100644 index 0000000..24b70de --- /dev/null +++ b/calico/tox.ini @@ -0,0 +1,41 @@ +[flake8] +max-line-length = 88 + +[tox] +skipsdist = True +envlist = lint,unit,integration + +[testenv] +setenv = + PYTHONPATH={toxinidir}:{toxinidir}/lib + PYTHONBREAKPOINT=ipdb.set_trace + +[testenv:unit] +deps = + pyyaml + pytest + charms.unit_test + ipdb +commands = pytest --tb native -s {posargs} {toxinidir}/tests/unit + +[testenv:validate-wheelhouse] +allowlist_externals = {toxinidir}/tests/validate-wheelhouse.sh +commands = {toxinidir}/tests/validate-wheelhouse.sh + +[testenv:integration] +deps = + pytest + pytest-operator + aiohttp + ipdb + git+https://github.com/canonical/kubernetes-rapper@main#egg=kubernetes-wrapper +# tox only passes through the upper-case versions by default, but some +# programs, such as wget or pip, only honor the lower-case versions +passenv = http_proxy https_proxy no_proxy +commands = pytest --tb native --show-capture=no --log-cli-level=INFO -s {posargs} {toxinidir}/tests/integration + +[testenv:lint] +deps = + flake8 +commands = + flake8 {toxinidir}/reactive {toxinidir}/lib {toxinidir}/tests diff --git a/calico/version b/calico/version new file mode 100644 index 0000000..20817dd --- /dev/null +++ b/calico/version @@ -0,0 +1 @@ +ccfa68be \ No newline at end of file diff --git a/calico/wheelhouse.txt b/calico/wheelhouse.txt new file mode 100644 index 0000000..ccf7d4a --- /dev/null +++ b/calico/wheelhouse.txt @@ -0,0 +1,23 @@ +# layer:basic +# pip is pinned to <19.0 to avoid https://github.com/pypa/pip/issues/6164 +# even with installing setuptools before upgrading pip ends up with pip seeing +# the older setuptools at the system level if include_system_packages is true +pip>=18.1,<19.0 +# pin Jinja2, PyYAML and MarkupSafe to the last versions supporting python 3.5 +# for trusty +Jinja2<=2.10.1 +PyYAML<=5.2 +MarkupSafe<2.0.0 +setuptools<42 +setuptools-scm<=1.17.0 +charmhelpers>=0.4.0,<1.0.0 +charms.reactive>=0.1.0,<2.0.0 +wheel<0.34 +# pin netaddr to avoid pulling importlib-resources +netaddr<=0.7.19 + +# calico +conctl-py35==0.1.2 +# pin click to avoid bringing in incompatible setuptools>=42 +click<8.0 + diff --git a/calico/wheelhouse/Jinja2-2.10.1.tar.gz b/calico/wheelhouse/Jinja2-2.10.1.tar.gz new file mode 100644 index 0000000..ffd1054 Binary files /dev/null and b/calico/wheelhouse/Jinja2-2.10.1.tar.gz differ diff --git a/calico/wheelhouse/MarkupSafe-1.1.1.tar.gz b/calico/wheelhouse/MarkupSafe-1.1.1.tar.gz new file mode 100644 index 0000000..a6dad8e Binary files /dev/null and b/calico/wheelhouse/MarkupSafe-1.1.1.tar.gz differ diff --git a/calico/wheelhouse/PyYAML-5.2.tar.gz b/calico/wheelhouse/PyYAML-5.2.tar.gz new file mode 100644 index 0000000..666d12a Binary files /dev/null and b/calico/wheelhouse/PyYAML-5.2.tar.gz differ diff --git a/calico/wheelhouse/Tempita-0.5.2.tar.gz b/calico/wheelhouse/Tempita-0.5.2.tar.gz new file mode 100644 index 0000000..755befc Binary files /dev/null and b/calico/wheelhouse/Tempita-0.5.2.tar.gz differ diff --git a/calico/wheelhouse/charmhelpers-0.20.23.tar.gz b/calico/wheelhouse/charmhelpers-0.20.23.tar.gz new file mode 100644 index 0000000..8fbc8ec Binary files /dev/null and b/calico/wheelhouse/charmhelpers-0.20.23.tar.gz differ diff --git a/calico/wheelhouse/charms.reactive-1.4.1.tar.gz b/calico/wheelhouse/charms.reactive-1.4.1.tar.gz new file mode 100644 index 0000000..03bc1fe Binary files /dev/null and b/calico/wheelhouse/charms.reactive-1.4.1.tar.gz differ diff --git a/calico/wheelhouse/click-7.1.2.tar.gz b/calico/wheelhouse/click-7.1.2.tar.gz new file mode 100644 index 0000000..698411c Binary files /dev/null and b/calico/wheelhouse/click-7.1.2.tar.gz differ diff --git a/calico/wheelhouse/conctl-py35-0.1.2.tar.gz b/calico/wheelhouse/conctl-py35-0.1.2.tar.gz new file mode 100644 index 0000000..651dde5 Binary files /dev/null and b/calico/wheelhouse/conctl-py35-0.1.2.tar.gz differ diff --git a/calico/wheelhouse/netaddr-0.7.19.tar.gz b/calico/wheelhouse/netaddr-0.7.19.tar.gz new file mode 100644 index 0000000..cc31d9d Binary files /dev/null and b/calico/wheelhouse/netaddr-0.7.19.tar.gz differ diff --git a/calico/wheelhouse/pbr-5.6.0.tar.gz b/calico/wheelhouse/pbr-5.6.0.tar.gz new file mode 100644 index 0000000..0d5c965 Binary files /dev/null and b/calico/wheelhouse/pbr-5.6.0.tar.gz differ diff --git a/calico/wheelhouse/pip-18.1.tar.gz b/calico/wheelhouse/pip-18.1.tar.gz new file mode 100644 index 0000000..a18192d Binary files /dev/null and b/calico/wheelhouse/pip-18.1.tar.gz differ diff --git a/calico/wheelhouse/pyaml-21.10.1.tar.gz b/calico/wheelhouse/pyaml-21.10.1.tar.gz new file mode 100644 index 0000000..b19aad3 Binary files /dev/null and b/calico/wheelhouse/pyaml-21.10.1.tar.gz differ diff --git a/calico/wheelhouse/setuptools-41.6.0.zip b/calico/wheelhouse/setuptools-41.6.0.zip new file mode 100644 index 0000000..3345759 Binary files /dev/null and b/calico/wheelhouse/setuptools-41.6.0.zip differ diff --git a/calico/wheelhouse/setuptools_scm-1.17.0.tar.gz b/calico/wheelhouse/setuptools_scm-1.17.0.tar.gz new file mode 100644 index 0000000..43b16c7 Binary files /dev/null and b/calico/wheelhouse/setuptools_scm-1.17.0.tar.gz differ diff --git a/calico/wheelhouse/six-1.16.0.tar.gz b/calico/wheelhouse/six-1.16.0.tar.gz new file mode 100644 index 0000000..5bf3a27 Binary files /dev/null and b/calico/wheelhouse/six-1.16.0.tar.gz differ diff --git a/calico/wheelhouse/wheel-0.33.6.tar.gz b/calico/wheelhouse/wheel-0.33.6.tar.gz new file mode 100644 index 0000000..c922c4e Binary files /dev/null and b/calico/wheelhouse/wheel-0.33.6.tar.gz differ diff --git a/containerd/.build.manifest b/containerd/.build.manifest new file mode 100644 index 0000000..82e6908 --- /dev/null +++ b/containerd/.build.manifest @@ -0,0 +1,656 @@ +{ + "layers": [ + { + "branch": "refs/heads/master", + "rev": "fcdcea4e5de3e1556c24e6704607862d0ba00a56", + "url": "layer:options" + }, + { + "branch": "refs/heads/master", + "rev": "a3ff62c32c993d80417f6e093e3ef95e42f62083", + "url": "layer:basic" + }, + { + "branch": "refs/heads/master", + "rev": "527dd64fc4b9a6b0f8d80a3c2c0b865155050275", + "url": "layer:debug" + }, + { + "branch": "refs/heads/master", + "rev": "a7d7b6423db37a47611310039e6ed1929c0a2eab", + "url": "layer:status" + }, + { + "branch": "refs/heads/master", + "rev": "be187bfe2ed511fc7ee29bf25f7374a2d6d34b2d", + "url": "layer:container-runtime-common" + }, + { + "branch": "refs/heads/master", + "rev": "e87057806fcbeb67d222b14e1ce2e4fafdf58f9d", + "url": "containerd" + }, + { + "branch": "refs/heads/master", + "rev": "6f927f10b97f45c566481cf57a29d433f17373e1", + "url": "interface:container-runtime" + }, + { + "branch": "refs/heads/master", + "rev": "b59ce0c44bc52c789175750ce18b42f76c9a4578", + "url": "interface:untrusted-container-runtime" + }, + { + "branch": "refs/heads/master", + "rev": "365ec9f348ccd561a9ec3e084c826f134676439e", + "url": "interface:docker-registry" + } + ], + "signatures": { + ".build.manifest": [ + "build", + "dynamic", + "unchecked" + ], + ".github/workflows/main.yaml": [ + "containerd", + "static", + "f413dfd54279707a2dc0ebf6f5e399a1a65170a879ca126c63f1d98f543d0dd7" + ], + ".github/workflows/main.yml": [ + "layer:basic", + "static", + "96a48a981ceb2a96f427a6b5226d2da6d7191981793804055d70a88ca1987473" + ], + ".gitignore": [ + "containerd", + "static", + "0046c3f0ce1c6b217c55dbc60caf1af0287efb5502984c6554e94fd4a6b59628" + ], + ".travis.yml": [ + "containerd", + "static", + "ab2c8c5a3ae50ec307e9e19ec30a20d4765161e0cb3bddb66f09c4a1b72b7f71" + ], + "LICENSE": [ + "containerd", + "static", + "c67d5f530080ecc4136a22d0543049b71016ea5dbbaa1d02b6d128d2d8fe3888" + ], + "Makefile": [ + "layer:basic", + "static", + "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301" + ], + "README.md": [ + "containerd", + "static", + "96e79c4e24b3f1cef60d2340d171d0200797e3ea2419a3e42ec631fa3e0126be" + ], + "actions.yaml": [ + "containerd", + "dynamic", + "58f291b03b7a1fbe3637633ed799765ce5b3057b8eb75db9dc77e4f0c045427b" + ], + "actions/debug": [ + "layer:debug", + "static", + "db0a42dae4c5045b2c06385bf22209dfe0e2ded55822ef847d84b01d9ff2b046" + ], + "actions/upgrade-containerd": [ + "containerd", + "static", + "12a77b76f2e4043b2405ea631ea0c736c55c4570256695719a657bccf6a36296" + ], + "bin/charm-env": [ + "layer:basic", + "static", + "fb6a20fac4102a6a4b6ffe903fcf666998f9a95a3647e6f9af7a1eeb44e58fd5" + ], + "bin/layer_option": [ + "layer:options", + "static", + "e959bf29da4c5edff28b2602c24113c4df9e25cdc9f2aa3b5d46c8577b2a40cc" + ], + "config.yaml": [ + "containerd", + "dynamic", + "9b0153cac5602ecd84e860bb784a6477d46ea61b62eb1f0d0946fce06d859c09" + ], + "copyright": [ + "layer:status", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "copyright.layer-basic": [ + "layer:basic", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-options": [ + "layer:options", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "debug-scripts/charm-unitdata": [ + "layer:debug", + "static", + "c952b9d31f3942e4e722cb3e70f5119707b69b8e76cc44e2e906bc6d9aef49b7" + ], + "debug-scripts/filesystem": [ + "layer:debug", + "static", + "d29cc8687f4422d024001c91b1ac756ee6bf8a2a125bc98db1199ba775eb8fd7" + ], + "debug-scripts/juju-logs": [ + "layer:debug", + "static", + "d260b35753a917368cb8c64c1312546a0a40ef49cba84c75bc6369549807c55e" + ], + "debug-scripts/juju-network-get": [ + "layer:debug", + "static", + "6d849a1f8e6569bd0d5ea38299f7937cb8b36a5f505e3532f6c756eabeb8b6c5" + ], + "debug-scripts/network": [ + "layer:debug", + "static", + "714afae5dcb45554ff1f05285501e3b7fcc656c8de51217e263b93dab25a9d2e" + ], + "debug-scripts/packages": [ + "layer:debug", + "static", + "e8177102dc2ca853cb9272c1257cf2cfd5253d2a074e602d07c8bc4ea8e27c75" + ], + "debug-scripts/sysctl": [ + "layer:debug", + "static", + "990035b320e09cc2228e1f2f880e795d51118b2959339eacddff9cbb74349c6a" + ], + "debug-scripts/systemd": [ + "layer:debug", + "static", + "23ddf533198bf5b1ce723acde31ada806aab8539292b514c721d8ec08af74106" + ], + "docs/status.md": [ + "layer:status", + "static", + "975dec9f8c938196e102e954a80226bda293407c4e5ae857c118bf692154702a" + ], + "hooks/config-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/containerd-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/containerd-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/containerd-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/containerd-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/containerd-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/docker-registry-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/docker-registry-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/docker-registry-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/docker-registry-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/docker-registry-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/hook.template": [ + "layer:basic", + "static", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/install": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-elected": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-settings-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/post-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/pre-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/relations/container-runtime/.gitignore": [ + "interface:container-runtime", + "static", + "a2ebfecdb6c1b58267fbe97e6e2ac02c2b963df7673fc1047270f0f0cff16732" + ], + "hooks/relations/container-runtime/LICENSE": [ + "interface:container-runtime", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "hooks/relations/container-runtime/README.md": [ + "interface:container-runtime", + "static", + "44273265818229d2c858c3af0e0eee3a7df05aaa9ab20d28c3872190d4b48611" + ], + "hooks/relations/container-runtime/__init__.py": [ + "interface:container-runtime", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/container-runtime/interface.yaml": [ + "interface:container-runtime", + "static", + "e5343dcb11a6817a6050df4ea1c463eeaa0dd4777098566d4e27b056775426c6" + ], + "hooks/relations/container-runtime/provides.py": [ + "interface:container-runtime", + "static", + "4e818da222f507604179a828629787a1250083c847277f6b5b8e028cfbbb6d06" + ], + "hooks/relations/container-runtime/requires.py": [ + "interface:container-runtime", + "static", + "95285168b02f1f70be15c03098833a85e60fa1658ed72a46acd42e8e85ded761" + ], + "hooks/relations/docker-registry/.gitignore": [ + "interface:docker-registry", + "static", + "83b4ca18cc39800b1d260b5633cd0252e21501b21e7c33e718db44f1a68a09b8" + ], + "hooks/relations/docker-registry/LICENSE": [ + "interface:docker-registry", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "hooks/relations/docker-registry/README.md": [ + "interface:docker-registry", + "static", + "a42f9e72af9a6fad058e8f156e79aeaadccf3e2b03b5b7dc750704b168f4347c" + ], + "hooks/relations/docker-registry/__init__.py": [ + "interface:docker-registry", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/docker-registry/interface.yaml": [ + "interface:docker-registry", + "static", + "29816ed0d72772b26108012481b64db1385ff42d3349fa175bb05331ca0f933c" + ], + "hooks/relations/docker-registry/provides.py": [ + "interface:docker-registry", + "static", + "618cc1b19c2e77106363938931a342918306d731be61665167bad4615a30d9c7" + ], + "hooks/relations/docker-registry/requires.py": [ + "interface:docker-registry", + "static", + "9db322b76a6bbdc15b36fba077bddbff7dfc6dbfe9c245fed548c00e6c354578" + ], + "hooks/relations/untrusted-container-runtime/.gitignore": [ + "interface:untrusted-container-runtime", + "static", + "a2ebfecdb6c1b58267fbe97e6e2ac02c2b963df7673fc1047270f0f0cff16732" + ], + "hooks/relations/untrusted-container-runtime/LICENSE": [ + "interface:untrusted-container-runtime", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "hooks/relations/untrusted-container-runtime/README.md": [ + "interface:untrusted-container-runtime", + "static", + "e3dc7db9ee98b716cb9a3a281fad88ca313bc11888a0da2f4b63c4306d91b64f" + ], + "hooks/relations/untrusted-container-runtime/__init__.py": [ + "interface:untrusted-container-runtime", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/untrusted-container-runtime/interface.yaml": [ + "interface:untrusted-container-runtime", + "static", + "1fcb0305295206dc2b9926bf1870cae2c6cd8eee6eef72b6060c85e4f2109a45" + ], + "hooks/relations/untrusted-container-runtime/provides.py": [ + "interface:untrusted-container-runtime", + "static", + "05a52be7ad18df5cac9fb5dcc27c2ab24fe12e65fa809e0ea4d395dbcb36e6f2" + ], + "hooks/relations/untrusted-container-runtime/requires.py": [ + "interface:untrusted-container-runtime", + "static", + "958e03e254ee27bee761a6af3e032a273204b356dc51438489cde726b1a6e060" + ], + "hooks/start": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/stop": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/untrusted-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/untrusted-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/untrusted-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/untrusted-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/untrusted-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/update-status": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/upgrade-charm": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "icon.svg": [ + "containerd", + "static", + "6d0d1bf06e7bc7da205c138566c92a35026d9d06ceaaea382e7b2a2b1c829fd4" + ], + "layer.yaml": [ + "containerd", + "dynamic", + "1ef37c9d5f7bb467d9baf12d27f220d9be7eeaee13fe52acedab02aa5d42e7db" + ], + "lib/charms/layer/__init__.py": [ + "layer:basic", + "static", + "dfe0d26c6bf409767de6e2546bc648f150e1b396243619bad3aa0553ab7e0e6f" + ], + "lib/charms/layer/basic.py": [ + "layer:basic", + "static", + "98b47134770ed6e4c0b2d4aad73cd5bc200bec84aa9c1c4e075fd70c3222a0c9" + ], + "lib/charms/layer/container_runtime_common.py": [ + "layer:container-runtime-common", + "static", + "1c6745c0966687d82da9dc8d10add74389c2b7585149afcb81f59405bde227f3" + ], + "lib/charms/layer/containerd.py": [ + "containerd", + "static", + "e4b1e9001625f1410ef319f0e357172f8a2db9edb45f593f39c16c8f7496e6ba" + ], + "lib/charms/layer/execd.py": [ + "layer:basic", + "static", + "fda8bd491032db1db8ddaf4e99e7cc878c6fb5432efe1f91cadb5b34765d076d" + ], + "lib/charms/layer/options.py": [ + "layer:options", + "static", + "8ae7a07d22542fc964f2d2bee8219d1c78a68dace70a1b38d36d4aea47b1c3b2" + ], + "lib/charms/layer/status.py": [ + "layer:status", + "static", + "d560a5e07b2e5f2b0f25f30e1f0278b06f3f90c01e4dbad5c83d71efc79018c6" + ], + "lib/debug_script.py": [ + "layer:debug", + "static", + "a4d56f2d3e712b1b5cadb657c7195c6268d0aac6d228991049fd769e0ddaf453" + ], + "make_docs": [ + "layer:status", + "static", + "c990f55c8e879793a62ed8464ee3d7e0d7d2225fdecaf17af24b0df0e2daa8c1" + ], + "metadata.yaml": [ + "containerd", + "dynamic", + "ef674a2122e46ff6ddb60ac60d79af3a56fd6b799e4ccf33cc15a430dac409db" + ], + "pydocmd.yml": [ + "layer:status", + "static", + "11d9293901f32f75f4256ae4ac2073b92ce1d7ef7b6c892ba9fbb98690a0b330" + ], + "reactive/__init__.py": [ + "layer:basic", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "reactive/container_runtime_common.py": [ + "layer:container-runtime-common", + "static", + "b612ab91a73e8b6c5bb02dae9a0813fe164e96875c175d48b1e736ab25091a11" + ], + "reactive/containerd.py": [ + "containerd", + "static", + "4e80ec104f9e8c2bfbf1577a698b1df7fa37ce10907ab2bb71f96b7f672e4639" + ], + "reactive/status.py": [ + "layer:status", + "static", + "30207fc206f24e91def5252f1c7f7c8e23c0aed0e93076babf5e03c05296d207" + ], + "requirements.txt": [ + "layer:basic", + "static", + "a00f75d80849e5b4fc5ad2e7536f947c25b1a4044b341caa8ee87a92d3a4c804" + ], + "scripts/enable_grub_cgroups.sh": [ + "layer:container-runtime-common", + "static", + "08f39868716900be03e2612a63d5fbbd661599484f05f5164c5d794415cfa260" + ], + "templates/config.toml": [ + "containerd", + "static", + "f149f8147f7f7997420c34530a51eafffa71038f1426e3b4e7d7e3fbf89afac3" + ], + "templates/config_v2.toml": [ + "containerd", + "static", + "e842b1318fceef9e839623fe082cc7d6575820331882876330337176a6cbc542" + ], + "templates/proxy.conf": [ + "containerd", + "static", + "a6f8922e02757d6ee914d160b7c677da555629b956a7068c3d2c8da4f56fa19a" + ], + "tests/conftest.py": [ + "layer:container-runtime-common", + "static", + "a93443f61e197f9845d65a72c8cdcb68709eb48451fe9624a23b2509ca69966c" + ], + "tests/test_cidr_notation.py": [ + "layer:container-runtime-common", + "static", + "64e6e8030abe0b9a088d81d7f502a2b8774db8d226c648c34de11a58416eea43" + ], + "tests/test_merge_config.py": [ + "layer:container-runtime-common", + "static", + "fe278ee4e888564dd4c09e2d4dac2db5d914b41fd82d78af30960f02fa8461fc" + ], + "tests/test_reactive.py": [ + "layer:container-runtime-common", + "static", + "432228776392fff168f3f770a9aab4419f4656bffc708e05a2310a8cb87eeaff" + ], + "tox.ini": [ + "layer:container-runtime-common", + "static", + "ecfad595576db991e0910219e2debaf1d4c1e056054f5dbacc8f3e32c0a03840" + ], + "version": [ + "containerd", + "dynamic", + "b56954e631fc8006577e9ce5f54fd067e695024061bc82733fb1408f1880a860" + ], + "wheelhouse.txt": [ + "containerd", + "dynamic", + "ae13f54eb8741a216957d0d6c39051a9d88641cb3050331d03742dc895c71959" + ], + "wheelhouse/Jinja2-2.10.1.tar.gz": [ + "layer:basic", + "dynamic", + "065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013" + ], + "wheelhouse/MarkupSafe-1.1.1.tar.gz": [ + "layer:basic", + "dynamic", + "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b" + ], + "wheelhouse/PyYAML-5.2.tar.gz": [ + "layer:basic", + "dynamic", + "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c" + ], + "wheelhouse/Tempita-0.5.2.tar.gz": [ + "__pip__", + "dynamic", + "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c" + ], + "wheelhouse/certifi-2021.10.8.tar.gz": [ + "__pip__", + "dynamic", + "78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872" + ], + "wheelhouse/charmhelpers-0.20.23.tar.gz": [ + "layer:basic", + "dynamic", + "59a9776594e91cd3e3e000043f8668b4d7b279422dbb17e320f01dc16385b80e" + ], + "wheelhouse/charms.reactive-1.4.1.tar.gz": [ + "layer:basic", + "dynamic", + "bba21b4fd40b26c240c9ef2aa10c6fdf73592031c68591da4e7ccc46ca9cb616" + ], + "wheelhouse/charset-normalizer-2.0.7.tar.gz": [ + "__pip__", + "dynamic", + "e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0" + ], + "wheelhouse/idna-3.3.tar.gz": [ + "__pip__", + "dynamic", + "9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "wheelhouse/netaddr-0.7.19.tar.gz": [ + "layer:basic", + "dynamic", + "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd" + ], + "wheelhouse/pbr-5.6.0.tar.gz": [ + "__pip__", + "dynamic", + "42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd" + ], + "wheelhouse/pip-18.1.tar.gz": [ + "layer:basic", + "dynamic", + "c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1" + ], + "wheelhouse/pyaml-21.10.1.tar.gz": [ + "__pip__", + "dynamic", + "c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383" + ], + "wheelhouse/requests-2.26.0.tar.gz": [ + "containerd", + "dynamic", + "b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + ], + "wheelhouse/setuptools-41.6.0.zip": [ + "layer:basic", + "dynamic", + "6afa61b391dcd16cb8890ec9f66cc4015a8a31a6e1c2b4e0c464514be1a3d722" + ], + "wheelhouse/setuptools_scm-1.17.0.tar.gz": [ + "layer:basic", + "dynamic", + "70a4cf5584e966ae92f54a764e6437af992ba42ac4bca7eb37cc5d02b98ec40a" + ], + "wheelhouse/six-1.16.0.tar.gz": [ + "__pip__", + "dynamic", + "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926" + ], + "wheelhouse/urllib3-1.26.7.tar.gz": [ + "__pip__", + "dynamic", + "4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece" + ], + "wheelhouse/wheel-0.33.6.tar.gz": [ + "layer:basic", + "dynamic", + "10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646" + ] + } +} \ No newline at end of file diff --git a/containerd/.github/workflows/main.yaml b/containerd/.github/workflows/main.yaml new file mode 100644 index 0000000..c2324fe --- /dev/null +++ b/containerd/.github/workflows/main.yaml @@ -0,0 +1,38 @@ +name: Run tests with Tox + +on: [push] + +jobs: + unit-tests: + name: Lint, Unit Tests + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.5, 3.6, 3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Tox + run: pip install tox + - name: Run Tox + run: tox # Run tox using the version of Python in `PATH` + + integration-tests: + name: Integration test with LXD + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@main + with: + provider: lxd + - name: Run integration test + run: tox -e integration diff --git a/containerd/.github/workflows/main.yml b/containerd/.github/workflows/main.yml new file mode 100644 index 0000000..565bfaf --- /dev/null +++ b/containerd/.github/workflows/main.yml @@ -0,0 +1,50 @@ +name: Test Suite +on: [pull_request] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.5, 3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + - name: Run lint + run: tox -e flake8 + functional-test: + name: Functional test with LXD + runs-on: ubuntu-latest + timeout-minutes: 360 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install Dependencies + run: | + pip install tox + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@master + - name: Run test + run: tox -e func + - name: Show Status + if: ${{ always() }} + run: | + model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/); + juju status -m "$model" + - name: Show Error Logs + if: ${{ always() }} + run: | + model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/); + juju debug-log -m "$model" --replay --no-tail --level ERROR diff --git a/containerd/.gitignore b/containerd/.gitignore new file mode 100644 index 0000000..32cbc67 --- /dev/null +++ b/containerd/.gitignore @@ -0,0 +1,4 @@ +.coverage +.tox/ +__pycache__/ +*.pyc diff --git a/containerd/.travis.yml b/containerd/.travis.yml new file mode 100644 index 0000000..ba89235 --- /dev/null +++ b/containerd/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: + - "3.5" + - "3.6" + - "3.7" + - "3.8" +install: + - pip install tox-travis +script: + - tox diff --git a/containerd/LICENSE b/containerd/LICENSE new file mode 100644 index 0000000..1e428b8 --- /dev/null +++ b/containerd/LICENSE @@ -0,0 +1,203 @@ + + 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 2019 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. + diff --git a/containerd/Makefile b/containerd/Makefile new file mode 100644 index 0000000..a1ad3a5 --- /dev/null +++ b/containerd/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/containerd/README.md b/containerd/README.md new file mode 100644 index 0000000..9a64349 --- /dev/null +++ b/containerd/README.md @@ -0,0 +1,14 @@ +# Charm for Containerd + +This subordinate charm deploys the [Containerd](https://containerd.io/) +engine within a running Juju model. Containerd is an open platform +for developers and sysadmins to build, ship, and run distributed applications +in containers. + +Containerd focuses on distributing applications as containers that can be quickly +assembled from components that are run the same on different servers without +environmental dependencies. This eliminates the friction between development, +QA, and production environments. + +This charm is a component of Charmed Kubernetes. For full information, +please visit the [official Charmed Kubernetes docs](https://www.ubuntu.com/kubernetes/docs/charm-containerd). diff --git a/containerd/actions.yaml b/containerd/actions.yaml new file mode 100644 index 0000000..68539b1 --- /dev/null +++ b/containerd/actions.yaml @@ -0,0 +1,4 @@ +"debug": + "description": "Collect debug data" +"upgrade-containerd": + "description": "Force upgrades Containerd to latest repository version" diff --git a/containerd/actions/debug b/containerd/actions/debug new file mode 100755 index 0000000..8ba160e --- /dev/null +++ b/containerd/actions/debug @@ -0,0 +1,102 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import tarfile +import tempfile +import traceback +from contextlib import contextmanager +from datetime import datetime +from charmhelpers.core.hookenv import action_set, local_unit + +archive_dir = None +log_file = None + + +@contextmanager +def archive_context(): + """ Open a context with a new temporary directory. + + When the context closes, the directory is archived, and the archive + location is added to Juju action output. """ + global archive_dir + global log_file + with tempfile.TemporaryDirectory() as temp_dir: + name = "debug-" + datetime.now().strftime("%Y%m%d%H%M%S") + archive_dir = os.path.join(temp_dir, name) + os.makedirs(archive_dir) + with open("%s/debug.log" % archive_dir, "w") as log_file: + yield + os.chdir(temp_dir) + tar_path = "/home/ubuntu/%s.tar.gz" % name + with tarfile.open(tar_path, "w:gz") as f: + f.add(name) + action_set({ + "path": tar_path, + "command": "juju scp %s:%s ." % (local_unit(), tar_path), + "message": " ".join([ + "Archive has been created on unit %s." % local_unit(), + "Use the juju scp command to copy it to your local machine." + ]) + }) + + +def log(msg): + """ Log a message that will be included in the debug archive. + + Must be run within archive_context """ + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for line in str(msg).splitlines(): + log_file.write(timestamp + " | " + line.rstrip() + "\n") + + +def run_script(script): + """ Run a single script. Must be run within archive_context """ + log("Running script: " + script) + script_dir = os.path.join(archive_dir, script) + os.makedirs(script_dir) + env = os.environ.copy() + env["PYTHONPATH"] = "lib" # allow same imports as reactive code + env["DEBUG_SCRIPT_DIR"] = script_dir + with open(script_dir + "/stdout", "w") as stdout: + with open(script_dir + "/stderr", "w") as stderr: + process = subprocess.Popen( + "debug-scripts/" + script, + stdout=stdout, stderr=stderr, env=env + ) + try: + exit_code = process.wait(timeout=300) + except subprocess.TimeoutExpired: + log("ERROR: still running, terminating") + process.terminate() + try: + exit_code = process.wait(timeout=10) + except subprocess.TimeoutExpired: + log("ERROR: still running, killing") + process.kill() + exit_code = process.wait(timeout=10) + if exit_code != 0: + log("ERROR: %s failed with exit code %d" % (script, exit_code)) + + +def run_all_scripts(): + """ Run all scripts. For the sake of robustness, log and ignore any + exceptions that occur. + + Must be run within archive_context """ + scripts = os.listdir("debug-scripts") + for script in scripts: + try: + run_script(script) + except: + log(traceback.format_exc()) + + +def main(): + """ Open an archive context and run all scripts. """ + with archive_context(): + run_all_scripts() + + +if __name__ == "__main__": + main() diff --git a/containerd/actions/upgrade-containerd b/containerd/actions/upgrade-containerd new file mode 100755 index 0000000..894bf23 --- /dev/null +++ b/containerd/actions/upgrade-containerd @@ -0,0 +1,43 @@ +#!/usr/local/sbin/charm-env python3 + +from charmhelpers.core.hookenv import ( + action_set, + action_fail +) + +from charmhelpers.fetch import ( + apt_install, + apt_update, + apt_hold, + apt_unhold +) + +from charmhelpers.core.host import service_restart + +from charms.reactive import remove_state + +from reactive.containerd import CONTAINERD_PACKAGE + + +def main(): + """ + Upgrade containerd to the latest in apt. + + :return: None + """ + try: + apt_update(fatal=True) + apt_unhold(CONTAINERD_PACKAGE) + apt_install(CONTAINERD_PACKAGE, fatal=True) + apt_hold(CONTAINERD_PACKAGE) + service_restart(CONTAINERD_PACKAGE) + + remove_state('containerd.version-published') + action_set({'runtime': CONTAINERD_PACKAGE}) + + except Exception as e: + action_fail(e) + + +if __name__ == '__main__': + main() diff --git a/containerd/bin/charm-env b/containerd/bin/charm-env new file mode 100755 index 0000000..d211ce9 --- /dev/null +++ b/containerd/bin/charm-env @@ -0,0 +1,107 @@ +#!/bin/bash + +VERSION="1.0.0" + + +find_charm_dirs() { + # Hopefully, $JUJU_CHARM_DIR is set so which venv to use in unambiguous. + if [[ -n "$JUJU_CHARM_DIR" || -n "$CHARM_DIR" ]]; then + if [[ -z "$JUJU_CHARM_DIR" ]]; then + # accept $CHARM_DIR to be more forgiving + export JUJU_CHARM_DIR="$CHARM_DIR" + fi + if [[ -z "$CHARM_DIR" ]]; then + # set CHARM_DIR as well to help with backwards compatibility + export CHARM_DIR="$JUJU_CHARM_DIR" + fi + return + fi + # Try to guess the value for JUJU_CHARM_DIR by looking for a non-subordinate + # (because there's got to be at least one principle) charm directory; + # if there are several, pick the first by alpha order. + agents_dir="/var/lib/juju/agents" + if [[ -d "$agents_dir" ]]; then + desired_charm="$1" + found_charm_dir="" + if [[ -n "$desired_charm" ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + charm_name="$(grep -o '^['\''"]\?name['\''"]\?:.*' $charm_dir/metadata.yaml 2> /dev/null | sed -e 's/.*: *//' -e 's/['\''"]//g')" + if [[ "$charm_name" == "$desired_charm" ]]; then + if [[ -n "$found_charm_dir" ]]; then + >&2 echo "Ambiguous possibilities for JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + found_charm_dir="$charm_dir" + fi + done + if [[ -z "$found_charm_dir" ]]; then + >&2 echo "Unable to determine JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + export JUJU_CHARM_DIR="$found_charm_dir" + export CHARM_DIR="$found_charm_dir" + return + fi + # shellcheck disable=SC2126 + non_subordinates="$(grep -L 'subordinate"\?:.*true' "$agents_dir"/unit-*/charm/metadata.yaml | wc -l)" + if [[ "$non_subordinates" -gt 1 ]]; then + >&2 echo 'Ambiguous possibilities for JUJU_CHARM_DIR; please use --charm or run within a Juju hook context' + exit 1 + elif [[ "$non_subordinates" -eq 1 ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + if grep -q 'subordinate"\?:.*true' "$charm_dir/metadata.yaml"; then + continue + fi + export JUJU_CHARM_DIR="$charm_dir" + export CHARM_DIR="$charm_dir" + return + done + fi + fi + >&2 echo 'Unable to determine JUJU_CHARM_DIR; please run within a Juju hook context' + exit 1 +} + +try_activate_venv() { + if [[ -d "$JUJU_CHARM_DIR/../.venv" ]]; then + . "$JUJU_CHARM_DIR/../.venv/bin/activate" + fi +} + +find_wrapped() { + PATH="${PATH/\/usr\/local\/sbin:}" which "$(basename "$0")" +} + + +if [[ "$1" == "--version" || "$1" == "-v" ]]; then + echo "$VERSION" + exit 0 +fi + + +# allow --charm option to hint which JUJU_CHARM_DIR to choose when ambiguous +# NB: --charm option must come first +# NB: option must be processed outside find_charm_dirs to modify $@ +charm_name="" +if [[ "$1" == "--charm" ]]; then + charm_name="$2" + shift; shift +fi + +find_charm_dirs "$charm_name" +try_activate_venv +export PYTHONPATH="$JUJU_CHARM_DIR/lib:$PYTHONPATH" + +if [[ "$(basename "$0")" == "charm-env" ]]; then + # being used as a shebang + exec "$@" +elif [[ "$0" == "$BASH_SOURCE" ]]; then + # being invoked as a symlink wrapping something to find in the venv + exec "$(find_wrapped)" "$@" +elif [[ "$(basename "$BASH_SOURCE")" == "charm-env" ]]; then + # being sourced directly; do nothing + /bin/true +else + # being sourced for wrapped bash helpers + . "$(find_wrapped)" +fi diff --git a/containerd/bin/layer_option b/containerd/bin/layer_option new file mode 100755 index 0000000..3253ef8 --- /dev/null +++ b/containerd/bin/layer_option @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from charms import layer + + +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 = layer.options.get(args.section, 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/containerd/config.yaml b/containerd/config.yaml new file mode 100644 index 0000000..48ec703 --- /dev/null +++ b/containerd/config.yaml @@ -0,0 +1,124 @@ +"options": + "enable-cgroups": + "type": "boolean" + "default": !!bool "false" + "description": | + Enable GRUB cgroup overrides cgroup_enable=memory swapaccount=1. WARNING + changing this option will reboot the host - use with caution on production + services. + "disable-juju-proxy": + "type": "boolean" + "default": !!bool "false" + "description": | + Ignore juju-http(s) proxy settings on this charm. + If set to true, all juju https proxy settings will be ignored + "custom-registry-ca": + "type": "string" + "default": "" + "description": | + Base64 encoded Certificate Authority (CA) bundle. Setting this config + allows container runtimes to pull images from registries with TLS + certificates signed by an external CA. + "custom_registries": + "type": "string" + "default": "[]" + "description": | + Registry endpoints and credentials. Setting this config allows Kubelet + to pull images from registries where auth is required. + + The value for this config must be a JSON array of credential objects, like this: + [{"host": "my.registry:port", "username": "user", "password": "pass"}] + + `host` could be registry host address, e.g.: myregistry.io:9000, 10.10.10.10:5432. + or a name, e.g.: myregistry.io, myregistry. + It will be derived from `url` if not provided, e.g.: + url: http://10.10.10.10:8000 --> host: 10.10.10.10:8000 + + If required, you can supply credentials with option keys 'username' and 'password', + or 'ca_file', 'cert_file', and 'key_file' for ssl/tls communication, + which should be base64 encoded file contents in string form + + "ca_file": "'"$(base64 -w 0 < my.custom.registry.pem)"'" + + example config) + juju config containerd custom_registries='[{ + "url": "https://registry.example.com", + "ca_file": "'"$(base64 -w 0 < ~/my.custom.ca.pem)"'", + "cert_file": "'"$(base64 -w 0 < ~/my.custom.cert.pem)"'", + "key_file": "'"$(base64 -w 0 < ~/my.custom.key.pem)"'", + }]' + "gpu_driver": + "type": "string" + "default": "auto" + "description": | + Override GPU driver installation. Options are "auto", "nvidia", "none". + "runtime": + "type": "string" + "default": "auto" + "description": | + Set a custom containerd runtime. Set "auto" to select based on hardware. + "shim": + "type": "string" + "default": "containerd-shim" + "description": | + Set a custom containerd shim. + "http_proxy": + "type": "string" + "default": "" + "description": | + URL to use for HTTP_PROXY to be used by Containerd. Useful in + egress-filtered environments where a proxy is the only option for + accessing the registry to pull images. + "https_proxy": + "type": "string" + "default": "" + "description": | + URL to use for HTTPS_PROXY to be used by Containerd. Useful in + egress-filtered environments where a proxy is the only option for + accessing the registry to pull images. + "no_proxy": + "type": "string" + "default": "" + "description": | + Comma-separated list of destinations (either domain names or IP + addresses) which should be accessed directly, rather than through + the proxy defined in http_proxy or https_proxy. Must be less than + 2023 characters long. + "config_version": + "type": "string" + "default": "v1" + "description": | + (Use carefully, v2 is only tested for nvidia gpu operator) + Use value "v2" for this config parameter to enable new configuration format. + Config file is parsed as version 1 by default. + Version 2 uses long plugin names, i.e. "io.containerd.grpc.v1.cri" vs "cri". + "nvidia_apt_key_urls": + "type": "string" + "default": | + https://nvidia.github.io/nvidia-container-runtime/gpgkey + https://developer.download.nvidia.com/compute/cuda/repos/{id}{version_id_no_dot}/x86_64/7fa2af80.pub + "description": | + Space-separated list of APT GPG key URLs to add when using Nvidia GPUs. + + Supported template options: + {id}: OS release ID, e.g. "ubuntu" + {version_id}: OS release version ID, e.g. "20.04" + {version_id_no_dot}: OS release version ID with no dot, e.g. "2004" + "nvidia_apt_sources": + "type": "string" + "default": | + deb https://nvidia.github.io/libnvidia-container/{id}{version_id}/$(ARCH) / + deb https://nvidia.github.io/nvidia-container-runtime/{id}{version_id}/$(ARCH) / + deb http://developer.download.nvidia.com/compute/cuda/repos/{id}{version_id_no_dot}/x86_64 / + "description": | + Newline-separated list of APT sources to add when using Nvidia GPUs. + + Supported template options: + {id}: OS release ID, e.g. "ubuntu" + {version_id}: OS release version ID, e.g. "20.04" + {version_id_no_dot}: OS release version ID with no dot, e.g. "2004" + "nvidia_apt_packages": + "type": "string" + "default": "cuda-drivers nvidia-container-runtime" + "description": | + Space-separated list of APT packages to install when using Nvidia GPUs. diff --git a/containerd/copyright b/containerd/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/containerd/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/containerd/copyright.layer-basic b/containerd/copyright.layer-basic new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/containerd/copyright.layer-basic @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/containerd/copyright.layer-options b/containerd/copyright.layer-options new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/containerd/copyright.layer-options @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/containerd/debug-scripts/charm-unitdata b/containerd/debug-scripts/charm-unitdata new file mode 100755 index 0000000..d2aac60 --- /dev/null +++ b/containerd/debug-scripts/charm-unitdata @@ -0,0 +1,12 @@ +#!/usr/local/sbin/charm-env python3 + +import debug_script +import json +from charmhelpers.core import unitdata + +kv = unitdata.kv() +data = kv.getrange("") + +with debug_script.open_file("unitdata.json", "w") as f: + json.dump(data, f, indent=2) + f.write("\n") diff --git a/containerd/debug-scripts/filesystem b/containerd/debug-scripts/filesystem new file mode 100755 index 0000000..c5ec6d8 --- /dev/null +++ b/containerd/debug-scripts/filesystem @@ -0,0 +1,17 @@ +#!/bin/sh +set -ux + +# report file system disk space usage +df -hT > $DEBUG_SCRIPT_DIR/df-hT +# estimate file space usage +du -h / 2>&1 > $DEBUG_SCRIPT_DIR/du-h +# list the mounted filesystems +mount > $DEBUG_SCRIPT_DIR/mount +# list the mounted systems with ascii trees +findmnt -A > $DEBUG_SCRIPT_DIR/findmnt +# list block devices +lsblk > $DEBUG_SCRIPT_DIR/lsblk +# list open files +lsof 2>&1 > $DEBUG_SCRIPT_DIR/lsof +# list local system locks +lslocks > $DEBUG_SCRIPT_DIR/lslocks diff --git a/containerd/debug-scripts/juju-logs b/containerd/debug-scripts/juju-logs new file mode 100755 index 0000000..d27c458 --- /dev/null +++ b/containerd/debug-scripts/juju-logs @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +cp -v /var/log/juju/* $DEBUG_SCRIPT_DIR diff --git a/containerd/debug-scripts/juju-network-get b/containerd/debug-scripts/juju-network-get new file mode 100755 index 0000000..983c8c4 --- /dev/null +++ b/containerd/debug-scripts/juju-network-get @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import yaml +import debug_script + +with open('metadata.yaml') as f: + metadata = yaml.load(f) + +relations = [] +for key in ['requires', 'provides', 'peers']: + relations += list(metadata.get(key, {}).keys()) + +os.mkdir(os.path.join(debug_script.dir, 'relations')) + +for relation in relations: + path = 'relations/' + relation + with debug_script.open_file(path, 'w') as f: + cmd = ['network-get', relation] + subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) diff --git a/containerd/debug-scripts/network b/containerd/debug-scripts/network new file mode 100755 index 0000000..944a355 --- /dev/null +++ b/containerd/debug-scripts/network @@ -0,0 +1,11 @@ +#!/bin/sh +set -ux + +ifconfig -a > $DEBUG_SCRIPT_DIR/ifconfig +cp -v /etc/resolv.conf $DEBUG_SCRIPT_DIR/resolv.conf +cp -v /etc/network/interfaces $DEBUG_SCRIPT_DIR/interfaces +netstat -planut > $DEBUG_SCRIPT_DIR/netstat +route -n > $DEBUG_SCRIPT_DIR/route +iptables-save > $DEBUG_SCRIPT_DIR/iptables-save +dig google.com > $DEBUG_SCRIPT_DIR/dig-google +ping -w 2 -i 0.1 google.com > $DEBUG_SCRIPT_DIR/ping-google diff --git a/containerd/debug-scripts/packages b/containerd/debug-scripts/packages new file mode 100755 index 0000000..b60a9cf --- /dev/null +++ b/containerd/debug-scripts/packages @@ -0,0 +1,7 @@ +#!/bin/sh +set -ux + +dpkg --list > $DEBUG_SCRIPT_DIR/dpkg-list +snap list > $DEBUG_SCRIPT_DIR/snap-list +pip2 list > $DEBUG_SCRIPT_DIR/pip2-list +pip3 list > $DEBUG_SCRIPT_DIR/pip3-list diff --git a/containerd/debug-scripts/sysctl b/containerd/debug-scripts/sysctl new file mode 100755 index 0000000..a86a6c8 --- /dev/null +++ b/containerd/debug-scripts/sysctl @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +sysctl -a > $DEBUG_SCRIPT_DIR/sysctl diff --git a/containerd/debug-scripts/systemd b/containerd/debug-scripts/systemd new file mode 100755 index 0000000..8bb9b6f --- /dev/null +++ b/containerd/debug-scripts/systemd @@ -0,0 +1,9 @@ +#!/bin/sh +set -ux + +systemctl --all > $DEBUG_SCRIPT_DIR/systemctl +journalctl > $DEBUG_SCRIPT_DIR/journalctl +systemd-analyze time > $DEBUG_SCRIPT_DIR/systemd-analyze-time +systemd-analyze blame > $DEBUG_SCRIPT_DIR/systemd-analyze-blame +systemd-analyze critical-chain > $DEBUG_SCRIPT_DIR/systemd-analyze-critical-chain +systemd-analyze dump > $DEBUG_SCRIPT_DIR/systemd-analyze-dump diff --git a/containerd/docs/status.md b/containerd/docs/status.md new file mode 100644 index 0000000..c6cceab --- /dev/null +++ b/containerd/docs/status.md @@ -0,0 +1,91 @@ +

WorkloadState

+ +```python +WorkloadState(self, /, *args, **kwargs) +``` + +Enum of the valid workload states. + +Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + +

maintenance

+ +```python +maintenance(message) +``` + +Set the status to the `MAINTENANCE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

maint

+ +```python +maint(message) +``` + +Shorthand alias for +[maintenance](status.md#charms.layer.status.maintenance). + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

blocked

+ +```python +blocked(message) +``` + +Set the status to the `BLOCKED` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

waiting

+ +```python +waiting(message) +``` + +Set the status to the `WAITING` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

active

+ +```python +active(message) +``` + +Set the status to the `ACTIVE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

status_set

+ +```python +status_set(workload_state, message) +``` + +Set the status to the given workload state with a message. + +__Parameters__ + +- __`workload_state` (WorkloadState or str)__: State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. +- __`message` (str)__: Message to convey to the operator. + diff --git a/containerd/hooks/config-changed b/containerd/hooks/config-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/config-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/containerd-relation-broken b/containerd/hooks/containerd-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/containerd-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/containerd-relation-changed b/containerd/hooks/containerd-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/containerd-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/containerd-relation-created b/containerd/hooks/containerd-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/containerd-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/containerd-relation-departed b/containerd/hooks/containerd-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/containerd-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/containerd-relation-joined b/containerd/hooks/containerd-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/containerd-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/docker-registry-relation-broken b/containerd/hooks/docker-registry-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/docker-registry-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/docker-registry-relation-changed b/containerd/hooks/docker-registry-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/docker-registry-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/docker-registry-relation-created b/containerd/hooks/docker-registry-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/docker-registry-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/docker-registry-relation-departed b/containerd/hooks/docker-registry-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/docker-registry-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/docker-registry-relation-joined b/containerd/hooks/docker-registry-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/docker-registry-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/hook.template b/containerd/hooks/hook.template new file mode 100644 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/hook.template @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/install b/containerd/hooks/install new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/install @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/leader-elected b/containerd/hooks/leader-elected new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/leader-elected @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/leader-settings-changed b/containerd/hooks/leader-settings-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/leader-settings-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/post-series-upgrade b/containerd/hooks/post-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/post-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/pre-series-upgrade b/containerd/hooks/pre-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/pre-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/relations/container-runtime/.gitignore b/containerd/hooks/relations/container-runtime/.gitignore new file mode 100644 index 0000000..894a44c --- /dev/null +++ b/containerd/hooks/relations/container-runtime/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/containerd/hooks/relations/container-runtime/LICENSE b/containerd/hooks/relations/container-runtime/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/containerd/hooks/relations/container-runtime/LICENSE @@ -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/containerd/hooks/relations/container-runtime/README.md b/containerd/hooks/relations/container-runtime/README.md new file mode 100644 index 0000000..4620013 --- /dev/null +++ b/containerd/hooks/relations/container-runtime/README.md @@ -0,0 +1,45 @@ +# interface-container-runtime + +## Overview + +This interface handles communication between subordinate charms, that provide a container runtime and charms requiring a container runtime. + +## Usage + +### Provides + +The providing side of the container interface provides a place for a container runtime to connect to. + +Your charm should respond to the `endpoint.{endpoint_name}.available` state, +which indicates that there is a container runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def update_kubelet_config(containerd): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + config = endpoint.get_config() + kubelet.config['container-runtime'] = \ + config['runtime'] +``` + +### Requires + +The requiring side of the container interface requires a place for a container runtime to connect to. + +Your charm should set `{endpoint_name}.available` state, +which indicates that the container is runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def pubish_config(): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + endpoint.set_config( + socket='unix:///var/run/containerd/containerd.sock', + runtime='remote', + nvidia_enabled=False + ) +``` diff --git a/containerd/hooks/relations/container-runtime/__init__.py b/containerd/hooks/relations/container-runtime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/containerd/hooks/relations/container-runtime/interface.yaml b/containerd/hooks/relations/container-runtime/interface.yaml new file mode 100644 index 0000000..294be1e --- /dev/null +++ b/containerd/hooks/relations/container-runtime/interface.yaml @@ -0,0 +1,4 @@ +name: container-runtime +summary: Interface for relating to container runtimes +version: 1 +maintainer: "Joe Borg " diff --git a/containerd/hooks/relations/container-runtime/provides.py b/containerd/hooks/relations/container-runtime/provides.py new file mode 100644 index 0000000..a9768a8 --- /dev/null +++ b/containerd/hooks/relations/container-runtime/provides.py @@ -0,0 +1,55 @@ +from charms.reactive import ( + Endpoint, + toggle_flag +) + + +class ContainerRuntimeProvides(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined) + + def _get_config(self, key): + """ + Get the published configuration for a given key. + + :param key: String dict key + :return: String value for given key + """ + return self.all_joined_units.received.get(key) + + def get_nvidia_enabled(self): + """ + Get the published nvidia config. + + :return: String + """ + return self._get_config(key='nvidia_enabled') + + def get_runtime(self): + """ + Get the published runtime config. + + :return: String + """ + return self._get_config(key='runtime') + + def get_socket(self): + """ + Get the published socket config. + + :return: String + """ + return self._get_config(key='socket') + + def set_config(self, sandbox_image=None): + """ + Set the configuration to be published. + + :param sandbox_image: String to optionally override the sandbox image + :return: None + """ + for relation in self.relations: + relation.to_publish.update({ + 'sandbox_image': sandbox_image + }) diff --git a/containerd/hooks/relations/container-runtime/requires.py b/containerd/hooks/relations/container-runtime/requires.py new file mode 100644 index 0000000..c461b68 --- /dev/null +++ b/containerd/hooks/relations/container-runtime/requires.py @@ -0,0 +1,61 @@ +from charms.reactive import ( + Endpoint, + clear_flag, + data_changed, + is_data_changed, + toggle_flag +) + + +class ContainerRuntimeRequires(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined) + toggle_flag(self.expand_name('endpoint.{endpoint_name}.reconfigure'), + self.is_joined and self._config_changed()) + + def _config_changed(self): + """ + Determine if our received data has changed. + + :return: Boolean + """ + # NB: this call should match whatever we're tracking in handle_remote_config + return is_data_changed('containerd.remote_config', + [self.get_sandbox_image()]) + + def handle_remote_config(self): + """ + Keep track of received data so we can know if it changes. + + :return: None + """ + clear_flag(self.expand_name('endpoint.{endpoint_name}.reconfigure')) + # Presently, we only care about one piece of remote config. Expand + # the list as needed. + data_changed('containerd.remote_config', + [self.get_sandbox_image()]) + + def get_sandbox_image(self): + """ + Get the sandbox image URI if a remote has published one. + + :return: String: remotely configured sandbox image + """ + return self.all_joined_units.received.get('sandbox_image') + + def set_config(self, socket, runtime, nvidia_enabled): + """ + Set the configuration to be published. + + :param socket: String uri to runtime socket + :param runtime: String runtime executable + :param nvidia_enabled: Boolean nvidia runtime enabled + :return: None + """ + for relation in self.relations: + relation.to_publish.update({ + 'socket': socket, + 'runtime': runtime, + 'nvidia_enabled': nvidia_enabled + }) diff --git a/containerd/hooks/relations/docker-registry/.gitignore b/containerd/hooks/relations/docker-registry/.gitignore new file mode 100644 index 0000000..3374ec2 --- /dev/null +++ b/containerd/hooks/relations/docker-registry/.gitignore @@ -0,0 +1,5 @@ +# Emacs save files +*~ +\#*\# +.\#* + diff --git a/containerd/hooks/relations/docker-registry/LICENSE b/containerd/hooks/relations/docker-registry/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/containerd/hooks/relations/docker-registry/LICENSE @@ -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/containerd/hooks/relations/docker-registry/README.md b/containerd/hooks/relations/docker-registry/README.md new file mode 100644 index 0000000..8257875 --- /dev/null +++ b/containerd/hooks/relations/docker-registry/README.md @@ -0,0 +1,35 @@ +# Overview + +This layer encapsulates the `docker-registry` interface communication +protocol and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:docker-registry` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:docker-registry'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `docker-registry` interface protocol: + +```yaml +requires: + docker-registry: + interface: docker-registry +``` + +React to changes from `docker-registry` as follows: + +```python +@when('endpoint.docker-registry.ready') + def registry_ready(): + registry = endpoint_from_flag('endpoint.docker-registry.ready') + configure_registry(registry.registry_netloc) + if registry.has_auth_basic(): + configure_auth(registry.basic_user, + registry.basic_password) +``` diff --git a/containerd/hooks/relations/docker-registry/__init__.py b/containerd/hooks/relations/docker-registry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/containerd/hooks/relations/docker-registry/interface.yaml b/containerd/hooks/relations/docker-registry/interface.yaml new file mode 100644 index 0000000..339f1c0 --- /dev/null +++ b/containerd/hooks/relations/docker-registry/interface.yaml @@ -0,0 +1,4 @@ +name: docker-registry +summary: Docker Registry Interface +version: 1 +repo: https://github.com/juju-solutions/interface-docker-registry diff --git a/containerd/hooks/relations/docker-registry/provides.py b/containerd/hooks/relations/docker-registry/provides.py new file mode 100644 index 0000000..58717b5 --- /dev/null +++ b/containerd/hooks/relations/docker-registry/provides.py @@ -0,0 +1,114 @@ +""" +This is the provides side of the interface layer, for use only by the +docker-registry charm itself. +The flags that are set by the provides side of this interface are: +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for docker-registry config. + The docker-registry integration charm should then iterate over each + request, perform whatever actions are necessary to satisfy those requests, + and then mark them as complete. +""" + +from operator import attrgetter + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class DockerRegistryProvides(Endpoint): + """ + Example usage: + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + @when('endpoint.docker-registry.joined') + def configure_client(): + registry = endpoint_from_flag('endpoint.docker-registry.joined') + registry.set_registry_config(netloc, **data) + @when('endpoint.docker-registry.requests-pending') + def handle_image_request(): + registry = endpoint_from_flag('endpoint.docker-registry.requests-pending') + for request in registry.requests: + request.image_data(name, tag) + registry.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + toggle_flag(self.expand_name('requests-pending'), + len(self.requests) > 0) + clear_flag(self.expand_name('changed')) + + @property + def requests(self): + """ + A list of the new or updated #RegistryRequests that + have been made. + """ + if not hasattr(self, '_requests'): + all_requests = [RegistryRequest(unit) + for unit in self.all_joined_units] + is_changed = attrgetter('is_changed') + self._requests = list(filter(is_changed, all_requests)) + return self._requests + + def mark_completed(self): + """ + Mark all requests as completed and remove the `requests-pending` flag. + """ + clear_flag(self.expand_name('requests-pending')) + self._requests = [] + + def set_registry_config(self, registry_netloc, **kwargs): + """ + Set the registry config. Minimally, a network location is required. + Other data (auth, tls, etc) may also be set. + """ + data = {'registry_netloc': registry_netloc} + for k, v in kwargs.items(): + data[k] = v + for relation in self.relations: + relation.to_publish.update(data) + + +class RegistryRequest: + """ + A request from a single remote unit to include an image in our registry. + """ + def __init__(self, unit): + self._unit = unit + + @property + def _to_publish(self): + return self._unit.relation.to_publish + + @property + def has_image(self): + """ + Whether or not an image has been processed via `image_data`. + """ + return 'image' in self._unit.relation.to_publish + + @property + def is_changed(self): + """ + Whether this request has changed since the last time it was + marked completed (if ever). + """ + return not self.has_image + + @property + def unit_name(self): + return self._unit.unit_name + + def image_data(self, image, tag): + """ + Set the image characteristics this request. + """ + data = { + 'image': image, + 'tag': tag, + } + self._unit.relation.to_publish.update(data) diff --git a/containerd/hooks/relations/docker-registry/requires.py b/containerd/hooks/relations/docker-registry/requires.py new file mode 100644 index 0000000..c638b57 --- /dev/null +++ b/containerd/hooks/relations/docker-registry/requires.py @@ -0,0 +1,114 @@ +""" +This is the requires side of the interface layer, for use in charms that wish +to request docker-registry data. The data will be provided by the +docker-registry charm. +The flags that are set by the requires side of this interface are: +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific registry data. This flag is automatically removed + if the relation is broken. It should not be removed by the charm. +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + config has been enabled for the registry instance on which the charm is + running. This flag is automatically removed if new integration features are + requested. It should not be removed by the charm. +""" + + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import clear_flag, toggle_flag + + +class DockerRegistryRequires(Endpoint): + """ + Interface to request registry config. + Example usage: + ```python + from charms.reactive import when, endpoint_from_flag + @when('endpoint.docker-registry.ready') + def registry_ready(): + registry = endpoint_from_flag('endpoint.docker-registry.joined') + update_config(registry.registry_netloc) + ``` + """ + + @property + def _received(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single docker-registry application with a + single unit. + """ + return self.all_joined_units.received + + @property + def _to_publish(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single docker-registry application with a + single unit. + """ + return self.relations[0].to_publish + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + toggle_flag(self.expand_name('ready'), self.is_ready) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + def has_auth_basic(self): + """ + Whether or not the registry has basic/htpasswd auth. + """ + return all(field is not None for field in [ + self.basic_password, + self.basic_user, + ]) + + def has_custom_url(self): + """ + Whether or not the registry has a custom URL. + """ + return all(field is not None for field in [ + self.registry_url, + ]) + + def has_tls(self): + """ + Whether or not the registry has TLS certificates configured. + """ + return all(field is not None for field in [ + self.tls_ca, + ]) + + @property + def is_ready(self): + """ + Whether or not the request for this instance has been completed. + """ + return all(field is not None for field in [ + self.registry_netloc, + ]) + + @property + def basic_password(self): + return self._received.get('basic_password') + + @property + def basic_user(self): + return self._received.get('basic_user') + + @property + def registry_netloc(self): + return self._received.get('registry_netloc') + + @property + def registry_url(self): + return self._received.get('registry_url') + + @property + def tls_ca(self): + return self._received.get('tls_ca') diff --git a/containerd/hooks/relations/untrusted-container-runtime/.gitignore b/containerd/hooks/relations/untrusted-container-runtime/.gitignore new file mode 100644 index 0000000..894a44c --- /dev/null +++ b/containerd/hooks/relations/untrusted-container-runtime/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/containerd/hooks/relations/untrusted-container-runtime/LICENSE b/containerd/hooks/relations/untrusted-container-runtime/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/containerd/hooks/relations/untrusted-container-runtime/LICENSE @@ -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/containerd/hooks/relations/untrusted-container-runtime/README.md b/containerd/hooks/relations/untrusted-container-runtime/README.md new file mode 100644 index 0000000..135dca5 --- /dev/null +++ b/containerd/hooks/relations/untrusted-container-runtime/README.md @@ -0,0 +1,54 @@ +# interface-untrusted-container-runtime + +## Overview + +This interface handles communication between subordinate container runtimes +and this subordinate untrusted container runtime, such as `containerd` and +`kata-containers`. + +## Usage + +### Provides + +The providing side of the container interface provides a place for an +untrusted container runtime to connect to. + +Your charm should respond to the `endpoint.{endpoint_name}.available` state, +which indicates that there is an untrusted container runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def update_kubelet_config(containerd): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + config = endpoint.get_config() + + render( + 'config.toml', + { + 'runtime_name': config['name'], + 'runtime_binary': config['binary_path'] + } + ) +``` + +### Requires + +The requiring side of the untrusted container interface requires a place for +an untrusted container runtime to connect to. + +Your charm should set `{endpoint_name}.available` state, +which indicates that the container is runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def pubish_config(): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + endpoint.set_config( + 'name': 'kata', + 'binary_path': '/usr/bin/kata-runtime' + ) +``` diff --git a/containerd/hooks/relations/untrusted-container-runtime/__init__.py b/containerd/hooks/relations/untrusted-container-runtime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/containerd/hooks/relations/untrusted-container-runtime/interface.yaml b/containerd/hooks/relations/untrusted-container-runtime/interface.yaml new file mode 100644 index 0000000..d0d7dbc --- /dev/null +++ b/containerd/hooks/relations/untrusted-container-runtime/interface.yaml @@ -0,0 +1,4 @@ +name: untrusted-container-runtime +summary: Interface for relating to untrusted container runtimes +version: 1 +maintainer: "Joe Borg " diff --git a/containerd/hooks/relations/untrusted-container-runtime/provides.py b/containerd/hooks/relations/untrusted-container-runtime/provides.py new file mode 100644 index 0000000..09deb26 --- /dev/null +++ b/containerd/hooks/relations/untrusted-container-runtime/provides.py @@ -0,0 +1,28 @@ +from charms.reactive import ( + Endpoint, + set_flag, + clear_flag +) + +from charms.reactive import ( + when, + when_not +) + + +class ContainerRuntimeProvides(Endpoint): + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('endpoint.{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('endpoint.{endpoint_name}.available')) + + def get_config(self): + """ + Get the configuration published. + + :return: Dictionary configuration + """ + return self.all_joined_units.received diff --git a/containerd/hooks/relations/untrusted-container-runtime/requires.py b/containerd/hooks/relations/untrusted-container-runtime/requires.py new file mode 100644 index 0000000..f717ba6 --- /dev/null +++ b/containerd/hooks/relations/untrusted-container-runtime/requires.py @@ -0,0 +1,34 @@ +from charms.reactive import ( + Endpoint, + set_flag, + clear_flag +) + +from charms.reactive import ( + when, + when_not +) + + +class ContainerRuntimeRequires(Endpoint): + @when('endpoint.{endpoint_name}.changed') + def changed(self): + set_flag(self.expand_name('endpoint.{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('endpoint.{endpoint_name}.available')) + + def set_config(self, name, binary_path): + """ + Set the configuration to be published. + + :param name: String name of runtime + :param binary_path: String runtime executable + :return: None + """ + for relation in self.relations: + relation.to_publish.update({ + 'name': name, + 'binary_path': binary_path + }) diff --git a/containerd/hooks/start b/containerd/hooks/start new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/start @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/stop b/containerd/hooks/stop new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/stop @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/untrusted-relation-broken b/containerd/hooks/untrusted-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/untrusted-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/untrusted-relation-changed b/containerd/hooks/untrusted-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/untrusted-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/untrusted-relation-created b/containerd/hooks/untrusted-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/untrusted-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/untrusted-relation-departed b/containerd/hooks/untrusted-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/untrusted-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/untrusted-relation-joined b/containerd/hooks/untrusted-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/untrusted-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/update-status b/containerd/hooks/update-status new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/update-status @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/hooks/upgrade-charm b/containerd/hooks/upgrade-charm new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/containerd/hooks/upgrade-charm @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/containerd/icon.svg b/containerd/icon.svg new file mode 100644 index 0000000..95e747e --- /dev/null +++ b/containerd/icon.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/containerd/layer.yaml b/containerd/layer.yaml new file mode 100644 index 0000000..91252cc --- /dev/null +++ b/containerd/layer.yaml @@ -0,0 +1,24 @@ +"includes": +- "layer:options" +- "layer:basic" +- "layer:status" +- "layer:debug" +- "layer:container-runtime-common" +- "interface:container-runtime" +- "interface:untrusted-container-runtime" +- "interface:docker-registry" +"exclude": [".travis.yml", "tests", "tox.ini", "test-requirements.txt", "unit_tests", + ".coverage", ".tox", "__pycache__"] +"options": + "basic": + "packages": [] + "python_packages": [] + "use_venv": !!bool "true" + "include_system_packages": !!bool "false" + "debug": {} + "status": + "patch-hookenv": !!bool "true" + "container-runtime-common": {} + "containerd": {} +"repo": "https://github.com/charmed-kubernetes/charm-containerd" +"is": "containerd" diff --git a/containerd/lib/charms/layer/__init__.py b/containerd/lib/charms/layer/__init__.py new file mode 100644 index 0000000..a8e0c64 --- /dev/null +++ b/containerd/lib/charms/layer/__init__.py @@ -0,0 +1,60 @@ +import sys +from importlib import import_module +from pathlib import Path + + +def import_layer_libs(): + """ + Ensure that all layer libraries are imported. + + This makes it possible to do the following: + + from charms import layer + + layer.foo.do_foo_thing() + + Note: This function must be called after bootstrap. + """ + for module_file in Path('lib/charms/layer').glob('*'): + module_name = module_file.stem + if module_name in ('__init__', 'basic', 'execd') or not ( + module_file.suffix == '.py' or module_file.is_dir() + ): + continue + import_module('charms.layer.{}'.format(module_name)) + + +# Terrible hack to support the old terrible interface. +# Try to get people to call layer.options.get() instead so +# that we can remove this garbage. +# Cribbed from https://stackoverfLow.com/a/48100440/4941864 +class OptionsBackwardsCompatibilityHack(sys.modules[__name__].__class__): + def __call__(self, section=None, layer_file=None): + if layer_file is None: + return self.get(section=section) + else: + return self.get(section=section, + layer_file=Path(layer_file)) + + +def patch_options_interface(): + from charms.layer import options + if sys.version_info.minor >= 5: + options.__class__ = OptionsBackwardsCompatibilityHack + else: + # Py 3.4 doesn't support changing the __class__, so we have to do it + # another way. The last line is needed because we already have a + # reference that doesn't get updated with sys.modules. + name = options.__name__ + hack = OptionsBackwardsCompatibilityHack(name) + hack.get = options.get + sys.modules[name] = hack + sys.modules[__name__].options = hack + + +try: + patch_options_interface() +except ImportError: + # This may fail if pyyaml hasn't been installed yet. But in that + # case, the bootstrap logic will try it again once it has. + pass diff --git a/containerd/lib/charms/layer/basic.py b/containerd/lib/charms/layer/basic.py new file mode 100644 index 0000000..bbdd074 --- /dev/null +++ b/containerd/lib/charms/layer/basic.py @@ -0,0 +1,501 @@ +import os +import sys +import re +import shutil +from distutils.version import LooseVersion +from pkg_resources import Requirement +from glob import glob +from subprocess import check_call, check_output, CalledProcessError +from time import sleep + +from charms import layer +from charms.layer.execd import execd_preinstall + + +def _get_subprocess_env(): + env = os.environ.copy() + env['LANG'] = env.get('LANG', 'C.UTF-8') + return env + + +def get_series(): + """ + Return series for a few known OS:es. + Tested as of 2019 november: + * centos6, centos7, rhel6. + * bionic + """ + series = "" + + # Looking for content in /etc/os-release + # works for ubuntu + some centos + if os.path.isfile('/etc/os-release'): + d = {} + with open('/etc/os-release', 'r') as rel: + for l in rel: + if not re.match(r'^\s*$', l): + k, v = l.split('=') + d[k.strip()] = v.strip().replace('"', '') + series = "{ID}{VERSION_ID}".format(**d) + + # Looking for content in /etc/redhat-release + # works for redhat enterprise systems + elif os.path.isfile('/etc/redhat-release'): + with open('/etc/redhat-release', 'r') as redhatlsb: + # CentOS Linux release 7.7.1908 (Core) + line = redhatlsb.readline() + release = int(line.split("release")[1].split()[0][0]) + series = "centos" + str(release) + + # Looking for content in /etc/lsb-release + # works for ubuntu + elif os.path.isfile('/etc/lsb-release'): + d = {} + with open('/etc/lsb-release', 'r') as lsb: + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + series = d['DISTRIB_CODENAME'] + + # This is what happens if we cant figure out the OS. + else: + series = "unknown" + return series + + +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 + + series = get_series() + + # OMG?! is build-essentials needed? + ubuntu_packages = ['python3-pip', + 'python3-setuptools', + 'python3-yaml', + 'python3-dev', + 'python3-wheel', + 'build-essential'] + + # I'm not going to "yum group info "Development Tools" + # omitting above madness + centos_packages = ['python3-pip', + 'python3-setuptools', + 'python3-devel', + 'python3-wheel'] + + packages_needed = [] + if 'centos' in series: + packages_needed = centos_packages + else: + packages_needed = ubuntu_packages + + 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') + hook_name = os.path.basename(sys.argv[0]) + is_bootstrapped = os.path.exists('wheelhouse/.bootstrapped') + is_charm_upgrade = hook_name == 'upgrade-charm' + is_series_upgrade = hook_name == 'post-series-upgrade' + is_post_upgrade = os.path.exists('wheelhouse/.upgraded') + is_upgrade = (not is_post_upgrade and + (is_charm_upgrade or is_series_upgrade)) + if is_bootstrapped and not is_upgrade: + # older subordinates might have downgraded charm-env, so we should + # restore it if necessary + install_or_update_charm_env() + activate_venv() + # the .upgrade file prevents us from getting stuck in a loop + # when re-execing to activate the venv; at this point, we've + # activated the venv, so it's safe to clear it + if is_post_upgrade: + os.unlink('wheelhouse/.upgraded') + return + if os.path.exists(venv): + try: + # focal installs or upgrades prior to PR 160 could leave the venv + # in a broken state which would prevent subsequent charm upgrades + _load_installed_versions(vpip) + except CalledProcessError: + is_broken_venv = True + else: + is_broken_venv = False + if is_upgrade or is_broken_venv: + # All upgrades should do a full clear of the venv, rather than + # just updating it, to bring in updates to Python itself + shutil.rmtree(venv) + if is_upgrade: + if os.path.exists('wheelhouse/.bootstrapped'): + os.unlink('wheelhouse/.bootstrapped') + # bootstrap wheelhouse + if os.path.exists('wheelhouse'): + pre_eoan = series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty', + 'ubuntu16.04', 'xenial', + 'ubuntu18.04', 'bionic') + pydistutils_lines = [ + "[easy_install]\n", + "find_links = file://{}/wheelhouse/\n".format(charm_dir), + "no_index=True\n", + "index_url=\n", # deliberately nothing here; disables it. + ] + if pre_eoan: + pydistutils_lines.append("allow_hosts = ''\n") + 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(pydistutils_lines) + if 'centos' in series: + yum_install(packages_needed) + else: + apt_install(packages_needed) + from charms.layer import options + cfg = options.get('basic') + # include packages defined in layer.yaml + if 'centos' in series: + yum_install(cfg.get('packages', [])) + else: + 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 = get_series() + if series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty'): + apt_install(['python-virtualenv']) + elif 'centos' in series: + yum_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, env=_get_subprocess_env()) + 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') + pre_install_pkgs = ['pip', 'setuptools', 'setuptools-scm'] + # we bundle these packages to work around bugs in older versions (such + # as https://github.com/pypa/pip/issues/56), but if the system already + # provided a newer version, downgrading it can cause other problems + _update_if_newer(pip, pre_install_pkgs) + # install the rest of the wheelhouse deps (extract the pkg names into + # a set so that we can ignore the pre-install packages and let pip + # choose the best version in case there are multiple from layer + # conflicts) + _versions = _load_wheelhouse_versions() + _pkgs = _versions.keys() - set(pre_install_pkgs) + # add back the versions such that each package in pkgs is + # ==. + # This ensures that pip 20.3.4+ will install the packages from the + # wheelhouse without (erroneously) flagging an error. + pkgs = _add_back_versions(_pkgs, _versions) + reinstall_flag = '--force-reinstall' + if not cfg.get('use_venv', True) and pre_eoan: + reinstall_flag = '--ignore-installed' + check_call([pip, 'install', '-U', reinstall_flag, '--no-index', + '--no-cache-dir', '-f', 'wheelhouse'] + list(pkgs), + env=_get_subprocess_env()) + # re-enable installation from pypi + os.remove('/root/.pydistutils.cfg') + + # install pyyaml for centos7, since, unlike the ubuntu image, the + # default image for centos doesn't include pyyaml; see the discussion: + # https://discourse.jujucharms.com/t/charms-for-centos-lets-begin + if 'centos' in series: + check_call([pip, 'install', '-U', 'pyyaml'], + env=_get_subprocess_env()) + + # install python packages from layer options + if cfg.get('python_packages'): + check_call([pip, 'install', '-U'] + cfg.get('python_packages'), + env=_get_subprocess_env()) + 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') + # setup wrappers to ensure envs are used for scripts + install_or_update_charm_env() + for wrapper in ('charms.reactive', 'charms.reactive.sh', + 'chlp', 'layer_option'): + src = os.path.join('/usr/local/sbin', 'charm-env') + dst = os.path.join('/usr/local/sbin', wrapper) + if not os.path.exists(dst): + os.symlink(src, dst) + if cfg.get('use_venv'): + shutil.copy2('bin/layer_option', vbin) + else: + shutil.copy2('bin/layer_option', '/usr/local/bin/') + # re-link the charm copy to the wrapper in case charms + # call bin/layer_option directly (as was the old pattern) + os.remove('bin/layer_option') + os.symlink('/usr/local/sbin/layer_option', 'bin/layer_option') + # flag us as having already bootstrapped so we don't do it again + open('wheelhouse/.bootstrapped', 'w').close() + if is_upgrade: + # flag us as having already upgraded so we don't do it again + open('wheelhouse/.upgraded', '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 _load_installed_versions(pip): + pip_freeze = check_output([pip, 'freeze']).decode('utf8') + versions = {} + for pkg_ver in pip_freeze.splitlines(): + try: + req = Requirement.parse(pkg_ver) + except ValueError: + continue + versions.update({ + req.project_name: LooseVersion(ver) + for op, ver in req.specs if op == '==' + }) + return versions + + +def _load_wheelhouse_versions(): + versions = {} + for wheel in glob('wheelhouse/*'): + pkg, ver = os.path.basename(wheel).rsplit('-', 1) + # nb: LooseVersion ignores the file extension + versions[pkg.replace('_', '-')] = LooseVersion(ver) + return versions + + +def _add_back_versions(pkgs, versions): + """Add back the version strings to each of the packages. + + The versions are LooseVersion() from _load_wheelhouse_versions(). This + function strips the ".zip" or ".tar.gz" from the end of the version string + and adds it back to the package in the form of == + + If a package name is not a key in the versions dictionary, then it is + returned in the list unchanged. + + :param pkgs: A list of package names + :type pkgs: List[str] + :param versions: A map of package to LooseVersion + :type versions: Dict[str, LooseVersion] + :returns: A list of (maybe) versioned packages + :rtype: List[str] + """ + def _strip_ext(s): + """Strip an extension (if it exists) from the string + + :param s: the string to strip an extension off if it exists + :type s: str + :returns: string without an extension of .zip or .tar.gz + :rtype: str + """ + for ending in [".zip", ".tar.gz"]: + if s.endswith(ending): + return s[:-len(ending)] + return s + + def _maybe_add_version(pkg): + """Maybe add back the version number to a package if it exists. + + Adds the version number, if the package exists in the lexically + captured `versions` dictionary, in the form ==. Strips + the extension if it exists. + + :param pkg: the package name to (maybe) add the version number to. + :type pkg: str + """ + try: + return "{}=={}".format(pkg, _strip_ext(str(versions[pkg]))) + except KeyError: + pass + return pkg + + return [_maybe_add_version(pkg) for pkg in pkgs] + + +def _update_if_newer(pip, pkgs): + installed = _load_installed_versions(pip) + wheelhouse = _load_wheelhouse_versions() + for pkg in pkgs: + if pkg not in installed or wheelhouse[pkg] > installed[pkg]: + check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', + pkg], env=_get_subprocess_env()) + + +def install_or_update_charm_env(): + # On Trusty python3-pkg-resources is not installed + try: + from pkg_resources import parse_version + except ImportError: + apt_install(['python3-pkg-resources']) + from pkg_resources import parse_version + + try: + installed_version = parse_version( + check_output(['/usr/local/sbin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + installed_version = parse_version('0.0.0') + try: + bundled_version = parse_version( + check_output(['bin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + bundled_version = parse_version('0.0.0') + if installed_version < bundled_version: + shutil.copy2('bin/charm-env', '/usr/local/sbin/') + + +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. + """ + from charms.layer import options + venv = os.path.abspath('../.venv') + vbin = os.path.join(venv, 'bin') + vpy = os.path.join(vbin, 'python') + use_venv = options.get('basic', 'use_venv') + if use_venv and '.venv' not in sys.executable: + # activate the venv + os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) + reload_interpreter(vpy) + layer.patch_options_interface() + layer.import_layer_libs() + + +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 = _get_subprocess_env() + + 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 + try: + # sometimes apt-get update needs to be run + check_call(['apt-get', 'update'], env=env) + except CalledProcessError: + # sometimes it's a dpkg lock issue + pass + sleep(5) + else: + break + + +def yum_install(packages): + """ Installs packages with yum. + This function largely mimics the apt_install function for consistency. + """ + if packages: + env = os.environ.copy() + cmd = ['yum', '-y', 'install'] + for attempt in range(3): + try: + check_call(cmd + packages, env=env) + except CalledProcessError: + if attempt == 2: + raise + try: + check_call(['yum', 'update'], env=env) + except CalledProcessError: + pass + sleep(5) + else: + break + else: + pass + + +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]) + + +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/containerd/lib/charms/layer/container_runtime_common.py b/containerd/lib/charms/layer/container_runtime_common.py new file mode 100644 index 0000000..a13572a --- /dev/null +++ b/containerd/lib/charms/layer/container_runtime_common.py @@ -0,0 +1,122 @@ +import os +import shutil +import ipaddress +from pathlib import Path + +from charmhelpers.core.hookenv import ( + log, + env_proxy_settings +) + + +certs_dir = Path('/root/cdk') +ca_crt_path = certs_dir / 'ca.crt' +server_crt_path = certs_dir / 'server.crt' +server_key_path = certs_dir / 'server.key' +client_crt_path = certs_dir / 'client.crt' +client_key_path = certs_dir / 'client.key' + + +def get_hosts(config): + """ + :param config: Dictionary + :return: String + """ + if config is not None: + hosts = [] + for address in config.get('NO_PROXY', '').split(','): + address = address.strip() + try: + net = ipaddress.ip_network(address) + ip_addresses = [str(ip) for ip in net.hosts()] + if ip_addresses == []: + hosts.append(address) + else: + hosts += ip_addresses + except ValueError: + hosts.append(address) + parsed_hosts = ','.join(hosts) + return parsed_hosts + + +def merge_config(config, environment): + """ + :param config: Dictionary + :param environment: Dictionary + :return: Dictionary + """ + keys = ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY'] + + for key in keys: + if config.get(key.lower(), '') == '' and \ + config.get(key, '') == '': + value = environment.get(key) if environment.get(key, '') != '' \ + else environment.get(key.lower(), '') + + if value != '': + config[key] = value + config[key.lower()] = value + # Normalize + for key in keys: + value = config.get(key) if config.get(key, '') != '' \ + else config.get(key.lower(), '') + config[key] = value + config[key.lower()] = value + + return config + + +def check_for_juju_https_proxy(config): + """ + If config values are defined take precedent. + + LP: https://bugs.launchpad.net/charm-layer-docker/+bug/1831712 + + :param config: Dictionary + :return: Dictionary + """ + environment_config = env_proxy_settings() + charm_config = dict(config()) + + if environment_config is None or \ + charm_config.get('disable-juju-proxy'): + return charm_config + + no_proxy = get_hosts(environment_config) + + environment_config.update({ + 'NO_PROXY': no_proxy, + 'no_proxy': no_proxy + }) + + return merge_config(charm_config, environment_config) + + +def manage_registry_certs(cert_dir, remove=False): + """ + Add or remove TLS data for a specific registry. + + When present, the container runtime will use certificates when + communicating with a specific registry. + + :param cert_dir: String directory to store the client certificates + :param remove: Boolean remove cert data (defauts to add) + :return: None + """ + if remove: + if os.path.isdir(cert_dir): + log('Disabling registry TLS: {}.'.format(cert_dir)) + shutil.rmtree(cert_dir) + else: + os.makedirs(cert_dir, exist_ok=True) + client_tls = { + client_crt_path: os.path.join(cert_dir, 'client.cert'), + client_key_path: os.path.join(cert_dir, 'client.key') + } + for f, link in client_tls.items(): + try: + os.remove(link) + except FileNotFoundError: + pass + log('Creating registry TLS link: {}.'.format(link)) + os.symlink(f, link) diff --git a/containerd/lib/charms/layer/containerd.py b/containerd/lib/charms/layer/containerd.py new file mode 100644 index 0000000..3c2e96b --- /dev/null +++ b/containerd/lib/charms/layer/containerd.py @@ -0,0 +1,37 @@ +from charmhelpers.core import hookenv, host, unitdata + + +def get_sandbox_image(): + """ + Return the container image location for the sandbox_image. + + Set an appropriate sandbox image based on known registries. Precedence should be: + - related docker-registry + - default charmed k8s registry (if related to kubernetes) + - upstream + + :return: str container image location + """ + db = unitdata.kv() + canonical_registry = 'rocks.canonical.com:443/cdk' + upstream_registry = 'k8s.gcr.io' + + docker_registry = db.get('registry', None) + if docker_registry: + sandbox_registry = docker_registry['url'] + else: + try: + deployment = hookenv.goal_state() + except NotImplementedError: + relations = [] + for rid in hookenv.relation_ids('containerd'): + relations.append(hookenv.remote_service_name(rid)) + else: + relations = deployment.get('relations', {}).get('containerd', {}) + + if any(k in relations for k in ('kubernetes-master', 'kubernetes-worker')): + sandbox_registry = canonical_registry + else: + sandbox_registry = upstream_registry + + return '{}/pause-{}:3.4.1'.format(sandbox_registry, host.arch()) diff --git a/containerd/lib/charms/layer/execd.py b/containerd/lib/charms/layer/execd.py new file mode 100644 index 0000000..438d9a1 --- /dev/null +++ b/containerd/lib/charms/layer/execd.py @@ -0,0 +1,114 @@ +# 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 . + +# This module may only import from the Python standard library. +import os +import sys +import subprocess +import time + +''' +execd/preinstall + +Read the layer-basic docs for more info on how to use this feature. +https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#exec-d-support +''' + + +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<" +"description": | + containerd manages the complete container lifecycle of its + host system, from image transfer and storage to container + execution and supervision to low-level storage to network + attachments and beyond. +"tags": +- "misc" +- "containers" +"series": +- "focal" +- "bionic" +- "xenial" +"requires": + "containerd": + "interface": "container-runtime" + "scope": "container" + "docker-registry": + "interface": "docker-registry" +"provides": + "untrusted": + "interface": "untrusted-container-runtime" + "scope": "container" +"subordinate": !!bool "true" diff --git a/containerd/pydocmd.yml b/containerd/pydocmd.yml new file mode 100644 index 0000000..ab3b2ef --- /dev/null +++ b/containerd/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'Status Management Layer' + +generate: + - status.md: + - charms.layer.status.WorkloadState + - charms.layer.status.maintenance + - charms.layer.status.maint + - charms.layer.status.blocked + - charms.layer.status.waiting + - charms.layer.status.active + - charms.layer.status.status_set + +pages: + - Status Management Layer: status.md + +gens_dir: docs diff --git a/containerd/reactive/__init__.py b/containerd/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/containerd/reactive/container_runtime_common.py b/containerd/reactive/container_runtime_common.py new file mode 100644 index 0000000..802c8de --- /dev/null +++ b/containerd/reactive/container_runtime_common.py @@ -0,0 +1,57 @@ +from base64 import b64decode +from subprocess import check_call + +from charms.layer import status +from charms.reactive import ( + clear_flag, + set_flag, + when, + when_not +) + +from charmhelpers.core import hookenv, host + + +@when_not('cgroups.modified') +def enable_grub_cgroups(): + """ + Run script to enable cgroups + in GRUB. Be aware, this will + reboot the host. + + :return: None + """ + cfg = hookenv.config() + if cfg.get('enable-cgroups'): + hookenv.log('Calling enable_grub_cgroups.sh and rebooting machine.') + check_call(['scripts/enable_grub_cgroups.sh']) + set_flag('cgroups.modified') + + +@when('config.changed.custom-registry-ca') +def install_custom_ca(): + """ + Installs a configured CA cert into the system-wide location. + """ + ca_cert = hookenv.config().get('custom-registry-ca') + if ca_cert: + try: + # decode to bytes, as that's what install_ca_cert wants + _ca = b64decode(ca_cert) + except Exception: + status.blocked('Invalid base64 value for custom-registry-ca config') + return + else: + host.install_ca_cert(_ca, name='juju-custom-registry') + charm = hookenv.charm_name() + hookenv.log('Custom registry CA has been installed for {}'.format(charm)) + + # manage appropriate charm flags to recycle the runtime daemon + if charm == 'docker': + clear_flag('docker.available') + set_flag('docker.restart') + elif charm == 'containerd': + set_flag('containerd.restart') + else: + hookenv.log('Unknown runtime: {}. ' + 'Cannot request a service restart.'.format(charm)) diff --git a/containerd/reactive/containerd.py b/containerd/reactive/containerd.py new file mode 100644 index 0000000..f971c3d --- /dev/null +++ b/containerd/reactive/containerd.py @@ -0,0 +1,727 @@ +import os +import base64 +import binascii +import json +import requests +import traceback + +from subprocess import ( + check_call, + check_output, + CalledProcessError +) + +from charms.reactive import ( + hook, + when, + when_not, + set_state, + is_state, + remove_state, + endpoint_from_flag, + register_trigger +) + +from charms.layer import containerd, status +from charms.layer.container_runtime_common import ( + ca_crt_path, + server_crt_path, + server_key_path, + check_for_juju_https_proxy +) + +from charmhelpers.core import ( + host, + unitdata +) + +from charmhelpers.core.templating import render +from charmhelpers.core.hookenv import ( + atexit, + config, + log, + application_version_set +) + +from charmhelpers.core.kernel import modprobe + +from charmhelpers.fetch import ( + apt_install, + apt_update, + apt_purge, + apt_hold, + apt_autoremove, + apt_unhold, + import_key +) + + +DB = unitdata.kv() + +CONTAINERD_PACKAGE = 'containerd' + +register_trigger( + when='config.changed.nvidia_apt_key_urls', + clear_flag='containerd.nvidia.ready' +) +register_trigger( + when='config.changed.nvidia_apt_sources', + clear_flag='containerd.nvidia.ready' +) +register_trigger( + when='config.changed.nvidia_apt_packages', + clear_flag='containerd.nvidia.ready' +) + + +def _check_containerd(): + """ + Check that containerd is running. + + `ctr version` calls both client and server side, so is a reasonable indication that everything's been set up + correctly. + + :return: Boolean + """ + try: + version = check_output(['ctr', 'version']) + except (FileNotFoundError, CalledProcessError): + return None + + return version + + +def _juju_proxy_changed(): + """ + Check to see if the Juju model HTTP(S) proxy settings have changed. + + These aren't propagated to the charm so we'll need to do it here. + + :return: Boolean + """ + cached = DB.get('config-cache', None) + if not cached: + return True # First pass. + + new = check_for_juju_https_proxy(config) + + if cached['http_proxy'] == new['http_proxy'] and \ + cached['https_proxy'] == new['https_proxy'] and \ + cached['no_proxy'] == new['no_proxy']: + return False + + return True + + +@atexit +def charm_status(): + """ + Set the charm's status after each hook is run. + + :return: None + """ + if is_state('upgrade.series.in-progress'): + status.blocked('Series upgrade in progress') + elif is_state('containerd.nvidia.invalid-option'): + status.blocked( + '{} is an invalid option for gpu_driver'.format( + config().get('gpu_driver') + ) + ) + elif _check_containerd(): + status.active('Container runtime available') + set_state('containerd.ready') + else: + status.blocked('Container runtime not available') + + +def strip_url(url): + """Strip the URL of protocol, slashes etc., and keep host:port. + + Examples: + url: http://10.10.10.10:8000 --> return: 10.10.10.10:8000 + url: https://myregistry.io:8000/ --> return: myregistry.io:8000 + url: myregistry.io:8000 --> return: myregistry.io:8000 + """ + return url.rstrip('/').split(sep='://', maxsplit=1)[-1] + + +def update_custom_tls_config(config_directory, registries, old_registries): + """ + Read registries config and remove old/write new tls files from/to disk. + + :param str config_directory: containerd config directory + :param List registries: juju config for custom registries + :param List old_registries: old juju config for custom registries + :return: None + """ + # Remove tls files of old registries; so not to leave uneeded, stale files. + for registry in old_registries: + for opt in ['ca', 'key', 'cert']: + file_b64 = registry.get('%s_file' % opt) + if file_b64: + registry[opt] = os.path.join( + config_directory, "%s.%s" % (strip_url(registry['url']), opt) + ) + if os.path.isfile(registry[opt]): + os.remove(registry[opt]) + + # Write tls files of new registries. + for registry in registries: + for opt in ['ca', 'key', 'cert']: + file_b64 = registry.get('%s_file' % opt) + if file_b64: + try: + file_contents = base64.b64decode(file_b64) + except (binascii.Error, TypeError): + log(traceback.format_exc()) + log("{}:{} didn't look like base64 data... skipping" + .format(registry['url'], opt)) + continue + registry[opt] = os.path.join( + config_directory, "%s.%s" % (strip_url(registry['url']), opt) + ) + with open(registry[opt], 'wb') as f: + f.write(file_contents) + + +def populate_host_for_custom_registries(custom_registries): + """Populate host field from url if missing for custom registries. + + Examples: + url: http://10.10.10.10:8000 --> host: 10.10.10.10:8000 + url: https://myregistry.io:8000/ --> host: myregistry.io:8000 + url: myregistry.io:8000 --> host: myregistry.io:8000 + """ + # only do minimal changes to custom_registries when conditions apply + # otherwise return it directly as it is + if isinstance(custom_registries, list): + for registry in custom_registries: + if not registry.get('host'): + url = registry.get('url') + if url: + registry['host'] = strip_url(url) + + return custom_registries + + +def insert_docker_io_to_custom_registries(custom_registries): + """ + Ensure the default docker.io registry exists. + + Also gives a way for configuration to override the url for it. + If a docker.io host entry doesn't exist, we'll add one. + """ + if isinstance(custom_registries, list): + if not any(d.get('host') == 'docker.io' for d in custom_registries): + custom_registries.insert(0, { + "host": "docker.io", + "url": "https://registry-1.docker.io" + }) + return custom_registries + + +def merge_custom_registries(config_directory, custom_registries, + old_custom_registries): + """ + Merge custom registries and Docker registries from relation. + + :param str config_directory: containerd config directory + :param str custom_registries: juju config for custom registries + :param str old_custom_registries: old juju config for custom registries + :return: List Dictionary merged registries + """ + registries = [] + registries += json.loads(custom_registries) + # json string already converted to python list here + registries = populate_host_for_custom_registries(registries) + registries = insert_docker_io_to_custom_registries(registries) + old_registries = [] + if (old_custom_registries): + old_registries += json.loads(old_custom_registries) + update_custom_tls_config(config_directory, registries, old_registries) + + docker_registry = DB.get('registry', None) + if docker_registry: + registries.append(docker_registry) + + return registries + + +@hook('update-status') +def update_status(): + """ + Triggered when update-status is called. + + :return: None + """ + if _juju_proxy_changed(): + set_state('containerd.juju-proxy.changed') + + +@hook('upgrade-charm') +def upgrade_charm(): + """ + Triggered when upgrade-charm is called. + + :return: None + """ + # Prevent containerd apt pkg from being implicitly updated. + apt_hold(CONTAINERD_PACKAGE) + + # Re-render config in case the template has changed in the new charm. + config_changed() + + # Clean up old nvidia sources.list.d files + old_source_files = [ + '/etc/apt/sources.list.d/nvidia-container-runtime.list', + '/etc/apt/sources.list.d/cuda.list' + ] + for source_file in old_source_files: + if os.path.exists(source_file): + os.remove(source_file) + remove_state('containerd.nvidia.ready') + + +@when_not('containerd.br_netfilter.enabled') +def enable_br_netfilter_module(): + """ + Enable br_netfilter to work around https://github.com/kubernetes/kubernetes/issues/21613. + + :return: None + """ + try: + modprobe('br_netfilter', persist=True) + except Exception: + log(traceback.format_exc()) + if host.is_container(): + log('LXD detected, ignoring failure to load br_netfilter') + else: + log('LXD not detected, will retry loading br_netfilter') + return + set_state('containerd.br_netfilter.enabled') + + +@when_not('containerd.ready', + 'containerd.installed', + 'endpoint.containerd.departed') +def install_containerd(): + """ + Install containerd and then create initial configuration. + + :return: None + """ + status.maintenance('Installing containerd via apt') + apt_update() + apt_install(CONTAINERD_PACKAGE, fatal=True) + apt_hold(CONTAINERD_PACKAGE) + + set_state('containerd.installed') + config_changed() + + +@when('containerd.installed') +@when_not('containerd.version-published') +def publish_version_to_juju(): + """ + Publish the containerd version to Juju. + + :return: None + """ + version_string = _check_containerd() + if not version_string: + return + version = version_string.split()[6].split(b'-')[0].decode() + + application_version_set(version) + set_state('containerd.version-published') + + +@when_not('containerd.nvidia.checked') +@when_not('endpoint.containerd.departed') +def check_for_gpu(): + """ + Check if an Nvidia GPU exists. + + :return: None + """ + valid_options = [ + 'auto', + 'none', + 'nvidia' + ] + + driver_config = config().get('gpu_driver') + if driver_config not in valid_options: + set_state('containerd.nvidia.invalid-option') + return + + out = check_output(['lspci', '-nnk']).rstrip().decode('utf-8').lower() + + if driver_config != 'none': + if (out.count('nvidia') > 0 and driver_config == 'auto') \ + or (driver_config == 'nvidia'): + set_state('containerd.nvidia.available') + else: + remove_state('containerd.nvidia.available') + remove_state('containerd.nvidia.ready') + + remove_state('containerd.nvidia.invalid-option') + set_state('containerd.nvidia.checked') + + +@when('containerd.nvidia.available') +@when_not('containerd.nvidia.ready', 'endpoint.containerd.departed') +def configure_nvidia(): + """ + Based on charm config, install and configure Nivida drivers. + + :return: None + """ + status.maintenance('Installing Nvidia drivers.') + + dist = host.lsb_release() + os_release_id = dist['DISTRIB_ID'].lower() + os_release_version_id = dist['DISTRIB_RELEASE'] + os_release_version_id_no_dot = os_release_version_id.replace('.', '') + proxies = { + "http": config('http_proxy'), + "https": config('https_proxy') + } + key_urls = config('nvidia_apt_key_urls').split() + for key_url in key_urls: + formatted_key_url = key_url.format( + id=os_release_id, + version_id=os_release_version_id, + version_id_no_dot=os_release_version_id_no_dot + ) + gpg_key = requests.get(formatted_key_url, proxies=proxies).text + import_key(gpg_key) + + sources = config('nvidia_apt_sources').splitlines() + formatted_sources = [ + source.format( + id=os_release_id, + version_id=os_release_version_id, + version_id_no_dot=os_release_version_id_no_dot + ) + for source in sources + ] + with open('/etc/apt/sources.list.d/nvidia.list', 'w') as f: + f.write('\n'.join(formatted_sources)) + + apt_update() + packages = config('nvidia_apt_packages').split() + apt_install(packages, fatal=True) + + set_state('containerd.nvidia.ready') + config_changed() + + +@when('endpoint.containerd.departed') +def purge_containerd(): + """ + Purge Containerd from the cluster. + + :return: None + """ + status.maintenance('Removing containerd from principal') + + host.service_stop('containerd.service') + apt_unhold(CONTAINERD_PACKAGE) + apt_purge(CONTAINERD_PACKAGE, fatal=True) + + if is_state('containerd.nvidia.ready'): + nvidia_packages = config('nvidia_apt_packages').split() + apt_purge(nvidia_packages, fatal=True) + + sources = [ + '/etc/apt/sources.list.d/nvidia.list' + ] + + for f in sources: + if os.path.isfile(f): + os.remove(f) + + apt_autoremove(purge=True, fatal=True) + + remove_state('containerd.ready') + remove_state('containerd.installed') + remove_state('containerd.nvidia.ready') + remove_state('containerd.nvidia.checked') + remove_state('containerd.nvidia.available') + remove_state('containerd.version-published') + + +@when('config.changed.gpu_driver') +def gpu_config_changed(): + """ + Remove the GPU checked state when the config is changed. + + :return: None + """ + remove_state('containerd.nvidia.checked') + + +@when('config.changed') +@when_not('endpoint.containerd.departed') +def config_changed(): + """ + Render the config template. + + :return: None + """ + if _juju_proxy_changed(): + set_state('containerd.juju-proxy.changed') + + # Create "dumb" context based on Config to avoid triggering config.changed + context = dict(config()) + if context['config_version'] == "v2": + template_config = "config_v2.toml" + else: + template_config = "config.toml" + + config_file = 'config.toml' + config_directory = '/etc/containerd' + + endpoint = endpoint_from_flag('endpoint.containerd.available') + if endpoint: + sandbox_image = endpoint.get_sandbox_image() + if sandbox_image: + log('Setting sandbox_image to: {}'.format(sandbox_image)) + context['sandbox_image'] = sandbox_image + else: + context['sandbox_image'] = containerd.get_sandbox_image() + else: + context['sandbox_image'] = containerd.get_sandbox_image() + + if not os.path.isdir(config_directory): + os.mkdir(config_directory) + + # If custom_registries changed, make sure to remove old tls files. + if config().changed('custom_registries'): + old_custom_registries = config().previous('custom_registries') + else: + old_custom_registries = None + + context['custom_registries'] = \ + merge_custom_registries(config_directory, context['custom_registries'], + old_custom_registries) + + untrusted = DB.get('untrusted') + if untrusted: + context['untrusted'] = True + context['untrusted_name'] = untrusted['name'] + context['untrusted_path'] = untrusted['binary_path'] + context['untrusted_binary'] = os.path.basename( + untrusted['binary_path']) + + else: + context['untrusted'] = False + + if is_state('containerd.nvidia.available') \ + and context.get('runtime') == 'auto': + context['runtime'] = 'nvidia-container-runtime' + if not is_state('containerd.nvidia.available') \ + and context.get('runtime') == 'auto': + context['runtime'] = 'runc' + + render( + template_config, + os.path.join(config_directory, config_file), + context + ) + + set_state('containerd.restart') + + +@when('containerd.installed') +@when('containerd.juju-proxy.changed') +@when_not('endpoint.containerd.departed') +def proxy_changed(): + """ + Apply new proxy settings. + + :return: None + """ + # Create "dumb" context based on Config + # to avoid triggering config.changed. + context = check_for_juju_https_proxy(config) + + service_file = 'proxy.conf' + service_directory = '/etc/systemd/system/containerd.service.d' + service_path = os.path.join(service_directory, service_file) + + if context.get('http_proxy') or \ + context.get('https_proxy') or context.get('no_proxy'): + + os.makedirs(service_directory, exist_ok=True) + + log('Proxy changed, writing new file to {}'.format(service_path)) + render( + service_file, + service_path, + context + ) + + else: + try: + log('Proxy cleaned, removing file {}'.format(service_path)) + os.remove(service_path) + except FileNotFoundError: + return # We don't need to restart the daemon. + + DB.set('config-cache', context) + + remove_state('containerd.juju-proxy.changed') + check_call(['systemctl', 'daemon-reload']) + set_state('containerd.restart') + + +@when('containerd.restart') +@when_not('endpoint.containerd.departed') +def restart_containerd(): + """ + Restart the containerd service. + + If the restart fails, this function will log a message and be retried on + the next hook. + """ + status.maintenance('Restarting containerd') + if host.service_restart('containerd.service'): + remove_state('containerd.restart') + else: + log('Failed to restart containerd; will retry') + + +@when('containerd.ready') +@when('endpoint.containerd.joined') +@when_not('endpoint.containerd.departed') +def publish_config(): + """ + Pass configuration to principal charm. + + :return: None + """ + endpoint = endpoint_from_flag('endpoint.containerd.joined') + endpoint.set_config( + socket='unix:///var/run/containerd/containerd.sock', + runtime='remote', # TODO handle in k8s worker. + nvidia_enabled=is_state('containerd.nvidia.available') + ) + + +@when('endpoint.untrusted.available') +@when_not('untrusted.configured') +@when_not('endpoint.containerd.departed') +def untrusted_available(): + """ + Handle untrusted container runtime. + + :return: None + """ + untrusted_runtime = endpoint_from_flag('endpoint.untrusted.available') + received = dict(untrusted_runtime.get_config()) + + if 'name' not in received.keys(): + return # Try until config is available. + + DB.set('untrusted', received) + config_changed() + + set_state('untrusted.configured') + + +@when('endpoint.untrusted.departed') +def untrusted_departed(): + """ + Handle untrusted container runtime. + + :return: None + """ + DB.unset('untrusted') + DB.flush() + config_changed() + + remove_state('untrusted.configured') + + +@when('endpoint.docker-registry.ready') +@when_not('containerd.registry.configured') +def configure_registry(): + """ + Add docker registry config when present. + + :return: None + """ + registry = endpoint_from_flag('endpoint.docker-registry.ready') + + docker_registry = { + 'url': registry.registry_netloc + } + + # Handle auth data. + if registry.has_auth_basic(): + docker_registry['username'] = registry.basic_user + docker_registry['password'] = registry.basic_password + + # Handle TLS data. + if registry.has_tls(): + # Ensure the CA that signed our registry cert is trusted. + host.install_ca_cert(registry.tls_ca, name='juju-docker-registry') + + docker_registry['ca'] = str(ca_crt_path) + docker_registry['key'] = str(server_key_path) + docker_registry['cert'] = str(server_crt_path) + + DB.set('registry', docker_registry) + + config_changed() + set_state('containerd.registry.configured') + + +@when('endpoint.docker-registry.changed', + 'containerd.registry.configured') +def reconfigure_registry(): + """ + Signal to update the registry config when something changes. + + :return: None + """ + remove_state('containerd.registry.configured') + + +@when('endpoint.containerd.reconfigure') +@when_not('endpoint.containerd.departed') +def container_runtime_relation_changed(): + """ + Run config_changed to use any new config from the endpoint. + + :return: None + """ + config_changed() + endpoint = endpoint_from_flag('endpoint.containerd.reconfigure') + endpoint.handle_remote_config() + + +@when('containerd.registry.configured') +@when_not('endpoint.docker-registry.joined') +def remove_registry(): + """ + Remove registry config when the registry is no longer present. + + :return: None + """ + docker_registry = DB.get('registry', None) + + if docker_registry: + # Remove from DB. + DB.unset('registry') + DB.flush() + + # Remove auth-related data. + log('Disabling auth for docker registry: {}.'.format( + docker_registry['url'])) + + config_changed() + remove_state('containerd.registry.configured') diff --git a/containerd/reactive/status.py b/containerd/reactive/status.py new file mode 100644 index 0000000..2f33f3f --- /dev/null +++ b/containerd/reactive/status.py @@ -0,0 +1,4 @@ +from charms import layer + + +layer.status._initialize() diff --git a/containerd/requirements.txt b/containerd/requirements.txt new file mode 100644 index 0000000..55543d9 --- /dev/null +++ b/containerd/requirements.txt @@ -0,0 +1,3 @@ +mock +flake8 +pytest diff --git a/containerd/revision b/containerd/revision new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/containerd/revision @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/containerd/scripts/enable_grub_cgroups.sh b/containerd/scripts/enable_grub_cgroups.sh new file mode 100755 index 0000000..27182fe --- /dev/null +++ b/containerd/scripts/enable_grub_cgroups.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Edits the grub defaults file to append the GRUB_CMDLINE_LINUX options and executes a +# `juju-reboot` to reboot the machine. + +status-set maintenance "Configuring and updating grub" + +sed -i 's/GRUB_CMDLINE_LINUX=\"\"/GRUB_CMDLINE_LINUX=\"cgroup_enable=memory swapaccount=1\"/' /etc/default/grub +update-grub +status-set maintenance "Rebooting the machine" + +juju-reboot diff --git a/containerd/templates/config.toml b/containerd/templates/config.toml new file mode 100644 index 0000000..edf1667 --- /dev/null +++ b/containerd/templates/config.toml @@ -0,0 +1,112 @@ +root = "/var/lib/containerd" +state = "/run/containerd" +oom_score = 0 + +[grpc] + address = "/run/containerd/containerd.sock" + uid = 0 + gid = 0 + max_recv_message_size = 16777216 + max_send_message_size = 16777216 + +[debug] + address = "" + uid = 0 + gid = 0 + level = "" + +[metrics] + address = "" + grpc_histogram = false + +[cgroup] + path = "" + +[plugins] + [plugins.cgroups] + no_prometheus = false + [plugins.cri] + stream_server_address = "127.0.0.1" + stream_server_port = "0" + enable_selinux = false + sandbox_image = "{{ sandbox_image }}" + stats_collect_period = 10 + systemd_cgroup = false + enable_tls_streaming = false + max_container_log_line_size = 16384 + [plugins.cri.containerd] + no_pivot = false + [plugins.cri.containerd.default_runtime] + runtime_type = "io.containerd.runtime.v1.linux" + {% if untrusted %} + [plugins.cri.containerd.untrusted_workload_runtime] + runtime_type= "io.containerd.{{ untrusted_name }}.v2" + {% endif %} + [plugins.cri.containerd.runtimes] + [plugins.cri.containerd.runtimes.runc] + runtime_type = "io.containerd.runc.v1" + {% if untrusted %} + [plugins.cri.containerd.runtimes.{{ untrusted_name }}] + runtime_type= "io.containerd.{{ untrusted_name }}.v2" + [plugins.cri.containerd.runtimes.{{ untrusted_name }}.options] + Runtime = "{{ untrusted_binary }}" + RuntimeRoot = "{{ untrusted_path }}" + {% endif %} + [plugins.cri.cni] + bin_dir = "/opt/cni/bin" + conf_dir = "/etc/cni/net.d" + conf_template = "" + [plugins.cri.registry] + [plugins.cri.registry.mirrors] + {% if custom_registries -%} + {% for registry in custom_registries -%} + {% if registry.host -%} + [plugins.cri.registry.mirrors."{{ registry.host }}"] + {% if registry.url -%} + endpoint = ["{{ registry.url}}"] + {% endif -%} + {% endif -%} + {% endfor -%} + {% endif -%} + {% if custom_registries %} + [plugins.cri.registry.auths] + {% for registry in custom_registries %} + {% if registry.username and registry.password %} + [plugins.cri.registry.auths."{{ registry.url }}"] + username = "{{ registry.username }}" + password = "{{ registry.password }}" + {% endif %} + {% endfor %} + [plugins.cri.registry.configs] + {% for registry in custom_registries %} + {% if registry.ca or registry.cert or registry.key or registry.insecure_skip_verify %} + [plugins.cri.registry.configs."{{ registry.url }}".tls] + ca_file = "{{ registry.ca if registry.ca else '' }}" + cert_file = "{{ registry.cert if registry.cert else '' }}" + key_file = "{{ registry.key if registry.key else '' }}" + insecure_skip_verify = {{ "true" if registry.insecure_skip_verify else "false" }} + {% endif %} + {% endfor %} + {% endif %} + [plugins.cri.x509_key_pair_streaming] + tls_cert_file = "" + tls_key_file = "" + [plugins.diff-service] + default = ["walking"] + [plugins.linux] + shim = "{{ shim }}" + runtime = "{{ runtime }}" + runtime_root = "" + no_shim = false + shim_debug = false + [plugins.opt] + path = "/opt/containerd" + [plugins.restart] + interval = "10s" + [plugins.scheduler] + pause_threshold = 0.02 + deletion_threshold = 0 + mutation_threshold = 100 + schedule_delay = "0s" + startup_delay = "100ms" + diff --git a/containerd/templates/config_v2.toml b/containerd/templates/config_v2.toml new file mode 100644 index 0000000..489144e --- /dev/null +++ b/containerd/templates/config_v2.toml @@ -0,0 +1,112 @@ +root = "/var/lib/containerd" +state = "/run/containerd" +oom_score = 0 +version = 2 + +[grpc] + address = "/run/containerd/containerd.sock" + uid = 0 + gid = 0 + max_recv_message_size = 16777216 + max_send_message_size = 16777216 + +[debug] + address = "" + uid = 0 + gid = 0 + level = "" + +[metrics] + address = "" + grpc_histogram = false + +[cgroup] + path = "" + +[plugins] + [plugins."io.containerd.monitor.v1.cgroups"] + no_prometheus = false + [plugins."io.containerd.grpc.v1.cri"] + stream_server_address = "127.0.0.1" + stream_server_port = "0" + enable_selinux = false + sandbox_image = "{{ sandbox_image }}" + stats_collect_period = 10 + systemd_cgroup = false + enable_tls_streaming = false + max_container_log_line_size = 16384 + [plugins."io.containerd.grpc.v1.cri".containerd] + no_pivot = false + {% if untrusted %} + [plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime] + runtime_type= "io.containerd.{{ untrusted_name }}.v2" + {% endif %} + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes] + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] + runtime_type = "io.containerd.runc.v1" + {% if untrusted %} + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.{{ untrusted_name }}] + runtime_type= "io.containerd.{{ untrusted_name }}.v2" + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.{{ untrusted_name }}.options] + Runtime = "{{ untrusted_binary }}" + RuntimeRoot = "{{ untrusted_path }}" + {% endif %} + [plugins."io.containerd.grpc.v1.cri".cni] + bin_dir = "/opt/cni/bin" + conf_dir = "/etc/cni/net.d" + conf_template = "" + [plugins."io.containerd.grpc.v1.cri".registry] + [plugins."io.containerd.grpc.v1.cri".registry.mirrors] + [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] + endpoint = ["https://registry-1.docker.io"] + {% if custom_registries -%} + {% for registry in custom_registries -%} + {% if registry.host -%} + [plugins."io.containerd.grpc.v1.cri".registry.mirrors."{{ registry.host }}"] + {% if registry.url -%} + endpoint = ["{{ registry.url}}"] + {% endif -%} + {% endif -%} + {% endfor -%} + {% endif -%} + {% if custom_registries %} + [plugins."io.containerd.grpc.v1.cri".registry.auths] + {% for registry in custom_registries %} + {% if registry.username and registry.password %} + [plugins."io.containerd.grpc.v1.cri".registry.auths."{{ registry.url }}"] + username = "{{ registry.username }}" + password = "{{ registry.password }}" + {% endif %} + {% endfor %} + [plugins."io.containerd.grpc.v1.cri".registry.configs] + {% for registry in custom_registries %} + {% if registry.ca or registry.cert or registry.key or registry.insecure_skip_verify %} + [plugins."io.containerd.grpc.v1.cri".registry.configs."{{ registry.url }}".tls] + ca_file = "{{ registry.ca if registry.ca else '' }}" + cert_file = "{{ registry.cert if registry.cert else '' }}" + key_file = "{{ registry.key if registry.key else '' }}" + insecure_skip_verify = {{ "true" if registry.insecure_skip_verify else "false" }} + {% endif %} + {% endfor %} + {% endif %} + [plugins."io.containerd.grpc.v1.cri".x509_key_pair_streaming] + tls_cert_file = "" + tls_key_file = "" + [plugins."io.containerd.service.v1.diff-service"] + default = ["walking"] + [plugins."io.containerd.runtime.v1.linux"] + shim = "{{ shim }}" + runtime = "{{ runtime }}" + runtime_root = "" + no_shim = false + shim_debug = false + [plugins."io.containerd.internal.v1.opt"] + path = "/opt/containerd" + [plugins."io.containerd.internal.v1.restart"] + interval = "10s" + [plugins."io.containerd.gc.v1.scheduler"] + pause_threshold = 0.02 + deletion_threshold = 0 + mutation_threshold = 100 + schedule_delay = "0s" + startup_delay = "100ms" diff --git a/containerd/templates/proxy.conf b/containerd/templates/proxy.conf new file mode 100644 index 0000000..9a9ced4 --- /dev/null +++ b/containerd/templates/proxy.conf @@ -0,0 +1,2 @@ +[Service] +Environment="HTTP_PROXY={{ http_proxy }}" "HTTPS_PROXY={{ https_proxy }}" "NO_PROXY={{ no_proxy }}" \ No newline at end of file diff --git a/containerd/tests/conftest.py b/containerd/tests/conftest.py new file mode 100644 index 0000000..2f710e8 --- /dev/null +++ b/containerd/tests/conftest.py @@ -0,0 +1,40 @@ +import os +import sys +from unittest.mock import MagicMock + + +def identity(x): + return x + + +# mock dependencies which we don't care about covering in our tests +ch = MagicMock() +sys.modules['charmhelpers'] = ch +sys.modules['charmhelpers.core'] = ch.core +sys.modules['charmhelpers.core.unitdata'] = ch.core.unitdata +sys.modules['charmhelpers.core.hookenv'] = ch.core.hookenv +sys.modules['charmhelpers.core.host'] = ch.core.host +sys.modules['charmhelpers.core.templating'] = ch.core.templating +sys.modules['charmhelpers.contrib'] = ch.contrib +sys.modules['charmhelpers.contrib.charmsupport'] = ch.contrib.charmsupport + +reactive = MagicMock() +sys.modules['charms.reactive'] = reactive +reactive.when.return_value = identity +reactive.when_any.return_value = identity +reactive.when_not.return_value = identity +reactive.when_none.return_value = identity +reactive.hook.return_value = identity + +leadership = MagicMock() +sys.modules['charms.leadership'] = leadership + +charms = MagicMock() +sys.modules['charms'] = charms +sys.modules['charms.coordinator'] = charms.coordinator +sys.modules['charms.layer'] = charms.layer +sys.modules['charms.layer.hacluster'] = charms.layer.hacluster +sys.modules['charms.layer.kubernetes_common'] = charms.layer.kubernetes_common +sys.modules['charms.layer.nagios'] = charms.layer.nagios + +os.environ['JUJU_MODEL_UUID'] = 'test-1234' diff --git a/containerd/tests/test_cidr_notation.py b/containerd/tests/test_cidr_notation.py new file mode 100644 index 0000000..35f38f1 --- /dev/null +++ b/containerd/tests/test_cidr_notation.py @@ -0,0 +1,25 @@ +from lib.charms.layer.container_runtime_common import ( + get_hosts +) + + +def test_get_hosts(): + CONFIG = { + 'NO_PROXY': "192.168.2.1, 192.168.2.0/29, hello.com" + } + + hosts = get_hosts(CONFIG) + + assert hosts == "192.168.2.1,192.168.2.1,\ +192.168.2.2,192.168.2.3,192.168.2.4,192.168.2.5,\ +192.168.2.6,hello.com" + + +def test_return_conf(): + CONFIG = { + 'NO_PROXY': "" + } + + hosts = get_hosts(CONFIG) + + assert hosts == "" diff --git a/containerd/tests/test_merge_config.py b/containerd/tests/test_merge_config.py new file mode 100644 index 0000000..3a2e2d5 --- /dev/null +++ b/containerd/tests/test_merge_config.py @@ -0,0 +1,76 @@ +from lib.charms.layer.container_runtime_common import ( + merge_config +) + + +def test_get_hosts(): + CONFIG = { + 'NO_PROXY': '192.168.2.1, 192.168.2.0/29, hello.com', + 'https_proxy': 'https://hop.proxy', + 'HTTP_PROXY': '', + + } + ENVIRONMENT = { + 'HTTPS_PROXY': 'https://proxy.hop', + 'HTTP_PROXY': 'http://proxy.hop', + 'no_proxy': 'not tha proxy' + } + + merged = merge_config(CONFIG, ENVIRONMENT) + + assert merged == { + 'NO_PROXY': '192.168.2.1, 192.168.2.0/29, hello.com', + 'HTTPS_PROXY': 'https://hop.proxy', + 'HTTP_PROXY': 'http://proxy.hop', + 'no_proxy': '192.168.2.1, 192.168.2.0/29, hello.com', + 'https_proxy': 'https://hop.proxy', + 'http_proxy': 'http://proxy.hop' + } + + +def test_get_hosts_no_local_conf(): + CONFIG = { + 'NO_PROXY': '', + 'https_proxy': '', + 'HTTP_PROXY': '', + } + ENVIRONMENT = { + 'HTTPS_PROXY': 'https://proxy.hop', + 'HTTP_PROXY': 'http://proxy.hop', + 'no_proxy': 'not tha proxy' + } + + merged = merge_config(CONFIG, ENVIRONMENT) + + assert merged == { + 'HTTPS_PROXY': 'https://proxy.hop', + 'HTTP_PROXY': 'http://proxy.hop', + 'NO_PROXY': 'not tha proxy', + 'https_proxy': 'https://proxy.hop', + 'http_proxy': 'http://proxy.hop', + 'no_proxy': 'not tha proxy' + } + + +def test_get_hosts_no_env_conf(): + ENVIRONMENT = { + 'NO_PROXY': '', + 'HTTPS_PROXY': '', + 'HTTP_PROXY': '', + } + CONFIG = { + 'HTTPS_PROXY': 'https://proxy.hop', + 'HTTP_PROXY': 'http://proxy.hop', + 'no_proxy': 'not tha proxy' + } + + merged = merge_config(CONFIG, ENVIRONMENT) + + assert merged == { + 'HTTPS_PROXY': 'https://proxy.hop', + 'HTTP_PROXY': 'http://proxy.hop', + 'NO_PROXY': 'not tha proxy', + 'no_proxy': 'not tha proxy', + 'https_proxy': 'https://proxy.hop', + 'http_proxy': 'http://proxy.hop', + } diff --git a/containerd/tests/test_reactive.py b/containerd/tests/test_reactive.py new file mode 100644 index 0000000..3c2a549 --- /dev/null +++ b/containerd/tests/test_reactive.py @@ -0,0 +1,62 @@ +import pytest +from unittest.mock import patch, ANY + +from charms.reactive import set_flag +from reactive import container_runtime_common + + +def patch_fixture(patch_target): + @pytest.fixture() + def _fixture(): + with patch(patch_target) as m: + yield m + return _fixture + + +check_call = patch_fixture('reactive.container_runtime_common.check_call') +hookenv = patch_fixture('reactive.container_runtime_common.hookenv') +install_ca = patch_fixture('charmhelpers.core.host.install_ca_cert') +status = patch_fixture('reactive.container_runtime_common.status') + + +def test_enable_cgroups(hookenv, check_call): + """Verify expected flags for enable-groups config.""" + # Should not set a flag when config is false + hookenv.config.return_value = {'enable-cgroups': False} + container_runtime_common.enable_grub_cgroups() + set_flag.assert_not_called() + + # Should set a flag when config is true + hookenv.config.return_value = {'enable-cgroups': True} + container_runtime_common.enable_grub_cgroups() + set_flag.assert_called_once_with('cgroups.modified') + + +def test_install_custom_ca(hookenv, install_ca, status): + """Verify we set a custom CA cert when appropriate.""" + # Should not block nor call install_ca_cert when no config is present + hookenv.config.return_value = {} + container_runtime_common.install_custom_ca() + status.blocked.assert_not_called() + install_ca.assert_not_called() + + status.reset_mock() + install_ca.reset_mock() + + # Should block and not call install_ca_cert if called with bad data + hookenv.config.return_value = {'custom-registry-ca': 'bad'} + container_runtime_common.install_custom_ca() + status.blocked.assert_called_once_with(ANY) + install_ca.assert_not_called() + + status.reset_mock() + install_ca.reset_mock() + + # Should call install_ca_cert and not block if called with good data + hookenv.config.return_value = {'custom-registry-ca': 'Z29vZAo='} + container_runtime_common.install_custom_ca() + status.blocked.assert_not_called() + install_ca.assert_called_once_with(ANY, name='juju-custom-registry') + + status.reset_mock() + install_ca.reset_mock() diff --git a/containerd/tox.ini b/containerd/tox.ini new file mode 100644 index 0000000..81cdf4d --- /dev/null +++ b/containerd/tox.ini @@ -0,0 +1,27 @@ +[flake8] +max-line-length = 88 + +[tox] +skipsdist = True +envlist = lint,py3 + +[tox:travis] +3.5: lint,py3 +3.6: lint,py3 +3.7: lint,py3 +3.8: lint,py3 + +[testenv] +basepython = python3 +setenv = + PYTHONPATH={toxinidir}:{toxinidir}/lib +deps = + pyyaml + pytest + flake8 + ipdb +commands = pytest --tb native -s {posargs} + +[testenv:lint] +envdir = {toxworkdir}/py3 +commands = flake8 {toxinidir}/reactive {toxinidir}/lib {toxinidir}/tests diff --git a/containerd/version b/containerd/version new file mode 100644 index 0000000..20817dd --- /dev/null +++ b/containerd/version @@ -0,0 +1 @@ +ccfa68be \ No newline at end of file diff --git a/containerd/wheelhouse.txt b/containerd/wheelhouse.txt new file mode 100644 index 0000000..b98ac9b --- /dev/null +++ b/containerd/wheelhouse.txt @@ -0,0 +1,21 @@ +# layer:basic +# pip is pinned to <19.0 to avoid https://github.com/pypa/pip/issues/6164 +# even with installing setuptools before upgrading pip ends up with pip seeing +# the older setuptools at the system level if include_system_packages is true +pip>=18.1,<19.0 +# pin Jinja2, PyYAML and MarkupSafe to the last versions supporting python 3.5 +# for trusty +Jinja2<=2.10.1 +PyYAML<=5.2 +MarkupSafe<2.0.0 +setuptools<42 +setuptools-scm<=1.17.0 +charmhelpers>=0.4.0,<1.0.0 +charms.reactive>=0.1.0,<2.0.0 +wheel<0.34 +# pin netaddr to avoid pulling importlib-resources +netaddr<=0.7.19 + +# containerd +requests>=2.0.0,<3.0.0 + diff --git a/containerd/wheelhouse/Jinja2-2.10.1.tar.gz b/containerd/wheelhouse/Jinja2-2.10.1.tar.gz new file mode 100644 index 0000000..ffd1054 Binary files /dev/null and b/containerd/wheelhouse/Jinja2-2.10.1.tar.gz differ diff --git a/containerd/wheelhouse/MarkupSafe-1.1.1.tar.gz b/containerd/wheelhouse/MarkupSafe-1.1.1.tar.gz new file mode 100644 index 0000000..a6dad8e Binary files /dev/null and b/containerd/wheelhouse/MarkupSafe-1.1.1.tar.gz differ diff --git a/containerd/wheelhouse/PyYAML-5.2.tar.gz b/containerd/wheelhouse/PyYAML-5.2.tar.gz new file mode 100644 index 0000000..666d12a Binary files /dev/null and b/containerd/wheelhouse/PyYAML-5.2.tar.gz differ diff --git a/containerd/wheelhouse/Tempita-0.5.2.tar.gz b/containerd/wheelhouse/Tempita-0.5.2.tar.gz new file mode 100644 index 0000000..755befc Binary files /dev/null and b/containerd/wheelhouse/Tempita-0.5.2.tar.gz differ diff --git a/containerd/wheelhouse/certifi-2021.10.8.tar.gz b/containerd/wheelhouse/certifi-2021.10.8.tar.gz new file mode 100644 index 0000000..9e1581b Binary files /dev/null and b/containerd/wheelhouse/certifi-2021.10.8.tar.gz differ diff --git a/containerd/wheelhouse/charmhelpers-0.20.23.tar.gz b/containerd/wheelhouse/charmhelpers-0.20.23.tar.gz new file mode 100644 index 0000000..8fbc8ec Binary files /dev/null and b/containerd/wheelhouse/charmhelpers-0.20.23.tar.gz differ diff --git a/containerd/wheelhouse/charms.reactive-1.4.1.tar.gz b/containerd/wheelhouse/charms.reactive-1.4.1.tar.gz new file mode 100644 index 0000000..03bc1fe Binary files /dev/null and b/containerd/wheelhouse/charms.reactive-1.4.1.tar.gz differ diff --git a/containerd/wheelhouse/charset-normalizer-2.0.7.tar.gz b/containerd/wheelhouse/charset-normalizer-2.0.7.tar.gz new file mode 100644 index 0000000..61df022 Binary files /dev/null and b/containerd/wheelhouse/charset-normalizer-2.0.7.tar.gz differ diff --git a/containerd/wheelhouse/idna-3.3.tar.gz b/containerd/wheelhouse/idna-3.3.tar.gz new file mode 100644 index 0000000..ff2bcbf Binary files /dev/null and b/containerd/wheelhouse/idna-3.3.tar.gz differ diff --git a/containerd/wheelhouse/netaddr-0.7.19.tar.gz b/containerd/wheelhouse/netaddr-0.7.19.tar.gz new file mode 100644 index 0000000..cc31d9d Binary files /dev/null and b/containerd/wheelhouse/netaddr-0.7.19.tar.gz differ diff --git a/containerd/wheelhouse/pbr-5.6.0.tar.gz b/containerd/wheelhouse/pbr-5.6.0.tar.gz new file mode 100644 index 0000000..0d5c965 Binary files /dev/null and b/containerd/wheelhouse/pbr-5.6.0.tar.gz differ diff --git a/containerd/wheelhouse/pip-18.1.tar.gz b/containerd/wheelhouse/pip-18.1.tar.gz new file mode 100644 index 0000000..a18192d Binary files /dev/null and b/containerd/wheelhouse/pip-18.1.tar.gz differ diff --git a/containerd/wheelhouse/pyaml-21.10.1.tar.gz b/containerd/wheelhouse/pyaml-21.10.1.tar.gz new file mode 100644 index 0000000..b19aad3 Binary files /dev/null and b/containerd/wheelhouse/pyaml-21.10.1.tar.gz differ diff --git a/containerd/wheelhouse/requests-2.26.0.tar.gz b/containerd/wheelhouse/requests-2.26.0.tar.gz new file mode 100644 index 0000000..101dc79 Binary files /dev/null and b/containerd/wheelhouse/requests-2.26.0.tar.gz differ diff --git a/containerd/wheelhouse/setuptools-41.6.0.zip b/containerd/wheelhouse/setuptools-41.6.0.zip new file mode 100644 index 0000000..3345759 Binary files /dev/null and b/containerd/wheelhouse/setuptools-41.6.0.zip differ diff --git a/containerd/wheelhouse/setuptools_scm-1.17.0.tar.gz b/containerd/wheelhouse/setuptools_scm-1.17.0.tar.gz new file mode 100644 index 0000000..43b16c7 Binary files /dev/null and b/containerd/wheelhouse/setuptools_scm-1.17.0.tar.gz differ diff --git a/containerd/wheelhouse/six-1.16.0.tar.gz b/containerd/wheelhouse/six-1.16.0.tar.gz new file mode 100644 index 0000000..5bf3a27 Binary files /dev/null and b/containerd/wheelhouse/six-1.16.0.tar.gz differ diff --git a/containerd/wheelhouse/urllib3-1.26.7.tar.gz b/containerd/wheelhouse/urllib3-1.26.7.tar.gz new file mode 100644 index 0000000..990abe6 Binary files /dev/null and b/containerd/wheelhouse/urllib3-1.26.7.tar.gz differ diff --git a/containerd/wheelhouse/wheel-0.33.6.tar.gz b/containerd/wheelhouse/wheel-0.33.6.tar.gz new file mode 100644 index 0000000..c922c4e Binary files /dev/null and b/containerd/wheelhouse/wheel-0.33.6.tar.gz differ diff --git a/coredns/.github/workflows/tests.yaml b/coredns/.github/workflows/tests.yaml new file mode 100644 index 0000000..9b2b55e --- /dev/null +++ b/coredns/.github/workflows/tests.yaml @@ -0,0 +1,92 @@ +name: Test Suite for CoreDNS + +on: + - pull_request + +jobs: + lint-and-unit-tests: + name: Lint & Unit tests + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install Tox + run: pip install tox + - name: Run lint & unit tests + run: tox + + func-test: + name: Functional test with MicroK8s + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Fix global gitconfig for confined snap + run: | + # GH automatically includes the git-lfs plugin and configures it in + # /etc/gitconfig. However, the confinement of the charmcraft snap + # means that it can see that this file exists but cannot read it, even + # if the file permissions should allow it; this breaks git usage within + # the snap. To get around this, we move it from the global gitconfig to + # the user's .gitconfig file. + cat /etc/gitconfig >> $HOME/.gitconfig + sudo rm /etc/gitconfig + - name: Install MicroK8s + uses: balchua/microk8s-actions@v0.1.3 + with: + rbac: 'true' + storage: 'true' + dns: 'true' # required for juju, will adjust later + - name: Install Dependencies + run: | + pip install tox + sudo snap install juju --classic + sudo snap install juju-wait --classic + sudo usermod -aG microk8s $USER + sudo snap install charmcraft --beta + sudo snap install yq + - name: Build charm + run: | + if ! charmcraft build; then + echo Build failed, full log: + cat "$(ls -1t "$HOME"/snap/charmcraft/common/charmcraft-log-* | head -n1)" + exit 1 + fi + - name: Bootstrap MicroK8s with Juju + run: sg microk8s 'juju bootstrap microk8s microk8s' + - name: Add model + run: juju add-model coredns microk8s + - name: Deploy CoreDNS + run: | + upstream_image=$(yq eval '.resources.coredns-image.upstream-source' metadata.yaml) + juju deploy ./coredns.charm --resource coredns-image=$upstream_image --config forward=8.8.8.8 + - name: Wait for stable environment + run: juju wait -wv + - name: Tell MicroK8s to use CoreDNS charm + run: | + cluster_ip=$(sudo microk8s.kubectl get svc -n coredns coredns -o jsonpath='{..spec.clusterIP}') + sudo sed -i -e "s/--cluster-dns=.*/--cluster-dns=$cluster_ip/" /var/snap/microk8s/current/args/kubelet + sudo systemctl restart snap.microk8s.daemon-kubelet + - name: Run functional test + run: tox -e func + - name: Juju Status + if: failure() + run: sudo juju status + - name: Juju Log + if: failure() + run: sudo juju debug-log --replay --no-tail -i coredns + - name: Microk8s Status + if: failure() + run: sudo microk8s.kubectl get all -A + - name: Microk8s Pod Log + if: failure() + run: sudo microk8s.kubectl logs -n coredns -l juju-app=coredns diff --git a/coredns/.gitignore b/coredns/.gitignore new file mode 100644 index 0000000..878f4f1 --- /dev/null +++ b/coredns/.gitignore @@ -0,0 +1,6 @@ +.tox/ +__pycache__/ +*.pyc +placeholders/ +*.charm +build/ diff --git a/coredns/CONTRIBUTING.md b/coredns/CONTRIBUTING.md new file mode 100644 index 0000000..e8f19f1 --- /dev/null +++ b/coredns/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Contributor Guide + +This Juju charm is open source ([Apache License 2.0](./LICENSE)) and we actively seek any community contibutions +for code, suggestions and documentation. +This page details a few notes, workflows and suggestions for how to make contributions most effective and help us +all build a better charm - please give them a read before working on any contributions. + +## Licensing + +This charm has been created under the [Apache License 2.0](./LICENSE), which will cover any contributions you may +make to this project. Please familiarise yourself with the terms of the license. + +Additionally, this charm uses the Harmony CLA agreement. It’s the easiest way for you to give us permission to +use your contributions. +In effect, you’re giving us a license, but you still own the copyright — so you retain the right to modify your +code and use it in other projects. Please [sign the CLA here](https://ubuntu.com/legal/contributors/agreement) before +making any contributions. + +## Code of conduct + +We have adopted the Ubuntu code of Conduct. You can read this in full [here](https://ubuntu.com/community/code-of-conduct). + +## Contributing code + +To contribute code to this project, please use the following workflow: + +1. [Submit a bug](https://bugs.launchpad.net/charm-coredns/+filebug) to explain the need for and track the change. +2. Create a branch on your fork of the repo with your changes, including a unit test covering the new or modified code. +3. Submit a PR. The PR description should include a link to the bug on Launchpad. +4. Update the Launchpad bug to include a link to the PR and the `review-needed` tag. +5. Once reviewed and merged, the change will become available on the edge channel and assigned to an appropriate milestone + for further release according to priority. + + diff --git a/coredns/LICENSE b/coredns/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/coredns/LICENSE @@ -0,0 +1,202 @@ + + 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. \ No newline at end of file diff --git a/coredns/Pipfile b/coredns/Pipfile new file mode 100644 index 0000000..466f522 --- /dev/null +++ b/coredns/Pipfile @@ -0,0 +1,16 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pytest = "*" +flake8 = "*" +ipdb = "*" + +[packages] +ops = "*" +oci-image = {git = "https://github.com/juju-solutions/resource-oci-image/"} + +[requires] +python_version = "3.8" diff --git a/coredns/Pipfile.lock b/coredns/Pipfile.lock new file mode 100644 index 0000000..f6cd306 --- /dev/null +++ b/coredns/Pipfile.lock @@ -0,0 +1,246 @@ +{ + "_meta": { + "hash": { + "sha256": "3a93ef1bf6ad71dacc9efebae3e194bb569d6bf8728161b19e95dbd7c407aa22" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "oci-image": { + "git": "https://github.com/juju-solutions/resource-oci-image/", + "ref": "c5778285d332edf3d9a538f9d0c06154b7ec1b0b" + }, + "ops": { + "hashes": [ + "sha256:23556db47b2c97a1bb72845b7c8ec88aa7a3e27717402903b5fea7b659616ab8", + "sha256:d102359496584617a00f6f42525a01d1b60269a3d41788cf025738cbe3348c99" + ], + "index": "pypi", + "version": "==0.10.0" + }, + "pyyaml": { + "hashes": [ + "sha256:02c78d77281d8f8d07a255e57abdbf43b02257f59f50cc6b636937d68efa5dd0", + "sha256:0dc9f2eb2e3c97640928dec63fd8dc1dd91e6b6ed236bd5ac00332b99b5c2ff9", + "sha256:124fd7c7bc1e95b1eafc60825f2daf67c73ce7b33f1194731240d24b0d1bf628", + "sha256:26fcb33776857f4072601502d93e1a619f166c9c00befb52826e7b774efaa9db", + "sha256:31ba07c54ef4a897758563e3a0fcc60077698df10180abe4b8165d9895c00ebf", + "sha256:3c49e39ac034fd64fd576d63bb4db53cda89b362768a67f07749d55f128ac18a", + "sha256:52bf0930903818e600ae6c2901f748bc4869c0c406056f679ab9614e5d21a166", + "sha256:5a3f345acff76cad4aa9cb171ee76c590f37394186325d53d1aa25318b0d4a09", + "sha256:5e7ac4e0e79a53451dc2814f6876c2fa6f71452de1498bbe29c0b54b69a986f4", + "sha256:7242790ab6c20316b8e7bb545be48d7ed36e26bbe279fd56f2c4a12510e60b4b", + "sha256:737bd70e454a284d456aa1fa71a0b429dd527bcbf52c5c33f7c8eee81ac16b89", + "sha256:8635d53223b1f561b081ff4adecb828fd484b8efffe542edcfdff471997f7c39", + "sha256:8b818b6c5a920cbe4203b5a6b14256f0e5244338244560da89b7b0f1313ea4b6", + "sha256:8bf38641b4713d77da19e91f8b5296b832e4db87338d6aeffe422d42f1ca896d", + "sha256:a36a48a51e5471513a5aea920cdad84cbd56d70a5057cca3499a637496ea379c", + "sha256:b2243dd033fd02c01212ad5c601dafb44fbb293065f430b0d3dbf03f3254d615", + "sha256:cc547d3ead3754712223abb7b403f0a184e4c3eae18c9bb7fd15adef1597cc4b", + "sha256:cc552b6434b90d9dbed6a4f13339625dc466fd82597119897e9489c953acbc22", + "sha256:f3790156c606299ff499ec44db422f66f05a7363b39eb9d5b064f17bd7d7c47b", + "sha256:f7a21e3d99aa3095ef0553e7ceba36fb693998fbb1226f1392ce33681047465f", + "sha256:fdc6b2cb4b19e431994f25a9160695cc59a4e861710cc6fc97161c5e845fc579" + ], + "index": "pypi", + "version": "==5.4" + } + }, + "develop": { + "attrs": { + "hashes": [ + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + ], + "version": "==20.3.0" + }, + "backcall": { + "hashes": [ + "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", + "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" + ], + "version": "==0.2.0" + }, + "decorator": { + "hashes": [ + "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760", + "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7" + ], + "version": "==4.4.2" + }, + "flake8": { + "hashes": [ + "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", + "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" + ], + "index": "pypi", + "version": "==3.8.3" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "ipdb": { + "hashes": [ + "sha256:d6f46d261c45a65e65a2f7ec69288a1c511e16206edb2875e7ec6b2f66997e78" + ], + "index": "pypi", + "version": "==0.13.3" + }, + "ipython": { + "hashes": [ + "sha256:04323f72d5b85b606330b6d7e2dc8d2683ad46c3905e955aa96ecc7a99388e70", + "sha256:34207ffb2f653bced2bc8e3756c1db86e7d93e44ed049daae9814fed66d408ec" + ], + "version": "==7.21.0" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "jedi": { + "hashes": [ + "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93", + "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707" + ], + "version": "==0.18.0" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "packaging": { + "hashes": [ + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + ], + "version": "==20.9" + }, + "parso": { + "hashes": [ + "sha256:15b00182f472319383252c18d5913b69269590616c947747bc50bf4ac768f410", + "sha256:8519430ad07087d4c997fda3a7918f7cfa27cb58972a8c89c2a0295a1c940e9e" + ], + "version": "==0.8.1" + }, + "pexpect": { + "hashes": [ + "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", + "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.8.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "version": "==0.13.1" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04", + "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc" + ], + "version": "==3.0.18" + }, + "ptyprocess": { + "hashes": [ + "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", + "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" + ], + "version": "==0.7.0" + }, + "py": { + "hashes": [ + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], + "version": "==1.10.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", + "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" + ], + "version": "==2.6.0" + }, + "pyflakes": { + "hashes": [ + "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", + "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + ], + "version": "==2.2.0" + }, + "pygments": { + "hashes": [ + "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94", + "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8" + ], + "version": "==2.8.1" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:1cd09785c0a50f9af72220dd12aa78cfa49cbffc356c61eab009ca189e018a33", + "sha256:d010e24666435b39a4cf48740b039885642b6c273a3f77be3e7e03554d2806b7" + ], + "index": "pypi", + "version": "==6.1.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "version": "==0.10.2" + }, + "traitlets": { + "hashes": [ + "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396", + "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426" + ], + "version": "==5.0.5" + }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" + } + } +} diff --git a/coredns/README.md b/coredns/README.md new file mode 100644 index 0000000..18f5691 --- /dev/null +++ b/coredns/README.md @@ -0,0 +1,15 @@ +# CoreDNS Operator + +[CoreDNS][] is a flexible, plugin-based DNS server, and is the recommended +solution for providing DNS to Kubernetes services within the cluster. +This operator enables integration with [Charmed Kubernetes][] via a +cross-model relation and allows for more customization than provided by the +deployment of CoreDNS provided by default by Charmed Kubernetes. + +More information on using this operator with Charmed Kubernetes can be found +[here](https://ubuntu.com/kubernetes/docs/cdk-addons#coredns), and bugs should +be filed [here](https://bugs.launchpad.net/charmed-kubernetes). + + +[CoreDNS]: https://coredns.io/ +[Charmed Kubernetes]: https://ubuntu.com/kubernetes/docs diff --git a/coredns/charmcraft.yaml b/coredns/charmcraft.yaml new file mode 100644 index 0000000..8ddda8d --- /dev/null +++ b/coredns/charmcraft.yaml @@ -0,0 +1,6 @@ +type: charm +parts: + charm: + build-packages: [git] + prime: + - ./files/* diff --git a/coredns/config.yaml b/coredns/config.yaml new file mode 100644 index 0000000..d502510 --- /dev/null +++ b/coredns/config.yaml @@ -0,0 +1,38 @@ +options: + domain: + description: The local domain for cluster DNS. + type: string + default: cluster.local + forward: + description: Where to forward non-cluster addresses. + type: string + default: /etc/resolv.conf + extra_servers: + description: Any additional servers to add to the Corefile. + type: string + default: '' + corefile: + description: >- + Configuration file to use for CoreDNS. This is interpreted as a Python + string. Template which will be given the `domain` and `forward` configs as + its context. + type: string + default: | + .:53 { + errors + health { + lameduck 5s + } + ready + kubernetes ${domain} in-addr.arpa ip6.arpa { + fallthrough in-addr.arpa ip6.arpa + pods insecure + } + prometheus :9153 + forward . ${forward} + cache 30 + loop + reload + loadbalance + } + ${extra_servers} diff --git a/coredns/dispatch b/coredns/dispatch new file mode 100755 index 0000000..fe31c05 --- /dev/null +++ b/coredns/dispatch @@ -0,0 +1,3 @@ +#!/bin/sh + +JUJU_DISPATCH_PATH="${JUJU_DISPATCH_PATH:-$0}" PYTHONPATH=lib:venv ./src/charm.py diff --git a/coredns/hooks/install b/coredns/hooks/install new file mode 100755 index 0000000..fe31c05 --- /dev/null +++ b/coredns/hooks/install @@ -0,0 +1,3 @@ +#!/bin/sh + +JUJU_DISPATCH_PATH="${JUJU_DISPATCH_PATH:-$0}" PYTHONPATH=lib:venv ./src/charm.py diff --git a/coredns/hooks/start b/coredns/hooks/start new file mode 100755 index 0000000..fe31c05 --- /dev/null +++ b/coredns/hooks/start @@ -0,0 +1,3 @@ +#!/bin/sh + +JUJU_DISPATCH_PATH="${JUJU_DISPATCH_PATH:-$0}" PYTHONPATH=lib:venv ./src/charm.py diff --git a/coredns/hooks/upgrade-charm b/coredns/hooks/upgrade-charm new file mode 100755 index 0000000..fe31c05 --- /dev/null +++ b/coredns/hooks/upgrade-charm @@ -0,0 +1,3 @@ +#!/bin/sh + +JUJU_DISPATCH_PATH="${JUJU_DISPATCH_PATH:-$0}" PYTHONPATH=lib:venv ./src/charm.py diff --git a/coredns/icon.svg b/coredns/icon.svg new file mode 100644 index 0000000..a5bac8f --- /dev/null +++ b/coredns/icon.svg @@ -0,0 +1 @@ +CoreDNS_Colour_Icon \ No newline at end of file diff --git a/coredns/metadata.yaml b/coredns/metadata.yaml new file mode 100644 index 0000000..6db0b27 --- /dev/null +++ b/coredns/metadata.yaml @@ -0,0 +1,21 @@ +name: coredns +summary: CoreDNS +maintainers: + - Cory Johns +description: | + CoreDNS provides DNS resolution for Kubernetes. +tags: + - networking +series: + - kubernetes +provides: + dns-provider: + interface: kube-dns +requires: {} +peers: {} +resources: + coredns-image: + type: oci-image + description: 'CoreDNS image' + upstream-source: coredns/coredns:1.6.7 +min-juju-version: 2.8.2 diff --git a/coredns/requirements.txt b/coredns/requirements.txt new file mode 100644 index 0000000..d0569c4 --- /dev/null +++ b/coredns/requirements.txt @@ -0,0 +1,4 @@ +-i https://pypi.org/simple +git+https://github.com/juju-solutions/resource-oci-image/@c5778285d332edf3d9a538f9d0c06154b7ec1b0b#egg=oci-image +ops==0.10.0 +pyyaml==5.3.1 diff --git a/coredns/revision b/coredns/revision new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/coredns/revision @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/coredns/src/charm.py b/coredns/src/charm.py new file mode 100755 index 0000000..6e4128e --- /dev/null +++ b/coredns/src/charm.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 + +import logging +from string import Template + +from ops.charm import CharmBase +from ops.main import main +from ops.model import ActiveStatus, MaintenanceStatus, WaitingStatus + +from oci_image import OCIImageResource, OCIImageResourceError + + +class CoreDNSCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + if not self.unit.is_leader(): + # We can't do anything useful when not the leader, so do nothing. + self.model.unit.status = WaitingStatus('Waiting for leadership') + return + self.log = logging.getLogger(__name__) + self.image = OCIImageResource(self, 'coredns-image') + for event in [self.on.install, + self.on.leader_elected, + self.on.upgrade_charm, + self.on.config_changed]: + self.framework.observe(event, self.main) + self.framework.observe(self.on.dns_provider_relation_joined, self.provide_dns) + + def main(self, event): + try: + image_details = self.image.fetch() + except OCIImageResourceError as e: + self.model.unit.status = e.status + return + + self.model.unit.status = MaintenanceStatus('Setting pod spec') + + corefile = Template(self.model.config['corefile']) + corefile = corefile.safe_substitute(self.model.config) + + # Adapted from coredns.yaml.sed in https://github.com/coredns/ at 75a1cad + self.model.pod.set_spec({ + 'version': 3, + 'service': { + 'updateStrategy': { + 'type': 'RollingUpdate', + 'rollingUpdate': {'maxUnavailable': 1}, + }, + 'annotations': { + 'prometheus.io/port': "9153", + 'prometheus.io/scrape': "true", + }, + }, + # Dropped by a regression; see: + # https://bugs.launchpad.net/juju/+bug/1895886 + # 'priorityClassName': 'system-cluster-critical', + 'containers': [{ + 'name': 'coredns', + 'imageDetails': image_details, + 'imagePullPolicy': 'IfNotPresent', + 'args': ['-conf', '/etc/coredns/Corefile'], + 'volumeConfig': [{ + 'name': 'config-volume', + 'mountPath': '/etc/coredns', + # Not supported + # 'readOnly': True, + 'files': [{ + 'path': 'Corefile', + 'mode': 0o444, + 'content': corefile, + }], + }], + 'ports': [ + { + 'name': 'dns', + 'containerPort': 53, + 'protocol': 'UDP', + }, + { + 'name': 'dns-tcp', + 'containerPort': 53, + 'protocol': 'TCP', + }, + { + 'name': 'metrics', + 'containerPort': 9153, + 'protocol': 'TCP', + }, + ], + # Can't be specified by the charm yet; see: + # https://bugs.launchpad.net/juju/+bug/1893123 + # 'resources': { + # 'limits': {'memory': '170Mi'}, + # 'requests': {'cpu': '100m', 'memory': '70Mi'}, + # }, + 'kubernetes': { + 'securityContext': { + 'allowPrivilegeEscalation': False, + 'capabilities': { + 'add': ['NET_BIND_SERVICE'], + 'drop': ['all'], + }, + 'readOnlyRootFilesystem': True, + }, + 'livenessProbe': { + 'httpGet': { + 'path': '/health', + 'port': 8080, + 'scheme': 'HTTP', + }, + 'initialDelaySeconds': 60, + 'timeoutSeconds': 5, + 'successThreshold': 1, + 'failureThreshold': 5, + }, + 'readinessProbe': { + 'httpGet': { + 'path': '/ready', + 'port': 8181, + 'scheme': 'HTTP', + }, + }, + }, + }], + 'serviceAccount': { + 'roles': [{ + 'global': True, + 'rules': [ + { + 'apigroups': ['discovery.k8s.io'], + 'resources': [ + 'endpointslices', + ], + 'verbs': ['list', 'watch'], + }, + { + 'apigroups': [''], + 'resources': [ + 'endpoints', + 'services', + 'pods', + 'namespaces', + ], + 'verbs': ['list', 'watch'], + }, + { + 'apigroups': [''], + 'resources': ['nodes'], + 'verbs': ['get'], + }, + ], + }], + }, + 'kubernetesResources': { + 'pod': { + 'dnsPolicy': 'Default', + # Not yet supported by Juju; see: + # https://bugs.launchpad.net/juju/+bug/1895887 + # 'tolerations': [{ + # 'key': 'CriticalAddonsOnly', + # 'operator': 'Exists', + # }], + # 'affinity': { + # 'podAntiAffinity': { + # 'preferredDuringScheduling' + + # 'IgnoredDuringExecution': [{ + # 'weight': 100, + # 'podAffinityTerm': { + # 'labelSelector': { + # 'matchExpressions': [{ + # 'key': 'k8s-app', + # 'operator': 'In', + # 'values': ["kube-dns"], + # }], + # }, + # 'topologyKey': 'kubernetes.io/hostname', + # }, + # }], + # }, + # }, + # Can be done by the operator via placement (--to), but can't + # be specified by the charm yet, per same bug as above. + # 'nodeSelector': { + # 'kubernetes.io/os': 'linux', + # }, + } + } + }) + self.model.unit.status = ActiveStatus() + + def provide_dns(self, event): + provided_data = event.relation.data[self.unit] + if not provided_data.get('ingress-address'): + event.defer() + return + provided_data.update({ + 'domain': self.model.config['domain'], + 'sdn-ip': str(provided_data['ingress-address']), + 'port': "53", + }) + + +if __name__ == "__main__": + main(CoreDNSCharm) diff --git a/coredns/tests/func/test_deploy.py b/coredns/tests/func/test_deploy.py new file mode 100644 index 0000000..1497cc3 --- /dev/null +++ b/coredns/tests/func/test_deploy.py @@ -0,0 +1,51 @@ +import subprocess +from pathlib import Path +from time import sleep + +import pytest + + +CHARM_DIR = Path(__file__).parent.parent.parent.resolve() +SPEC_FILE = Path(__file__).parent / 'validate-dns-spec.yaml' + + +def test_charm(): + model = run('juju', 'switch').split('/')[-1] + coredns_ready = run( + 'kubectl', 'get', 'pod', '-n', model, '-l', 'juju-app=coredns', + '-o', 'jsonpath={..status.containerStatuses[0].ready}') + assert coredns_ready == 'true' + run('kubectl', 'apply', '-f', SPEC_FILE) + try: + wait_for_output('kubectl', 'get', 'pod/validate-dns', + expected='Running') + for name in ("www.ubuntu.com", "kubernetes.default.svc.cluster.local"): + run('kubectl', 'exec', 'validate-dns', '--', 'nslookup', name) + finally: + run('kubectl', 'delete', '-f', SPEC_FILE) + + +def run(*args): + args = [str(a) for a in args] + try: + res = subprocess.run(args, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + return res.stdout.decode('utf8').strip() + except subprocess.CalledProcessError as e: + pytest.fail(f'Command {args} failed ({e.returncode}):\n' + f'stdout:\n{e.stdout.decode("utf8")}\n' + f'stderr:\n{e.stderr.decode("utf8")}\n') + + +def wait_for_output(*args, expected='', timeout=3 * 60): + args = [str(a) for a in args] + output = None + for attempt in range(int(timeout / 5)): + output = run(*args) + if expected in output: + break + sleep(5) + else: + pytest.fail(f'Timed out waiting for "{expected}" from {args}:\n{output}') diff --git a/coredns/tests/func/validate-dns-spec.yaml b/coredns/tests/func/validate-dns-spec.yaml new file mode 100644 index 0000000..cfe5d27 --- /dev/null +++ b/coredns/tests/func/validate-dns-spec.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Pod +metadata: + name: validate-dns +spec: + containers: + - name: busybox + image: busybox + imagePullPolicy: IfNotPresent + args: ['sleep', '3600'] + restartPolicy: Always diff --git a/coredns/tests/unit/test_charm.py b/coredns/tests/unit/test_charm.py new file mode 100644 index 0000000..f0f95fd --- /dev/null +++ b/coredns/tests/unit/test_charm.py @@ -0,0 +1,42 @@ +import pytest + +from ops.model import ActiveStatus, BlockedStatus, WaitingStatus +from ops.testing import Harness +import yaml + +from charm import CoreDNSCharm + + +if yaml.__with_libyaml__: + _DefaultDumper = yaml.CSafeDumper +else: + _DefaultDumper = yaml.SafeDumper + + +@pytest.fixture +def harness(): + return Harness(CoreDNSCharm) + + +def test_not_leader(harness): + harness.begin() + assert isinstance(harness.charm.model.unit.status, WaitingStatus) + + +def test_missing_image(harness): + harness.set_leader(True) + harness.begin_with_initial_hooks() + assert isinstance(harness.charm.model.unit.status, BlockedStatus) + + +def test_main(harness): + harness.set_leader(True) + harness.add_oci_resource('coredns-image', { + 'registrypath': 'coredns/coredns:1.6.7', + 'username': '', + 'password': '', + }) + harness.begin_with_initial_hooks() + assert isinstance(harness.charm.model.unit.status, ActiveStatus) + # confirm that we can serialize the pod spec + yaml.dump(harness.get_pod_spec(), Dumper=_DefaultDumper) diff --git a/coredns/tox.ini b/coredns/tox.ini new file mode 100644 index 0000000..a3006a7 --- /dev/null +++ b/coredns/tox.ini @@ -0,0 +1,27 @@ +[flake8] +max-line-length = 88 + +[tox] +skipsdist = True +envlist = lint,unit + +[testenv] +basepython = python3 +setenv = + PYTHONPATH={toxinidir}/src + PYTHONBREAKPOINT=ipdb.set_trace +passenv = HOME +deps = pipenv +commands = + pipenv install --dev --ignore-pipfile + pipenv run pytest --tb native -s {posargs:tests/unit} + +[testenv:lint] +commands = + pipenv install --dev --ignore-pipfile + pipenv run flake8 {toxinidir}/src {toxinidir}/tests + +[testenv:func] +commands = + pipenv install --dev --ignore-pipfile + pipenv run pytest --tb native -s {posargs:tests/func} diff --git a/coredns/venv/PyYAML-5.3.1.dist-info/INSTALLER b/coredns/venv/PyYAML-5.3.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/coredns/venv/PyYAML-5.3.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/coredns/venv/PyYAML-5.3.1.dist-info/LICENSE b/coredns/venv/PyYAML-5.3.1.dist-info/LICENSE new file mode 100644 index 0000000..3d82c28 --- /dev/null +++ b/coredns/venv/PyYAML-5.3.1.dist-info/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017-2020 Ingy döt Net +Copyright (c) 2006-2016 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/coredns/venv/PyYAML-5.3.1.dist-info/METADATA b/coredns/venv/PyYAML-5.3.1.dist-info/METADATA new file mode 100644 index 0000000..a70dd20 --- /dev/null +++ b/coredns/venv/PyYAML-5.3.1.dist-info/METADATA @@ -0,0 +1,41 @@ +Metadata-Version: 2.1 +Name: PyYAML +Version: 5.3.1 +Summary: YAML parser and emitter for Python +Home-page: https://github.com/yaml/pyyaml +Author: Kirill Simonov +Author-email: xi@resolvent.net +License: MIT +Download-URL: https://pypi.org/project/PyYAML/ +Platform: Any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Cython +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Text Processing :: Markup +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* + +YAML is a data serialization format designed for human readability +and interaction with scripting languages. PyYAML is a YAML parser +and emitter for Python. + +PyYAML features a complete YAML 1.1 parser, Unicode support, pickle +support, capable extension API, and sensible error messages. PyYAML +supports standard YAML tags and provides Python-specific tags that +allow to represent an arbitrary Python object. + +PyYAML is applicable for a broad range of tasks from complex +configuration files to object serialization and persistence. + diff --git a/coredns/venv/PyYAML-5.3.1.dist-info/RECORD b/coredns/venv/PyYAML-5.3.1.dist-info/RECORD new file mode 100644 index 0000000..a01343d --- /dev/null +++ b/coredns/venv/PyYAML-5.3.1.dist-info/RECORD @@ -0,0 +1,41 @@ +PyYAML-5.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +PyYAML-5.3.1.dist-info/LICENSE,sha256=xAESRJ8lS5dTBFklJIMT6ScO-jbSJrItgtTMbEPFfyk,1101 +PyYAML-5.3.1.dist-info/METADATA,sha256=xTsZFjd8T4M-5rC2M3BHgx_KTTpEPy5vFDIXrbzRXPQ,1758 +PyYAML-5.3.1.dist-info/RECORD,, +PyYAML-5.3.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +PyYAML-5.3.1.dist-info/WHEEL,sha256=hzx2-39jWfx-No5BPGm7YN661ryRYBuLP8gZdbxDo8I,103 +PyYAML-5.3.1.dist-info/top_level.txt,sha256=rpj0IVMTisAjh_1vG3Ccf9v5jpCQwAz6cD1IVU5ZdhQ,11 +yaml/__init__.py,sha256=XFUNbKTg4afAd0BETjGQ1mKQ97_g5jbE1C0WoKc74dc,13170 +yaml/__pycache__/__init__.cpython-38.pyc,, +yaml/__pycache__/composer.cpython-38.pyc,, +yaml/__pycache__/constructor.cpython-38.pyc,, +yaml/__pycache__/cyaml.cpython-38.pyc,, +yaml/__pycache__/dumper.cpython-38.pyc,, +yaml/__pycache__/emitter.cpython-38.pyc,, +yaml/__pycache__/error.cpython-38.pyc,, +yaml/__pycache__/events.cpython-38.pyc,, +yaml/__pycache__/loader.cpython-38.pyc,, +yaml/__pycache__/nodes.cpython-38.pyc,, +yaml/__pycache__/parser.cpython-38.pyc,, +yaml/__pycache__/reader.cpython-38.pyc,, +yaml/__pycache__/representer.cpython-38.pyc,, +yaml/__pycache__/resolver.cpython-38.pyc,, +yaml/__pycache__/scanner.cpython-38.pyc,, +yaml/__pycache__/serializer.cpython-38.pyc,, +yaml/__pycache__/tokens.cpython-38.pyc,, +yaml/composer.py,sha256=_Ko30Wr6eDWUeUpauUGT3Lcg9QPBnOPVlTnIMRGJ9FM,4883 +yaml/constructor.py,sha256=O3Uaf0_J_5GQBoeI9ZNhpJAhtdagr_X2HzDgGbZOMnw,28627 +yaml/cyaml.py,sha256=LiMkvchNonfoy1F6ec9L2BiUz3r0bwF4hympASJX1Ic,3846 +yaml/dumper.py,sha256=PLctZlYwZLp7XmeUdwRuv4nYOZ2UBnDIUy8-lKfLF-o,2837 +yaml/emitter.py,sha256=jghtaU7eFwg31bG0B7RZea_29Adi9CKmXq_QjgQpCkQ,43006 +yaml/error.py,sha256=Ah9z-toHJUbE9j-M8YpxgSRM5CgLCcwVzJgLLRF2Fxo,2533 +yaml/events.py,sha256=50_TksgQiE4up-lKo_V-nBy-tAIxkIPQxY5qDhKCeHw,2445 +yaml/loader.py,sha256=UVa-zIqmkFSCIYq_PgSGm4NSJttHY2Rf_zQ4_b1fHN0,2061 +yaml/nodes.py,sha256=gPKNj8pKCdh2d4gr3gIYINnPOaOxGhJAUiYhGRnPE84,1440 +yaml/parser.py,sha256=ilWp5vvgoHFGzvOZDItFoGjD6D42nhlZrZyjAwa0oJo,25495 +yaml/reader.py,sha256=0dmzirOiDG4Xo41RnuQS7K9rkY3xjHiVasfDMNTqCNw,6794 +yaml/representer.py,sha256=82UM3ZxUQKqsKAF4ltWOxCS6jGPIFtXpGs7mvqyv4Xs,14184 +yaml/resolver.py,sha256=DJCjpQr8YQCEYYjKEYqTl0GrsZil2H4aFOI9b0Oe-U4,8970 +yaml/scanner.py,sha256=KeQIKGNlSyPE8QDwionHxy9CgbqE5teJEz05FR9-nAg,51277 +yaml/serializer.py,sha256=ChuFgmhU01hj4xgI8GaKv6vfM2Bujwa9i7d2FAHj7cA,4165 +yaml/tokens.py,sha256=lTQIzSVw8Mg9wv459-TjiOQe6wVziqaRlqX2_89rp54,2573 diff --git a/coredns/venv/PyYAML-5.3.1.dist-info/REQUESTED b/coredns/venv/PyYAML-5.3.1.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/coredns/venv/PyYAML-5.3.1.dist-info/WHEEL b/coredns/venv/PyYAML-5.3.1.dist-info/WHEEL new file mode 100644 index 0000000..bb3795f --- /dev/null +++ b/coredns/venv/PyYAML-5.3.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.36.2) +Root-Is-Purelib: false +Tag: cp38-cp38-linux_x86_64 + diff --git a/coredns/venv/PyYAML-5.3.1.dist-info/top_level.txt b/coredns/venv/PyYAML-5.3.1.dist-info/top_level.txt new file mode 100644 index 0000000..e6475e9 --- /dev/null +++ b/coredns/venv/PyYAML-5.3.1.dist-info/top_level.txt @@ -0,0 +1,2 @@ +_yaml +yaml diff --git a/coredns/venv/__pycache__/oci_image.cpython-38.pyc b/coredns/venv/__pycache__/oci_image.cpython-38.pyc new file mode 100644 index 0000000..031a742 Binary files /dev/null and b/coredns/venv/__pycache__/oci_image.cpython-38.pyc differ diff --git a/coredns/venv/oci_image-1.0.0.dist-info/INSTALLER b/coredns/venv/oci_image-1.0.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/coredns/venv/oci_image-1.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/coredns/venv/oci_image-1.0.0.dist-info/METADATA b/coredns/venv/oci_image-1.0.0.dist-info/METADATA new file mode 100644 index 0000000..b21b997 --- /dev/null +++ b/coredns/venv/oci_image-1.0.0.dist-info/METADATA @@ -0,0 +1,63 @@ +Metadata-Version: 2.1 +Name: oci-image +Version: 1.0.0 +Summary: Helper for dealing with OCI Image resources in the charm operator framework +Home-page: https://github.com/juju-solutions/resource-oci-image +Author: Cory Johns +Author-email: johnsca@gmail.com +License: Apache License 2.0 +Platform: UNKNOWN + +# OCI Image Resource helper + +This is a helper for working with OCI image resources in the charm operator +framework. + +## Installation + +Add it to your `requirements.txt`. Since it's not in PyPI, you'll need to use +the GitHub archive URL (or `git+` URL, if you want to pin to a specific commit): + +``` +https://github.com/juju-solutions/resource-oci-image/archive/master.zip +``` + +## Usage + +The `OCIImageResource` class will wrap the framework resource for the given +resource name, and calling `fetch` on it will either return the image info +or raise an `OCIImageResourceError` if it can't fetch or parse the image +info. The exception will have a `status` attribute you can use directly, +or a `status_message` attribute if you just want that. + +Example usage: + +```python +from ops.charm import CharmBase +from ops.main import main +from oci_image import OCIImageResource, OCIImageResourceError + +class MyCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.image = OCIImageResource(self, 'resource-name') + self.framework.observe(self.on.start, self.on_start) + + def on_start(self, event): + try: + image_info = self.image.fetch() + except OCIImageResourceError as e: + self.model.unit.status = e.status + event.defer() + return + + self.model.pod.set_spec({'containers': [{ + 'name': 'my-charm', + 'imageDetails': image_info, + }]}) + +if __name__ == "__main__": + main(MyCharm) +``` + + diff --git a/coredns/venv/oci_image-1.0.0.dist-info/RECORD b/coredns/venv/oci_image-1.0.0.dist-info/RECORD new file mode 100644 index 0000000..487344e --- /dev/null +++ b/coredns/venv/oci_image-1.0.0.dist-info/RECORD @@ -0,0 +1,9 @@ +__pycache__/oci_image.cpython-38.pyc,, +oci_image-1.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +oci_image-1.0.0.dist-info/METADATA,sha256=QIpPa4JcSPa_Ci0n-DaCNp4PkKovZudFW8FnpnauJnQ,1808 +oci_image-1.0.0.dist-info/RECORD,, +oci_image-1.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +oci_image-1.0.0.dist-info/WHEEL,sha256=OqRkF0eY5GHssMorFjlbTIq072vpHpF60fIQA6lS9xA,92 +oci_image-1.0.0.dist-info/direct_url.json,sha256=sUsaIeKXs7oqCE-NdmqTsNJ8rmr97YMi0wuRNVObj0Y,215 +oci_image-1.0.0.dist-info/top_level.txt,sha256=M4dLaObLx7irI4EO-A4_VJP_b-A6dDD7hB5QyVKdHOY,10 +oci_image.py,sha256=c75VR2vSmOp9pPTP2cnsxo23CqhhFbRtnIOtMjzDyXY,1794 diff --git a/coredns/venv/oci_image-1.0.0.dist-info/REQUESTED b/coredns/venv/oci_image-1.0.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/coredns/venv/oci_image-1.0.0.dist-info/WHEEL b/coredns/venv/oci_image-1.0.0.dist-info/WHEEL new file mode 100644 index 0000000..385faab --- /dev/null +++ b/coredns/venv/oci_image-1.0.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.36.2) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/coredns/venv/oci_image-1.0.0.dist-info/direct_url.json b/coredns/venv/oci_image-1.0.0.dist-info/direct_url.json new file mode 100644 index 0000000..56c97aa --- /dev/null +++ b/coredns/venv/oci_image-1.0.0.dist-info/direct_url.json @@ -0,0 +1 @@ +{"url": "https://github.com/juju-solutions/resource-oci-image/", "vcs_info": {"commit_id": "c5778285d332edf3d9a538f9d0c06154b7ec1b0b", "requested_revision": "c5778285d332edf3d9a538f9d0c06154b7ec1b0b", "vcs": "git"}} \ No newline at end of file diff --git a/coredns/venv/oci_image-1.0.0.dist-info/top_level.txt b/coredns/venv/oci_image-1.0.0.dist-info/top_level.txt new file mode 100644 index 0000000..cd69623 --- /dev/null +++ b/coredns/venv/oci_image-1.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +oci_image diff --git a/coredns/venv/oci_image.py b/coredns/venv/oci_image.py new file mode 100644 index 0000000..f4d3818 --- /dev/null +++ b/coredns/venv/oci_image.py @@ -0,0 +1,53 @@ +from pathlib import Path + +import yaml +from ops.framework import Object +from ops.model import BlockedStatus, ModelError + + +class OCIImageResource(Object): + def __init__(self, charm, resource_name): + super().__init__(charm, resource_name) + self.resource_name = resource_name + + def fetch(self): + try: + resource_path = self.model.resources.fetch(self.resource_name) + except ModelError as e: + raise MissingResourceError(self.resource_name) from e + if not resource_path.exists(): + raise MissingResourceError(self.resource_name) + resource_text = Path(resource_path).read_text() + if not resource_text: + raise MissingResourceError(self.resource_name) + try: + resource_data = yaml.safe_load(resource_text) + except yaml.YAMLError as e: + raise InvalidResourceError(self.resource_name) from e + else: + # Translate the data from the format used by the charm store to the + # format used by the Juju K8s pod spec, since that is how this is + # typically used. + return { + 'imagePath': resource_data['registrypath'], + 'username': resource_data['username'], + 'password': resource_data['password'], + } + + +class OCIImageResourceError(ModelError): + status_type = BlockedStatus + status_message = 'Resource error' + + def __init__(self, resource_name): + super().__init__(resource_name) + self.status = self.status_type( + f'{self.status_message}: {resource_name}') + + +class MissingResourceError(OCIImageResourceError): + status_message = 'Missing resource' + + +class InvalidResourceError(OCIImageResourceError): + status_message = 'Invalid resource' diff --git a/coredns/venv/ops-0.10.0.dist-info/INSTALLER b/coredns/venv/ops-0.10.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/coredns/venv/ops-0.10.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/coredns/venv/ops-0.10.0.dist-info/LICENSE.txt b/coredns/venv/ops-0.10.0.dist-info/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/coredns/venv/ops-0.10.0.dist-info/LICENSE.txt @@ -0,0 +1,202 @@ + + 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/coredns/venv/ops-0.10.0.dist-info/METADATA b/coredns/venv/ops-0.10.0.dist-info/METADATA new file mode 100644 index 0000000..a8a0e8e --- /dev/null +++ b/coredns/venv/ops-0.10.0.dist-info/METADATA @@ -0,0 +1,167 @@ +Metadata-Version: 2.1 +Name: ops +Version: 0.10.0 +Summary: The Python library behind great charms +Home-page: https://github.com/canonical/operator +Author: The Charmcraft team at Canonical Ltd. +Author-email: charmcraft@lists.launchpad.net +License: Apache-2.0 +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: POSIX :: Linux +Requires-Python: >=3.5 +Description-Content-Type: text/markdown +Requires-Dist: PyYAML + +# The Operator Framework + +The Operator Framework provides a simple, lightweight, and powerful way of +writing Juju charms, the best way to encapsulate operational experience in code. + +The framework will help you to: + +* model the integration of your services +* manage the lifecycle of your application +* create reusable and scalable components +* keep your code simple and readable + +## Getting Started + +Charms written using the operator framework are just Python code. The intention +is for it to feel very natural for somebody used to coding in Python, and +reasonably easy to pick up for somebody who might be a domain expert but not +necessarily a pythonista themselves. + +The dependencies of the operator framework are kept as minimal as possible; +currently that's Python 3.5 or greater, and `PyYAML` (both are included by +default in Ubuntu's cloud images from 16.04 on). + + +## A Quick Introduction + +Operator framework charms are just Python code. The entry point to your charm is +a particular Python file. It could be anything that makes sense to your project, +but let's assume this is `src/charm.py`. This file must be executable (and it +must have the appropriate shebang line). + +You need the usual `metadata.yaml` and (probably) `config.yaml` files, and a +`requirements.txt` for any Python dependencies. In other words, your project +might look like this: + +``` +my-charm +├── config.yaml +├── metadata.yaml +├── requirements.txt +└── src/ + └── charm.py +``` + +`src/charm.py` here is the entry point to your charm code. At a minimum, it +needs to define a subclass of `CharmBase` and pass that into the framework's +`main` function: + +```python +from ops.charm import CharmBase +from ops.main import main + +class MyCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + self.framework.observe(self.on.start, self.on_start) + + def on_start(self, event): + # Handle the start event here. + +if __name__ == "__main__": + main(MyCharm) +``` + +That should be enough for you to be able to run + +``` +$ charmcraft build +Done, charm left in 'my-charm.charm' +$ juju deploy ./my-charm.charm +``` + +> 🛈 More information on [`charmcraft`](https://pypi.org/project/charmcraft/) can +> also be found on its [github page](https://github.com/canonical/charmcraft). + +Happy charming! + +## Testing your charms + +The operator framework provides a testing harness, so that you can test that +your charm does the right thing when presented with different scenarios, without +having to have a full deployment to do so. `pydoc3 ops.testing` has the details +for that, including this example: + +```python +harness = Harness(MyCharm) +# Do initial setup here +relation_id = harness.add_relation('db', 'postgresql') +# Now instantiate the charm to see events as the model changes +harness.begin() +harness.add_relation_unit(relation_id, 'postgresql/0') +harness.update_relation_data(relation_id, 'postgresql/0', {'key': 'val'}) +# Check that charm has properly handled the relation_joined event for postgresql/0 +self.assertEqual(harness.charm. ...) +``` + +## Talk to us + +If you need help, have ideas, or would just like to chat with us, reach out on +IRC: we're in [#smooth-operator] on freenode (or try the [webchat]). + +We also pay attention to Juju's [discourse]; most discussion at this +stage is on IRC, however. + +You can also deep dive into the [API docs] if that's your thing. + +[webchat]: https://webchat.freenode.net/#smooth-operator +[#smooth-operator]: irc://chat.freenode.net/%23smooth-operator +[discourse]: https://discourse.juju.is/c/charming +[API docs]: https://ops.rtfd.io/ + +## Operator Framework development + +If you want to work in the framework *itself* you will need Python >= 3.5 and +the dependencies declared in `requirements-dev.txt` installed in your system. +Or you can use a virtualenv: + + virtualenv --python=python3 env + source env/bin/activate + pip install -r requirements-dev.txt + +Then you can try `./run_tests`, it should all go green. + +If you see the error `yaml does not have libyaml extensions, using slower pure +Python yaml`, you need to reinstall pyyaml with the correct extensions: + + apt-get install libyaml-dev + pip install --force-reinstall --no-cache-dir pyyaml + +If you want to build the documentation you'll need the requirements from +`docs/requirements.txt`, or in your virtualenv + + pip install -r docs/requirements.txt + +and then you can run `./build_docs`. + + diff --git a/coredns/venv/ops-0.10.0.dist-info/RECORD b/coredns/venv/ops-0.10.0.dist-info/RECORD new file mode 100644 index 0000000..fe067e0 --- /dev/null +++ b/coredns/venv/ops-0.10.0.dist-info/RECORD @@ -0,0 +1,29 @@ +ops-0.10.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +ops-0.10.0.dist-info/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358 +ops-0.10.0.dist-info/METADATA,sha256=AI7mL-PWkkYQ4f_NCulM5VcIQrMskxPIYp108DZrOcA,5577 +ops-0.10.0.dist-info/RECORD,, +ops-0.10.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ops-0.10.0.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92 +ops-0.10.0.dist-info/top_level.txt,sha256=enC05wWafSg8iDKIvj3gvtAtEP2kYCyN5Gmd689q-_I,4 +ops/__init__.py,sha256=WaHb0dfp1KEe6jFV8Pm_mcdJ3ModiWujnQ6xLjNzPNQ,819 +ops/__pycache__/__init__.cpython-38.pyc,, +ops/__pycache__/charm.cpython-38.pyc,, +ops/__pycache__/framework.cpython-38.pyc,, +ops/__pycache__/jujuversion.cpython-38.pyc,, +ops/__pycache__/log.cpython-38.pyc,, +ops/__pycache__/main.cpython-38.pyc,, +ops/__pycache__/model.cpython-38.pyc,, +ops/__pycache__/storage.cpython-38.pyc,, +ops/__pycache__/testing.cpython-38.pyc,, +ops/__pycache__/version.cpython-38.pyc,, +ops/charm.py,sha256=i1fcd-pMzRV6f9AfMy0S_Jr_rZso3s9Xi-5GZWEs3nc,22512 +ops/framework.py,sha256=T9PWR4FXBI6Yd3XGwwNO51rJlyMUeO5vPdd4GmEjdzY,38298 +ops/jujuversion.py,sha256=T5KafqBHbQiHJ1OVoVbseUnZz7og4gPUz7CayXcHddk,3845 +ops/lib/__init__.py,sha256=7i2EN1jCUkVZT5NCi_q_ilBBzpCkWaW9mnBc3vBYCns,9188 +ops/lib/__pycache__/__init__.cpython-38.pyc,, +ops/log.py,sha256=7jNn71--WpFngrZIwnJoaTRiaVrNVkLHK2enVu_VRA8,1860 +ops/main.py,sha256=TcOAS3VE1nMt-jF9uUzoyDWGTNl-OoAkS7XqQraWH3c,15375 +ops/model.py,sha256=katD2gQc35VArVMfGdI2AjPobFegQjShmDqVCKeLXZc,46796 +ops/storage.py,sha256=dal0athxe35cnWE8ol9N7nEUQDMcphDgRrQrmyGQDoA,11859 +ops/testing.py,sha256=HRjgq2ikVijGRMjVN2g-HJr8oQJ0ul8QEUUZv9D2_go,34727 +ops/version.py,sha256=6wsm0bsNX30wL9YmCZai2X5ISKQZYBIFJAbgmBn2Ri4,47 diff --git a/coredns/venv/ops-0.10.0.dist-info/REQUESTED b/coredns/venv/ops-0.10.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/coredns/venv/ops-0.10.0.dist-info/WHEEL b/coredns/venv/ops-0.10.0.dist-info/WHEEL new file mode 100644 index 0000000..b552003 --- /dev/null +++ b/coredns/venv/ops-0.10.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.34.2) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/coredns/venv/ops-0.10.0.dist-info/top_level.txt b/coredns/venv/ops-0.10.0.dist-info/top_level.txt new file mode 100644 index 0000000..2d81d3b --- /dev/null +++ b/coredns/venv/ops-0.10.0.dist-info/top_level.txt @@ -0,0 +1 @@ +ops diff --git a/coredns/venv/ops/__init__.py b/coredns/venv/ops/__init__.py new file mode 100644 index 0000000..f17b296 --- /dev/null +++ b/coredns/venv/ops/__init__.py @@ -0,0 +1,20 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""The Operator Framework.""" + +from .version import version as __version__ # noqa: F401 (imported but unused) + +# Import here the bare minimum to break the circular import between modules +from . import charm # noqa: F401 (imported but unused) diff --git a/coredns/venv/ops/__pycache__/__init__.cpython-38.pyc b/coredns/venv/ops/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..c37336c Binary files /dev/null and b/coredns/venv/ops/__pycache__/__init__.cpython-38.pyc differ diff --git a/coredns/venv/ops/__pycache__/charm.cpython-38.pyc b/coredns/venv/ops/__pycache__/charm.cpython-38.pyc new file mode 100644 index 0000000..0cdaada Binary files /dev/null and b/coredns/venv/ops/__pycache__/charm.cpython-38.pyc differ diff --git a/coredns/venv/ops/__pycache__/framework.cpython-38.pyc b/coredns/venv/ops/__pycache__/framework.cpython-38.pyc new file mode 100644 index 0000000..37a070e Binary files /dev/null and b/coredns/venv/ops/__pycache__/framework.cpython-38.pyc differ diff --git a/coredns/venv/ops/__pycache__/jujuversion.cpython-38.pyc b/coredns/venv/ops/__pycache__/jujuversion.cpython-38.pyc new file mode 100644 index 0000000..be159fd Binary files /dev/null and b/coredns/venv/ops/__pycache__/jujuversion.cpython-38.pyc differ diff --git a/coredns/venv/ops/__pycache__/log.cpython-38.pyc b/coredns/venv/ops/__pycache__/log.cpython-38.pyc new file mode 100644 index 0000000..83de3b9 Binary files /dev/null and b/coredns/venv/ops/__pycache__/log.cpython-38.pyc differ diff --git a/coredns/venv/ops/__pycache__/main.cpython-38.pyc b/coredns/venv/ops/__pycache__/main.cpython-38.pyc new file mode 100644 index 0000000..f4f10a5 Binary files /dev/null and b/coredns/venv/ops/__pycache__/main.cpython-38.pyc differ diff --git a/coredns/venv/ops/__pycache__/model.cpython-38.pyc b/coredns/venv/ops/__pycache__/model.cpython-38.pyc new file mode 100644 index 0000000..db6dc9e Binary files /dev/null and b/coredns/venv/ops/__pycache__/model.cpython-38.pyc differ diff --git a/coredns/venv/ops/__pycache__/storage.cpython-38.pyc b/coredns/venv/ops/__pycache__/storage.cpython-38.pyc new file mode 100644 index 0000000..1bb4a07 Binary files /dev/null and b/coredns/venv/ops/__pycache__/storage.cpython-38.pyc differ diff --git a/coredns/venv/ops/__pycache__/testing.cpython-38.pyc b/coredns/venv/ops/__pycache__/testing.cpython-38.pyc new file mode 100644 index 0000000..2d9cb29 Binary files /dev/null and b/coredns/venv/ops/__pycache__/testing.cpython-38.pyc differ diff --git a/coredns/venv/ops/__pycache__/version.cpython-38.pyc b/coredns/venv/ops/__pycache__/version.cpython-38.pyc new file mode 100644 index 0000000..1be0b7a Binary files /dev/null and b/coredns/venv/ops/__pycache__/version.cpython-38.pyc differ diff --git a/coredns/venv/ops/charm.py b/coredns/venv/ops/charm.py new file mode 100644 index 0000000..d898de8 --- /dev/null +++ b/coredns/venv/ops/charm.py @@ -0,0 +1,575 @@ +# Copyright 2019-2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import enum +import os +import pathlib +import typing + +import yaml + +from ops.framework import Object, EventSource, EventBase, Framework, ObjectEvents +from ops import model + + +def _loadYaml(source): + if yaml.__with_libyaml__: + return yaml.load(source, Loader=yaml.CSafeLoader) + return yaml.load(source, Loader=yaml.SafeLoader) + + +class HookEvent(EventBase): + """A base class for events that trigger because of a Juju hook firing.""" + + +class ActionEvent(EventBase): + """A base class for events that trigger when a user asks for an Action to be run. + + To read the parameters for the action, see the instance variable `params`. + To respond with the result of the action, call `set_results`. To add progress + messages that are visible as the action is progressing use `log`. + + :ivar params: The parameters passed to the action (read by action-get) + """ + + def defer(self): + """Action events are not deferable like other events. + + This is because an action runs synchronously and the user is waiting for the result. + """ + raise RuntimeError('cannot defer action events') + + def restore(self, snapshot: dict) -> None: + """Used by the operator framework to record the action. + + Not meant to be called directly by Charm code. + """ + env_action_name = os.environ.get('JUJU_ACTION_NAME') + event_action_name = self.handle.kind[:-len('_action')].replace('_', '-') + if event_action_name != env_action_name: + # This could only happen if the dev manually emits the action, or from a bug. + raise RuntimeError('action event kind does not match current action') + # Params are loaded at restore rather than __init__ because + # the model is not available in __init__. + self.params = self.framework.model._backend.action_get() + + def set_results(self, results: typing.Mapping) -> None: + """Report the result of the action. + + Args: + results: The result of the action as a Dict + """ + self.framework.model._backend.action_set(results) + + def log(self, message: str) -> None: + """Send a message that a user will see while the action is running. + + Args: + message: The message for the user. + """ + self.framework.model._backend.action_log(message) + + def fail(self, message: str = '') -> None: + """Report that this action has failed. + + Args: + message: Optional message to record why it has failed. + """ + self.framework.model._backend.action_fail(message) + + +class InstallEvent(HookEvent): + """Represents the `install` hook from Juju.""" + + +class StartEvent(HookEvent): + """Represents the `start` hook from Juju.""" + + +class StopEvent(HookEvent): + """Represents the `stop` hook from Juju.""" + + +class RemoveEvent(HookEvent): + """Represents the `remove` hook from Juju. """ + + +class ConfigChangedEvent(HookEvent): + """Represents the `config-changed` hook from Juju.""" + + +class UpdateStatusEvent(HookEvent): + """Represents the `update-status` hook from Juju.""" + + +class UpgradeCharmEvent(HookEvent): + """Represents the `upgrade-charm` hook from Juju. + + This will be triggered when a user has run `juju upgrade-charm`. It is run after Juju + has unpacked the upgraded charm code, and so this event will be handled with new code. + """ + + +class PreSeriesUpgradeEvent(HookEvent): + """Represents the `pre-series-upgrade` hook from Juju. + + This happens when a user has run `juju upgrade-series MACHINE prepare` and + will fire for each unit that is running on the machine, telling them that + the user is preparing to upgrade the Machine's series (eg trusty->bionic). + The charm should take actions to prepare for the upgrade (a database charm + would want to write out a version-independent dump of the database, so that + when a new version of the database is available in a new series, it can be + used.) + Once all units on a machine have run `pre-series-upgrade`, the user will + initiate the steps to actually upgrade the machine (eg `do-release-upgrade`). + When the upgrade has been completed, the :class:`PostSeriesUpgradeEvent` will fire. + """ + + +class PostSeriesUpgradeEvent(HookEvent): + """Represents the `post-series-upgrade` hook from Juju. + + This is run after the user has done a distribution upgrade (or rolled back + and kept the same series). It is called in response to + `juju upgrade-series MACHINE complete`. Charms are expected to do whatever + steps are necessary to reconfigure their applications for the new series. + """ + + +class LeaderElectedEvent(HookEvent): + """Represents the `leader-elected` hook from Juju. + + Juju will trigger this when a new lead unit is chosen for a given application. + This represents the leader of the charm information (not necessarily the primary + of a running application). The main utility is that charm authors can know + that only one unit will be a leader at any given time, so they can do + configuration, etc, that would otherwise require coordination between units. + (eg, selecting a password for a new relation) + """ + + +class LeaderSettingsChangedEvent(HookEvent): + """Represents the `leader-settings-changed` hook from Juju. + + Deprecated. This represents when a lead unit would call `leader-set` to inform + the other units of an application that they have new information to handle. + This has been deprecated in favor of using a Peer relation, and having the + leader set a value in the Application data bag for that peer relation. + (see :class:`RelationChangedEvent`). + """ + + +class CollectMetricsEvent(HookEvent): + """Represents the `collect-metrics` hook from Juju. + + Note that events firing during a CollectMetricsEvent are currently + sandboxed in how they can interact with Juju. To report metrics + use :meth:`.add_metrics`. + """ + + def add_metrics(self, metrics: typing.Mapping, labels: typing.Mapping = None) -> None: + """Record metrics that have been gathered by the charm for this unit. + + Args: + metrics: A collection of {key: float} pairs that contains the + metrics that have been gathered + labels: {key:value} strings that can be applied to the + metrics that are being gathered + """ + self.framework.model._backend.add_metrics(metrics, labels) + + +class RelationEvent(HookEvent): + """A base class representing the various relation lifecycle events. + + Charmers should not be creating RelationEvents directly. The events will be + generated by the framework from Juju related events. Users can observe them + from the various `CharmBase.on[relation_name].relation_*` events. + + Attributes: + relation: The Relation involved in this event + app: The remote application that has triggered this event + unit: The remote unit that has triggered this event. This may be None + if the relation event was triggered as an Application level event + """ + + def __init__(self, handle, relation, app=None, unit=None): + super().__init__(handle) + + if unit is not None and unit.app != app: + raise RuntimeError( + 'cannot create RelationEvent with application {} and unit {}'.format(app, unit)) + + self.relation = relation + self.app = app + self.unit = unit + + def snapshot(self) -> dict: + """Used by the framework to serialize the event to disk. + + Not meant to be called by Charm code. + """ + snapshot = { + 'relation_name': self.relation.name, + 'relation_id': self.relation.id, + } + if self.app: + snapshot['app_name'] = self.app.name + if self.unit: + snapshot['unit_name'] = self.unit.name + return snapshot + + def restore(self, snapshot: dict) -> None: + """Used by the framework to deserialize the event from disk. + + Not meant to be called by Charm code. + """ + self.relation = self.framework.model.get_relation( + snapshot['relation_name'], snapshot['relation_id']) + + app_name = snapshot.get('app_name') + if app_name: + self.app = self.framework.model.get_app(app_name) + else: + self.app = None + + unit_name = snapshot.get('unit_name') + if unit_name: + self.unit = self.framework.model.get_unit(unit_name) + else: + self.unit = None + + +class RelationCreatedEvent(RelationEvent): + """Represents the `relation-created` hook from Juju. + + This is triggered when a new relation to another app is added in Juju. This + can occur before units for those applications have started. All existing + relations should be established before start. + """ + + +class RelationJoinedEvent(RelationEvent): + """Represents the `relation-joined` hook from Juju. + + This is triggered whenever a new unit of a related application joins the relation. + (eg, a unit was added to an existing related app, or a new relation was established + with an application that already had units.) + """ + + +class RelationChangedEvent(RelationEvent): + """Represents the `relation-changed` hook from Juju. + + This is triggered whenever there is a change to the data bucket for a related + application or unit. Look at `event.relation.data[event.unit/app]` to see the + new information. + """ + + +class RelationDepartedEvent(RelationEvent): + """Represents the `relation-departed` hook from Juju. + + This is the inverse of the RelationJoinedEvent, representing when a unit + is leaving the relation (the unit is being removed, the app is being removed, + the relation is being removed). It is fired once for each unit that is + going away. + """ + + +class RelationBrokenEvent(RelationEvent): + """Represents the `relation-broken` hook from Juju. + + If a relation is being removed (`juju remove-relation` or `juju remove-application`), + once all the units have been removed, RelationBrokenEvent will fire to signal + that the relationship has been fully terminated. + """ + + +class StorageEvent(HookEvent): + """Base class representing Storage related events.""" + + +class StorageAttachedEvent(StorageEvent): + """Represents the `storage-attached` hook from Juju. + + Called when new storage is available for the charm to use. + """ + + +class StorageDetachingEvent(StorageEvent): + """Represents the `storage-detaching` hook from Juju. + + Called when storage a charm has been using is going away. + """ + + +class CharmEvents(ObjectEvents): + """The events that are generated by Juju in response to the lifecycle of an application.""" + + install = EventSource(InstallEvent) + start = EventSource(StartEvent) + stop = EventSource(StopEvent) + remove = EventSource(RemoveEvent) + update_status = EventSource(UpdateStatusEvent) + config_changed = EventSource(ConfigChangedEvent) + upgrade_charm = EventSource(UpgradeCharmEvent) + pre_series_upgrade = EventSource(PreSeriesUpgradeEvent) + post_series_upgrade = EventSource(PostSeriesUpgradeEvent) + leader_elected = EventSource(LeaderElectedEvent) + leader_settings_changed = EventSource(LeaderSettingsChangedEvent) + collect_metrics = EventSource(CollectMetricsEvent) + + +class CharmBase(Object): + """Base class that represents the Charm overall. + + Usually this initialization is done by ops.main.main() rather than Charm authors + directly instantiating a Charm. + + Args: + framework: The framework responsible for managing the Model and events for this + Charm. + key: Ignored; will remove after deprecation period of the signature change. + """ + + on = CharmEvents() + + def __init__(self, framework: Framework, key: typing.Optional = None): + super().__init__(framework, None) + + for relation_name in self.framework.meta.relations: + relation_name = relation_name.replace('-', '_') + self.on.define_event(relation_name + '_relation_created', RelationCreatedEvent) + self.on.define_event(relation_name + '_relation_joined', RelationJoinedEvent) + self.on.define_event(relation_name + '_relation_changed', RelationChangedEvent) + self.on.define_event(relation_name + '_relation_departed', RelationDepartedEvent) + self.on.define_event(relation_name + '_relation_broken', RelationBrokenEvent) + + for storage_name in self.framework.meta.storages: + storage_name = storage_name.replace('-', '_') + self.on.define_event(storage_name + '_storage_attached', StorageAttachedEvent) + self.on.define_event(storage_name + '_storage_detaching', StorageDetachingEvent) + + for action_name in self.framework.meta.actions: + action_name = action_name.replace('-', '_') + self.on.define_event(action_name + '_action', ActionEvent) + + @property + def app(self) -> model.Application: + """Application that this unit is part of.""" + return self.framework.model.app + + @property + def unit(self) -> model.Unit: + """Unit that this execution is responsible for.""" + return self.framework.model.unit + + @property + def meta(self) -> 'CharmMeta': + """CharmMeta of this charm. + """ + return self.framework.meta + + @property + def charm_dir(self) -> pathlib.Path: + """Root directory of the Charm as it is running. + """ + return self.framework.charm_dir + + +class CharmMeta: + """Object containing the metadata for the charm. + + This is read from metadata.yaml and/or actions.yaml. Generally charms will + define this information, rather than reading it at runtime. This class is + mostly for the framework to understand what the charm has defined. + + The maintainers, tags, terms, series, and extra_bindings attributes are all + lists of strings. The requires, provides, peers, relations, storage, + resources, and payloads attributes are all mappings of names to instances + of the respective RelationMeta, StorageMeta, ResourceMeta, or PayloadMeta. + + The relations attribute is a convenience accessor which includes all of the + requires, provides, and peers RelationMeta items. If needed, the role of + the relation definition can be obtained from its role attribute. + + Attributes: + name: The name of this charm + summary: Short description of what this charm does + description: Long description for this charm + maintainers: A list of strings of the email addresses of the maintainers + of this charm. + tags: Charm store tag metadata for categories associated with this charm. + terms: Charm store terms that should be agreed to before this charm can + be deployed. (Used for things like licensing issues.) + series: The list of supported OS series that this charm can support. + The first entry in the list is the default series that will be + used by deploy if no other series is requested by the user. + subordinate: True/False whether this charm is intended to be used as a + subordinate charm. + min_juju_version: If supplied, indicates this charm needs features that + are not available in older versions of Juju. + requires: A dict of {name: :class:`RelationMeta` } for each 'requires' relation. + provides: A dict of {name: :class:`RelationMeta` } for each 'provides' relation. + peers: A dict of {name: :class:`RelationMeta` } for each 'peer' relation. + relations: A dict containing all :class:`RelationMeta` attributes (merged from other + sections) + storages: A dict of {name: :class:`StorageMeta`} for each defined storage. + resources: A dict of {name: :class:`ResourceMeta`} for each defined resource. + payloads: A dict of {name: :class:`PayloadMeta`} for each defined payload. + extra_bindings: A dict of additional named bindings that a charm can use + for network configuration. + actions: A dict of {name: :class:`ActionMeta`} for actions that the charm has defined. + Args: + raw: a mapping containing the contents of metadata.yaml + actions_raw: a mapping containing the contents of actions.yaml + """ + + def __init__(self, raw: dict = {}, actions_raw: dict = {}): + self.name = raw.get('name', '') + self.summary = raw.get('summary', '') + self.description = raw.get('description', '') + self.maintainers = [] + if 'maintainer' in raw: + self.maintainers.append(raw['maintainer']) + if 'maintainers' in raw: + self.maintainers.extend(raw['maintainers']) + self.tags = raw.get('tags', []) + self.terms = raw.get('terms', []) + self.series = raw.get('series', []) + self.subordinate = raw.get('subordinate', False) + self.min_juju_version = raw.get('min-juju-version') + self.requires = {name: RelationMeta(RelationRole.requires, name, rel) + for name, rel in raw.get('requires', {}).items()} + self.provides = {name: RelationMeta(RelationRole.provides, name, rel) + for name, rel in raw.get('provides', {}).items()} + self.peers = {name: RelationMeta(RelationRole.peer, name, rel) + for name, rel in raw.get('peers', {}).items()} + self.relations = {} + self.relations.update(self.requires) + self.relations.update(self.provides) + self.relations.update(self.peers) + self.storages = {name: StorageMeta(name, storage) + for name, storage in raw.get('storage', {}).items()} + self.resources = {name: ResourceMeta(name, res) + for name, res in raw.get('resources', {}).items()} + self.payloads = {name: PayloadMeta(name, payload) + for name, payload in raw.get('payloads', {}).items()} + self.extra_bindings = raw.get('extra-bindings', {}) + self.actions = {name: ActionMeta(name, action) for name, action in actions_raw.items()} + + @classmethod + def from_yaml( + cls, metadata: typing.Union[str, typing.TextIO], + actions: typing.Optional[typing.Union[str, typing.TextIO]] = None): + """Instantiate a CharmMeta from a YAML description of metadata.yaml. + + Args: + metadata: A YAML description of charm metadata (name, relations, etc.) + This can be a simple string, or a file-like object. (passed to `yaml.safe_load`). + actions: YAML description of Actions for this charm (eg actions.yaml) + """ + meta = _loadYaml(metadata) + raw_actions = {} + if actions is not None: + raw_actions = _loadYaml(actions) + return cls(meta, raw_actions) + + +class RelationRole(enum.Enum): + peer = 'peer' + requires = 'requires' + provides = 'provides' + + def is_peer(self) -> bool: + """Return whether the current role is peer. + + A convenience to avoid having to import charm. + """ + return self is RelationRole.peer + + +class RelationMeta: + """Object containing metadata about a relation definition. + + Should not be constructed directly by Charm code. Is gotten from one of + :attr:`CharmMeta.peers`, :attr:`CharmMeta.requires`, :attr:`CharmMeta.provides`, + or :attr:`CharmMeta.relations`. + + Attributes: + role: This is one of peer/requires/provides + relation_name: Name of this relation from metadata.yaml + interface_name: Optional definition of the interface protocol. + scope: "global" or "container" scope based on how the relation should be used. + """ + + def __init__(self, role: RelationRole, relation_name: str, raw: dict): + if not isinstance(role, RelationRole): + raise TypeError("role should be a Role, not {!r}".format(role)) + self.role = role + self.relation_name = relation_name + self.interface_name = raw['interface'] + self.scope = raw.get('scope') + + +class StorageMeta: + """Object containing metadata about a storage definition.""" + + def __init__(self, name, raw): + self.storage_name = name + self.type = raw['type'] + self.description = raw.get('description', '') + self.shared = raw.get('shared', False) + self.read_only = raw.get('read-only', False) + self.minimum_size = raw.get('minimum-size') + self.location = raw.get('location') + self.multiple_range = None + if 'multiple' in raw: + range = raw['multiple']['range'] + if '-' not in range: + self.multiple_range = (int(range), int(range)) + else: + range = range.split('-') + self.multiple_range = (int(range[0]), int(range[1]) if range[1] else None) + + +class ResourceMeta: + """Object containing metadata about a resource definition.""" + + def __init__(self, name, raw): + self.resource_name = name + self.type = raw['type'] + self.filename = raw.get('filename', None) + self.description = raw.get('description', '') + + +class PayloadMeta: + """Object containing metadata about a payload definition.""" + + def __init__(self, name, raw): + self.payload_name = name + self.type = raw['type'] + + +class ActionMeta: + """Object containing metadata about an action's definition.""" + + def __init__(self, name, raw=None): + raw = raw or {} + self.name = name + self.title = raw.get('title', '') + self.description = raw.get('description', '') + self.parameters = raw.get('params', {}) # {: } + self.required = raw.get('required', []) # [, ...] diff --git a/coredns/venv/ops/framework.py b/coredns/venv/ops/framework.py new file mode 100644 index 0000000..ad500ca --- /dev/null +++ b/coredns/venv/ops/framework.py @@ -0,0 +1,1073 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import collections.abc +import inspect +import keyword +import logging +import marshal +import os +import pathlib +import pdb +import re +import sys +import types +import weakref + +from ops import charm +from ops.storage import ( + NoSnapshotError, + SQLiteStorage, +) + +logger = logging.getLogger(__name__) + + +class Handle: + """Handle defines a name for an object in the form of a hierarchical path. + + The provided parent is the object (or that object's handle) that this handle + sits under, or None if the object identified by this handle stands by itself + as the root of its own hierarchy. + + The handle kind is a string that defines a namespace so objects with the + same parent and kind will have unique keys. + + The handle key is a string uniquely identifying the object. No other objects + under the same parent and kind may have the same key. + """ + + def __init__(self, parent, kind, key): + if parent and not isinstance(parent, Handle): + parent = parent.handle + self._parent = parent + self._kind = kind + self._key = key + if parent: + if key: + self._path = "{}/{}[{}]".format(parent, kind, key) + else: + self._path = "{}/{}".format(parent, kind) + else: + if key: + self._path = "{}[{}]".format(kind, key) + else: + self._path = "{}".format(kind) + + def nest(self, kind, key): + return Handle(self, kind, key) + + def __hash__(self): + return hash((self.parent, self.kind, self.key)) + + def __eq__(self, other): + return (self.parent, self.kind, self.key) == (other.parent, other.kind, other.key) + + def __str__(self): + return self.path + + @property + def parent(self): + return self._parent + + @property + def kind(self): + return self._kind + + @property + def key(self): + return self._key + + @property + def path(self): + return self._path + + @classmethod + def from_path(cls, path): + handle = None + for pair in path.split("/"): + pair = pair.split("[") + good = False + if len(pair) == 1: + kind, key = pair[0], None + good = True + elif len(pair) == 2: + kind, key = pair + if key and key[-1] == ']': + key = key[:-1] + good = True + if not good: + raise RuntimeError("attempted to restore invalid handle path {}".format(path)) + handle = Handle(handle, kind, key) + return handle + + +class EventBase: + + def __init__(self, handle): + self.handle = handle + self.deferred = False + + def __repr__(self): + return "<%s via %s>" % (self.__class__.__name__, self.handle) + + def defer(self): + logger.debug("Deferring %s.", self) + self.deferred = True + + def snapshot(self): + """Return the snapshot data that should be persisted. + + Subclasses must override to save any custom state. + """ + return None + + def restore(self, snapshot): + """Restore the value state from the given snapshot. + + Subclasses must override to restore their custom state. + """ + self.deferred = False + + +class EventSource: + """EventSource wraps an event type with a descriptor to facilitate observing and emitting. + + It is generally used as: + + class SomethingHappened(EventBase): + pass + + class SomeObject(Object): + something_happened = EventSource(SomethingHappened) + + With that, instances of that type will offer the someobj.something_happened + attribute which is a BoundEvent and may be used to emit and observe the event. + """ + + def __init__(self, event_type): + if not isinstance(event_type, type) or not issubclass(event_type, EventBase): + raise RuntimeError( + 'Event requires a subclass of EventBase as an argument, got {}'.format(event_type)) + self.event_type = event_type + self.event_kind = None + self.emitter_type = None + + def _set_name(self, emitter_type, event_kind): + if self.event_kind is not None: + raise RuntimeError( + 'EventSource({}) reused as {}.{} and {}.{}'.format( + self.event_type.__name__, + self.emitter_type.__name__, + self.event_kind, + emitter_type.__name__, + event_kind, + )) + self.event_kind = event_kind + self.emitter_type = emitter_type + + def __get__(self, emitter, emitter_type=None): + if emitter is None: + return self + # Framework might not be available if accessed as CharmClass.on.event + # rather than charm_instance.on.event, but in that case it couldn't be + # emitted anyway, so there's no point to registering it. + framework = getattr(emitter, 'framework', None) + if framework is not None: + framework.register_type(self.event_type, emitter, self.event_kind) + return BoundEvent(emitter, self.event_type, self.event_kind) + + +class BoundEvent: + + def __repr__(self): + return ''.format( + self.event_type.__name__, + type(self.emitter).__name__, + self.event_kind, + hex(id(self)), + ) + + def __init__(self, emitter, event_type, event_kind): + self.emitter = emitter + self.event_type = event_type + self.event_kind = event_kind + + def emit(self, *args, **kwargs): + """Emit event to all registered observers. + + The current storage state is committed before and after each observer is notified. + """ + framework = self.emitter.framework + key = framework._next_event_key() + event = self.event_type(Handle(self.emitter, self.event_kind, key), *args, **kwargs) + framework._emit(event) + + +class HandleKind: + """Helper descriptor to define the Object.handle_kind field. + + The handle_kind for an object defaults to its type name, but it may + be explicitly overridden if desired. + """ + + def __get__(self, obj, obj_type): + kind = obj_type.__dict__.get("handle_kind") + if kind: + return kind + return obj_type.__name__ + + +class _Metaclass(type): + """Helper class to ensure proper instantiation of Object-derived classes. + + This class currently has a single purpose: events derived from EventSource + that are class attributes of Object-derived classes need to be told what + their name is in that class. For example, in + + class SomeObject(Object): + something_happened = EventSource(SomethingHappened) + + the instance of EventSource needs to know it's called 'something_happened'. + + Starting from python 3.6 we could use __set_name__ on EventSource for this, + but until then this (meta)class does the equivalent work. + + TODO: when we drop support for 3.5 drop this class, and rename _set_name in + EventSource to __set_name__; everything should continue to work. + + """ + + def __new__(typ, *a, **kw): + k = super().__new__(typ, *a, **kw) + # k is now the Object-derived class; loop over its class attributes + for n, v in vars(k).items(): + # we could do duck typing here if we want to support + # non-EventSource-derived shenanigans. We don't. + if isinstance(v, EventSource): + # this is what 3.6+ does automatically for us: + v._set_name(k, n) + return k + + +class Object(metaclass=_Metaclass): + + handle_kind = HandleKind() + + def __init__(self, parent, key): + kind = self.handle_kind + if isinstance(parent, Framework): + self.framework = parent + # Avoid Framework instances having a circular reference to themselves. + if self.framework is self: + self.framework = weakref.proxy(self.framework) + self.handle = Handle(None, kind, key) + else: + self.framework = parent.framework + self.handle = Handle(parent, kind, key) + self.framework._track(self) + + # TODO Detect conflicting handles here. + + @property + def model(self): + return self.framework.model + + +class ObjectEvents(Object): + """Convenience type to allow defining .on attributes at class level.""" + + handle_kind = "on" + + def __init__(self, parent=None, key=None): + if parent is not None: + super().__init__(parent, key) + else: + self._cache = weakref.WeakKeyDictionary() + + def __get__(self, emitter, emitter_type): + if emitter is None: + return self + instance = self._cache.get(emitter) + if instance is None: + # Same type, different instance, more data. Doing this unusual construct + # means people can subclass just this one class to have their own 'on'. + instance = self._cache[emitter] = type(self)(emitter) + return instance + + @classmethod + def define_event(cls, event_kind, event_type): + """Define an event on this type at runtime. + + cls: a type to define an event on. + + event_kind: an attribute name that will be used to access the + event. Must be a valid python identifier, not be a keyword + or an existing attribute. + + event_type: a type of the event to define. + + """ + prefix = 'unable to define an event with event_kind that ' + if not event_kind.isidentifier(): + raise RuntimeError(prefix + 'is not a valid python identifier: ' + event_kind) + elif keyword.iskeyword(event_kind): + raise RuntimeError(prefix + 'is a python keyword: ' + event_kind) + try: + getattr(cls, event_kind) + raise RuntimeError( + prefix + 'overlaps with an existing type {} attribute: {}'.format(cls, event_kind)) + except AttributeError: + pass + + event_descriptor = EventSource(event_type) + event_descriptor._set_name(cls, event_kind) + setattr(cls, event_kind, event_descriptor) + + def events(self): + """Return a mapping of event_kinds to bound_events for all available events. + """ + events_map = {} + # We have to iterate over the class rather than instance to allow for properties which + # might call this method (e.g., event views), leading to infinite recursion. + for attr_name, attr_value in inspect.getmembers(type(self)): + if isinstance(attr_value, EventSource): + # We actually care about the bound_event, however, since it + # provides the most info for users of this method. + event_kind = attr_name + bound_event = getattr(self, event_kind) + events_map[event_kind] = bound_event + return events_map + + def __getitem__(self, key): + return PrefixedEvents(self, key) + + +class PrefixedEvents: + + def __init__(self, emitter, key): + self._emitter = emitter + self._prefix = key.replace("-", "_") + '_' + + def __getattr__(self, name): + return getattr(self._emitter, self._prefix + name) + + +class PreCommitEvent(EventBase): + pass + + +class CommitEvent(EventBase): + pass + + +class FrameworkEvents(ObjectEvents): + pre_commit = EventSource(PreCommitEvent) + commit = EventSource(CommitEvent) + + +class NoTypeError(Exception): + + def __init__(self, handle_path): + self.handle_path = handle_path + + def __str__(self): + return "cannot restore {} since no class was registered for it".format(self.handle_path) + + +# the message to show to the user when a pdb breakpoint goes active +_BREAKPOINT_WELCOME_MESSAGE = """ +Starting pdb to debug charm operator. +Run `h` for help, `c` to continue, or `exit`/CTRL-d to abort. +Future breakpoints may interrupt execution again. +More details at https://discourse.jujucharms.com/t/debugging-charm-hooks + +""" + + +_event_regex = r'^(|.*/)on/[a-zA-Z_]+\[\d+\]$' + + +class Framework(Object): + + on = FrameworkEvents() + + # Override properties from Object so that we can set them in __init__. + model = None + meta = None + charm_dir = None + + def __init__(self, storage, charm_dir, meta, model): + + super().__init__(self, None) + + self.charm_dir = charm_dir + self.meta = meta + self.model = model + self._observers = [] # [(observer_path, method_name, parent_path, event_key)] + self._observer = weakref.WeakValueDictionary() # {observer_path: observer} + self._objects = weakref.WeakValueDictionary() + self._type_registry = {} # {(parent_path, kind): cls} + self._type_known = set() # {cls} + + if isinstance(storage, (str, pathlib.Path)): + logger.warning( + "deprecated: Framework now takes a Storage not a path") + storage = SQLiteStorage(storage) + self._storage = storage + + # We can't use the higher-level StoredState because it relies on events. + self.register_type(StoredStateData, None, StoredStateData.handle_kind) + stored_handle = Handle(None, StoredStateData.handle_kind, '_stored') + try: + self._stored = self.load_snapshot(stored_handle) + except NoSnapshotError: + self._stored = StoredStateData(self, '_stored') + self._stored['event_count'] = 0 + + # Hook into builtin breakpoint, so if Python >= 3.7, devs will be able to just do + # breakpoint(); if Python < 3.7, this doesn't affect anything + sys.breakpointhook = self.breakpoint + + # Flag to indicate that we already presented the welcome message in a debugger breakpoint + self._breakpoint_welcomed = False + + # Parse once the env var, which may be used multiple times later + debug_at = os.environ.get('JUJU_DEBUG_AT') + self._juju_debug_at = debug_at.split(',') if debug_at else () + + def close(self): + self._storage.close() + + def _track(self, obj): + """Track object and ensure it is the only object created using its handle path.""" + if obj is self: + # Framework objects don't track themselves + return + if obj.handle.path in self.framework._objects: + raise RuntimeError( + 'two objects claiming to be {} have been created'.format(obj.handle.path)) + self._objects[obj.handle.path] = obj + + def _forget(self, obj): + """Stop tracking the given object. See also _track.""" + self._objects.pop(obj.handle.path, None) + + def commit(self): + # Give a chance for objects to persist data they want to before a commit is made. + self.on.pre_commit.emit() + # Make sure snapshots are saved by instances of StoredStateData. Any possible state + # modifications in on_commit handlers of instances of other classes will not be persisted. + self.on.commit.emit() + # Save our event count after all events have been emitted. + self.save_snapshot(self._stored) + self._storage.commit() + + def register_type(self, cls, parent, kind=None): + if parent and not isinstance(parent, Handle): + parent = parent.handle + if parent: + parent_path = parent.path + else: + parent_path = None + if not kind: + kind = cls.handle_kind + self._type_registry[(parent_path, kind)] = cls + self._type_known.add(cls) + + def save_snapshot(self, value): + """Save a persistent snapshot of the provided value. + + The provided value must implement the following interface: + + value.handle = Handle(...) + value.snapshot() => {...} # Simple builtin types only. + value.restore(snapshot) # Restore custom state from prior snapshot. + """ + if type(value) not in self._type_known: + raise RuntimeError( + 'cannot save {} values before registering that type'.format(type(value).__name__)) + data = value.snapshot() + + # Use marshal as a validator, enforcing the use of simple types, as we later the + # information is really pickled, which is too error prone for future evolution of the + # stored data (e.g. if the developer stores a custom object and later changes its + # class name; when unpickling the original class will not be there and event + # data loading will fail). + try: + marshal.dumps(data) + except ValueError: + msg = "unable to save the data for {}, it must contain only simple types: {!r}" + raise ValueError(msg.format(value.__class__.__name__, data)) + + self._storage.save_snapshot(value.handle.path, data) + + def load_snapshot(self, handle): + parent_path = None + if handle.parent: + parent_path = handle.parent.path + cls = self._type_registry.get((parent_path, handle.kind)) + if not cls: + raise NoTypeError(handle.path) + data = self._storage.load_snapshot(handle.path) + obj = cls.__new__(cls) + obj.framework = self + obj.handle = handle + obj.restore(data) + self._track(obj) + return obj + + def drop_snapshot(self, handle): + self._storage.drop_snapshot(handle.path) + + def observe(self, bound_event: BoundEvent, observer: types.MethodType): + """Register observer to be called when bound_event is emitted. + + The bound_event is generally provided as an attribute of the object that emits + the event, and is created in this style: + + class SomeObject: + something_happened = Event(SomethingHappened) + + That event may be observed as: + + framework.observe(someobj.something_happened, self._on_something_happened) + + Raises: + RuntimeError: if bound_event or observer are the wrong type. + """ + if not isinstance(bound_event, BoundEvent): + raise RuntimeError( + 'Framework.observe requires a BoundEvent as second parameter, got {}'.format( + bound_event)) + if not isinstance(observer, types.MethodType): + # help users of older versions of the framework + if isinstance(observer, charm.CharmBase): + raise TypeError( + 'observer methods must now be explicitly provided;' + ' please replace observe(self.on.{0}, self)' + ' with e.g. observe(self.on.{0}, self._on_{0})'.format( + bound_event.event_kind)) + raise RuntimeError( + 'Framework.observe requires a method as third parameter, got {}'.format(observer)) + + event_type = bound_event.event_type + event_kind = bound_event.event_kind + emitter = bound_event.emitter + + self.register_type(event_type, emitter, event_kind) + + if hasattr(emitter, "handle"): + emitter_path = emitter.handle.path + else: + raise RuntimeError( + 'event emitter {} must have a "handle" attribute'.format(type(emitter).__name__)) + + # Validate that the method has an acceptable call signature. + sig = inspect.signature(observer) + # Self isn't included in the params list, so the first arg will be the event. + extra_params = list(sig.parameters.values())[1:] + + method_name = observer.__name__ + observer = observer.__self__ + if not sig.parameters: + raise TypeError( + '{}.{} must accept event parameter'.format(type(observer).__name__, method_name)) + elif any(param.default is inspect.Parameter.empty for param in extra_params): + # Allow for additional optional params, since there's no reason to exclude them, but + # required params will break. + raise TypeError( + '{}.{} has extra required parameter'.format(type(observer).__name__, method_name)) + + # TODO Prevent the exact same parameters from being registered more than once. + + self._observer[observer.handle.path] = observer + self._observers.append((observer.handle.path, method_name, emitter_path, event_kind)) + + def _next_event_key(self): + """Return the next event key that should be used, incrementing the internal counter.""" + # Increment the count first; this means the keys will start at 1, and 0 + # means no events have been emitted. + self._stored['event_count'] += 1 + return str(self._stored['event_count']) + + def _emit(self, event): + """See BoundEvent.emit for the public way to call this.""" + + saved = False + event_path = event.handle.path + event_kind = event.handle.kind + parent_path = event.handle.parent.path + # TODO Track observers by (parent_path, event_kind) rather than as a list of + # all observers. Avoiding linear search through all observers for every event + for observer_path, method_name, _parent_path, _event_kind in self._observers: + if _parent_path != parent_path: + continue + if _event_kind and _event_kind != event_kind: + continue + if not saved: + # Save the event for all known observers before the first notification + # takes place, so that either everyone interested sees it, or nobody does. + self.save_snapshot(event) + saved = True + # Again, only commit this after all notices are saved. + self._storage.save_notice(event_path, observer_path, method_name) + if saved: + self._reemit(event_path) + + def reemit(self): + """Reemit previously deferred events to the observers that deferred them. + + Only the specific observers that have previously deferred the event will be + notified again. Observers that asked to be notified about events after it's + been first emitted won't be notified, as that would mean potentially observing + events out of order. + """ + self._reemit() + + def _reemit(self, single_event_path=None): + last_event_path = None + deferred = True + for event_path, observer_path, method_name in self._storage.notices(single_event_path): + event_handle = Handle.from_path(event_path) + + if last_event_path != event_path: + if not deferred and last_event_path is not None: + self._storage.drop_snapshot(last_event_path) + last_event_path = event_path + deferred = False + + try: + event = self.load_snapshot(event_handle) + except NoTypeError: + self._storage.drop_notice(event_path, observer_path, method_name) + continue + + event.deferred = False + observer = self._observer.get(observer_path) + if observer: + if single_event_path is None: + logger.debug("Re-emitting %s.", event) + custom_handler = getattr(observer, method_name, None) + if custom_handler: + event_is_from_juju = isinstance(event, charm.HookEvent) + event_is_action = isinstance(event, charm.ActionEvent) + if (event_is_from_juju or event_is_action) and 'hook' in self._juju_debug_at: + # Present the welcome message and run under PDB. + self._show_debug_code_message() + pdb.runcall(custom_handler, event) + else: + # Regular call to the registered method. + custom_handler(event) + + if event.deferred: + deferred = True + else: + self._storage.drop_notice(event_path, observer_path, method_name) + # We intentionally consider this event to be dead and reload it from + # scratch in the next path. + self.framework._forget(event) + + if not deferred and last_event_path is not None: + self._storage.drop_snapshot(last_event_path) + + def _show_debug_code_message(self): + """Present the welcome message (only once!) when using debugger functionality.""" + if not self._breakpoint_welcomed: + self._breakpoint_welcomed = True + print(_BREAKPOINT_WELCOME_MESSAGE, file=sys.stderr, end='') + + def breakpoint(self, name=None): + """Add breakpoint, optionally named, at the place where this method is called. + + For the breakpoint to be activated the JUJU_DEBUG_AT environment variable + must be set to "all" or to the specific name parameter provided, if any. In every + other situation calling this method does nothing. + + The framework also provides a standard breakpoint named "hook", that will + stop execution when a hook event is about to be handled. + + For those reasons, the "all" and "hook" breakpoint names are reserved. + """ + # If given, validate the name comply with all the rules + if name is not None: + if not isinstance(name, str): + raise TypeError('breakpoint names must be strings') + if name in ('hook', 'all'): + raise ValueError('breakpoint names "all" and "hook" are reserved') + if not re.match(r'^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$', name): + raise ValueError('breakpoint names must look like "foo" or "foo-bar"') + + indicated_breakpoints = self._juju_debug_at + if not indicated_breakpoints: + return + + if 'all' in indicated_breakpoints or name in indicated_breakpoints: + self._show_debug_code_message() + + # If we call set_trace() directly it will open the debugger *here*, so indicating + # it to use our caller's frame + code_frame = inspect.currentframe().f_back + pdb.Pdb().set_trace(code_frame) + else: + logger.warning( + "Breakpoint %r skipped (not found in the requested breakpoints: %s)", + name, indicated_breakpoints) + + def remove_unreferenced_events(self): + """Remove events from storage that are not referenced. + + In older versions of the framework, events that had no observers would get recorded but + never deleted. This makes a best effort to find these events and remove them from the + database. + """ + event_regex = re.compile(_event_regex) + to_remove = [] + for handle_path in self._storage.list_snapshots(): + if event_regex.match(handle_path): + notices = self._storage.notices(handle_path) + if next(notices, None) is None: + # There are no notices for this handle_path, it is valid to remove it + to_remove.append(handle_path) + for handle_path in to_remove: + self._storage.drop_snapshot(handle_path) + + +class StoredStateData(Object): + + def __init__(self, parent, attr_name): + super().__init__(parent, attr_name) + self._cache = {} + self.dirty = False + + def __getitem__(self, key): + return self._cache.get(key) + + def __setitem__(self, key, value): + self._cache[key] = value + self.dirty = True + + def __contains__(self, key): + return key in self._cache + + def snapshot(self): + return self._cache + + def restore(self, snapshot): + self._cache = snapshot + self.dirty = False + + def on_commit(self, event): + if self.dirty: + self.framework.save_snapshot(self) + self.dirty = False + + +class BoundStoredState: + + def __init__(self, parent, attr_name): + parent.framework.register_type(StoredStateData, parent) + + handle = Handle(parent, StoredStateData.handle_kind, attr_name) + try: + data = parent.framework.load_snapshot(handle) + except NoSnapshotError: + data = StoredStateData(parent, attr_name) + + # __dict__ is used to avoid infinite recursion. + self.__dict__["_data"] = data + self.__dict__["_attr_name"] = attr_name + + parent.framework.observe(parent.framework.on.commit, self._data.on_commit) + + def __getattr__(self, key): + # "on" is the only reserved key that can't be used in the data map. + if key == "on": + return self._data.on + if key not in self._data: + raise AttributeError("attribute '{}' is not stored".format(key)) + return _wrap_stored(self._data, self._data[key]) + + def __setattr__(self, key, value): + if key == "on": + raise AttributeError("attribute 'on' is reserved and cannot be set") + + value = _unwrap_stored(self._data, value) + + if not isinstance(value, (type(None), int, float, str, bytes, list, dict, set)): + raise AttributeError( + 'attribute {!r} cannot be a {}: must be int/float/dict/list/etc'.format( + key, type(value).__name__)) + + self._data[key] = _unwrap_stored(self._data, value) + + def set_default(self, **kwargs): + """"Set the value of any given key if it has not already been set""" + for k, v in kwargs.items(): + if k not in self._data: + self._data[k] = v + + +class StoredState: + """A class used to store data the charm needs persisted across invocations. + + Example:: + + class MyClass(Object): + _stored = StoredState() + + Instances of `MyClass` can transparently save state between invocations by + setting attributes on `_stored`. Initial state should be set with + `set_default` on the bound object, that is:: + + class MyClass(Object): + _stored = StoredState() + + def __init__(self, parent, key): + super().__init__(parent, key) + self._stored.set_default(seen=set()) + self.framework.observe(self.on.seen, self._on_seen) + + def _on_seen(self, event): + self._stored.seen.add(event.uuid) + + """ + + def __init__(self): + self.parent_type = None + self.attr_name = None + + def __get__(self, parent, parent_type=None): + if self.parent_type is not None and self.parent_type not in parent_type.mro(): + # the StoredState instance is being shared between two unrelated classes + # -> unclear what is exepcted of us -> bail out + raise RuntimeError( + 'StoredState shared by {} and {}'.format( + self.parent_type.__name__, parent_type.__name__)) + + if parent is None: + # accessing via the class directly (e.g. MyClass.stored) + return self + + bound = None + if self.attr_name is not None: + bound = parent.__dict__.get(self.attr_name) + if bound is not None: + # we already have the thing from a previous pass, huzzah + return bound + + # need to find ourselves amongst the parent's bases + for cls in parent_type.mro(): + for attr_name, attr_value in cls.__dict__.items(): + if attr_value is not self: + continue + # we've found ourselves! is it the first time? + if bound is not None: + # the StoredState instance is being stored in two different + # attributes -> unclear what is expected of us -> bail out + raise RuntimeError("StoredState shared by {0}.{1} and {0}.{2}".format( + cls.__name__, self.attr_name, attr_name)) + # we've found ourselves for the first time; save where, and bind the object + self.attr_name = attr_name + self.parent_type = cls + bound = BoundStoredState(parent, attr_name) + + if bound is not None: + # cache the bound object to avoid the expensive lookup the next time + # (don't use setattr, to keep things symmetric with the fast-path lookup above) + parent.__dict__[self.attr_name] = bound + return bound + + raise AttributeError( + 'cannot find {} attribute in type {}'.format( + self.__class__.__name__, parent_type.__name__)) + + +def _wrap_stored(parent_data, value): + t = type(value) + if t is dict: + return StoredDict(parent_data, value) + if t is list: + return StoredList(parent_data, value) + if t is set: + return StoredSet(parent_data, value) + return value + + +def _unwrap_stored(parent_data, value): + t = type(value) + if t is StoredDict or t is StoredList or t is StoredSet: + return value._under + return value + + +class StoredDict(collections.abc.MutableMapping): + + def __init__(self, stored_data, under): + self._stored_data = stored_data + self._under = under + + def __getitem__(self, key): + return _wrap_stored(self._stored_data, self._under[key]) + + def __setitem__(self, key, value): + self._under[key] = _unwrap_stored(self._stored_data, value) + self._stored_data.dirty = True + + def __delitem__(self, key): + del self._under[key] + self._stored_data.dirty = True + + def __iter__(self): + return self._under.__iter__() + + def __len__(self): + return len(self._under) + + def __eq__(self, other): + if isinstance(other, StoredDict): + return self._under == other._under + elif isinstance(other, collections.abc.Mapping): + return self._under == other + else: + return NotImplemented + + +class StoredList(collections.abc.MutableSequence): + + def __init__(self, stored_data, under): + self._stored_data = stored_data + self._under = under + + def __getitem__(self, index): + return _wrap_stored(self._stored_data, self._under[index]) + + def __setitem__(self, index, value): + self._under[index] = _unwrap_stored(self._stored_data, value) + self._stored_data.dirty = True + + def __delitem__(self, index): + del self._under[index] + self._stored_data.dirty = True + + def __len__(self): + return len(self._under) + + def insert(self, index, value): + self._under.insert(index, value) + self._stored_data.dirty = True + + def append(self, value): + self._under.append(value) + self._stored_data.dirty = True + + def __eq__(self, other): + if isinstance(other, StoredList): + return self._under == other._under + elif isinstance(other, collections.abc.Sequence): + return self._under == other + else: + return NotImplemented + + def __lt__(self, other): + if isinstance(other, StoredList): + return self._under < other._under + elif isinstance(other, collections.abc.Sequence): + return self._under < other + else: + return NotImplemented + + def __le__(self, other): + if isinstance(other, StoredList): + return self._under <= other._under + elif isinstance(other, collections.abc.Sequence): + return self._under <= other + else: + return NotImplemented + + def __gt__(self, other): + if isinstance(other, StoredList): + return self._under > other._under + elif isinstance(other, collections.abc.Sequence): + return self._under > other + else: + return NotImplemented + + def __ge__(self, other): + if isinstance(other, StoredList): + return self._under >= other._under + elif isinstance(other, collections.abc.Sequence): + return self._under >= other + else: + return NotImplemented + + +class StoredSet(collections.abc.MutableSet): + + def __init__(self, stored_data, under): + self._stored_data = stored_data + self._under = under + + def add(self, key): + self._under.add(key) + self._stored_data.dirty = True + + def discard(self, key): + self._under.discard(key) + self._stored_data.dirty = True + + def __contains__(self, key): + return key in self._under + + def __iter__(self): + return self._under.__iter__() + + def __len__(self): + return len(self._under) + + @classmethod + def _from_iterable(cls, it): + """Construct an instance of the class from any iterable input. + + Per https://docs.python.org/3/library/collections.abc.html + if the Set mixin is being used in a class with a different constructor signature, + you will need to override _from_iterable() with a classmethod that can construct + new instances from an iterable argument. + """ + return set(it) + + def __le__(self, other): + if isinstance(other, StoredSet): + return self._under <= other._under + elif isinstance(other, collections.abc.Set): + return self._under <= other + else: + return NotImplemented + + def __ge__(self, other): + if isinstance(other, StoredSet): + return self._under >= other._under + elif isinstance(other, collections.abc.Set): + return self._under >= other + else: + return NotImplemented + + def __eq__(self, other): + if isinstance(other, StoredSet): + return self._under == other._under + elif isinstance(other, collections.abc.Set): + return self._under == other + else: + return NotImplemented diff --git a/coredns/venv/ops/jujuversion.py b/coredns/venv/ops/jujuversion.py new file mode 100644 index 0000000..9837c50 --- /dev/null +++ b/coredns/venv/ops/jujuversion.py @@ -0,0 +1,106 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import re +from functools import total_ordering + + +@total_ordering +class JujuVersion: + + PATTERN = r'''^ + (?P\d{1,9})\.(?P\d{1,9}) # and numbers are always there + ((?:\.|-(?P[a-z]+))(?P\d{1,9}))? # sometimes with . or - + (\.(?P\d{1,9}))?$ # and sometimes with a number. + ''' + + def __init__(self, version): + m = re.match(self.PATTERN, version, re.VERBOSE) + if not m: + raise RuntimeError('"{}" is not a valid Juju version string'.format(version)) + + d = m.groupdict() + self.major = int(m.group('major')) + self.minor = int(m.group('minor')) + self.tag = d['tag'] or '' + self.patch = int(d['patch'] or 0) + self.build = int(d['build'] or 0) + + def __repr__(self): + if self.tag: + s = '{}.{}-{}{}'.format(self.major, self.minor, self.tag, self.patch) + else: + s = '{}.{}.{}'.format(self.major, self.minor, self.patch) + if self.build > 0: + s += '.{}'.format(self.build) + return s + + def __eq__(self, other): + if self is other: + return True + if isinstance(other, str): + other = type(self)(other) + elif not isinstance(other, JujuVersion): + raise RuntimeError('cannot compare Juju version "{}" with "{}"'.format(self, other)) + return ( + self.major == other.major + and self.minor == other.minor + and self.tag == other.tag + and self.build == other.build + and self.patch == other.patch) + + def __lt__(self, other): + if self is other: + return False + if isinstance(other, str): + other = type(self)(other) + elif not isinstance(other, JujuVersion): + raise RuntimeError('cannot compare Juju version "{}" with "{}"'.format(self, other)) + + if self.major != other.major: + return self.major < other.major + elif self.minor != other.minor: + return self.minor < other.minor + elif self.tag != other.tag: + if not self.tag: + return False + elif not other.tag: + return True + return self.tag < other.tag + elif self.patch != other.patch: + return self.patch < other.patch + elif self.build != other.build: + return self.build < other.build + return False + + @classmethod + def from_environ(cls) -> 'JujuVersion': + """Build a JujuVersion from JUJU_VERSION.""" + v = os.environ.get('JUJU_VERSION') + if v is None: + v = '0.0.0' + return cls(v) + + def has_app_data(self) -> bool: + """Determine whether this juju version knows about app data.""" + return (self.major, self.minor, self.patch) >= (2, 7, 0) + + def is_dispatch_aware(self) -> bool: + """Determine whether this juju version knows about dispatch.""" + return (self.major, self.minor, self.patch) >= (2, 8, 0) + + def has_controller_storage(self) -> bool: + """Determine whether this juju version supports controller-side storage.""" + return (self.major, self.minor, self.patch) >= (2, 8, 0) diff --git a/coredns/venv/ops/lib/__init__.py b/coredns/venv/ops/lib/__init__.py new file mode 100644 index 0000000..98c0cd1 --- /dev/null +++ b/coredns/venv/ops/lib/__init__.py @@ -0,0 +1,262 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import os +import re +import sys + +from ast import literal_eval +from importlib.util import module_from_spec +from importlib.machinery import ModuleSpec +from pkgutil import get_importer +from types import ModuleType +from typing import List + +__all__ = ('use', 'autoimport') + +logger = logging.getLogger(__name__) + +_libraries = None + +_libline_re = re.compile(r'''^LIB([A-Z]+)\s*=\s*([0-9]+|['"][a-zA-Z0-9_.\-@]+['"])''') +_libname_re = re.compile(r'''^[a-z][a-z0-9]+$''') + +# Not perfect, but should do for now. +_libauthor_re = re.compile(r'''^[A-Za-z0-9_+.-]+@[a-z0-9_-]+(?:\.[a-z0-9_-]+)*\.[a-z]{2,3}$''') + + +def use(name: str, api: int, author: str) -> ModuleType: + """Use a library from the ops libraries. + + Args: + name: the name of the library requested. + api: the API version of the library. + author: the author of the library. If not given, requests the + one in the standard library. + Raises: + ImportError: if the library cannot be found. + TypeError: if the name, api, or author are the wrong type. + ValueError: if the name, api, or author are invalid. + """ + if not isinstance(name, str): + raise TypeError("invalid library name: {!r} (must be a str)".format(name)) + if not isinstance(author, str): + raise TypeError("invalid library author: {!r} (must be a str)".format(author)) + if not isinstance(api, int): + raise TypeError("invalid library API: {!r} (must be an int)".format(api)) + if api < 0: + raise ValueError('invalid library api: {} (must be ≥0)'.format(api)) + if not _libname_re.match(name): + raise ValueError("invalid library name: {!r} (chars and digits only)".format(name)) + if not _libauthor_re.match(author): + raise ValueError("invalid library author email: {!r}".format(author)) + + if _libraries is None: + autoimport() + + versions = _libraries.get((name, author), ()) + for lib in versions: + if lib.api == api: + return lib.import_module() + + others = ', '.join(str(lib.api) for lib in versions) + if others: + msg = 'cannot find "{}" from "{}" with API version {} (have {})'.format( + name, author, api, others) + else: + msg = 'cannot find library "{}" from "{}"'.format(name, author) + + raise ImportError(msg, name=name) + + +def autoimport(): + """Find all libs in the path and enable use of them. + + You only need to call this if you've installed a package or + otherwise changed sys.path in the current run, and need to see the + changes. Otherwise libraries are found on first call of `use`. + """ + global _libraries + _libraries = {} + for spec in _find_all_specs(sys.path): + lib = _parse_lib(spec) + if lib is None: + continue + + versions = _libraries.setdefault((lib.name, lib.author), []) + versions.append(lib) + versions.sort(reverse=True) + + +def _find_all_specs(path): + for sys_dir in path: + if sys_dir == "": + sys_dir = "." + try: + top_dirs = os.listdir(sys_dir) + except (FileNotFoundError, NotADirectoryError): + continue + except OSError as e: + logger.debug("Tried to look for ops.lib packages under '%s': %s", sys_dir, e) + continue + logger.debug("Looking for ops.lib packages under '%s'", sys_dir) + for top_dir in top_dirs: + opslib = os.path.join(sys_dir, top_dir, 'opslib') + try: + lib_dirs = os.listdir(opslib) + except (FileNotFoundError, NotADirectoryError): + continue + except OSError as e: + logger.debug(" Tried '%s': %s", opslib, e) # *lots* of things checked here + continue + else: + logger.debug(" Trying '%s'", opslib) + finder = get_importer(opslib) + if finder is None: + logger.debug(" Finder for '%s' is None", opslib) + continue + if not hasattr(finder, 'find_spec'): + logger.debug(" Finder for '%s' has no find_spec", opslib) + continue + for lib_dir in lib_dirs: + spec_name = "{}.opslib.{}".format(top_dir, lib_dir) + spec = finder.find_spec(spec_name) + if spec is None: + logger.debug(" No spec for %r", spec_name) + continue + if spec.loader is None: + # a namespace package; not supported + logger.debug(" No loader for %r (probably a namespace package)", spec_name) + continue + + logger.debug(" Found %r", spec_name) + yield spec + + +# only the first this many lines of a file are looked at for the LIB* constants +_MAX_LIB_LINES = 99 +# these keys, with these types, are needed to have an opslib +_NEEDED_KEYS = {'NAME': str, 'AUTHOR': str, 'API': int, 'PATCH': int} + + +def _join_and(keys: List[str]) -> str: + if len(keys) == 0: + return "" + if len(keys) == 1: + return keys[0] + return ", ".join(keys[:-1]) + ", and " + keys[-1] + + +class _Missing: + """A silly little helper to only work out the difference between + what was found and what was needed when logging""" + + def __init__(self, found): + self._found = found + + def __str__(self): + exp = set(_NEEDED_KEYS) + got = set(self._found) + if len(got) == 0: + return "missing {}".format(_join_and(sorted(exp))) + return "got {}, but missing {}".format( + _join_and(sorted(got)), + _join_and(sorted(exp - got))) + + +def _parse_lib(spec): + if spec.origin is None: + # "can't happen" + logger.warning("No origin for %r (no idea why; please report)", spec.name) + return None + + logger.debug(" Parsing %r", spec.name) + + try: + with open(spec.origin, 'rt', encoding='utf-8') as f: + libinfo = {} + for n, line in enumerate(f): + if len(libinfo) == len(_NEEDED_KEYS): + break + if n > _MAX_LIB_LINES: + logger.debug( + " Missing opslib metadata after reading to line %d: %s", + _MAX_LIB_LINES, _Missing(libinfo)) + return None + m = _libline_re.match(line) + if m is None: + continue + key, value = m.groups() + if key in _NEEDED_KEYS: + value = literal_eval(value) + if not isinstance(value, _NEEDED_KEYS[key]): + logger.debug( + " Bad type for %s: expected %s, got %s", + key, _NEEDED_KEYS[key].__name__, type(value).__name__) + return None + libinfo[key] = value + else: + if len(libinfo) != len(_NEEDED_KEYS): + logger.debug( + " Missing opslib metadata after reading to end of file: %s", + _Missing(libinfo)) + return None + except Exception as e: + logger.debug(" Failed: %s", e) + return None + + lib = _Lib(spec, libinfo['NAME'], libinfo['AUTHOR'], libinfo['API'], libinfo['PATCH']) + logger.debug(" Success: found library %s", lib) + + return lib + + +class _Lib: + + def __init__(self, spec: ModuleSpec, name: str, author: str, api: int, patch: int): + self.spec = spec + self.name = name + self.author = author + self.api = api + self.patch = patch + + self._module = None + + def __repr__(self): + return "<_Lib {}>".format(self) + + def __str__(self): + return "{0.name} by {0.author}, API {0.api}, patch {0.patch}".format(self) + + def import_module(self) -> ModuleType: + if self._module is None: + module = module_from_spec(self.spec) + self.spec.loader.exec_module(module) + self._module = module + return self._module + + def __eq__(self, other): + if not isinstance(other, _Lib): + return NotImplemented + a = (self.name, self.author, self.api, self.patch) + b = (other.name, other.author, other.api, other.patch) + return a == b + + def __lt__(self, other): + if not isinstance(other, _Lib): + return NotImplemented + a = (self.name, self.author, self.api, self.patch) + b = (other.name, other.author, other.api, other.patch) + return a < b diff --git a/coredns/venv/ops/lib/__pycache__/__init__.cpython-38.pyc b/coredns/venv/ops/lib/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..f06f4b5 Binary files /dev/null and b/coredns/venv/ops/lib/__pycache__/__init__.cpython-38.pyc differ diff --git a/coredns/venv/ops/log.py b/coredns/venv/ops/log.py new file mode 100644 index 0000000..4aac554 --- /dev/null +++ b/coredns/venv/ops/log.py @@ -0,0 +1,51 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import logging + + +class JujuLogHandler(logging.Handler): + """A handler for sending logs to Juju via juju-log.""" + + def __init__(self, model_backend, level=logging.DEBUG): + super().__init__(level) + self.model_backend = model_backend + + def emit(self, record): + self.model_backend.juju_log(record.levelname, self.format(record)) + + +def setup_root_logging(model_backend, debug=False): + """Setup python logging to forward messages to juju-log. + + By default, logging is set to DEBUG level, and messages will be filtered by Juju. + Charmers can also set their own default log level with:: + + logging.getLogger().setLevel(logging.INFO) + + model_backend -- a ModelBackend to use for juju-log + debug -- if True, write logs to stderr as well as to juju-log. + """ + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.addHandler(JujuLogHandler(model_backend)) + if debug: + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + sys.excepthook = lambda etype, value, tb: logger.error( + "Uncaught exception while in charm code:", exc_info=(etype, value, tb)) diff --git a/coredns/venv/ops/main.py b/coredns/venv/ops/main.py new file mode 100644 index 0000000..3e1ea94 --- /dev/null +++ b/coredns/venv/ops/main.py @@ -0,0 +1,404 @@ +# Copyright 2019-2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import logging +import os +import shutil +import subprocess +import sys +import typing +import warnings +from pathlib import Path + +import yaml + +import ops.charm +import ops.framework +import ops.model +import ops.storage + +from ops.log import setup_root_logging +from ops.jujuversion import JujuVersion + +CHARM_STATE_FILE = '.unit-state.db' + + +logger = logging.getLogger() + + +def _exe_path(path: Path) -> typing.Optional[Path]: + """Find and return the full path to the given binary. + + Here path is the absolute path to a binary, but might be missing an extension. + """ + p = shutil.which(path.name, mode=os.F_OK, path=str(path.parent)) + if p is None: + return None + return Path(p) + + +def _get_charm_dir(): + charm_dir = os.environ.get("JUJU_CHARM_DIR") + if charm_dir is None: + # Assume $JUJU_CHARM_DIR/lib/op/main.py structure. + charm_dir = Path('{}/../../..'.format(__file__)).resolve() + else: + charm_dir = Path(charm_dir).resolve() + return charm_dir + + +def _create_event_link(charm, bound_event, link_to): + """Create a symlink for a particular event. + + charm -- A charm object. + bound_event -- An event for which to create a symlink. + link_to -- What the event link should point to + """ + if issubclass(bound_event.event_type, ops.charm.HookEvent): + event_dir = charm.framework.charm_dir / 'hooks' + event_path = event_dir / bound_event.event_kind.replace('_', '-') + elif issubclass(bound_event.event_type, ops.charm.ActionEvent): + if not bound_event.event_kind.endswith("_action"): + raise RuntimeError( + 'action event name {} needs _action suffix'.format(bound_event.event_kind)) + event_dir = charm.framework.charm_dir / 'actions' + # The event_kind is suffixed with "_action" while the executable is not. + event_path = event_dir / bound_event.event_kind[:-len('_action')].replace('_', '-') + else: + raise RuntimeError( + 'cannot create a symlink: unsupported event type {}'.format(bound_event.event_type)) + + event_dir.mkdir(exist_ok=True) + if not event_path.exists(): + target_path = os.path.relpath(link_to, str(event_dir)) + + # Ignore the non-symlink files or directories + # assuming the charm author knows what they are doing. + logger.debug( + 'Creating a new relative symlink at %s pointing to %s', + event_path, target_path) + event_path.symlink_to(target_path) + + +def _setup_event_links(charm_dir, charm): + """Set up links for supported events that originate from Juju. + + Whether a charm can handle an event or not can be determined by + introspecting which events are defined on it. + + Hooks or actions are created as symlinks to the charm code file + which is determined by inspecting symlinks provided by the charm + author at hooks/install or hooks/start. + + charm_dir -- A root directory of the charm. + charm -- An instance of the Charm class. + + """ + # XXX: on windows this function does not accomplish what it wants to: + # it creates symlinks with no extension pointing to a .py + # and juju only knows how to handle .exe, .bat, .cmd, and .ps1 + # so it does its job, but does not accomplish anything as the + # hooks aren't 'callable'. + link_to = os.path.realpath(os.environ.get("JUJU_DISPATCH_PATH", sys.argv[0])) + for bound_event in charm.on.events().values(): + # Only events that originate from Juju need symlinks. + if issubclass(bound_event.event_type, (ops.charm.HookEvent, ops.charm.ActionEvent)): + _create_event_link(charm, bound_event, link_to) + + +def _emit_charm_event(charm, event_name): + """Emits a charm event based on a Juju event name. + + charm -- A charm instance to emit an event from. + event_name -- A Juju event name to emit on a charm. + """ + event_to_emit = None + try: + event_to_emit = getattr(charm.on, event_name) + except AttributeError: + logger.debug("Event %s not defined for %s.", event_name, charm) + + # If the event is not supported by the charm implementation, do + # not error out or try to emit it. This is to support rollbacks. + if event_to_emit is not None: + args, kwargs = _get_event_args(charm, event_to_emit) + logger.debug('Emitting Juju event %s.', event_name) + event_to_emit.emit(*args, **kwargs) + + +def _get_event_args(charm, bound_event): + event_type = bound_event.event_type + model = charm.framework.model + + if issubclass(event_type, ops.charm.RelationEvent): + relation_name = os.environ['JUJU_RELATION'] + relation_id = int(os.environ['JUJU_RELATION_ID'].split(':')[-1]) + relation = model.get_relation(relation_name, relation_id) + else: + relation = None + + remote_app_name = os.environ.get('JUJU_REMOTE_APP', '') + remote_unit_name = os.environ.get('JUJU_REMOTE_UNIT', '') + if remote_app_name or remote_unit_name: + if not remote_app_name: + if '/' not in remote_unit_name: + raise RuntimeError('invalid remote unit name: {}'.format(remote_unit_name)) + remote_app_name = remote_unit_name.split('/')[0] + args = [relation, model.get_app(remote_app_name)] + if remote_unit_name: + args.append(model.get_unit(remote_unit_name)) + return args, {} + elif relation: + return [relation], {} + return [], {} + + +class _Dispatcher: + """Encapsulate how to figure out what event Juju wants us to run. + + Also knows how to run “legacy” hooks when Juju called us via a top-level + ``dispatch`` binary. + + Args: + charm_dir: the toplevel directory of the charm + + Attributes: + event_name: the name of the event to run + is_dispatch_aware: are we running under a Juju that knows about the + dispatch binary, and is that binary present? + + """ + + def __init__(self, charm_dir: Path): + self._charm_dir = charm_dir + self._exec_path = Path(os.environ.get('JUJU_DISPATCH_PATH', sys.argv[0])) + + dispatch = charm_dir / 'dispatch' + if JujuVersion.from_environ().is_dispatch_aware() and _exe_path(dispatch) is not None: + self._init_dispatch() + else: + self._init_legacy() + + def ensure_event_links(self, charm): + """Make sure necessary symlinks are present on disk""" + + if self.is_dispatch_aware: + # links aren't needed + return + + # When a charm is force-upgraded and a unit is in an error state Juju + # does not run upgrade-charm and instead runs the failed hook followed + # by config-changed. Given the nature of force-upgrading the hook setup + # code is not triggered on config-changed. + # + # 'start' event is included as Juju does not fire the install event for + # K8s charms (see LP: #1854635). + if (self.event_name in ('install', 'start', 'upgrade_charm') + or self.event_name.endswith('_storage_attached')): + _setup_event_links(self._charm_dir, charm) + + def run_any_legacy_hook(self): + """Run any extant legacy hook. + + If there is both a dispatch file and a legacy hook for the + current event, run the wanted legacy hook. + """ + + if not self.is_dispatch_aware: + # we *are* the legacy hook + return + + dispatch_path = _exe_path(self._charm_dir / self._dispatch_path) + if dispatch_path is None: + logger.debug("Legacy %s does not exist.", self._dispatch_path) + return + + # super strange that there isn't an is_executable + if not os.access(str(dispatch_path), os.X_OK): + logger.warning("Legacy %s exists but is not executable.", self._dispatch_path) + return + + if dispatch_path.resolve() == Path(sys.argv[0]).resolve(): + logger.debug("Legacy %s is just a link to ourselves.", self._dispatch_path) + return + + argv = sys.argv.copy() + argv[0] = str(dispatch_path) + logger.info("Running legacy %s.", self._dispatch_path) + try: + subprocess.run(argv, check=True) + except subprocess.CalledProcessError as e: + logger.warning("Legacy %s exited with status %d.", self._dispatch_path, e.returncode) + sys.exit(e.returncode) + except OSError as e: + logger.warning("Unable to run legacy %s: %s", self._dispatch_path, e) + sys.exit(1) + else: + logger.debug("Legacy %s exited with status 0.", self._dispatch_path) + + def _set_name_from_path(self, path: Path): + """Sets the name attribute to that which can be inferred from the given path.""" + name = path.name.replace('-', '_') + if path.parent.name == 'actions': + name = '{}_action'.format(name) + self.event_name = name + + def _init_legacy(self): + """Set up the 'legacy' dispatcher. + + The current Juju doesn't know about 'dispatch' and calls hooks + explicitly. + """ + self.is_dispatch_aware = False + self._set_name_from_path(self._exec_path) + + def _init_dispatch(self): + """Set up the new 'dispatch' dispatcher. + + The current Juju will run 'dispatch' if it exists, and otherwise fall + back to the old behaviour. + + JUJU_DISPATCH_PATH will be set to the wanted hook, e.g. hooks/install, + in both cases. + """ + self._dispatch_path = Path(os.environ['JUJU_DISPATCH_PATH']) + + if 'OPERATOR_DISPATCH' in os.environ: + logger.debug("Charm called itself via %s.", self._dispatch_path) + sys.exit(0) + os.environ['OPERATOR_DISPATCH'] = '1' + + self.is_dispatch_aware = True + self._set_name_from_path(self._dispatch_path) + + def is_restricted_context(self): + """"Return True if we are running in a restricted Juju context. + + When in a restricted context, most commands (relation-get, config-get, + state-get) are not available. As such, we change how we interact with + Juju. + """ + return self.event_name in ('collect_metrics',) + + +def _should_use_controller_storage(db_path: Path, meta: ops.charm.CharmMeta) -> bool: + """Figure out whether we want to use controller storage or not.""" + # if you've previously used local state, carry on using that + if db_path.exists(): + logger.debug("Using local storage: %s already exists", db_path) + return False + + # if you're not in k8s you don't need controller storage + if 'kubernetes' not in meta.series: + logger.debug("Using local storage: not a kubernetes charm") + return False + + # are we in a new enough Juju? + cur_version = JujuVersion.from_environ() + + if cur_version.has_controller_storage(): + logger.debug("Using controller storage: JUJU_VERSION=%s", cur_version) + return True + else: + logger.debug("Using local storage: JUJU_VERSION=%s", cur_version) + return False + + +def main(charm_class: ops.charm.CharmBase, use_juju_for_storage: bool = None): + """Setup the charm and dispatch the observed event. + + The event name is based on the way this executable was called (argv[0]). + + Args: + charm_class: your charm class. + use_juju_for_storage: whether to use controller-side storage. If not specified + then kubernetes charms that haven't previously used local storage and that + are running on a new enough Juju default to controller-side storage, + otherwise local storage is used. + """ + charm_dir = _get_charm_dir() + + model_backend = ops.model._ModelBackend() + debug = ('JUJU_DEBUG' in os.environ) + setup_root_logging(model_backend, debug=debug) + logger.debug("Operator Framework %s up and running.", ops.__version__) + + dispatcher = _Dispatcher(charm_dir) + dispatcher.run_any_legacy_hook() + + metadata = (charm_dir / 'metadata.yaml').read_text() + actions_meta = charm_dir / 'actions.yaml' + if actions_meta.exists(): + actions_metadata = actions_meta.read_text() + else: + actions_metadata = None + + if not yaml.__with_libyaml__: + logger.debug('yaml does not have libyaml extensions, using slower pure Python yaml loader') + meta = ops.charm.CharmMeta.from_yaml(metadata, actions_metadata) + model = ops.model.Model(meta, model_backend) + + charm_state_path = charm_dir / CHARM_STATE_FILE + + if use_juju_for_storage and not ops.storage.juju_backend_available(): + # raise an exception; the charm is broken and needs fixing. + msg = 'charm set use_juju_for_storage=True, but Juju version {} does not support it' + raise RuntimeError(msg.format(JujuVersion.from_environ())) + + if use_juju_for_storage is None: + use_juju_for_storage = _should_use_controller_storage(charm_state_path, meta) + + if use_juju_for_storage: + if dispatcher.is_restricted_context(): + # TODO: jam 2020-06-30 This unconditionally avoids running a collect metrics event + # Though we eventually expect that juju will run collect-metrics in a + # non-restricted context. Once we can determine that we are running collect-metrics + # in a non-restricted context, we should fire the event as normal. + logger.debug('"%s" is not supported when using Juju for storage\n' + 'see: https://github.com/canonical/operator/issues/348', + dispatcher.event_name) + # Note that we don't exit nonzero, because that would cause Juju to rerun the hook + return + store = ops.storage.JujuStorage() + else: + store = ops.storage.SQLiteStorage(charm_state_path) + framework = ops.framework.Framework(store, charm_dir, meta, model) + try: + sig = inspect.signature(charm_class) + try: + sig.bind(framework) + except TypeError: + msg = ( + "the second argument, 'key', has been deprecated and will be " + "removed after the 0.7 release") + warnings.warn(msg, DeprecationWarning) + charm = charm_class(framework, None) + else: + charm = charm_class(framework) + dispatcher.ensure_event_links(charm) + + # TODO: Remove the collect_metrics check below as soon as the relevant + # Juju changes are made. + # + # Skip reemission of deferred events for collect-metrics events because + # they do not have the full access to all hook tools. + if not dispatcher.is_restricted_context(): + framework.reemit() + + _emit_charm_event(charm, dispatcher.event_name) + + framework.commit() + finally: + framework.close() diff --git a/coredns/venv/ops/model.py b/coredns/venv/ops/model.py new file mode 100644 index 0000000..55addf5 --- /dev/null +++ b/coredns/venv/ops/model.py @@ -0,0 +1,1284 @@ +# Copyright 2019 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 datetime +import decimal +import ipaddress +import json +import os +import re +import shutil +import tempfile +import time +import typing +import weakref + +from abc import ABC, abstractmethod +from collections.abc import Mapping, MutableMapping +from pathlib import Path +from subprocess import run, PIPE, CalledProcessError +import yaml + +import ops +from ops.jujuversion import JujuVersion + + +if yaml.__with_libyaml__: + _DefaultDumper = yaml.CSafeDumper +else: + _DefaultDumper = yaml.SafeDumper + + +class Model: + """Represents the Juju Model as seen from this unit. + + This should not be instantiated directly by Charmers, but can be accessed as `self.model` + from any class that derives from Object. + """ + + def __init__(self, meta: 'ops.charm.CharmMeta', backend: '_ModelBackend'): + self._cache = _ModelCache(backend) + self._backend = backend + self._unit = self.get_unit(self._backend.unit_name) + self._relations = RelationMapping(meta.relations, self.unit, self._backend, self._cache) + self._config = ConfigData(self._backend) + self._resources = Resources(list(meta.resources), self._backend) + self._pod = Pod(self._backend) + self._storages = StorageMapping(list(meta.storages), self._backend) + self._bindings = BindingMapping(self._backend) + + @property + def unit(self) -> 'Unit': + """A :class:`Unit` that represents the unit that is running this code (eg yourself)""" + return self._unit + + @property + def app(self): + """A :class:`Application` that represents the application this unit is a part of.""" + return self._unit.app + + @property + def relations(self) -> 'RelationMapping': + """Mapping of endpoint to list of :class:`Relation` + + Answers the question "what am I currently related to". + See also :meth:`.get_relation`. + """ + return self._relations + + @property + def config(self) -> 'ConfigData': + """Return a mapping of config for the current application.""" + return self._config + + @property + def resources(self) -> 'Resources': + """Access to resources for this charm. + + Use ``model.resources.fetch(resource_name)`` to get the path on disk + where the resource can be found. + """ + return self._resources + + @property + def storages(self) -> 'StorageMapping': + """Mapping of storage_name to :class:`Storage` as defined in metadata.yaml""" + return self._storages + + @property + def pod(self) -> 'Pod': + """Use ``model.pod.set_spec`` to set the container specification for Kubernetes charms.""" + return self._pod + + @property + def name(self) -> str: + """Return the name of the Model that this unit is running in. + + This is read from the environment variable ``JUJU_MODEL_NAME``. + """ + return self._backend.model_name + + def get_unit(self, unit_name: str) -> 'Unit': + """Get an arbitrary unit by name. + + Internally this uses a cache, so asking for the same unit two times will + return the same object. + """ + return self._cache.get(Unit, unit_name) + + def get_app(self, app_name: str) -> 'Application': + """Get an application by name. + + Internally this uses a cache, so asking for the same application two times will + return the same object. + """ + return self._cache.get(Application, app_name) + + def get_relation( + self, relation_name: str, + relation_id: typing.Optional[int] = None) -> 'Relation': + """Get a specific Relation instance. + + If relation_id is not given, this will return the Relation instance if the + relation is established only once or None if it is not established. If this + same relation is established multiple times the error TooManyRelatedAppsError is raised. + + Args: + relation_name: The name of the endpoint for this charm + relation_id: An identifier for a specific relation. Used to disambiguate when a + given application has more than one relation on a given endpoint. + Raises: + TooManyRelatedAppsError: is raised if there is more than one relation to the + supplied relation_name and no relation_id was supplied + """ + return self.relations._get_unique(relation_name, relation_id) + + def get_binding(self, binding_key: typing.Union[str, 'Relation']) -> 'Binding': + """Get a network space binding. + + Args: + binding_key: The relation name or instance to obtain bindings for. + Returns: + If ``binding_key`` is a relation name, the method returns the default binding + for that relation. If a relation instance is provided, the method first looks + up a more specific binding for that specific relation ID, and if none is found + falls back to the default binding for the relation name. + """ + return self._bindings.get(binding_key) + + +class _ModelCache: + + def __init__(self, backend): + self._backend = backend + self._weakrefs = weakref.WeakValueDictionary() + + def get(self, entity_type, *args): + key = (entity_type,) + args + entity = self._weakrefs.get(key) + if entity is None: + entity = entity_type(*args, backend=self._backend, cache=self) + self._weakrefs[key] = entity + return entity + + +class Application: + """Represents a named application in the model. + + This might be your application, or might be an application that you are related to. + Charmers should not instantiate Application objects directly, but should use + :meth:`Model.get_app` if they need a reference to a given application. + + Attributes: + name: The name of this application (eg, 'mysql'). This name may differ from the name of + the charm, if the user has deployed it to a different name. + """ + + def __init__(self, name, backend, cache): + self.name = name + self._backend = backend + self._cache = cache + self._is_our_app = self.name == self._backend.app_name + self._status = None + + def _invalidate(self): + self._status = None + + @property + def status(self) -> 'StatusBase': + """Used to report or read the status of the overall application. + + Can only be read and set by the lead unit of the application. + + The status of remote units is always Unknown. + + Raises: + RuntimeError: if you try to set the status of another application, or if you try to + set the status of this application as a unit that is not the leader. + InvalidStatusError: if you try to set the status to something that is not a + :class:`StatusBase` + + Example:: + + self.model.app.status = BlockedStatus('I need a human to come help me') + """ + if not self._is_our_app: + return UnknownStatus() + + if not self._backend.is_leader(): + raise RuntimeError('cannot get application status as a non-leader unit') + + if self._status: + return self._status + + s = self._backend.status_get(is_app=True) + self._status = StatusBase.from_name(s['status'], s['message']) + return self._status + + @status.setter + def status(self, value: 'StatusBase'): + if not isinstance(value, StatusBase): + raise InvalidStatusError( + 'invalid value provided for application {} status: {}'.format(self, value) + ) + + if not self._is_our_app: + raise RuntimeError('cannot to set status for a remote application {}'.format(self)) + + if not self._backend.is_leader(): + raise RuntimeError('cannot set application status as a non-leader unit') + + self._backend.status_set(value.name, value.message, is_app=True) + self._status = value + + def __repr__(self): + return '<{}.{} {}>'.format(type(self).__module__, type(self).__name__, self.name) + + +class Unit: + """Represents a named unit in the model. + + This might be your unit, another unit of your application, or a unit of another application + that you are related to. + + Attributes: + name: The name of the unit (eg, 'mysql/0') + app: The Application the unit is a part of. + """ + + def __init__(self, name, backend, cache): + self.name = name + + app_name = name.split('/')[0] + self.app = cache.get(Application, app_name) + + self._backend = backend + self._cache = cache + self._is_our_unit = self.name == self._backend.unit_name + self._status = None + + def _invalidate(self): + self._status = None + + @property + def status(self) -> 'StatusBase': + """Used to report or read the status of a specific unit. + + The status of any unit other than yourself is always Unknown. + + Raises: + RuntimeError: if you try to set the status of a unit other than yourself. + InvalidStatusError: if you try to set the status to something other than + a :class:`StatusBase` + Example:: + + self.model.unit.status = MaintenanceStatus('reconfiguring the frobnicators') + """ + if not self._is_our_unit: + return UnknownStatus() + + if self._status: + return self._status + + s = self._backend.status_get(is_app=False) + self._status = StatusBase.from_name(s['status'], s['message']) + return self._status + + @status.setter + def status(self, value: 'StatusBase'): + if not isinstance(value, StatusBase): + raise InvalidStatusError( + 'invalid value provided for unit {} status: {}'.format(self, value) + ) + + if not self._is_our_unit: + raise RuntimeError('cannot set status for a remote unit {}'.format(self)) + + self._backend.status_set(value.name, value.message, is_app=False) + self._status = value + + def __repr__(self): + return '<{}.{} {}>'.format(type(self).__module__, type(self).__name__, self.name) + + def is_leader(self) -> bool: + """Return whether this unit is the leader of its application. + + This can only be called for your own unit. + Returns: + True if you are the leader, False otherwise + Raises: + RuntimeError: if called for a unit that is not yourself + """ + if self._is_our_unit: + # This value is not cached as it is not guaranteed to persist for the whole duration + # of a hook execution. + return self._backend.is_leader() + else: + raise RuntimeError( + 'leadership status of remote units ({}) is not visible to other' + ' applications'.format(self) + ) + + def set_workload_version(self, version: str) -> None: + """Record the version of the software running as the workload. + + This shouldn't be confused with the revision of the charm. This is informative only; + shown in the output of 'juju status'. + """ + if not isinstance(version, str): + raise TypeError("workload version must be a str, not {}: {!r}".format( + type(version).__name__, version)) + self._backend.application_version_set(version) + + +class LazyMapping(Mapping, ABC): + """Represents a dict that isn't populated until it is accessed. + + Charm authors should generally never need to use this directly, but it forms + the basis for many of the dicts that the framework tracks. + """ + + _lazy_data = None + + @abstractmethod + def _load(self): + raise NotImplementedError() + + @property + def _data(self): + data = self._lazy_data + if data is None: + data = self._lazy_data = self._load() + return data + + def _invalidate(self): + self._lazy_data = None + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __getitem__(self, key): + return self._data[key] + + def __repr__(self): + return repr(self._data) + + +class RelationMapping(Mapping): + """Map of relation names to lists of :class:`Relation` instances.""" + + def __init__(self, relations_meta, our_unit, backend, cache): + self._peers = set() + for name, relation_meta in relations_meta.items(): + if relation_meta.role.is_peer(): + self._peers.add(name) + self._our_unit = our_unit + self._backend = backend + self._cache = cache + self._data = {relation_name: None for relation_name in relations_meta} + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __getitem__(self, relation_name): + is_peer = relation_name in self._peers + relation_list = self._data[relation_name] + if relation_list is None: + relation_list = self._data[relation_name] = [] + for rid in self._backend.relation_ids(relation_name): + relation = Relation(relation_name, rid, is_peer, + self._our_unit, self._backend, self._cache) + relation_list.append(relation) + return relation_list + + def _invalidate(self, relation_name): + """Used to wipe the cache of a given relation_name. + + Not meant to be used by Charm authors. The content of relation data is + static for the lifetime of a hook, so it is safe to cache in memory once + accessed. + """ + self._data[relation_name] = None + + def _get_unique(self, relation_name, relation_id=None): + if relation_id is not None: + if not isinstance(relation_id, int): + raise ModelError('relation id {} must be int or None not {}'.format( + relation_id, + type(relation_id).__name__)) + for relation in self[relation_name]: + if relation.id == relation_id: + return relation + else: + # The relation may be dead, but it is not forgotten. + is_peer = relation_name in self._peers + return Relation(relation_name, relation_id, is_peer, + self._our_unit, self._backend, self._cache) + num_related = len(self[relation_name]) + if num_related == 0: + return None + elif num_related == 1: + return self[relation_name][0] + else: + # TODO: We need something in the framework to catch and gracefully handle + # errors, ideally integrating the error catching with Juju's mechanisms. + raise TooManyRelatedAppsError(relation_name, num_related, 1) + + +class BindingMapping: + """Mapping of endpoints to network bindings. + + Charm authors should not instantiate this directly, but access it via + :meth:`Model.get_binding` + """ + + def __init__(self, backend): + self._backend = backend + self._data = {} + + def get(self, binding_key: typing.Union[str, 'Relation']) -> 'Binding': + """Get a specific Binding for an endpoint/relation. + + Not used directly by Charm authors. See :meth:`Model.get_binding` + """ + if isinstance(binding_key, Relation): + binding_name = binding_key.name + relation_id = binding_key.id + elif isinstance(binding_key, str): + binding_name = binding_key + relation_id = None + else: + raise ModelError('binding key must be str or relation instance, not {}' + ''.format(type(binding_key).__name__)) + binding = self._data.get(binding_key) + if binding is None: + binding = Binding(binding_name, relation_id, self._backend) + self._data[binding_key] = binding + return binding + + +class Binding: + """Binding to a network space. + + Attributes: + name: The name of the endpoint this binding represents (eg, 'db') + """ + + def __init__(self, name, relation_id, backend): + self.name = name + self._relation_id = relation_id + self._backend = backend + self._network = None + + @property + def network(self) -> 'Network': + """The network information for this binding.""" + if self._network is None: + try: + self._network = Network(self._backend.network_get(self.name, self._relation_id)) + except RelationNotFoundError: + if self._relation_id is None: + raise + # If a relation is dead, we can still get network info associated with an + # endpoint itself + self._network = Network(self._backend.network_get(self.name)) + return self._network + + +class Network: + """Network space details. + + Charm authors should not instantiate this directly, but should get access to the Network + definition from :meth:`Model.get_binding` and its ``network`` attribute. + + Attributes: + interfaces: A list of :class:`NetworkInterface` details. This includes the + information about how your application should be configured (eg, what + IP addresses should you bind to.) + Note that multiple addresses for a single interface are represented as multiple + interfaces. (eg, ``[NetworkInfo('ens1', '10.1.1.1/32'), + NetworkInfo('ens1', '10.1.2.1/32'])``) + ingress_addresses: A list of :class:`ipaddress.ip_address` objects representing the IP + addresses that other units should use to get in touch with you. + egress_subnets: A list of :class:`ipaddress.ip_network` representing the subnets that + other units will see you connecting from. Due to things like NAT it isn't always + possible to narrow it down to a single address, but when it is clear, the CIDRs + will be constrained to a single address. (eg, 10.0.0.1/32) + Args: + network_info: A dict of network information as returned by ``network-get``. + """ + + def __init__(self, network_info: dict): + self.interfaces = [] + # Treat multiple addresses on an interface as multiple logical + # interfaces with the same name. + for interface_info in network_info['bind-addresses']: + interface_name = interface_info['interface-name'] + for address_info in interface_info['addresses']: + self.interfaces.append(NetworkInterface(interface_name, address_info)) + self.ingress_addresses = [] + for address in network_info['ingress-addresses']: + self.ingress_addresses.append(ipaddress.ip_address(address)) + self.egress_subnets = [] + for subnet in network_info['egress-subnets']: + self.egress_subnets.append(ipaddress.ip_network(subnet)) + + @property + def bind_address(self): + """A single address that your application should bind() to. + + For the common case where there is a single answer. This represents a single + address from :attr:`.interfaces` that can be used to configure where your + application should bind() and listen(). + """ + return self.interfaces[0].address + + @property + def ingress_address(self): + """The address other applications should use to connect to your unit. + + Due to things like public/private addresses, NAT and tunneling, the address you bind() + to is not always the address other people can use to connect() to you. + This is just the first address from :attr:`.ingress_addresses`. + """ + return self.ingress_addresses[0] + + +class NetworkInterface: + """Represents a single network interface that the charm needs to know about. + + Charmers should not instantiate this type directly. Instead use :meth:`Model.get_binding` + to get the network information for a given endpoint. + + Attributes: + name: The name of the interface (eg. 'eth0', or 'ens1') + subnet: An :class:`ipaddress.ip_network` representation of the IP for the network + interface. This may be a single address (eg '10.0.1.2/32') + """ + + def __init__(self, name: str, address_info: dict): + self.name = name + # TODO: expose a hardware address here, see LP: #1864070. + self.address = ipaddress.ip_address(address_info['value']) + cidr = address_info['cidr'] + if not cidr: + # The cidr field may be empty, see LP: #1864102. + # In this case, make it a /32 or /128 IP network. + self.subnet = ipaddress.ip_network(address_info['value']) + else: + self.subnet = ipaddress.ip_network(cidr) + # TODO: expose a hostname/canonical name for the address here, see LP: #1864086. + + +class Relation: + """Represents an established relation between this application and another application. + + This class should not be instantiated directly, instead use :meth:`Model.get_relation` + or :attr:`RelationEvent.relation`. + + Attributes: + name: The name of the local endpoint of the relation (eg 'db') + id: The identifier for a particular relation (integer) + app: An :class:`Application` representing the remote application of this relation. + For peer relations this will be the local application. + units: A set of :class:`Unit` for units that have started and joined this relation. + data: A :class:`RelationData` holding the data buckets for each entity + of a relation. Accessed via eg Relation.data[unit]['foo'] + """ + + def __init__( + self, relation_name: str, relation_id: int, is_peer: bool, our_unit: Unit, + backend: '_ModelBackend', cache: '_ModelCache'): + self.name = relation_name + self.id = relation_id + self.app = None + self.units = set() + + # For peer relations, both the remote and the local app are the same. + if is_peer: + self.app = our_unit.app + try: + for unit_name in backend.relation_list(self.id): + unit = cache.get(Unit, unit_name) + self.units.add(unit) + if self.app is None: + self.app = unit.app + except RelationNotFoundError: + # If the relation is dead, just treat it as if it has no remote units. + pass + self.data = RelationData(self, our_unit, backend) + + def __repr__(self): + return '<{}.{} {}:{}>'.format(type(self).__module__, + type(self).__name__, + self.name, + self.id) + + +class RelationData(Mapping): + """Represents the various data buckets of a given relation. + + Each unit and application involved in a relation has their own data bucket. + Eg: ``{entity: RelationDataContent}`` + where entity can be either a :class:`Unit` or a :class:`Application`. + + Units can read and write their own data, and if they are the leader, + they can read and write their application data. They are allowed to read + remote unit and application data. + + This class should not be created directly. It should be accessed via + :attr:`Relation.data` + """ + + def __init__(self, relation: Relation, our_unit: Unit, backend: '_ModelBackend'): + self.relation = weakref.proxy(relation) + self._data = { + our_unit: RelationDataContent(self.relation, our_unit, backend), + our_unit.app: RelationDataContent(self.relation, our_unit.app, backend), + } + self._data.update({ + unit: RelationDataContent(self.relation, unit, backend) + for unit in self.relation.units}) + # The relation might be dead so avoid a None key here. + if self.relation.app is not None: + self._data.update({ + self.relation.app: RelationDataContent(self.relation, self.relation.app, backend), + }) + + def __contains__(self, key): + return key in self._data + + def __len__(self): + return len(self._data) + + def __iter__(self): + return iter(self._data) + + def __getitem__(self, key): + return self._data[key] + + def __repr__(self): + return repr(self._data) + + +# We mix in MutableMapping here to get some convenience implementations, but whether it's actually +# mutable or not is controlled by the flag. +class RelationDataContent(LazyMapping, MutableMapping): + + def __init__(self, relation, entity, backend): + self.relation = relation + self._entity = entity + self._backend = backend + self._is_app = isinstance(entity, Application) + + def _load(self): + try: + return self._backend.relation_get(self.relation.id, self._entity.name, self._is_app) + except RelationNotFoundError: + # Dead relations tell no tales (and have no data). + return {} + + def _is_mutable(self): + if self._is_app: + is_our_app = self._backend.app_name == self._entity.name + if not is_our_app: + return False + # Whether the application data bag is mutable or not depends on + # whether this unit is a leader or not, but this is not guaranteed + # to be always true during the same hook execution. + return self._backend.is_leader() + else: + is_our_unit = self._backend.unit_name == self._entity.name + if is_our_unit: + return True + return False + + def __setitem__(self, key, value): + if not self._is_mutable(): + raise RelationDataError('cannot set relation data for {}'.format(self._entity.name)) + if not isinstance(value, str): + raise RelationDataError('relation data values must be strings') + + self._backend.relation_set(self.relation.id, key, value, self._is_app) + + # Don't load data unnecessarily if we're only updating. + if self._lazy_data is not None: + if value == '': + # Match the behavior of Juju, which is that setting the value to an + # empty string will remove the key entirely from the relation data. + self._data.pop(key, None) + else: + self._data[key] = value + + def __delitem__(self, key): + # Match the behavior of Juju, which is that setting the value to an empty + # string will remove the key entirely from the relation data. + self.__setitem__(key, '') + + +class ConfigData(LazyMapping): + + def __init__(self, backend): + self._backend = backend + + def _load(self): + return self._backend.config_get() + + +class StatusBase: + """Status values specific to applications and units. + + To access a status by name, see :meth:`StatusBase.from_name`, most use cases will just + directly use the child class to indicate their status. + """ + + _statuses = {} + name = None + + def __init__(self, message: str): + self.message = message + + def __new__(cls, *args, **kwargs): + if cls is StatusBase: + raise TypeError("cannot instantiate a base class") + return super().__new__(cls) + + def __eq__(self, other): + if not isinstance(self, type(other)): + return False + return self.message == other.message + + def __repr__(self): + return "{.__class__.__name__}({!r})".format(self, self.message) + + @classmethod + def from_name(cls, name: str, message: str): + if name == 'unknown': + # unknown is special + return UnknownStatus() + else: + return cls._statuses[name](message) + + @classmethod + def register(cls, child): + if child.name is None: + raise AttributeError('cannot register a Status which has no name') + cls._statuses[child.name] = child + return child + + +@StatusBase.register +class UnknownStatus(StatusBase): + """The unit status is unknown. + + A unit-agent has finished calling install, config-changed and start, but the + charm has not called status-set yet. + + """ + name = 'unknown' + + def __init__(self): + # Unknown status cannot be set and does not have a message associated with it. + super().__init__('') + + def __repr__(self): + return "UnknownStatus()" + + +@StatusBase.register +class ActiveStatus(StatusBase): + """The unit is ready. + + The unit believes it is correctly offering all the services it has been asked to offer. + """ + name = 'active' + + def __init__(self, message: str = ''): + super().__init__(message) + + +@StatusBase.register +class BlockedStatus(StatusBase): + """The unit requires manual intervention. + + An operator has to manually intervene to unblock the unit and let it proceed. + """ + name = 'blocked' + + +@StatusBase.register +class MaintenanceStatus(StatusBase): + """The unit is performing maintenance tasks. + + The unit is not yet providing services, but is actively doing work in preparation + for providing those services. This is a "spinning" state, not an error state. It + reflects activity on the unit itself, not on peers or related units. + + """ + name = 'maintenance' + + +@StatusBase.register +class WaitingStatus(StatusBase): + """A unit is unable to progress. + + The unit is unable to progress to an active state because an application to which + it is related is not running. + + """ + name = 'waiting' + + +class Resources: + """Object representing resources for the charm. + """ + + def __init__(self, names: typing.Iterable[str], backend: '_ModelBackend'): + self._backend = backend + self._paths = {name: None for name in names} + + def fetch(self, name: str) -> Path: + """Fetch the resource from the controller or store. + + If successfully fetched, this returns a Path object to where the resource is stored + on disk, otherwise it raises a ModelError. + """ + if name not in self._paths: + raise RuntimeError('invalid resource name: {}'.format(name)) + if self._paths[name] is None: + self._paths[name] = Path(self._backend.resource_get(name)) + return self._paths[name] + + +class Pod: + """Represents the definition of a pod spec in Kubernetes models. + + Currently only supports simple access to setting the Juju pod spec via :attr:`.set_spec`. + """ + + def __init__(self, backend: '_ModelBackend'): + self._backend = backend + + def set_spec(self, spec: typing.Mapping, k8s_resources: typing.Mapping = None): + """Set the specification for pods that Juju should start in kubernetes. + + See `juju help-tool pod-spec-set` for details of what should be passed. + + Args: + spec: The mapping defining the pod specification + k8s_resources: Additional kubernetes specific specification. + + Returns: + None + """ + if not self._backend.is_leader(): + raise ModelError('cannot set a pod spec as this unit is not a leader') + self._backend.pod_spec_set(spec, k8s_resources) + + +class StorageMapping(Mapping): + """Map of storage names to lists of Storage instances.""" + + def __init__(self, storage_names: typing.Iterable[str], backend: '_ModelBackend'): + self._backend = backend + self._storage_map = {storage_name: None for storage_name in storage_names} + + def __contains__(self, key: str): + return key in self._storage_map + + def __len__(self): + return len(self._storage_map) + + def __iter__(self): + return iter(self._storage_map) + + def __getitem__(self, storage_name: str) -> typing.List['Storage']: + storage_list = self._storage_map[storage_name] + if storage_list is None: + storage_list = self._storage_map[storage_name] = [] + for storage_id in self._backend.storage_list(storage_name): + storage_list.append(Storage(storage_name, storage_id, self._backend)) + return storage_list + + def request(self, storage_name: str, count: int = 1): + """Requests new storage instances of a given name. + + Uses storage-add tool to request additional storage. Juju will notify the unit + via -storage-attached events when it becomes available. + """ + if storage_name not in self._storage_map: + raise ModelError(('cannot add storage {!r}:' + ' it is not present in the charm metadata').format(storage_name)) + self._backend.storage_add(storage_name, count) + + +class Storage: + """"Represents a storage as defined in metadata.yaml + + Attributes: + name: Simple string name of the storage + id: The provider id for storage + """ + + def __init__(self, storage_name, storage_id, backend): + self.name = storage_name + self.id = storage_id + self._backend = backend + self._location = None + + @property + def location(self): + if self._location is None: + raw = self._backend.storage_get('{}/{}'.format(self.name, self.id), "location") + self._location = Path(raw) + return self._location + + +class ModelError(Exception): + """Base class for exceptions raised when interacting with the Model.""" + pass + + +class TooManyRelatedAppsError(ModelError): + """Raised by :meth:`Model.get_relation` if there is more than one related application.""" + + def __init__(self, relation_name, num_related, max_supported): + super().__init__('Too many remote applications on {} ({} > {})'.format( + relation_name, num_related, max_supported)) + self.relation_name = relation_name + self.num_related = num_related + self.max_supported = max_supported + + +class RelationDataError(ModelError): + """Raised by ``Relation.data[entity][key] = 'foo'`` if the data is invalid. + + This is raised if you're either trying to set a value to something that isn't a string, + or if you are trying to set a value in a bucket that you don't have access to. (eg, + another application/unit or setting your application data but you aren't the leader.) + """ + + +class RelationNotFoundError(ModelError): + """Backend error when querying juju for a given relation and that relation doesn't exist.""" + + +class InvalidStatusError(ModelError): + """Raised if trying to set an Application or Unit status to something invalid.""" + + +class _ModelBackend: + """Represents the connection between the Model representation and talking to Juju. + + Charm authors should not directly interact with the ModelBackend, it is a private + implementation of Model. + """ + + LEASE_RENEWAL_PERIOD = datetime.timedelta(seconds=30) + + def __init__(self, unit_name=None, model_name=None): + if unit_name is None: + self.unit_name = os.environ['JUJU_UNIT_NAME'] + else: + self.unit_name = unit_name + if model_name is None: + model_name = os.environ.get('JUJU_MODEL_NAME') + self.model_name = model_name + self.app_name = self.unit_name.split('/')[0] + + self._is_leader = None + self._leader_check_time = None + + def _run(self, *args, return_output=False, use_json=False): + kwargs = dict(stdout=PIPE, stderr=PIPE, check=True) + args = (shutil.which(args[0]),) + args[1:] + if use_json: + args += ('--format=json',) + try: + result = run(args, **kwargs) + except CalledProcessError as e: + raise ModelError(e.stderr) + if return_output: + if result.stdout is None: + return '' + else: + text = result.stdout.decode('utf8') + if use_json: + return json.loads(text) + else: + return text + + def relation_ids(self, relation_name): + relation_ids = self._run('relation-ids', relation_name, return_output=True, use_json=True) + return [int(relation_id.split(':')[-1]) for relation_id in relation_ids] + + def relation_list(self, relation_id): + try: + return self._run('relation-list', '-r', str(relation_id), + return_output=True, use_json=True) + except ModelError as e: + if 'relation not found' in str(e): + raise RelationNotFoundError() from e + raise + + def relation_get(self, relation_id, member_name, is_app): + if not isinstance(is_app, bool): + raise TypeError('is_app parameter to relation_get must be a boolean') + + if is_app: + version = JujuVersion.from_environ() + if not version.has_app_data(): + raise RuntimeError( + 'getting application data is not supported on Juju version {}'.format(version)) + + args = ['relation-get', '-r', str(relation_id), '-', member_name] + if is_app: + args.append('--app') + + try: + return self._run(*args, return_output=True, use_json=True) + except ModelError as e: + if 'relation not found' in str(e): + raise RelationNotFoundError() from e + raise + + def relation_set(self, relation_id, key, value, is_app): + if not isinstance(is_app, bool): + raise TypeError('is_app parameter to relation_set must be a boolean') + + if is_app: + version = JujuVersion.from_environ() + if not version.has_app_data(): + raise RuntimeError( + 'setting application data is not supported on Juju version {}'.format(version)) + + args = ['relation-set', '-r', str(relation_id), '{}={}'.format(key, value)] + if is_app: + args.append('--app') + + try: + return self._run(*args) + except ModelError as e: + if 'relation not found' in str(e): + raise RelationNotFoundError() from e + raise + + def config_get(self): + return self._run('config-get', return_output=True, use_json=True) + + def is_leader(self): + """Obtain the current leadership status for the unit the charm code is executing on. + + The value is cached for the duration of a lease which is 30s in Juju. + """ + now = time.monotonic() + if self._leader_check_time is None: + check = True + else: + time_since_check = datetime.timedelta(seconds=now - self._leader_check_time) + check = (time_since_check > self.LEASE_RENEWAL_PERIOD or self._is_leader is None) + if check: + # Current time MUST be saved before running is-leader to ensure the cache + # is only used inside the window that is-leader itself asserts. + self._leader_check_time = now + self._is_leader = self._run('is-leader', return_output=True, use_json=True) + + return self._is_leader + + def resource_get(self, resource_name): + return self._run('resource-get', resource_name, return_output=True).strip() + + def pod_spec_set(self, spec, k8s_resources): + tmpdir = Path(tempfile.mkdtemp('-pod-spec-set')) + try: + spec_path = tmpdir / 'spec.yaml' + with spec_path.open("wt", encoding="utf8") as f: + yaml.dump(spec, stream=f, Dumper=_DefaultDumper) + args = ['--file', str(spec_path)] + if k8s_resources: + k8s_res_path = tmpdir / 'k8s-resources.yaml' + with k8s_res_path.open("wt", encoding="utf8") as f: + yaml.dump(k8s_resources, stream=f, Dumper=_DefaultDumper) + args.extend(['--k8s-resources', str(k8s_res_path)]) + self._run('pod-spec-set', *args) + finally: + shutil.rmtree(str(tmpdir)) + + def status_get(self, *, is_app=False): + """Get a status of a unit or an application. + + Args: + is_app: A boolean indicating whether the status should be retrieved for a unit + or an application. + """ + content = self._run( + 'status-get', '--include-data', '--application={}'.format(is_app), + use_json=True, + return_output=True) + # Unit status looks like (in YAML): + # message: 'load: 0.28 0.26 0.26' + # status: active + # status-data: {} + # Application status looks like (in YAML): + # application-status: + # message: 'load: 0.28 0.26 0.26' + # status: active + # status-data: {} + # units: + # uo/0: + # message: 'load: 0.28 0.26 0.26' + # status: active + # status-data: {} + + if is_app: + return {'status': content['application-status']['status'], + 'message': content['application-status']['message']} + else: + return content + + def status_set(self, status, message='', *, is_app=False): + """Set a status of a unit or an application. + + Args: + app: A boolean indicating whether the status should be set for a unit or an + application. + """ + if not isinstance(is_app, bool): + raise TypeError('is_app parameter must be boolean') + return self._run('status-set', '--application={}'.format(is_app), status, message) + + def storage_list(self, name): + return [int(s.split('/')[1]) for s in self._run('storage-list', name, + return_output=True, use_json=True)] + + def storage_get(self, storage_name_id, attribute): + return self._run('storage-get', '-s', storage_name_id, attribute, + return_output=True, use_json=True) + + def storage_add(self, name, count=1): + if not isinstance(count, int) or isinstance(count, bool): + raise TypeError('storage count must be integer, got: {} ({})'.format(count, + type(count))) + self._run('storage-add', '{}={}'.format(name, count)) + + def action_get(self): + return self._run('action-get', return_output=True, use_json=True) + + def action_set(self, results): + self._run('action-set', *["{}={}".format(k, v) for k, v in results.items()]) + + def action_log(self, message): + self._run('action-log', message) + + def action_fail(self, message=''): + self._run('action-fail', message) + + def application_version_set(self, version): + self._run('application-version-set', '--', version) + + def juju_log(self, level, message): + self._run('juju-log', '--log-level', level, message) + + def network_get(self, binding_name, relation_id=None): + """Return network info provided by network-get for a given binding. + + Args: + binding_name: A name of a binding (relation name or extra-binding name). + relation_id: An optional relation id to get network info for. + """ + cmd = ['network-get', binding_name] + if relation_id is not None: + cmd.extend(['-r', str(relation_id)]) + try: + return self._run(*cmd, return_output=True, use_json=True) + except ModelError as e: + if 'relation not found' in str(e): + raise RelationNotFoundError() from e + raise + + def add_metrics(self, metrics, labels=None): + cmd = ['add-metric'] + + if labels: + label_args = [] + for k, v in labels.items(): + _ModelBackendValidator.validate_metric_label(k) + _ModelBackendValidator.validate_label_value(k, v) + label_args.append('{}={}'.format(k, v)) + cmd.extend(['--labels', ','.join(label_args)]) + + metric_args = [] + for k, v in metrics.items(): + _ModelBackendValidator.validate_metric_key(k) + metric_value = _ModelBackendValidator.format_metric_value(v) + metric_args.append('{}={}'.format(k, metric_value)) + cmd.extend(metric_args) + self._run(*cmd) + + +class _ModelBackendValidator: + """Provides facilities for validating inputs and formatting them for model backends.""" + + METRIC_KEY_REGEX = re.compile(r'^[a-zA-Z](?:[a-zA-Z0-9-_]*[a-zA-Z0-9])?$') + + @classmethod + def validate_metric_key(cls, key): + if cls.METRIC_KEY_REGEX.match(key) is None: + raise ModelError( + 'invalid metric key {!r}: must match {}'.format( + key, cls.METRIC_KEY_REGEX.pattern)) + + @classmethod + def validate_metric_label(cls, label_name): + if cls.METRIC_KEY_REGEX.match(label_name) is None: + raise ModelError( + 'invalid metric label name {!r}: must match {}'.format( + label_name, cls.METRIC_KEY_REGEX.pattern)) + + @classmethod + def format_metric_value(cls, value): + try: + decimal_value = decimal.Decimal.from_float(value) + except TypeError as e: + e2 = ModelError('invalid metric value {!r} provided:' + ' must be a positive finite float'.format(value)) + raise e2 from e + if decimal_value.is_nan() or decimal_value.is_infinite() or decimal_value < 0: + raise ModelError('invalid metric value {!r} provided:' + ' must be a positive finite float'.format(value)) + return str(decimal_value) + + @classmethod + def validate_label_value(cls, label, value): + # Label values cannot be empty, contain commas or equal signs as those are + # used by add-metric as separators. + if not value: + raise ModelError( + 'metric label {} has an empty value, which is not allowed'.format(label)) + v = str(value) + if re.search('[,=]', v) is not None: + raise ModelError( + 'metric label values must not contain "," or "=": {}={!r}'.format(label, value)) diff --git a/coredns/venv/ops/storage.py b/coredns/venv/ops/storage.py new file mode 100644 index 0000000..ec82d64 --- /dev/null +++ b/coredns/venv/ops/storage.py @@ -0,0 +1,318 @@ +# Copyright 2019-2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import timedelta +import pickle +import shutil +import subprocess +import sqlite3 +import typing + +import yaml + + +def _run(args, **kw): + cmd = shutil.which(args[0]) + if cmd is None: + raise FileNotFoundError(args[0]) + return subprocess.run([cmd, *args[1:]], **kw) + + +class SQLiteStorage: + + DB_LOCK_TIMEOUT = timedelta(hours=1) + + def __init__(self, filename): + # The isolation_level argument is set to None such that the implicit + # transaction management behavior of the sqlite3 module is disabled. + self._db = sqlite3.connect(str(filename), + isolation_level=None, + timeout=self.DB_LOCK_TIMEOUT.total_seconds()) + self._setup() + + def _setup(self): + # Make sure that the database is locked until the connection is closed, + # not until the transaction ends. + self._db.execute("PRAGMA locking_mode=EXCLUSIVE") + c = self._db.execute("BEGIN") + c.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='snapshot'") + if c.fetchone()[0] == 0: + # Keep in mind what might happen if the process dies somewhere below. + # The system must not be rendered permanently broken by that. + self._db.execute("CREATE TABLE snapshot (handle TEXT PRIMARY KEY, data BLOB)") + self._db.execute(''' + CREATE TABLE notice ( + sequence INTEGER PRIMARY KEY AUTOINCREMENT, + event_path TEXT, + observer_path TEXT, + method_name TEXT) + ''') + self._db.commit() + + def close(self): + self._db.close() + + def commit(self): + self._db.commit() + + # There's commit but no rollback. For abort to be supported, we'll need logic that + # can rollback decisions made by third-party code in terms of the internal state + # of objects that have been snapshotted, and hooks to let them know about it and + # take the needed actions to undo their logic until the last snapshot. + # This is doable but will increase significantly the chances for mistakes. + + def save_snapshot(self, handle_path: str, snapshot_data: typing.Any) -> None: + """Part of the Storage API, persist a snapshot data under the given handle. + + Args: + handle_path: The string identifying the snapshot. + snapshot_data: The data to be persisted. (as returned by Object.snapshot()). This + might be a dict/tuple/int, but must only contain 'simple' python types. + """ + # Use pickle for serialization, so the value remains portable. + raw_data = pickle.dumps(snapshot_data) + self._db.execute("REPLACE INTO snapshot VALUES (?, ?)", (handle_path, raw_data)) + + def load_snapshot(self, handle_path: str) -> typing.Any: + """Part of the Storage API, retrieve a snapshot that was previously saved. + + Args: + handle_path: The string identifying the snapshot. + Raises: + NoSnapshotError: if there is no snapshot for the given handle_path. + """ + c = self._db.cursor() + c.execute("SELECT data FROM snapshot WHERE handle=?", (handle_path,)) + row = c.fetchone() + if row: + return pickle.loads(row[0]) + raise NoSnapshotError(handle_path) + + def drop_snapshot(self, handle_path: str): + """Part of the Storage API, remove a snapshot that was previously saved. + + Dropping a snapshot that doesn't exist is treated as a no-op. + """ + self._db.execute("DELETE FROM snapshot WHERE handle=?", (handle_path,)) + + def list_snapshots(self) -> typing.Generator[str, None, None]: + """Return the name of all snapshots that are currently saved.""" + c = self._db.cursor() + c.execute("SELECT handle FROM snapshot") + while True: + rows = c.fetchmany() + if not rows: + break + for row in rows: + yield row[0] + + def save_notice(self, event_path: str, observer_path: str, method_name: str) -> None: + """Part of the Storage API, record an notice (event and observer)""" + self._db.execute('INSERT INTO notice VALUES (NULL, ?, ?, ?)', + (event_path, observer_path, method_name)) + + def drop_notice(self, event_path: str, observer_path: str, method_name: str) -> None: + """Part of the Storage API, remove a notice that was previously recorded.""" + self._db.execute(''' + DELETE FROM notice + WHERE event_path=? + AND observer_path=? + AND method_name=? + ''', (event_path, observer_path, method_name)) + + def notices(self, event_path: typing.Optional[str]) ->\ + typing.Generator[typing.Tuple[str, str, str], None, None]: + """Part of the Storage API, return all notices that begin with event_path. + + Args: + event_path: If supplied, will only yield events that match event_path. If not + supplied (or None/'') will return all events. + Returns: + Iterable of (event_path, observer_path, method_name) tuples + """ + if event_path: + c = self._db.execute(''' + SELECT event_path, observer_path, method_name + FROM notice + WHERE event_path=? + ORDER BY sequence + ''', (event_path,)) + else: + c = self._db.execute(''' + SELECT event_path, observer_path, method_name + FROM notice + ORDER BY sequence + ''') + while True: + rows = c.fetchmany() + if not rows: + break + for row in rows: + yield tuple(row) + + +class JujuStorage: + """"Storing the content tracked by the Framework in Juju. + + This uses :class:`_JujuStorageBackend` to interact with state-get/state-set + as the way to store state for the framework and for components. + """ + + NOTICE_KEY = "#notices#" + + def __init__(self, backend: '_JujuStorageBackend' = None): + self._backend = backend + if backend is None: + self._backend = _JujuStorageBackend() + + def close(self): + return + + def commit(self): + return + + def save_snapshot(self, handle_path: str, snapshot_data: typing.Any) -> None: + self._backend.set(handle_path, snapshot_data) + + def load_snapshot(self, handle_path): + try: + content = self._backend.get(handle_path) + except KeyError: + raise NoSnapshotError(handle_path) + return content + + def drop_snapshot(self, handle_path): + self._backend.delete(handle_path) + + def save_notice(self, event_path: str, observer_path: str, method_name: str): + notice_list = self._load_notice_list() + notice_list.append([event_path, observer_path, method_name]) + self._save_notice_list(notice_list) + + def drop_notice(self, event_path: str, observer_path: str, method_name: str): + notice_list = self._load_notice_list() + notice_list.remove([event_path, observer_path, method_name]) + self._save_notice_list(notice_list) + + def notices(self, event_path: str): + notice_list = self._load_notice_list() + for row in notice_list: + if row[0] != event_path: + continue + yield tuple(row) + + def _load_notice_list(self) -> typing.List[typing.Tuple[str]]: + try: + notice_list = self._backend.get(self.NOTICE_KEY) + except KeyError: + return [] + if notice_list is None: + return [] + return notice_list + + def _save_notice_list(self, notices: typing.List[typing.Tuple[str]]) -> None: + self._backend.set(self.NOTICE_KEY, notices) + + +class _SimpleLoader(getattr(yaml, 'CSafeLoader', yaml.SafeLoader)): + """Handle a couple basic python types. + + yaml.SafeLoader can handle all the basic int/float/dict/set/etc that we want. The only one + that it *doesn't* handle is tuples. We don't want to support arbitrary types, so we just + subclass SafeLoader and add tuples back in. + """ + # Taken from the example at: + # https://stackoverflow.com/questions/9169025/how-can-i-add-a-python-tuple-to-a-yaml-file-using-pyyaml + + construct_python_tuple = yaml.Loader.construct_python_tuple + + +_SimpleLoader.add_constructor( + u'tag:yaml.org,2002:python/tuple', + _SimpleLoader.construct_python_tuple) + + +class _SimpleDumper(getattr(yaml, 'CSafeDumper', yaml.SafeDumper)): + """Add types supported by 'marshal' + + YAML can support arbitrary types, but that is generally considered unsafe (like pickle). So + we want to only support dumping out types that are safe to load. + """ + + +_SimpleDumper.represent_tuple = yaml.Dumper.represent_tuple +_SimpleDumper.add_representer(tuple, _SimpleDumper.represent_tuple) + + +def juju_backend_available() -> bool: + """Check if Juju state storage is available.""" + p = shutil.which('state-get') + return p is not None + + +class _JujuStorageBackend: + """Implements the interface from the Operator framework to Juju's state-get/set/etc.""" + + def set(self, key: str, value: typing.Any) -> None: + """Set a key to a given value. + + Args: + key: The string key that will be used to find the value later + value: Arbitrary content that will be returned by get(). + Raises: + CalledProcessError: if 'state-set' returns an error code. + """ + # default_flow_style=None means that it can use Block for + # complex types (types that have nested types) but use flow + # for simple types (like an array). Not all versions of PyYAML + # have the same default style. + encoded_value = yaml.dump(value, Dumper=_SimpleDumper, default_flow_style=None) + content = yaml.dump( + {key: encoded_value}, encoding='utf8', default_style='|', + default_flow_style=False, + Dumper=_SimpleDumper) + _run(["state-set", "--file", "-"], input=content, check=True) + + def get(self, key: str) -> typing.Any: + """Get the bytes value associated with a given key. + + Args: + key: The string key that will be used to find the value + Raises: + CalledProcessError: if 'state-get' returns an error code. + """ + # We don't capture stderr here so it can end up in debug logs. + p = _run(["state-get", key], stdout=subprocess.PIPE, check=True, universal_newlines=True) + if p.stdout == '' or p.stdout == '\n': + raise KeyError(key) + return yaml.load(p.stdout, Loader=_SimpleLoader) + + def delete(self, key: str) -> None: + """Remove a key from being tracked. + + Args: + key: The key to stop storing + Raises: + CalledProcessError: if 'state-delete' returns an error code. + """ + _run(["state-delete", key], check=True) + + +class NoSnapshotError(Exception): + + def __init__(self, handle_path): + self.handle_path = handle_path + + def __str__(self): + return 'no snapshot data found for {} object'.format(self.handle_path) diff --git a/coredns/venv/ops/testing.py b/coredns/venv/ops/testing.py new file mode 100644 index 0000000..416dced --- /dev/null +++ b/coredns/venv/ops/testing.py @@ -0,0 +1,818 @@ +# Copyright 2020 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import pathlib +import random +import tempfile +import typing +import yaml +from contextlib import contextmanager +from textwrap import dedent + +from ops import ( + charm, + framework, + model, + storage, +) + + +# OptionalYAML is something like metadata.yaml or actions.yaml. You can +# pass in a file-like object or the string directly. +OptionalYAML = typing.Optional[typing.Union[str, typing.TextIO]] + + +# noinspection PyProtectedMember +class Harness: + """This class represents a way to build up the model that will drive a test suite. + + The model that is created is from the viewpoint of the charm that you are testing. + + Example:: + + harness = Harness(MyCharm) + # Do initial setup here + relation_id = harness.add_relation('db', 'postgresql') + # Now instantiate the charm to see events as the model changes + harness.begin() + harness.add_relation_unit(relation_id, 'postgresql/0') + harness.update_relation_data(relation_id, 'postgresql/0', {'key': 'val'}) + # Check that charm has properly handled the relation_joined event for postgresql/0 + self.assertEqual(harness.charm. ...) + + Args: + charm_cls: The Charm class that you'll be testing. + meta: charm.CharmBase is a A string or file-like object containing the contents of + metadata.yaml. If not supplied, we will look for a 'metadata.yaml' file in the + parent directory of the Charm, and if not found fall back to a trivial + 'name: test-charm' metadata. + actions: A string or file-like object containing the contents of + actions.yaml. If not supplied, we will look for a 'actions.yaml' file in the + parent directory of the Charm. + config: A string or file-like object containing the contents of + config.yaml. If not supplied, we will look for a 'config.yaml' file in the + parent directory of the Charm. + """ + + def __init__( + self, + charm_cls: typing.Type[charm.CharmBase], + *, + meta: OptionalYAML = None, + actions: OptionalYAML = None, + config: OptionalYAML = None): + self._charm_cls = charm_cls + self._charm = None + self._charm_dir = 'no-disk-path' # this may be updated by _create_meta + self._meta = self._create_meta(meta, actions) + self._unit_name = self._meta.name + '/0' + self._framework = None + self._hooks_enabled = True + self._relation_id_counter = 0 + self._backend = _TestingModelBackend(self._unit_name, self._meta) + self._model = model.Model(self._meta, self._backend) + self._storage = storage.SQLiteStorage(':memory:') + self._oci_resources = {} + self._framework = framework.Framework( + self._storage, self._charm_dir, self._meta, self._model) + self._update_config(key_values=self._load_config_defaults(config)) + + @property + def charm(self) -> charm.CharmBase: + """Return the instance of the charm class that was passed to __init__. + + Note that the Charm is not instantiated until you have called + :meth:`.begin()`. + """ + return self._charm + + @property + def model(self) -> model.Model: + """Return the :class:`~ops.model.Model` that is being driven by this Harness.""" + return self._model + + @property + def framework(self) -> framework.Framework: + """Return the Framework that is being driven by this Harness.""" + return self._framework + + def begin(self) -> None: + """Instantiate the Charm and start handling events. + + Before calling :meth:`.begin`(), there is no Charm instance, so changes to the Model won't + emit events. You must call :meth:`.begin` before :attr:`.charm` is valid. + """ + if self._charm is not None: + raise RuntimeError('cannot call the begin method on the harness more than once') + + # The Framework adds attributes to class objects for events, etc. As such, we can't re-use + # the original class against multiple Frameworks. So create a locally defined class + # and register it. + # TODO: jam 2020-03-16 We are looking to changes this to Instance attributes instead of + # Class attributes which should clean up this ugliness. The API can stay the same + class TestEvents(self._charm_cls.on.__class__): + pass + + TestEvents.__name__ = self._charm_cls.on.__class__.__name__ + + class TestCharm(self._charm_cls): + on = TestEvents() + + # Note: jam 2020-03-01 This is so that errors in testing say MyCharm has no attribute foo, + # rather than TestCharm has no attribute foo. + TestCharm.__name__ = self._charm_cls.__name__ + self._charm = TestCharm(self._framework) + + def begin_with_initial_hooks(self) -> None: + """Called when you want the Harness to fire the same hooks that Juju would fire at startup. + + This triggers install, relation-created, config-changed, start, and any relation-joined + hooks. Based on what relations have been defined before you called begin(). + Note that all of these are fired before returning control to the test suite, so if you + want to introspect what happens at each step, you need to fire them directly + (eg Charm.on.install.emit()). + + To use this with all the normal hooks, you should instantiate the harness, setup any + relations that you want active when the charm starts, and then call this method. + + Example:: + + harness = Harness(MyCharm) + # Do initial setup here + relation_id = harness.add_relation('db', 'postgresql') + harness.add_relation_unit(relation_id, 'postgresql/0') + harness.update_relation_data(relation_id, 'postgresql/0', {'key': 'val'}) + harness.set_leader(True) + harness.update_config({'initial': 'config'}) + harness.begin_with_initial_hooks() + # This will cause + # install, db-relation-created('postgresql'), leader-elected, config-changed, start + # db-relation-joined('postrgesql/0'), db-relation-changed('postgresql/0') + # To be fired. + """ + self.begin() + # TODO: jam 2020-08-03 This should also handle storage-attached hooks once we have support + # for dealing with storage. + self._charm.on.install.emit() + # Juju itself iterates what relation to fire based on a map[int]relation, so it doesn't + # guarantee a stable ordering between relation events. It *does* give a stable ordering + # of joined units for a given relation. + items = list(self._meta.relations.items()) + random.shuffle(items) + this_app_name = self._meta.name + for relname, rel_meta in items: + if rel_meta.role == charm.RelationRole.peer: + # If the user has directly added a relation, leave it be, but otherwise ensure + # that peer relations are always established at before leader-elected. + rel_ids = self._backend._relation_ids_map.get(relname) + if rel_ids is None: + self.add_relation(relname, self._meta.name) + else: + random.shuffle(rel_ids) + for rel_id in rel_ids: + self._emit_relation_created(relname, rel_id, this_app_name) + else: + rel_ids = self._backend._relation_ids_map.get(relname, []) + random.shuffle(rel_ids) + for rel_id in rel_ids: + app_name = self._backend._relation_app_and_units[rel_id]["app"] + self._emit_relation_created(relname, rel_id, app_name) + if self._backend._is_leader: + self._charm.on.leader_elected.emit() + else: + self._charm.on.leader_settings_changed.emit() + self._charm.on.config_changed.emit() + self._charm.on.start.emit() + all_ids = list(self._backend._relation_names.items()) + random.shuffle(all_ids) + for rel_id, rel_name in all_ids: + rel_app_and_units = self._backend._relation_app_and_units[rel_id] + app_name = rel_app_and_units["app"] + # Note: Juju *does* fire relation events for a given relation in the sorted order of + # the unit names. It also always fires relation-changed immediately after + # relation-joined for the same unit. + # Juju only fires relation-changed (app) if there is data for the related application + relation = self._model.get_relation(rel_name, rel_id) + if self._backend._relation_data[rel_id].get(app_name): + app = self._model.get_app(app_name) + self._charm.on[rel_name].relation_changed.emit( + relation, app, None) + for unit_name in sorted(rel_app_and_units["units"]): + remote_unit = self._model.get_unit(unit_name) + self._charm.on[rel_name].relation_joined.emit( + relation, remote_unit.app, remote_unit) + self._charm.on[rel_name].relation_changed.emit( + relation, remote_unit.app, remote_unit) + + def cleanup(self) -> None: + """Called by your test infrastructure to cleanup any temporary directories/files/etc. + + Currently this only needs to be called if you test with resources. But it is reasonable + to always include a `testcase.addCleanup(harness.cleanup)` just in case. + """ + self._backend._cleanup() + + def _create_meta(self, charm_metadata, action_metadata): + """Create a CharmMeta object. + + Handle the cases where a user doesn't supply explicit metadata snippets. + """ + filename = inspect.getfile(self._charm_cls) + charm_dir = pathlib.Path(filename).parents[1] + + if charm_metadata is None: + metadata_path = charm_dir / 'metadata.yaml' + if metadata_path.is_file(): + charm_metadata = metadata_path.read_text() + self._charm_dir = charm_dir + else: + # The simplest of metadata that the framework can support + charm_metadata = 'name: test-charm' + elif isinstance(charm_metadata, str): + charm_metadata = dedent(charm_metadata) + + if action_metadata is None: + actions_path = charm_dir / 'actions.yaml' + if actions_path.is_file(): + action_metadata = actions_path.read_text() + self._charm_dir = charm_dir + elif isinstance(action_metadata, str): + action_metadata = dedent(action_metadata) + + return charm.CharmMeta.from_yaml(charm_metadata, action_metadata) + + def _load_config_defaults(self, charm_config): + """Load default values from config.yaml + + Handle the case where a user doesn't supply explicit config snippets. + """ + filename = inspect.getfile(self._charm_cls) + charm_dir = pathlib.Path(filename).parents[1] + + if charm_config is None: + config_path = charm_dir / 'config.yaml' + if config_path.is_file(): + charm_config = config_path.read_text() + self._charm_dir = charm_dir + else: + # The simplest of config that the framework can support + charm_config = '{}' + elif isinstance(charm_config, str): + charm_config = dedent(charm_config) + charm_config = yaml.load(charm_config, Loader=yaml.SafeLoader) + charm_config = charm_config.get('options', {}) + return {key: value['default'] for key, value in charm_config.items() + if 'default' in value} + + def add_oci_resource(self, resource_name: str, + contents: typing.Mapping[str, str] = None) -> None: + """Add oci resources to the backend. + + This will register an oci resource and create a temporary file for processing metadata + about the resource. A default set of values will be used for all the file contents + unless a specific contents dict is provided. + + Args: + resource_name: Name of the resource to add custom contents to. + contents: Optional custom dict to write for the named resource. + """ + if not contents: + contents = {'registrypath': 'registrypath', + 'username': 'username', + 'password': 'password', + } + if resource_name not in self._meta.resources.keys(): + raise RuntimeError('Resource {} is not a defined resources'.format(resource_name)) + if self._meta.resources[resource_name].type != "oci-image": + raise RuntimeError('Resource {} is not an OCI Image'.format(resource_name)) + + as_yaml = yaml.dump(contents, Dumper=yaml.SafeDumper) + self._backend._resources_map[resource_name] = ('contents.yaml', as_yaml) + + def add_resource(self, resource_name: str, content: typing.AnyStr) -> None: + """Add content for a resource to the backend. + + This will register the content, so that a call to `Model.resources.fetch(resource_name)` + will return a path to a file containing that content. + + Args: + resource_name: The name of the resource being added + contents: Either string or bytes content, which will be the content of the filename + returned by resource-get. If contents is a string, it will be encoded in utf-8 + """ + if resource_name not in self._meta.resources.keys(): + raise RuntimeError('Resource {} is not a defined resources'.format(resource_name)) + record = self._meta.resources[resource_name] + if record.type != "file": + raise RuntimeError( + 'Resource {} is not a file, but actually {}'.format(resource_name, record.type)) + filename = record.filename + if filename is None: + filename = resource_name + + self._backend._resources_map[resource_name] = (filename, content) + + def populate_oci_resources(self) -> None: + """Populate all OCI resources.""" + for name, data in self._meta.resources.items(): + if data.type == "oci-image": + self.add_oci_resource(name) + + def disable_hooks(self) -> None: + """Stop emitting hook events when the model changes. + + This can be used by developers to stop changes to the model from emitting events that + the charm will react to. Call :meth:`.enable_hooks` + to re-enable them. + """ + self._hooks_enabled = False + + def enable_hooks(self) -> None: + """Re-enable hook events from charm.on when the model is changed. + + By default hook events are enabled once you call :meth:`.begin`, + but if you have used :meth:`.disable_hooks`, this can be used to + enable them again. + """ + self._hooks_enabled = True + + @contextmanager + def hooks_disabled(self): + """A context manager to run code with hooks disabled. + + Example:: + + with harness.hooks_disabled(): + # things in here don't fire events + harness.set_leader(True) + harness.update_config(unset=['foo', 'bar']) + # things here will again fire events + """ + self.disable_hooks() + try: + yield None + finally: + self.enable_hooks() + + def _next_relation_id(self): + rel_id = self._relation_id_counter + self._relation_id_counter += 1 + return rel_id + + def add_relation(self, relation_name: str, remote_app: str) -> int: + """Declare that there is a new relation between this app and `remote_app`. + + Args: + relation_name: The relation on Charm that is being related to + remote_app: The name of the application that is being related to + + Return: + The relation_id created by this add_relation. + """ + rel_id = self._next_relation_id() + self._backend._relation_ids_map.setdefault(relation_name, []).append(rel_id) + self._backend._relation_names[rel_id] = relation_name + self._backend._relation_list_map[rel_id] = [] + self._backend._relation_data[rel_id] = { + remote_app: {}, + self._backend.unit_name: {}, + self._backend.app_name: {}, + } + self._backend._relation_app_and_units[rel_id] = { + "app": remote_app, + "units": [], + } + # Reload the relation_ids list + if self._model is not None: + self._model.relations._invalidate(relation_name) + self._emit_relation_created(relation_name, rel_id, remote_app) + return rel_id + + def _emit_relation_created(self, relation_name: str, relation_id: int, + remote_app: str) -> None: + """Trigger relation-created for a given relation with a given remote application.""" + if self._charm is None or not self._hooks_enabled: + return + if self._charm is None or not self._hooks_enabled: + return + relation = self._model.get_relation(relation_name, relation_id) + app = self._model.get_app(remote_app) + self._charm.on[relation_name].relation_created.emit( + relation, app) + + def add_relation_unit(self, relation_id: int, remote_unit_name: str) -> None: + """Add a new unit to a relation. + + Example:: + + rel_id = harness.add_relation('db', 'postgresql') + harness.add_relation_unit(rel_id, 'postgresql/0') + + This will trigger a `relation_joined` event. This would naturally be + followed by a `relation_changed` event, which you can trigger with + :meth:`.update_relation_data`. This separation is artificial in the + sense that Juju will always fire the two, but is intended to make + testing relations and their data bags slightly more natural. + + Args: + relation_id: The integer relation identifier (as returned by add_relation). + remote_unit_name: A string representing the remote unit that is being added. + Return: + None + """ + self._backend._relation_list_map[relation_id].append(remote_unit_name) + self._backend._relation_data[relation_id][remote_unit_name] = {} + # TODO: jam 2020-08-03 This is where we could assert that the unit name matches the + # application name (eg you don't have a relation to 'foo' but add units of 'bar/0' + self._backend._relation_app_and_units[relation_id]["units"].append(remote_unit_name) + relation_name = self._backend._relation_names[relation_id] + # Make sure that the Model reloads the relation_list for this relation_id, as well as + # reloading the relation data for this unit. + if self._model is not None: + remote_unit = self._model.get_unit(remote_unit_name) + relation = self._model.get_relation(relation_name, relation_id) + unit_cache = relation.data.get(remote_unit, None) + if unit_cache is not None: + unit_cache._invalidate() + self._model.relations._invalidate(relation_name) + if self._charm is None or not self._hooks_enabled: + return + self._charm.on[relation_name].relation_joined.emit( + relation, remote_unit.app, remote_unit) + + def get_relation_data(self, relation_id: int, app_or_unit: str) -> typing.Mapping: + """Get the relation data bucket for a single app or unit in a given relation. + + This ignores all of the safety checks of who can and can't see data in relations (eg, + non-leaders can't read their own application's relation data because there are no events + that keep that data up-to-date for the unit). + + Args: + relation_id: The relation whose content we want to look at. + app_or_unit: The name of the application or unit whose data we want to read + Return: + a dict containing the relation data for `app_or_unit` or None. + Raises: + KeyError: if relation_id doesn't exist + """ + return self._backend._relation_data[relation_id].get(app_or_unit, None) + + def get_pod_spec(self) -> (typing.Mapping, typing.Mapping): + """Return the content of the pod spec as last set by the charm. + + This returns both the pod spec and any k8s_resources that were supplied. + See the signature of Model.pod.set_spec + """ + return self._backend._pod_spec + + def get_workload_version(self) -> str: + """Read the workload version that was set by the unit.""" + return self._backend._workload_version + + def set_model_name(self, name: str) -> None: + """Set the name of the Model that this is representing. + + This cannot be called once begin() has been called. But it lets you set the value that + will be returned by Model.name. + """ + if self._charm is not None: + raise RuntimeError('cannot set the Model name after begin()') + self._backend.model_name = name + + def update_relation_data( + self, + relation_id: int, + app_or_unit: str, + key_values: typing.Mapping, + ) -> None: + """Update the relation data for a given unit or application in a given relation. + + This also triggers the `relation_changed` event for this relation_id. + + Args: + relation_id: The integer relation_id representing this relation. + app_or_unit: The unit or application name that is being updated. + This can be the local or remote application. + key_values: Each key/value will be updated in the relation data. + """ + relation_name = self._backend._relation_names[relation_id] + relation = self._model.get_relation(relation_name, relation_id) + if '/' in app_or_unit: + entity = self._model.get_unit(app_or_unit) + else: + entity = self._model.get_app(app_or_unit) + rel_data = relation.data.get(entity, None) + if rel_data is not None: + # rel_data may have cached now-stale data, so _invalidate() it. + # Note, this won't cause the data to be loaded if it wasn't already. + rel_data._invalidate() + + new_values = self._backend._relation_data[relation_id][app_or_unit].copy() + for k, v in key_values.items(): + if v == '': + new_values.pop(k, None) + else: + new_values[k] = v + self._backend._relation_data[relation_id][app_or_unit] = new_values + + if app_or_unit == self._model.unit.name: + # No events for our own unit + return + if app_or_unit == self._model.app.name: + # updating our own app only generates an event if it is a peer relation and we + # aren't the leader + is_peer = self._meta.relations[relation_name].role.is_peer() + if not is_peer: + return + if self._model.unit.is_leader(): + return + self._emit_relation_changed(relation_id, app_or_unit) + + def _emit_relation_changed(self, relation_id, app_or_unit): + if self._charm is None or not self._hooks_enabled: + return + rel_name = self._backend._relation_names[relation_id] + relation = self.model.get_relation(rel_name, relation_id) + if '/' in app_or_unit: + app_name = app_or_unit.split('/')[0] + unit_name = app_or_unit + app = self.model.get_app(app_name) + unit = self.model.get_unit(unit_name) + args = (relation, app, unit) + else: + app_name = app_or_unit + app = self.model.get_app(app_name) + args = (relation, app) + self._charm.on[rel_name].relation_changed.emit(*args) + + def _update_config( + self, + key_values: typing.Mapping[str, str] = None, + unset: typing.Iterable[str] = (), + ) -> None: + """Update the config as seen by the charm. + + This will *not* trigger a `config_changed` event, and is intended for internal use. + + Note that the `key_values` mapping will only add or update configuration items. + To remove existing ones, see the `unset` parameter. + + Args: + key_values: A Mapping of key:value pairs to update in config. + unset: An iterable of keys to remove from Config. (Note that this does + not currently reset the config values to the default defined in config.yaml.) + """ + # NOTE: jam 2020-03-01 Note that this sort of works "by accident". Config + # is a LazyMapping, but its _load returns a dict and this method mutates + # the dict that Config is caching. Arguably we should be doing some sort + # of charm.framework.model.config._invalidate() + config = self._backend._config + if key_values is not None: + for key, value in key_values.items(): + config[key] = value + for key in unset: + config.pop(key, None) + + def update_config( + self, + key_values: typing.Mapping[str, str] = None, + unset: typing.Iterable[str] = (), + ) -> None: + """Update the config as seen by the charm. + + This will trigger a `config_changed` event. + + Note that the `key_values` mapping will only add or update configuration items. + To remove existing ones, see the `unset` parameter. + + Args: + key_values: A Mapping of key:value pairs to update in config. + unset: An iterable of keys to remove from Config. (Note that this does + not currently reset the config values to the default defined in config.yaml.) + """ + self._update_config(key_values, unset) + if self._charm is None or not self._hooks_enabled: + return + self._charm.on.config_changed.emit() + + def set_leader(self, is_leader: bool = True) -> None: + """Set whether this unit is the leader or not. + + If this charm becomes a leader then `leader_elected` will be triggered. + + Args: + is_leader: True/False as to whether this unit is the leader. + """ + was_leader = self._backend._is_leader + self._backend._is_leader = is_leader + # Note: jam 2020-03-01 currently is_leader is cached at the ModelBackend level, not in + # the Model objects, so this automatically gets noticed. + if is_leader and not was_leader and self._charm is not None and self._hooks_enabled: + self._charm.on.leader_elected.emit() + + def _get_backend_calls(self, reset: bool = True) -> list: + """Return the calls that we have made to the TestingModelBackend. + + This is useful mostly for testing the framework itself, so that we can assert that we + do/don't trigger extra calls. + + Args: + reset: If True, reset the calls list back to empty, if false, the call list is + preserved. + Return: + ``[(call1, args...), (call2, args...)]`` + """ + calls = self._backend._calls.copy() + if reset: + self._backend._calls.clear() + return calls + + +def _record_calls(cls): + """Replace methods on cls with methods that record that they have been called. + + Iterate all attributes of cls, and for public methods, replace them with a wrapped method + that records the method called along with the arguments and keyword arguments. + """ + for meth_name, orig_method in cls.__dict__.items(): + if meth_name.startswith('_'): + continue + + def decorator(orig_method): + def wrapped(self, *args, **kwargs): + full_args = (orig_method.__name__,) + args + if kwargs: + full_args = full_args + (kwargs,) + self._calls.append(full_args) + return orig_method(self, *args, **kwargs) + return wrapped + + setattr(cls, meth_name, decorator(orig_method)) + return cls + + +class _ResourceEntry: + """Tracks the contents of a Resource.""" + + def __init__(self, resource_name): + self.name = resource_name + + +@_record_calls +class _TestingModelBackend: + """This conforms to the interface for ModelBackend but provides canned data. + + DO NOT use this class directly, it is used by `Harness`_ to drive the model. + `Harness`_ is responsible for maintaining the internal consistency of the values here, + as the only public methods of this type are for implementing ModelBackend. + """ + + def __init__(self, unit_name, meta): + self.unit_name = unit_name + self.app_name = self.unit_name.split('/')[0] + self.model_name = None + self._calls = [] + self._meta = meta + self._is_leader = None + self._relation_ids_map = {} # relation name to [relation_ids,...] + self._relation_names = {} # reverse map from relation_id to relation_name + self._relation_list_map = {} # relation_id: [unit_name,...] + self._relation_data = {} # {relation_id: {name: data}} + # {relation_id: {"app": app_name, "units": ["app/0",...]} + self._relation_app_and_units = {} + self._config = {} + self._is_leader = False + self._resources_map = {} # {resource_name: resource_content} + self._pod_spec = None + self._app_status = {'status': 'unknown', 'message': ''} + self._unit_status = {'status': 'maintenance', 'message': ''} + self._workload_version = None + self._resource_dir = None + + def _cleanup(self): + if self._resource_dir is not None: + self._resource_dir.cleanup() + self._resource_dir = None + + def _get_resource_dir(self) -> pathlib.Path: + if self._resource_dir is None: + # In actual Juju, the resource path for a charm's resource is + # $AGENT_DIR/resources/$RESOURCE_NAME/$RESOURCE_FILENAME + # However, charms shouldn't depend on this. + self._resource_dir = tempfile.TemporaryDirectory(prefix='tmp-ops-test-resource-') + return pathlib.Path(self._resource_dir.name) + + def relation_ids(self, relation_name): + try: + return self._relation_ids_map[relation_name] + except KeyError as e: + if relation_name not in self._meta.relations: + raise model.ModelError('{} is not a known relation'.format(relation_name)) from e + return [] + + def relation_list(self, relation_id): + try: + return self._relation_list_map[relation_id] + except KeyError as e: + raise model.RelationNotFoundError from e + + def relation_get(self, relation_id, member_name, is_app): + if is_app and '/' in member_name: + member_name = member_name.split('/')[0] + if relation_id not in self._relation_data: + raise model.RelationNotFoundError() + return self._relation_data[relation_id][member_name].copy() + + def relation_set(self, relation_id, key, value, is_app): + relation = self._relation_data[relation_id] + if is_app: + bucket_key = self.app_name + else: + bucket_key = self.unit_name + if bucket_key not in relation: + relation[bucket_key] = {} + bucket = relation[bucket_key] + if value == '': + bucket.pop(key, None) + else: + bucket[key] = value + + def config_get(self): + return self._config + + def is_leader(self): + return self._is_leader + + def application_version_set(self, version): + self._workload_version = version + + def resource_get(self, resource_name): + if resource_name not in self._resources_map: + raise model.ModelError( + "ERROR could not download resource: HTTP request failed: " + "Get https://.../units/unit-{}/resources/{}: resource#{}/{} not found".format( + self.unit_name.replace('/', '-'), resource_name, self.app_name, resource_name + )) + filename, contents = self._resources_map[resource_name] + resource_dir = self._get_resource_dir() + resource_filename = resource_dir / resource_name / filename + if not resource_filename.exists(): + if isinstance(contents, bytes): + mode = 'wb' + else: + mode = 'wt' + resource_filename.parent.mkdir(exist_ok=True) + with resource_filename.open(mode=mode) as resource_file: + resource_file.write(contents) + return resource_filename + + def pod_spec_set(self, spec, k8s_resources): + self._pod_spec = (spec, k8s_resources) + + def status_get(self, *, is_app=False): + if is_app: + return self._app_status + else: + return self._unit_status + + def status_set(self, status, message='', *, is_app=False): + if is_app: + self._app_status = {'status': status, 'message': message} + else: + self._unit_status = {'status': status, 'message': message} + + def storage_list(self, name): + raise NotImplementedError(self.storage_list) + + def storage_get(self, storage_name_id, attribute): + raise NotImplementedError(self.storage_get) + + def storage_add(self, name, count=1): + raise NotImplementedError(self.storage_add) + + def action_get(self): + raise NotImplementedError(self.action_get) + + def action_set(self, results): + raise NotImplementedError(self.action_set) + + def action_log(self, message): + raise NotImplementedError(self.action_log) + + def action_fail(self, message=''): + raise NotImplementedError(self.action_fail) + + def network_get(self, endpoint_name, relation_id=None): + raise NotImplementedError(self.network_get) diff --git a/coredns/venv/ops/version.py b/coredns/venv/ops/version.py new file mode 100644 index 0000000..9ecc830 --- /dev/null +++ b/coredns/venv/ops/version.py @@ -0,0 +1,3 @@ +# this is a generated file + +version = '0.10.0' diff --git a/coredns/venv/yaml/__init__.py b/coredns/venv/yaml/__init__.py new file mode 100644 index 0000000..13d687c --- /dev/null +++ b/coredns/venv/yaml/__init__.py @@ -0,0 +1,427 @@ + +from .error import * + +from .tokens import * +from .events import * +from .nodes import * + +from .loader import * +from .dumper import * + +__version__ = '5.3.1' +try: + from .cyaml import * + __with_libyaml__ = True +except ImportError: + __with_libyaml__ = False + +import io + +#------------------------------------------------------------------------------ +# Warnings control +#------------------------------------------------------------------------------ + +# 'Global' warnings state: +_warnings_enabled = { + 'YAMLLoadWarning': True, +} + +# Get or set global warnings' state +def warnings(settings=None): + if settings is None: + return _warnings_enabled + + if type(settings) is dict: + for key in settings: + if key in _warnings_enabled: + _warnings_enabled[key] = settings[key] + +# Warn when load() is called without Loader=... +class YAMLLoadWarning(RuntimeWarning): + pass + +def load_warning(method): + if _warnings_enabled['YAMLLoadWarning'] is False: + return + + import warnings + + message = ( + "calling yaml.%s() without Loader=... is deprecated, as the " + "default Loader is unsafe. Please read " + "https://msg.pyyaml.org/load for full details." + ) % method + + warnings.warn(message, YAMLLoadWarning, stacklevel=3) + +#------------------------------------------------------------------------------ +def scan(stream, Loader=Loader): + """ + Scan a YAML stream and produce scanning tokens. + """ + loader = Loader(stream) + try: + while loader.check_token(): + yield loader.get_token() + finally: + loader.dispose() + +def parse(stream, Loader=Loader): + """ + Parse a YAML stream and produce parsing events. + """ + loader = Loader(stream) + try: + while loader.check_event(): + yield loader.get_event() + finally: + loader.dispose() + +def compose(stream, Loader=Loader): + """ + Parse the first YAML document in a stream + and produce the corresponding representation tree. + """ + loader = Loader(stream) + try: + return loader.get_single_node() + finally: + loader.dispose() + +def compose_all(stream, Loader=Loader): + """ + Parse all YAML documents in a stream + and produce corresponding representation trees. + """ + loader = Loader(stream) + try: + while loader.check_node(): + yield loader.get_node() + finally: + loader.dispose() + +def load(stream, Loader=None): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + """ + if Loader is None: + load_warning('load') + Loader = FullLoader + + loader = Loader(stream) + try: + return loader.get_single_data() + finally: + loader.dispose() + +def load_all(stream, Loader=None): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + """ + if Loader is None: + load_warning('load_all') + Loader = FullLoader + + loader = Loader(stream) + try: + while loader.check_data(): + yield loader.get_data() + finally: + loader.dispose() + +def full_load(stream): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + + Resolve all tags except those known to be + unsafe on untrusted input. + """ + return load(stream, FullLoader) + +def full_load_all(stream): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + + Resolve all tags except those known to be + unsafe on untrusted input. + """ + return load_all(stream, FullLoader) + +def safe_load(stream): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + + Resolve only basic YAML tags. This is known + to be safe for untrusted input. + """ + return load(stream, SafeLoader) + +def safe_load_all(stream): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + + Resolve only basic YAML tags. This is known + to be safe for untrusted input. + """ + return load_all(stream, SafeLoader) + +def unsafe_load(stream): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + + Resolve all tags, even those known to be + unsafe on untrusted input. + """ + return load(stream, UnsafeLoader) + +def unsafe_load_all(stream): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + + Resolve all tags, even those known to be + unsafe on untrusted input. + """ + return load_all(stream, UnsafeLoader) + +def emit(events, stream=None, Dumper=Dumper, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None): + """ + Emit YAML parsing events into a stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + stream = io.StringIO() + getvalue = stream.getvalue + dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + try: + for event in events: + dumper.emit(event) + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def serialize_all(nodes, stream=None, Dumper=Dumper, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + """ + Serialize a sequence of representation trees into a YAML stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + if encoding is None: + stream = io.StringIO() + else: + stream = io.BytesIO() + getvalue = stream.getvalue + dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + encoding=encoding, version=version, tags=tags, + explicit_start=explicit_start, explicit_end=explicit_end) + try: + dumper.open() + for node in nodes: + dumper.serialize(node) + dumper.close() + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def serialize(node, stream=None, Dumper=Dumper, **kwds): + """ + Serialize a representation tree into a YAML stream. + If stream is None, return the produced string instead. + """ + return serialize_all([node], stream, Dumper=Dumper, **kwds) + +def dump_all(documents, stream=None, Dumper=Dumper, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + """ + Serialize a sequence of Python objects into a YAML stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + if encoding is None: + stream = io.StringIO() + else: + stream = io.BytesIO() + getvalue = stream.getvalue + dumper = Dumper(stream, default_style=default_style, + default_flow_style=default_flow_style, + canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + encoding=encoding, version=version, tags=tags, + explicit_start=explicit_start, explicit_end=explicit_end, sort_keys=sort_keys) + try: + dumper.open() + for data in documents: + dumper.represent(data) + dumper.close() + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def dump(data, stream=None, Dumper=Dumper, **kwds): + """ + Serialize a Python object into a YAML stream. + If stream is None, return the produced string instead. + """ + return dump_all([data], stream, Dumper=Dumper, **kwds) + +def safe_dump_all(documents, stream=None, **kwds): + """ + Serialize a sequence of Python objects into a YAML stream. + Produce only basic YAML tags. + If stream is None, return the produced string instead. + """ + return dump_all(documents, stream, Dumper=SafeDumper, **kwds) + +def safe_dump(data, stream=None, **kwds): + """ + Serialize a Python object into a YAML stream. + Produce only basic YAML tags. + If stream is None, return the produced string instead. + """ + return dump_all([data], stream, Dumper=SafeDumper, **kwds) + +def add_implicit_resolver(tag, regexp, first=None, + Loader=None, Dumper=Dumper): + """ + Add an implicit scalar detector. + If an implicit scalar value matches the given regexp, + the corresponding tag is assigned to the scalar. + first is a sequence of possible initial characters or None. + """ + if Loader is None: + loader.Loader.add_implicit_resolver(tag, regexp, first) + loader.FullLoader.add_implicit_resolver(tag, regexp, first) + loader.UnsafeLoader.add_implicit_resolver(tag, regexp, first) + else: + Loader.add_implicit_resolver(tag, regexp, first) + Dumper.add_implicit_resolver(tag, regexp, first) + +def add_path_resolver(tag, path, kind=None, Loader=None, Dumper=Dumper): + """ + Add a path based resolver for the given tag. + A path is a list of keys that forms a path + to a node in the representation tree. + Keys can be string values, integers, or None. + """ + if Loader is None: + loader.Loader.add_path_resolver(tag, path, kind) + loader.FullLoader.add_path_resolver(tag, path, kind) + loader.UnsafeLoader.add_path_resolver(tag, path, kind) + else: + Loader.add_path_resolver(tag, path, kind) + Dumper.add_path_resolver(tag, path, kind) + +def add_constructor(tag, constructor, Loader=None): + """ + Add a constructor for the given tag. + Constructor is a function that accepts a Loader instance + and a node object and produces the corresponding Python object. + """ + if Loader is None: + loader.Loader.add_constructor(tag, constructor) + loader.FullLoader.add_constructor(tag, constructor) + loader.UnsafeLoader.add_constructor(tag, constructor) + else: + Loader.add_constructor(tag, constructor) + +def add_multi_constructor(tag_prefix, multi_constructor, Loader=None): + """ + Add a multi-constructor for the given tag prefix. + Multi-constructor is called for a node if its tag starts with tag_prefix. + Multi-constructor accepts a Loader instance, a tag suffix, + and a node object and produces the corresponding Python object. + """ + if Loader is None: + loader.Loader.add_multi_constructor(tag_prefix, multi_constructor) + loader.FullLoader.add_multi_constructor(tag_prefix, multi_constructor) + loader.UnsafeLoader.add_multi_constructor(tag_prefix, multi_constructor) + else: + Loader.add_multi_constructor(tag_prefix, multi_constructor) + +def add_representer(data_type, representer, Dumper=Dumper): + """ + Add a representer for the given type. + Representer is a function accepting a Dumper instance + and an instance of the given data type + and producing the corresponding representation node. + """ + Dumper.add_representer(data_type, representer) + +def add_multi_representer(data_type, multi_representer, Dumper=Dumper): + """ + Add a representer for the given type. + Multi-representer is a function accepting a Dumper instance + and an instance of the given data type or subtype + and producing the corresponding representation node. + """ + Dumper.add_multi_representer(data_type, multi_representer) + +class YAMLObjectMetaclass(type): + """ + The metaclass for YAMLObject. + """ + def __init__(cls, name, bases, kwds): + super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds) + if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None: + if isinstance(cls.yaml_loader, list): + for loader in cls.yaml_loader: + loader.add_constructor(cls.yaml_tag, cls.from_yaml) + else: + cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml) + + cls.yaml_dumper.add_representer(cls, cls.to_yaml) + +class YAMLObject(metaclass=YAMLObjectMetaclass): + """ + An object that can dump itself to a YAML stream + and load itself from a YAML stream. + """ + + __slots__ = () # no direct instantiation, so allow immutable subclasses + + yaml_loader = [Loader, FullLoader, UnsafeLoader] + yaml_dumper = Dumper + + yaml_tag = None + yaml_flow_style = None + + @classmethod + def from_yaml(cls, loader, node): + """ + Convert a representation node to a Python object. + """ + return loader.construct_yaml_object(node, cls) + + @classmethod + def to_yaml(cls, dumper, data): + """ + Convert a Python object to a representation node. + """ + return dumper.represent_yaml_object(cls.yaml_tag, data, cls, + flow_style=cls.yaml_flow_style) + diff --git a/coredns/venv/yaml/__pycache__/__init__.cpython-38.pyc b/coredns/venv/yaml/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..18da437 Binary files /dev/null and b/coredns/venv/yaml/__pycache__/__init__.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/composer.cpython-38.pyc b/coredns/venv/yaml/__pycache__/composer.cpython-38.pyc new file mode 100644 index 0000000..483755e Binary files /dev/null and b/coredns/venv/yaml/__pycache__/composer.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/constructor.cpython-38.pyc b/coredns/venv/yaml/__pycache__/constructor.cpython-38.pyc new file mode 100644 index 0000000..b0e3021 Binary files /dev/null and b/coredns/venv/yaml/__pycache__/constructor.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/cyaml.cpython-38.pyc b/coredns/venv/yaml/__pycache__/cyaml.cpython-38.pyc new file mode 100644 index 0000000..0e4fdf0 Binary files /dev/null and b/coredns/venv/yaml/__pycache__/cyaml.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/dumper.cpython-38.pyc b/coredns/venv/yaml/__pycache__/dumper.cpython-38.pyc new file mode 100644 index 0000000..0f2c1e6 Binary files /dev/null and b/coredns/venv/yaml/__pycache__/dumper.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/emitter.cpython-38.pyc b/coredns/venv/yaml/__pycache__/emitter.cpython-38.pyc new file mode 100644 index 0000000..fbe86dd Binary files /dev/null and b/coredns/venv/yaml/__pycache__/emitter.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/error.cpython-38.pyc b/coredns/venv/yaml/__pycache__/error.cpython-38.pyc new file mode 100644 index 0000000..481c6cf Binary files /dev/null and b/coredns/venv/yaml/__pycache__/error.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/events.cpython-38.pyc b/coredns/venv/yaml/__pycache__/events.cpython-38.pyc new file mode 100644 index 0000000..ee8ca1b Binary files /dev/null and b/coredns/venv/yaml/__pycache__/events.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/loader.cpython-38.pyc b/coredns/venv/yaml/__pycache__/loader.cpython-38.pyc new file mode 100644 index 0000000..ed7fdcf Binary files /dev/null and b/coredns/venv/yaml/__pycache__/loader.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/nodes.cpython-38.pyc b/coredns/venv/yaml/__pycache__/nodes.cpython-38.pyc new file mode 100644 index 0000000..7766e80 Binary files /dev/null and b/coredns/venv/yaml/__pycache__/nodes.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/parser.cpython-38.pyc b/coredns/venv/yaml/__pycache__/parser.cpython-38.pyc new file mode 100644 index 0000000..7047475 Binary files /dev/null and b/coredns/venv/yaml/__pycache__/parser.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/reader.cpython-38.pyc b/coredns/venv/yaml/__pycache__/reader.cpython-38.pyc new file mode 100644 index 0000000..d1c3b1b Binary files /dev/null and b/coredns/venv/yaml/__pycache__/reader.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/representer.cpython-38.pyc b/coredns/venv/yaml/__pycache__/representer.cpython-38.pyc new file mode 100644 index 0000000..c4544e2 Binary files /dev/null and b/coredns/venv/yaml/__pycache__/representer.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/resolver.cpython-38.pyc b/coredns/venv/yaml/__pycache__/resolver.cpython-38.pyc new file mode 100644 index 0000000..ec45829 Binary files /dev/null and b/coredns/venv/yaml/__pycache__/resolver.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/scanner.cpython-38.pyc b/coredns/venv/yaml/__pycache__/scanner.cpython-38.pyc new file mode 100644 index 0000000..70d13ce Binary files /dev/null and b/coredns/venv/yaml/__pycache__/scanner.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/serializer.cpython-38.pyc b/coredns/venv/yaml/__pycache__/serializer.cpython-38.pyc new file mode 100644 index 0000000..e4ce884 Binary files /dev/null and b/coredns/venv/yaml/__pycache__/serializer.cpython-38.pyc differ diff --git a/coredns/venv/yaml/__pycache__/tokens.cpython-38.pyc b/coredns/venv/yaml/__pycache__/tokens.cpython-38.pyc new file mode 100644 index 0000000..2ad8c48 Binary files /dev/null and b/coredns/venv/yaml/__pycache__/tokens.cpython-38.pyc differ diff --git a/coredns/venv/yaml/composer.py b/coredns/venv/yaml/composer.py new file mode 100644 index 0000000..6d15cb4 --- /dev/null +++ b/coredns/venv/yaml/composer.py @@ -0,0 +1,139 @@ + +__all__ = ['Composer', 'ComposerError'] + +from .error import MarkedYAMLError +from .events import * +from .nodes import * + +class ComposerError(MarkedYAMLError): + pass + +class Composer: + + def __init__(self): + self.anchors = {} + + def check_node(self): + # Drop the STREAM-START event. + if self.check_event(StreamStartEvent): + self.get_event() + + # If there are more documents available? + return not self.check_event(StreamEndEvent) + + def get_node(self): + # Get the root node of the next document. + if not self.check_event(StreamEndEvent): + return self.compose_document() + + def get_single_node(self): + # Drop the STREAM-START event. + self.get_event() + + # Compose a document if the stream is not empty. + document = None + if not self.check_event(StreamEndEvent): + document = self.compose_document() + + # Ensure that the stream contains no more documents. + if not self.check_event(StreamEndEvent): + event = self.get_event() + raise ComposerError("expected a single document in the stream", + document.start_mark, "but found another document", + event.start_mark) + + # Drop the STREAM-END event. + self.get_event() + + return document + + def compose_document(self): + # Drop the DOCUMENT-START event. + self.get_event() + + # Compose the root node. + node = self.compose_node(None, None) + + # Drop the DOCUMENT-END event. + self.get_event() + + self.anchors = {} + return node + + def compose_node(self, parent, index): + if self.check_event(AliasEvent): + event = self.get_event() + anchor = event.anchor + if anchor not in self.anchors: + raise ComposerError(None, None, "found undefined alias %r" + % anchor, event.start_mark) + return self.anchors[anchor] + event = self.peek_event() + anchor = event.anchor + if anchor is not None: + if anchor in self.anchors: + raise ComposerError("found duplicate anchor %r; first occurrence" + % anchor, self.anchors[anchor].start_mark, + "second occurrence", event.start_mark) + self.descend_resolver(parent, index) + if self.check_event(ScalarEvent): + node = self.compose_scalar_node(anchor) + elif self.check_event(SequenceStartEvent): + node = self.compose_sequence_node(anchor) + elif self.check_event(MappingStartEvent): + node = self.compose_mapping_node(anchor) + self.ascend_resolver() + return node + + def compose_scalar_node(self, anchor): + event = self.get_event() + tag = event.tag + if tag is None or tag == '!': + tag = self.resolve(ScalarNode, event.value, event.implicit) + node = ScalarNode(tag, event.value, + event.start_mark, event.end_mark, style=event.style) + if anchor is not None: + self.anchors[anchor] = node + return node + + def compose_sequence_node(self, anchor): + start_event = self.get_event() + tag = start_event.tag + if tag is None or tag == '!': + tag = self.resolve(SequenceNode, None, start_event.implicit) + node = SequenceNode(tag, [], + start_event.start_mark, None, + flow_style=start_event.flow_style) + if anchor is not None: + self.anchors[anchor] = node + index = 0 + while not self.check_event(SequenceEndEvent): + node.value.append(self.compose_node(node, index)) + index += 1 + end_event = self.get_event() + node.end_mark = end_event.end_mark + return node + + def compose_mapping_node(self, anchor): + start_event = self.get_event() + tag = start_event.tag + if tag is None or tag == '!': + tag = self.resolve(MappingNode, None, start_event.implicit) + node = MappingNode(tag, [], + start_event.start_mark, None, + flow_style=start_event.flow_style) + if anchor is not None: + self.anchors[anchor] = node + while not self.check_event(MappingEndEvent): + #key_event = self.peek_event() + item_key = self.compose_node(node, None) + #if item_key in node.value: + # raise ComposerError("while composing a mapping", start_event.start_mark, + # "found duplicate key", key_event.start_mark) + item_value = self.compose_node(node, item_key) + #node.value[item_key] = item_value + node.value.append((item_key, item_value)) + end_event = self.get_event() + node.end_mark = end_event.end_mark + return node + diff --git a/coredns/venv/yaml/constructor.py b/coredns/venv/yaml/constructor.py new file mode 100644 index 0000000..1948b12 --- /dev/null +++ b/coredns/venv/yaml/constructor.py @@ -0,0 +1,748 @@ + +__all__ = [ + 'BaseConstructor', + 'SafeConstructor', + 'FullConstructor', + 'UnsafeConstructor', + 'Constructor', + 'ConstructorError' +] + +from .error import * +from .nodes import * + +import collections.abc, datetime, base64, binascii, re, sys, types + +class ConstructorError(MarkedYAMLError): + pass + +class BaseConstructor: + + yaml_constructors = {} + yaml_multi_constructors = {} + + def __init__(self): + self.constructed_objects = {} + self.recursive_objects = {} + self.state_generators = [] + self.deep_construct = False + + def check_data(self): + # If there are more documents available? + return self.check_node() + + def check_state_key(self, key): + """Block special attributes/methods from being set in a newly created + object, to prevent user-controlled methods from being called during + deserialization""" + if self.get_state_keys_blacklist_regexp().match(key): + raise ConstructorError(None, None, + "blacklisted key '%s' in instance state found" % (key,), None) + + def get_data(self): + # Construct and return the next document. + if self.check_node(): + return self.construct_document(self.get_node()) + + def get_single_data(self): + # Ensure that the stream contains a single document and construct it. + node = self.get_single_node() + if node is not None: + return self.construct_document(node) + return None + + def construct_document(self, node): + data = self.construct_object(node) + while self.state_generators: + state_generators = self.state_generators + self.state_generators = [] + for generator in state_generators: + for dummy in generator: + pass + self.constructed_objects = {} + self.recursive_objects = {} + self.deep_construct = False + return data + + def construct_object(self, node, deep=False): + if node in self.constructed_objects: + return self.constructed_objects[node] + if deep: + old_deep = self.deep_construct + self.deep_construct = True + if node in self.recursive_objects: + raise ConstructorError(None, None, + "found unconstructable recursive node", node.start_mark) + self.recursive_objects[node] = None + constructor = None + tag_suffix = None + if node.tag in self.yaml_constructors: + constructor = self.yaml_constructors[node.tag] + else: + for tag_prefix in self.yaml_multi_constructors: + if tag_prefix is not None and node.tag.startswith(tag_prefix): + tag_suffix = node.tag[len(tag_prefix):] + constructor = self.yaml_multi_constructors[tag_prefix] + break + else: + if None in self.yaml_multi_constructors: + tag_suffix = node.tag + constructor = self.yaml_multi_constructors[None] + elif None in self.yaml_constructors: + constructor = self.yaml_constructors[None] + elif isinstance(node, ScalarNode): + constructor = self.__class__.construct_scalar + elif isinstance(node, SequenceNode): + constructor = self.__class__.construct_sequence + elif isinstance(node, MappingNode): + constructor = self.__class__.construct_mapping + if tag_suffix is None: + data = constructor(self, node) + else: + data = constructor(self, tag_suffix, node) + if isinstance(data, types.GeneratorType): + generator = data + data = next(generator) + if self.deep_construct: + for dummy in generator: + pass + else: + self.state_generators.append(generator) + self.constructed_objects[node] = data + del self.recursive_objects[node] + if deep: + self.deep_construct = old_deep + return data + + def construct_scalar(self, node): + if not isinstance(node, ScalarNode): + raise ConstructorError(None, None, + "expected a scalar node, but found %s" % node.id, + node.start_mark) + return node.value + + def construct_sequence(self, node, deep=False): + if not isinstance(node, SequenceNode): + raise ConstructorError(None, None, + "expected a sequence node, but found %s" % node.id, + node.start_mark) + return [self.construct_object(child, deep=deep) + for child in node.value] + + def construct_mapping(self, node, deep=False): + if not isinstance(node, MappingNode): + raise ConstructorError(None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark) + mapping = {} + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + if not isinstance(key, collections.abc.Hashable): + raise ConstructorError("while constructing a mapping", node.start_mark, + "found unhashable key", key_node.start_mark) + value = self.construct_object(value_node, deep=deep) + mapping[key] = value + return mapping + + def construct_pairs(self, node, deep=False): + if not isinstance(node, MappingNode): + raise ConstructorError(None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark) + pairs = [] + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + value = self.construct_object(value_node, deep=deep) + pairs.append((key, value)) + return pairs + + @classmethod + def add_constructor(cls, tag, constructor): + if not 'yaml_constructors' in cls.__dict__: + cls.yaml_constructors = cls.yaml_constructors.copy() + cls.yaml_constructors[tag] = constructor + + @classmethod + def add_multi_constructor(cls, tag_prefix, multi_constructor): + if not 'yaml_multi_constructors' in cls.__dict__: + cls.yaml_multi_constructors = cls.yaml_multi_constructors.copy() + cls.yaml_multi_constructors[tag_prefix] = multi_constructor + +class SafeConstructor(BaseConstructor): + + def construct_scalar(self, node): + if isinstance(node, MappingNode): + for key_node, value_node in node.value: + if key_node.tag == 'tag:yaml.org,2002:value': + return self.construct_scalar(value_node) + return super().construct_scalar(node) + + def flatten_mapping(self, node): + merge = [] + index = 0 + while index < len(node.value): + key_node, value_node = node.value[index] + if key_node.tag == 'tag:yaml.org,2002:merge': + del node.value[index] + if isinstance(value_node, MappingNode): + self.flatten_mapping(value_node) + merge.extend(value_node.value) + elif isinstance(value_node, SequenceNode): + submerge = [] + for subnode in value_node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing a mapping", + node.start_mark, + "expected a mapping for merging, but found %s" + % subnode.id, subnode.start_mark) + self.flatten_mapping(subnode) + submerge.append(subnode.value) + submerge.reverse() + for value in submerge: + merge.extend(value) + else: + raise ConstructorError("while constructing a mapping", node.start_mark, + "expected a mapping or list of mappings for merging, but found %s" + % value_node.id, value_node.start_mark) + elif key_node.tag == 'tag:yaml.org,2002:value': + key_node.tag = 'tag:yaml.org,2002:str' + index += 1 + else: + index += 1 + if merge: + node.value = merge + node.value + + def construct_mapping(self, node, deep=False): + if isinstance(node, MappingNode): + self.flatten_mapping(node) + return super().construct_mapping(node, deep=deep) + + def construct_yaml_null(self, node): + self.construct_scalar(node) + return None + + bool_values = { + 'yes': True, + 'no': False, + 'true': True, + 'false': False, + 'on': True, + 'off': False, + } + + def construct_yaml_bool(self, node): + value = self.construct_scalar(node) + return self.bool_values[value.lower()] + + def construct_yaml_int(self, node): + value = self.construct_scalar(node) + value = value.replace('_', '') + sign = +1 + if value[0] == '-': + sign = -1 + if value[0] in '+-': + value = value[1:] + if value == '0': + return 0 + elif value.startswith('0b'): + return sign*int(value[2:], 2) + elif value.startswith('0x'): + return sign*int(value[2:], 16) + elif value[0] == '0': + return sign*int(value, 8) + elif ':' in value: + digits = [int(part) for part in value.split(':')] + digits.reverse() + base = 1 + value = 0 + for digit in digits: + value += digit*base + base *= 60 + return sign*value + else: + return sign*int(value) + + inf_value = 1e300 + while inf_value != inf_value*inf_value: + inf_value *= inf_value + nan_value = -inf_value/inf_value # Trying to make a quiet NaN (like C99). + + def construct_yaml_float(self, node): + value = self.construct_scalar(node) + value = value.replace('_', '').lower() + sign = +1 + if value[0] == '-': + sign = -1 + if value[0] in '+-': + value = value[1:] + if value == '.inf': + return sign*self.inf_value + elif value == '.nan': + return self.nan_value + elif ':' in value: + digits = [float(part) for part in value.split(':')] + digits.reverse() + base = 1 + value = 0.0 + for digit in digits: + value += digit*base + base *= 60 + return sign*value + else: + return sign*float(value) + + def construct_yaml_binary(self, node): + try: + value = self.construct_scalar(node).encode('ascii') + except UnicodeEncodeError as exc: + raise ConstructorError(None, None, + "failed to convert base64 data into ascii: %s" % exc, + node.start_mark) + try: + if hasattr(base64, 'decodebytes'): + return base64.decodebytes(value) + else: + return base64.decodestring(value) + except binascii.Error as exc: + raise ConstructorError(None, None, + "failed to decode base64 data: %s" % exc, node.start_mark) + + timestamp_regexp = re.compile( + r'''^(?P[0-9][0-9][0-9][0-9]) + -(?P[0-9][0-9]?) + -(?P[0-9][0-9]?) + (?:(?:[Tt]|[ \t]+) + (?P[0-9][0-9]?) + :(?P[0-9][0-9]) + :(?P[0-9][0-9]) + (?:\.(?P[0-9]*))? + (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?) + (?::(?P[0-9][0-9]))?))?)?$''', re.X) + + def construct_yaml_timestamp(self, node): + value = self.construct_scalar(node) + match = self.timestamp_regexp.match(node.value) + values = match.groupdict() + year = int(values['year']) + month = int(values['month']) + day = int(values['day']) + if not values['hour']: + return datetime.date(year, month, day) + hour = int(values['hour']) + minute = int(values['minute']) + second = int(values['second']) + fraction = 0 + tzinfo = None + if values['fraction']: + fraction = values['fraction'][:6] + while len(fraction) < 6: + fraction += '0' + fraction = int(fraction) + if values['tz_sign']: + tz_hour = int(values['tz_hour']) + tz_minute = int(values['tz_minute'] or 0) + delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute) + if values['tz_sign'] == '-': + delta = -delta + tzinfo = datetime.timezone(delta) + elif values['tz']: + tzinfo = datetime.timezone.utc + return datetime.datetime(year, month, day, hour, minute, second, fraction, + tzinfo=tzinfo) + + def construct_yaml_omap(self, node): + # Note: we do not check for duplicate keys, because it's too + # CPU-expensive. + omap = [] + yield omap + if not isinstance(node, SequenceNode): + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a sequence, but found %s" % node.id, node.start_mark) + for subnode in node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a mapping of length 1, but found %s" % subnode.id, + subnode.start_mark) + if len(subnode.value) != 1: + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a single mapping item, but found %d items" % len(subnode.value), + subnode.start_mark) + key_node, value_node = subnode.value[0] + key = self.construct_object(key_node) + value = self.construct_object(value_node) + omap.append((key, value)) + + def construct_yaml_pairs(self, node): + # Note: the same code as `construct_yaml_omap`. + pairs = [] + yield pairs + if not isinstance(node, SequenceNode): + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a sequence, but found %s" % node.id, node.start_mark) + for subnode in node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a mapping of length 1, but found %s" % subnode.id, + subnode.start_mark) + if len(subnode.value) != 1: + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a single mapping item, but found %d items" % len(subnode.value), + subnode.start_mark) + key_node, value_node = subnode.value[0] + key = self.construct_object(key_node) + value = self.construct_object(value_node) + pairs.append((key, value)) + + def construct_yaml_set(self, node): + data = set() + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_yaml_str(self, node): + return self.construct_scalar(node) + + def construct_yaml_seq(self, node): + data = [] + yield data + data.extend(self.construct_sequence(node)) + + def construct_yaml_map(self, node): + data = {} + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_yaml_object(self, node, cls): + data = cls.__new__(cls) + yield data + if hasattr(data, '__setstate__'): + state = self.construct_mapping(node, deep=True) + data.__setstate__(state) + else: + state = self.construct_mapping(node) + data.__dict__.update(state) + + def construct_undefined(self, node): + raise ConstructorError(None, None, + "could not determine a constructor for the tag %r" % node.tag, + node.start_mark) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:null', + SafeConstructor.construct_yaml_null) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:bool', + SafeConstructor.construct_yaml_bool) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:int', + SafeConstructor.construct_yaml_int) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:float', + SafeConstructor.construct_yaml_float) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:binary', + SafeConstructor.construct_yaml_binary) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:timestamp', + SafeConstructor.construct_yaml_timestamp) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:omap', + SafeConstructor.construct_yaml_omap) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:pairs', + SafeConstructor.construct_yaml_pairs) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:set', + SafeConstructor.construct_yaml_set) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:str', + SafeConstructor.construct_yaml_str) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:seq', + SafeConstructor.construct_yaml_seq) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:map', + SafeConstructor.construct_yaml_map) + +SafeConstructor.add_constructor(None, + SafeConstructor.construct_undefined) + +class FullConstructor(SafeConstructor): + # 'extend' is blacklisted because it is used by + # construct_python_object_apply to add `listitems` to a newly generate + # python instance + def get_state_keys_blacklist(self): + return ['^extend$', '^__.*__$'] + + def get_state_keys_blacklist_regexp(self): + if not hasattr(self, 'state_keys_blacklist_regexp'): + self.state_keys_blacklist_regexp = re.compile('(' + '|'.join(self.get_state_keys_blacklist()) + ')') + return self.state_keys_blacklist_regexp + + def construct_python_str(self, node): + return self.construct_scalar(node) + + def construct_python_unicode(self, node): + return self.construct_scalar(node) + + def construct_python_bytes(self, node): + try: + value = self.construct_scalar(node).encode('ascii') + except UnicodeEncodeError as exc: + raise ConstructorError(None, None, + "failed to convert base64 data into ascii: %s" % exc, + node.start_mark) + try: + if hasattr(base64, 'decodebytes'): + return base64.decodebytes(value) + else: + return base64.decodestring(value) + except binascii.Error as exc: + raise ConstructorError(None, None, + "failed to decode base64 data: %s" % exc, node.start_mark) + + def construct_python_long(self, node): + return self.construct_yaml_int(node) + + def construct_python_complex(self, node): + return complex(self.construct_scalar(node)) + + def construct_python_tuple(self, node): + return tuple(self.construct_sequence(node)) + + def find_python_module(self, name, mark, unsafe=False): + if not name: + raise ConstructorError("while constructing a Python module", mark, + "expected non-empty name appended to the tag", mark) + if unsafe: + try: + __import__(name) + except ImportError as exc: + raise ConstructorError("while constructing a Python module", mark, + "cannot find module %r (%s)" % (name, exc), mark) + if name not in sys.modules: + raise ConstructorError("while constructing a Python module", mark, + "module %r is not imported" % name, mark) + return sys.modules[name] + + def find_python_name(self, name, mark, unsafe=False): + if not name: + raise ConstructorError("while constructing a Python object", mark, + "expected non-empty name appended to the tag", mark) + if '.' in name: + module_name, object_name = name.rsplit('.', 1) + else: + module_name = 'builtins' + object_name = name + if unsafe: + try: + __import__(module_name) + except ImportError as exc: + raise ConstructorError("while constructing a Python object", mark, + "cannot find module %r (%s)" % (module_name, exc), mark) + if module_name not in sys.modules: + raise ConstructorError("while constructing a Python object", mark, + "module %r is not imported" % module_name, mark) + module = sys.modules[module_name] + if not hasattr(module, object_name): + raise ConstructorError("while constructing a Python object", mark, + "cannot find %r in the module %r" + % (object_name, module.__name__), mark) + return getattr(module, object_name) + + def construct_python_name(self, suffix, node): + value = self.construct_scalar(node) + if value: + raise ConstructorError("while constructing a Python name", node.start_mark, + "expected the empty value, but found %r" % value, node.start_mark) + return self.find_python_name(suffix, node.start_mark) + + def construct_python_module(self, suffix, node): + value = self.construct_scalar(node) + if value: + raise ConstructorError("while constructing a Python module", node.start_mark, + "expected the empty value, but found %r" % value, node.start_mark) + return self.find_python_module(suffix, node.start_mark) + + def make_python_instance(self, suffix, node, + args=None, kwds=None, newobj=False, unsafe=False): + if not args: + args = [] + if not kwds: + kwds = {} + cls = self.find_python_name(suffix, node.start_mark) + if not (unsafe or isinstance(cls, type)): + raise ConstructorError("while constructing a Python instance", node.start_mark, + "expected a class, but found %r" % type(cls), + node.start_mark) + if newobj and isinstance(cls, type): + return cls.__new__(cls, *args, **kwds) + else: + return cls(*args, **kwds) + + def set_python_instance_state(self, instance, state, unsafe=False): + if hasattr(instance, '__setstate__'): + instance.__setstate__(state) + else: + slotstate = {} + if isinstance(state, tuple) and len(state) == 2: + state, slotstate = state + if hasattr(instance, '__dict__'): + if not unsafe and state: + for key in state.keys(): + self.check_state_key(key) + instance.__dict__.update(state) + elif state: + slotstate.update(state) + for key, value in slotstate.items(): + if not unsafe: + self.check_state_key(key) + setattr(instance, key, value) + + def construct_python_object(self, suffix, node): + # Format: + # !!python/object:module.name { ... state ... } + instance = self.make_python_instance(suffix, node, newobj=True) + yield instance + deep = hasattr(instance, '__setstate__') + state = self.construct_mapping(node, deep=deep) + self.set_python_instance_state(instance, state) + + def construct_python_object_apply(self, suffix, node, newobj=False): + # Format: + # !!python/object/apply # (or !!python/object/new) + # args: [ ... arguments ... ] + # kwds: { ... keywords ... } + # state: ... state ... + # listitems: [ ... listitems ... ] + # dictitems: { ... dictitems ... } + # or short format: + # !!python/object/apply [ ... arguments ... ] + # The difference between !!python/object/apply and !!python/object/new + # is how an object is created, check make_python_instance for details. + if isinstance(node, SequenceNode): + args = self.construct_sequence(node, deep=True) + kwds = {} + state = {} + listitems = [] + dictitems = {} + else: + value = self.construct_mapping(node, deep=True) + args = value.get('args', []) + kwds = value.get('kwds', {}) + state = value.get('state', {}) + listitems = value.get('listitems', []) + dictitems = value.get('dictitems', {}) + instance = self.make_python_instance(suffix, node, args, kwds, newobj) + if state: + self.set_python_instance_state(instance, state) + if listitems: + instance.extend(listitems) + if dictitems: + for key in dictitems: + instance[key] = dictitems[key] + return instance + + def construct_python_object_new(self, suffix, node): + return self.construct_python_object_apply(suffix, node, newobj=True) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/none', + FullConstructor.construct_yaml_null) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/bool', + FullConstructor.construct_yaml_bool) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/str', + FullConstructor.construct_python_str) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/unicode', + FullConstructor.construct_python_unicode) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/bytes', + FullConstructor.construct_python_bytes) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/int', + FullConstructor.construct_yaml_int) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/long', + FullConstructor.construct_python_long) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/float', + FullConstructor.construct_yaml_float) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/complex', + FullConstructor.construct_python_complex) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/list', + FullConstructor.construct_yaml_seq) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/tuple', + FullConstructor.construct_python_tuple) + +FullConstructor.add_constructor( + 'tag:yaml.org,2002:python/dict', + FullConstructor.construct_yaml_map) + +FullConstructor.add_multi_constructor( + 'tag:yaml.org,2002:python/name:', + FullConstructor.construct_python_name) + +FullConstructor.add_multi_constructor( + 'tag:yaml.org,2002:python/module:', + FullConstructor.construct_python_module) + +FullConstructor.add_multi_constructor( + 'tag:yaml.org,2002:python/object:', + FullConstructor.construct_python_object) + +FullConstructor.add_multi_constructor( + 'tag:yaml.org,2002:python/object/new:', + FullConstructor.construct_python_object_new) + +class UnsafeConstructor(FullConstructor): + + def find_python_module(self, name, mark): + return super(UnsafeConstructor, self).find_python_module(name, mark, unsafe=True) + + def find_python_name(self, name, mark): + return super(UnsafeConstructor, self).find_python_name(name, mark, unsafe=True) + + def make_python_instance(self, suffix, node, args=None, kwds=None, newobj=False): + return super(UnsafeConstructor, self).make_python_instance( + suffix, node, args, kwds, newobj, unsafe=True) + + def set_python_instance_state(self, instance, state): + return super(UnsafeConstructor, self).set_python_instance_state( + instance, state, unsafe=True) + +UnsafeConstructor.add_multi_constructor( + 'tag:yaml.org,2002:python/object/apply:', + UnsafeConstructor.construct_python_object_apply) + +# Constructor is same as UnsafeConstructor. Need to leave this in place in case +# people have extended it directly. +class Constructor(UnsafeConstructor): + pass diff --git a/coredns/venv/yaml/cyaml.py b/coredns/venv/yaml/cyaml.py new file mode 100644 index 0000000..1e606c7 --- /dev/null +++ b/coredns/venv/yaml/cyaml.py @@ -0,0 +1,101 @@ + +__all__ = [ + 'CBaseLoader', 'CSafeLoader', 'CFullLoader', 'CUnsafeLoader', 'CLoader', + 'CBaseDumper', 'CSafeDumper', 'CDumper' +] + +from _yaml import CParser, CEmitter + +from .constructor import * + +from .serializer import * +from .representer import * + +from .resolver import * + +class CBaseLoader(CParser, BaseConstructor, BaseResolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + BaseConstructor.__init__(self) + BaseResolver.__init__(self) + +class CSafeLoader(CParser, SafeConstructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + SafeConstructor.__init__(self) + Resolver.__init__(self) + +class CFullLoader(CParser, FullConstructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + FullConstructor.__init__(self) + Resolver.__init__(self) + +class CUnsafeLoader(CParser, UnsafeConstructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + UnsafeConstructor.__init__(self) + Resolver.__init__(self) + +class CLoader(CParser, Constructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + Constructor.__init__(self) + Resolver.__init__(self) + +class CBaseDumper(CEmitter, BaseRepresenter, BaseResolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + +class CSafeDumper(CEmitter, SafeRepresenter, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + SafeRepresenter.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + +class CDumper(CEmitter, Serializer, Representer, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + diff --git a/coredns/venv/yaml/dumper.py b/coredns/venv/yaml/dumper.py new file mode 100644 index 0000000..6aadba5 --- /dev/null +++ b/coredns/venv/yaml/dumper.py @@ -0,0 +1,62 @@ + +__all__ = ['BaseDumper', 'SafeDumper', 'Dumper'] + +from .emitter import * +from .serializer import * +from .representer import * +from .resolver import * + +class BaseDumper(Emitter, Serializer, BaseRepresenter, BaseResolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + +class SafeDumper(Emitter, Serializer, SafeRepresenter, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + SafeRepresenter.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + +class Dumper(Emitter, Serializer, Representer, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=False, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None, sort_keys=True): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style, sort_keys=sort_keys) + Resolver.__init__(self) + diff --git a/coredns/venv/yaml/emitter.py b/coredns/venv/yaml/emitter.py new file mode 100644 index 0000000..a664d01 --- /dev/null +++ b/coredns/venv/yaml/emitter.py @@ -0,0 +1,1137 @@ + +# Emitter expects events obeying the following grammar: +# stream ::= STREAM-START document* STREAM-END +# document ::= DOCUMENT-START node DOCUMENT-END +# node ::= SCALAR | sequence | mapping +# sequence ::= SEQUENCE-START node* SEQUENCE-END +# mapping ::= MAPPING-START (node node)* MAPPING-END + +__all__ = ['Emitter', 'EmitterError'] + +from .error import YAMLError +from .events import * + +class EmitterError(YAMLError): + pass + +class ScalarAnalysis: + def __init__(self, scalar, empty, multiline, + allow_flow_plain, allow_block_plain, + allow_single_quoted, allow_double_quoted, + allow_block): + self.scalar = scalar + self.empty = empty + self.multiline = multiline + self.allow_flow_plain = allow_flow_plain + self.allow_block_plain = allow_block_plain + self.allow_single_quoted = allow_single_quoted + self.allow_double_quoted = allow_double_quoted + self.allow_block = allow_block + +class Emitter: + + DEFAULT_TAG_PREFIXES = { + '!' : '!', + 'tag:yaml.org,2002:' : '!!', + } + + def __init__(self, stream, canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None): + + # The stream should have the methods `write` and possibly `flush`. + self.stream = stream + + # Encoding can be overridden by STREAM-START. + self.encoding = None + + # Emitter is a state machine with a stack of states to handle nested + # structures. + self.states = [] + self.state = self.expect_stream_start + + # Current event and the event queue. + self.events = [] + self.event = None + + # The current indentation level and the stack of previous indents. + self.indents = [] + self.indent = None + + # Flow level. + self.flow_level = 0 + + # Contexts. + self.root_context = False + self.sequence_context = False + self.mapping_context = False + self.simple_key_context = False + + # Characteristics of the last emitted character: + # - current position. + # - is it a whitespace? + # - is it an indention character + # (indentation space, '-', '?', or ':')? + self.line = 0 + self.column = 0 + self.whitespace = True + self.indention = True + + # Whether the document requires an explicit document indicator + self.open_ended = False + + # Formatting details. + self.canonical = canonical + self.allow_unicode = allow_unicode + self.best_indent = 2 + if indent and 1 < indent < 10: + self.best_indent = indent + self.best_width = 80 + if width and width > self.best_indent*2: + self.best_width = width + self.best_line_break = '\n' + if line_break in ['\r', '\n', '\r\n']: + self.best_line_break = line_break + + # Tag prefixes. + self.tag_prefixes = None + + # Prepared anchor and tag. + self.prepared_anchor = None + self.prepared_tag = None + + # Scalar analysis and style. + self.analysis = None + self.style = None + + def dispose(self): + # Reset the state attributes (to clear self-references) + self.states = [] + self.state = None + + def emit(self, event): + self.events.append(event) + while not self.need_more_events(): + self.event = self.events.pop(0) + self.state() + self.event = None + + # In some cases, we wait for a few next events before emitting. + + def need_more_events(self): + if not self.events: + return True + event = self.events[0] + if isinstance(event, DocumentStartEvent): + return self.need_events(1) + elif isinstance(event, SequenceStartEvent): + return self.need_events(2) + elif isinstance(event, MappingStartEvent): + return self.need_events(3) + else: + return False + + def need_events(self, count): + level = 0 + for event in self.events[1:]: + if isinstance(event, (DocumentStartEvent, CollectionStartEvent)): + level += 1 + elif isinstance(event, (DocumentEndEvent, CollectionEndEvent)): + level -= 1 + elif isinstance(event, StreamEndEvent): + level = -1 + if level < 0: + return False + return (len(self.events) < count+1) + + def increase_indent(self, flow=False, indentless=False): + self.indents.append(self.indent) + if self.indent is None: + if flow: + self.indent = self.best_indent + else: + self.indent = 0 + elif not indentless: + self.indent += self.best_indent + + # States. + + # Stream handlers. + + def expect_stream_start(self): + if isinstance(self.event, StreamStartEvent): + if self.event.encoding and not hasattr(self.stream, 'encoding'): + self.encoding = self.event.encoding + self.write_stream_start() + self.state = self.expect_first_document_start + else: + raise EmitterError("expected StreamStartEvent, but got %s" + % self.event) + + def expect_nothing(self): + raise EmitterError("expected nothing, but got %s" % self.event) + + # Document handlers. + + def expect_first_document_start(self): + return self.expect_document_start(first=True) + + def expect_document_start(self, first=False): + if isinstance(self.event, DocumentStartEvent): + if (self.event.version or self.event.tags) and self.open_ended: + self.write_indicator('...', True) + self.write_indent() + if self.event.version: + version_text = self.prepare_version(self.event.version) + self.write_version_directive(version_text) + self.tag_prefixes = self.DEFAULT_TAG_PREFIXES.copy() + if self.event.tags: + handles = sorted(self.event.tags.keys()) + for handle in handles: + prefix = self.event.tags[handle] + self.tag_prefixes[prefix] = handle + handle_text = self.prepare_tag_handle(handle) + prefix_text = self.prepare_tag_prefix(prefix) + self.write_tag_directive(handle_text, prefix_text) + implicit = (first and not self.event.explicit and not self.canonical + and not self.event.version and not self.event.tags + and not self.check_empty_document()) + if not implicit: + self.write_indent() + self.write_indicator('---', True) + if self.canonical: + self.write_indent() + self.state = self.expect_document_root + elif isinstance(self.event, StreamEndEvent): + if self.open_ended: + self.write_indicator('...', True) + self.write_indent() + self.write_stream_end() + self.state = self.expect_nothing + else: + raise EmitterError("expected DocumentStartEvent, but got %s" + % self.event) + + def expect_document_end(self): + if isinstance(self.event, DocumentEndEvent): + self.write_indent() + if self.event.explicit: + self.write_indicator('...', True) + self.write_indent() + self.flush_stream() + self.state = self.expect_document_start + else: + raise EmitterError("expected DocumentEndEvent, but got %s" + % self.event) + + def expect_document_root(self): + self.states.append(self.expect_document_end) + self.expect_node(root=True) + + # Node handlers. + + def expect_node(self, root=False, sequence=False, mapping=False, + simple_key=False): + self.root_context = root + self.sequence_context = sequence + self.mapping_context = mapping + self.simple_key_context = simple_key + if isinstance(self.event, AliasEvent): + self.expect_alias() + elif isinstance(self.event, (ScalarEvent, CollectionStartEvent)): + self.process_anchor('&') + self.process_tag() + if isinstance(self.event, ScalarEvent): + self.expect_scalar() + elif isinstance(self.event, SequenceStartEvent): + if self.flow_level or self.canonical or self.event.flow_style \ + or self.check_empty_sequence(): + self.expect_flow_sequence() + else: + self.expect_block_sequence() + elif isinstance(self.event, MappingStartEvent): + if self.flow_level or self.canonical or self.event.flow_style \ + or self.check_empty_mapping(): + self.expect_flow_mapping() + else: + self.expect_block_mapping() + else: + raise EmitterError("expected NodeEvent, but got %s" % self.event) + + def expect_alias(self): + if self.event.anchor is None: + raise EmitterError("anchor is not specified for alias") + self.process_anchor('*') + self.state = self.states.pop() + + def expect_scalar(self): + self.increase_indent(flow=True) + self.process_scalar() + self.indent = self.indents.pop() + self.state = self.states.pop() + + # Flow sequence handlers. + + def expect_flow_sequence(self): + self.write_indicator('[', True, whitespace=True) + self.flow_level += 1 + self.increase_indent(flow=True) + self.state = self.expect_first_flow_sequence_item + + def expect_first_flow_sequence_item(self): + if isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + self.write_indicator(']', False) + self.state = self.states.pop() + else: + if self.canonical or self.column > self.best_width: + self.write_indent() + self.states.append(self.expect_flow_sequence_item) + self.expect_node(sequence=True) + + def expect_flow_sequence_item(self): + if isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + if self.canonical: + self.write_indicator(',', False) + self.write_indent() + self.write_indicator(']', False) + self.state = self.states.pop() + else: + self.write_indicator(',', False) + if self.canonical or self.column > self.best_width: + self.write_indent() + self.states.append(self.expect_flow_sequence_item) + self.expect_node(sequence=True) + + # Flow mapping handlers. + + def expect_flow_mapping(self): + self.write_indicator('{', True, whitespace=True) + self.flow_level += 1 + self.increase_indent(flow=True) + self.state = self.expect_first_flow_mapping_key + + def expect_first_flow_mapping_key(self): + if isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + self.write_indicator('}', False) + self.state = self.states.pop() + else: + if self.canonical or self.column > self.best_width: + self.write_indent() + if not self.canonical and self.check_simple_key(): + self.states.append(self.expect_flow_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator('?', True) + self.states.append(self.expect_flow_mapping_value) + self.expect_node(mapping=True) + + def expect_flow_mapping_key(self): + if isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + if self.canonical: + self.write_indicator(',', False) + self.write_indent() + self.write_indicator('}', False) + self.state = self.states.pop() + else: + self.write_indicator(',', False) + if self.canonical or self.column > self.best_width: + self.write_indent() + if not self.canonical and self.check_simple_key(): + self.states.append(self.expect_flow_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator('?', True) + self.states.append(self.expect_flow_mapping_value) + self.expect_node(mapping=True) + + def expect_flow_mapping_simple_value(self): + self.write_indicator(':', False) + self.states.append(self.expect_flow_mapping_key) + self.expect_node(mapping=True) + + def expect_flow_mapping_value(self): + if self.canonical or self.column > self.best_width: + self.write_indent() + self.write_indicator(':', True) + self.states.append(self.expect_flow_mapping_key) + self.expect_node(mapping=True) + + # Block sequence handlers. + + def expect_block_sequence(self): + indentless = (self.mapping_context and not self.indention) + self.increase_indent(flow=False, indentless=indentless) + self.state = self.expect_first_block_sequence_item + + def expect_first_block_sequence_item(self): + return self.expect_block_sequence_item(first=True) + + def expect_block_sequence_item(self, first=False): + if not first and isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.state = self.states.pop() + else: + self.write_indent() + self.write_indicator('-', True, indention=True) + self.states.append(self.expect_block_sequence_item) + self.expect_node(sequence=True) + + # Block mapping handlers. + + def expect_block_mapping(self): + self.increase_indent(flow=False) + self.state = self.expect_first_block_mapping_key + + def expect_first_block_mapping_key(self): + return self.expect_block_mapping_key(first=True) + + def expect_block_mapping_key(self, first=False): + if not first and isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.state = self.states.pop() + else: + self.write_indent() + if self.check_simple_key(): + self.states.append(self.expect_block_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator('?', True, indention=True) + self.states.append(self.expect_block_mapping_value) + self.expect_node(mapping=True) + + def expect_block_mapping_simple_value(self): + self.write_indicator(':', False) + self.states.append(self.expect_block_mapping_key) + self.expect_node(mapping=True) + + def expect_block_mapping_value(self): + self.write_indent() + self.write_indicator(':', True, indention=True) + self.states.append(self.expect_block_mapping_key) + self.expect_node(mapping=True) + + # Checkers. + + def check_empty_sequence(self): + return (isinstance(self.event, SequenceStartEvent) and self.events + and isinstance(self.events[0], SequenceEndEvent)) + + def check_empty_mapping(self): + return (isinstance(self.event, MappingStartEvent) and self.events + and isinstance(self.events[0], MappingEndEvent)) + + def check_empty_document(self): + if not isinstance(self.event, DocumentStartEvent) or not self.events: + return False + event = self.events[0] + return (isinstance(event, ScalarEvent) and event.anchor is None + and event.tag is None and event.implicit and event.value == '') + + def check_simple_key(self): + length = 0 + if isinstance(self.event, NodeEvent) and self.event.anchor is not None: + if self.prepared_anchor is None: + self.prepared_anchor = self.prepare_anchor(self.event.anchor) + length += len(self.prepared_anchor) + if isinstance(self.event, (ScalarEvent, CollectionStartEvent)) \ + and self.event.tag is not None: + if self.prepared_tag is None: + self.prepared_tag = self.prepare_tag(self.event.tag) + length += len(self.prepared_tag) + if isinstance(self.event, ScalarEvent): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + length += len(self.analysis.scalar) + return (length < 128 and (isinstance(self.event, AliasEvent) + or (isinstance(self.event, ScalarEvent) + and not self.analysis.empty and not self.analysis.multiline) + or self.check_empty_sequence() or self.check_empty_mapping())) + + # Anchor, Tag, and Scalar processors. + + def process_anchor(self, indicator): + if self.event.anchor is None: + self.prepared_anchor = None + return + if self.prepared_anchor is None: + self.prepared_anchor = self.prepare_anchor(self.event.anchor) + if self.prepared_anchor: + self.write_indicator(indicator+self.prepared_anchor, True) + self.prepared_anchor = None + + def process_tag(self): + tag = self.event.tag + if isinstance(self.event, ScalarEvent): + if self.style is None: + self.style = self.choose_scalar_style() + if ((not self.canonical or tag is None) and + ((self.style == '' and self.event.implicit[0]) + or (self.style != '' and self.event.implicit[1]))): + self.prepared_tag = None + return + if self.event.implicit[0] and tag is None: + tag = '!' + self.prepared_tag = None + else: + if (not self.canonical or tag is None) and self.event.implicit: + self.prepared_tag = None + return + if tag is None: + raise EmitterError("tag is not specified") + if self.prepared_tag is None: + self.prepared_tag = self.prepare_tag(tag) + if self.prepared_tag: + self.write_indicator(self.prepared_tag, True) + self.prepared_tag = None + + def choose_scalar_style(self): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + if self.event.style == '"' or self.canonical: + return '"' + if not self.event.style and self.event.implicit[0]: + if (not (self.simple_key_context and + (self.analysis.empty or self.analysis.multiline)) + and (self.flow_level and self.analysis.allow_flow_plain + or (not self.flow_level and self.analysis.allow_block_plain))): + return '' + if self.event.style and self.event.style in '|>': + if (not self.flow_level and not self.simple_key_context + and self.analysis.allow_block): + return self.event.style + if not self.event.style or self.event.style == '\'': + if (self.analysis.allow_single_quoted and + not (self.simple_key_context and self.analysis.multiline)): + return '\'' + return '"' + + def process_scalar(self): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + if self.style is None: + self.style = self.choose_scalar_style() + split = (not self.simple_key_context) + #if self.analysis.multiline and split \ + # and (not self.style or self.style in '\'\"'): + # self.write_indent() + if self.style == '"': + self.write_double_quoted(self.analysis.scalar, split) + elif self.style == '\'': + self.write_single_quoted(self.analysis.scalar, split) + elif self.style == '>': + self.write_folded(self.analysis.scalar) + elif self.style == '|': + self.write_literal(self.analysis.scalar) + else: + self.write_plain(self.analysis.scalar, split) + self.analysis = None + self.style = None + + # Analyzers. + + def prepare_version(self, version): + major, minor = version + if major != 1: + raise EmitterError("unsupported YAML version: %d.%d" % (major, minor)) + return '%d.%d' % (major, minor) + + def prepare_tag_handle(self, handle): + if not handle: + raise EmitterError("tag handle must not be empty") + if handle[0] != '!' or handle[-1] != '!': + raise EmitterError("tag handle must start and end with '!': %r" % handle) + for ch in handle[1:-1]: + if not ('0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_'): + raise EmitterError("invalid character %r in the tag handle: %r" + % (ch, handle)) + return handle + + def prepare_tag_prefix(self, prefix): + if not prefix: + raise EmitterError("tag prefix must not be empty") + chunks = [] + start = end = 0 + if prefix[0] == '!': + end = 1 + while end < len(prefix): + ch = prefix[end] + if '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-;/?!:@&=+$,_.~*\'()[]': + end += 1 + else: + if start < end: + chunks.append(prefix[start:end]) + start = end = end+1 + data = ch.encode('utf-8') + for ch in data: + chunks.append('%%%02X' % ord(ch)) + if start < end: + chunks.append(prefix[start:end]) + return ''.join(chunks) + + def prepare_tag(self, tag): + if not tag: + raise EmitterError("tag must not be empty") + if tag == '!': + return tag + handle = None + suffix = tag + prefixes = sorted(self.tag_prefixes.keys()) + for prefix in prefixes: + if tag.startswith(prefix) \ + and (prefix == '!' or len(prefix) < len(tag)): + handle = self.tag_prefixes[prefix] + suffix = tag[len(prefix):] + chunks = [] + start = end = 0 + while end < len(suffix): + ch = suffix[end] + if '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-;/?:@&=+$,_.~*\'()[]' \ + or (ch == '!' and handle != '!'): + end += 1 + else: + if start < end: + chunks.append(suffix[start:end]) + start = end = end+1 + data = ch.encode('utf-8') + for ch in data: + chunks.append('%%%02X' % ch) + if start < end: + chunks.append(suffix[start:end]) + suffix_text = ''.join(chunks) + if handle: + return '%s%s' % (handle, suffix_text) + else: + return '!<%s>' % suffix_text + + def prepare_anchor(self, anchor): + if not anchor: + raise EmitterError("anchor must not be empty") + for ch in anchor: + if not ('0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_'): + raise EmitterError("invalid character %r in the anchor: %r" + % (ch, anchor)) + return anchor + + def analyze_scalar(self, scalar): + + # Empty scalar is a special case. + if not scalar: + return ScalarAnalysis(scalar=scalar, empty=True, multiline=False, + allow_flow_plain=False, allow_block_plain=True, + allow_single_quoted=True, allow_double_quoted=True, + allow_block=False) + + # Indicators and special characters. + block_indicators = False + flow_indicators = False + line_breaks = False + special_characters = False + + # Important whitespace combinations. + leading_space = False + leading_break = False + trailing_space = False + trailing_break = False + break_space = False + space_break = False + + # Check document indicators. + if scalar.startswith('---') or scalar.startswith('...'): + block_indicators = True + flow_indicators = True + + # First character or preceded by a whitespace. + preceded_by_whitespace = True + + # Last character or followed by a whitespace. + followed_by_whitespace = (len(scalar) == 1 or + scalar[1] in '\0 \t\r\n\x85\u2028\u2029') + + # The previous character is a space. + previous_space = False + + # The previous character is a break. + previous_break = False + + index = 0 + while index < len(scalar): + ch = scalar[index] + + # Check for indicators. + if index == 0: + # Leading indicators are special characters. + if ch in '#,[]{}&*!|>\'\"%@`': + flow_indicators = True + block_indicators = True + if ch in '?:': + flow_indicators = True + if followed_by_whitespace: + block_indicators = True + if ch == '-' and followed_by_whitespace: + flow_indicators = True + block_indicators = True + else: + # Some indicators cannot appear within a scalar as well. + if ch in ',?[]{}': + flow_indicators = True + if ch == ':': + flow_indicators = True + if followed_by_whitespace: + block_indicators = True + if ch == '#' and preceded_by_whitespace: + flow_indicators = True + block_indicators = True + + # Check for line breaks, special, and unicode characters. + if ch in '\n\x85\u2028\u2029': + line_breaks = True + if not (ch == '\n' or '\x20' <= ch <= '\x7E'): + if (ch == '\x85' or '\xA0' <= ch <= '\uD7FF' + or '\uE000' <= ch <= '\uFFFD' + or '\U00010000' <= ch < '\U0010ffff') and ch != '\uFEFF': + unicode_characters = True + if not self.allow_unicode: + special_characters = True + else: + special_characters = True + + # Detect important whitespace combinations. + if ch == ' ': + if index == 0: + leading_space = True + if index == len(scalar)-1: + trailing_space = True + if previous_break: + break_space = True + previous_space = True + previous_break = False + elif ch in '\n\x85\u2028\u2029': + if index == 0: + leading_break = True + if index == len(scalar)-1: + trailing_break = True + if previous_space: + space_break = True + previous_space = False + previous_break = True + else: + previous_space = False + previous_break = False + + # Prepare for the next character. + index += 1 + preceded_by_whitespace = (ch in '\0 \t\r\n\x85\u2028\u2029') + followed_by_whitespace = (index+1 >= len(scalar) or + scalar[index+1] in '\0 \t\r\n\x85\u2028\u2029') + + # Let's decide what styles are allowed. + allow_flow_plain = True + allow_block_plain = True + allow_single_quoted = True + allow_double_quoted = True + allow_block = True + + # Leading and trailing whitespaces are bad for plain scalars. + if (leading_space or leading_break + or trailing_space or trailing_break): + allow_flow_plain = allow_block_plain = False + + # We do not permit trailing spaces for block scalars. + if trailing_space: + allow_block = False + + # Spaces at the beginning of a new line are only acceptable for block + # scalars. + if break_space: + allow_flow_plain = allow_block_plain = allow_single_quoted = False + + # Spaces followed by breaks, as well as special character are only + # allowed for double quoted scalars. + if space_break or special_characters: + allow_flow_plain = allow_block_plain = \ + allow_single_quoted = allow_block = False + + # Although the plain scalar writer supports breaks, we never emit + # multiline plain scalars. + if line_breaks: + allow_flow_plain = allow_block_plain = False + + # Flow indicators are forbidden for flow plain scalars. + if flow_indicators: + allow_flow_plain = False + + # Block indicators are forbidden for block plain scalars. + if block_indicators: + allow_block_plain = False + + return ScalarAnalysis(scalar=scalar, + empty=False, multiline=line_breaks, + allow_flow_plain=allow_flow_plain, + allow_block_plain=allow_block_plain, + allow_single_quoted=allow_single_quoted, + allow_double_quoted=allow_double_quoted, + allow_block=allow_block) + + # Writers. + + def flush_stream(self): + if hasattr(self.stream, 'flush'): + self.stream.flush() + + def write_stream_start(self): + # Write BOM if needed. + if self.encoding and self.encoding.startswith('utf-16'): + self.stream.write('\uFEFF'.encode(self.encoding)) + + def write_stream_end(self): + self.flush_stream() + + def write_indicator(self, indicator, need_whitespace, + whitespace=False, indention=False): + if self.whitespace or not need_whitespace: + data = indicator + else: + data = ' '+indicator + self.whitespace = whitespace + self.indention = self.indention and indention + self.column += len(data) + self.open_ended = False + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_indent(self): + indent = self.indent or 0 + if not self.indention or self.column > indent \ + or (self.column == indent and not self.whitespace): + self.write_line_break() + if self.column < indent: + self.whitespace = True + data = ' '*(indent-self.column) + self.column = indent + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_line_break(self, data=None): + if data is None: + data = self.best_line_break + self.whitespace = True + self.indention = True + self.line += 1 + self.column = 0 + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_version_directive(self, version_text): + data = '%%YAML %s' % version_text + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_line_break() + + def write_tag_directive(self, handle_text, prefix_text): + data = '%%TAG %s %s' % (handle_text, prefix_text) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_line_break() + + # Scalar streams. + + def write_single_quoted(self, text, split=True): + self.write_indicator('\'', True) + spaces = False + breaks = False + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if spaces: + if ch is None or ch != ' ': + if start+1 == end and self.column > self.best_width and split \ + and start != 0 and end != len(text): + self.write_indent() + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + elif breaks: + if ch is None or ch not in '\n\x85\u2028\u2029': + if text[start] == '\n': + self.write_line_break() + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + self.write_indent() + start = end + else: + if ch is None or ch in ' \n\x85\u2028\u2029' or ch == '\'': + if start < end: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch == '\'': + data = '\'\'' + self.column += 2 + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + 1 + if ch is not None: + spaces = (ch == ' ') + breaks = (ch in '\n\x85\u2028\u2029') + end += 1 + self.write_indicator('\'', False) + + ESCAPE_REPLACEMENTS = { + '\0': '0', + '\x07': 'a', + '\x08': 'b', + '\x09': 't', + '\x0A': 'n', + '\x0B': 'v', + '\x0C': 'f', + '\x0D': 'r', + '\x1B': 'e', + '\"': '\"', + '\\': '\\', + '\x85': 'N', + '\xA0': '_', + '\u2028': 'L', + '\u2029': 'P', + } + + def write_double_quoted(self, text, split=True): + self.write_indicator('"', True) + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if ch is None or ch in '"\\\x85\u2028\u2029\uFEFF' \ + or not ('\x20' <= ch <= '\x7E' + or (self.allow_unicode + and ('\xA0' <= ch <= '\uD7FF' + or '\uE000' <= ch <= '\uFFFD'))): + if start < end: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch is not None: + if ch in self.ESCAPE_REPLACEMENTS: + data = '\\'+self.ESCAPE_REPLACEMENTS[ch] + elif ch <= '\xFF': + data = '\\x%02X' % ord(ch) + elif ch <= '\uFFFF': + data = '\\u%04X' % ord(ch) + else: + data = '\\U%08X' % ord(ch) + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end+1 + if 0 < end < len(text)-1 and (ch == ' ' or start >= end) \ + and self.column+(end-start) > self.best_width and split: + data = text[start:end]+'\\' + if start < end: + start = end + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_indent() + self.whitespace = False + self.indention = False + if text[start] == ' ': + data = '\\' + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + end += 1 + self.write_indicator('"', False) + + def determine_block_hints(self, text): + hints = '' + if text: + if text[0] in ' \n\x85\u2028\u2029': + hints += str(self.best_indent) + if text[-1] not in '\n\x85\u2028\u2029': + hints += '-' + elif len(text) == 1 or text[-2] in '\n\x85\u2028\u2029': + hints += '+' + return hints + + def write_folded(self, text): + hints = self.determine_block_hints(text) + self.write_indicator('>'+hints, True) + if hints[-1:] == '+': + self.open_ended = True + self.write_line_break() + leading_space = True + spaces = False + breaks = True + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if breaks: + if ch is None or ch not in '\n\x85\u2028\u2029': + if not leading_space and ch is not None and ch != ' ' \ + and text[start] == '\n': + self.write_line_break() + leading_space = (ch == ' ') + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + if ch is not None: + self.write_indent() + start = end + elif spaces: + if ch != ' ': + if start+1 == end and self.column > self.best_width: + self.write_indent() + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + else: + if ch is None or ch in ' \n\x85\u2028\u2029': + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + if ch is None: + self.write_line_break() + start = end + if ch is not None: + breaks = (ch in '\n\x85\u2028\u2029') + spaces = (ch == ' ') + end += 1 + + def write_literal(self, text): + hints = self.determine_block_hints(text) + self.write_indicator('|'+hints, True) + if hints[-1:] == '+': + self.open_ended = True + self.write_line_break() + breaks = True + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if breaks: + if ch is None or ch not in '\n\x85\u2028\u2029': + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + if ch is not None: + self.write_indent() + start = end + else: + if ch is None or ch in '\n\x85\u2028\u2029': + data = text[start:end] + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + if ch is None: + self.write_line_break() + start = end + if ch is not None: + breaks = (ch in '\n\x85\u2028\u2029') + end += 1 + + def write_plain(self, text, split=True): + if self.root_context: + self.open_ended = True + if not text: + return + if not self.whitespace: + data = ' ' + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.whitespace = False + self.indention = False + spaces = False + breaks = False + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if spaces: + if ch != ' ': + if start+1 == end and self.column > self.best_width and split: + self.write_indent() + self.whitespace = False + self.indention = False + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + elif breaks: + if ch not in '\n\x85\u2028\u2029': + if text[start] == '\n': + self.write_line_break() + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + self.write_indent() + self.whitespace = False + self.indention = False + start = end + else: + if ch is None or ch in ' \n\x85\u2028\u2029': + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch is not None: + spaces = (ch == ' ') + breaks = (ch in '\n\x85\u2028\u2029') + end += 1 diff --git a/coredns/venv/yaml/error.py b/coredns/venv/yaml/error.py new file mode 100644 index 0000000..b796b4d --- /dev/null +++ b/coredns/venv/yaml/error.py @@ -0,0 +1,75 @@ + +__all__ = ['Mark', 'YAMLError', 'MarkedYAMLError'] + +class Mark: + + def __init__(self, name, index, line, column, buffer, pointer): + self.name = name + self.index = index + self.line = line + self.column = column + self.buffer = buffer + self.pointer = pointer + + def get_snippet(self, indent=4, max_length=75): + if self.buffer is None: + return None + head = '' + start = self.pointer + while start > 0 and self.buffer[start-1] not in '\0\r\n\x85\u2028\u2029': + start -= 1 + if self.pointer-start > max_length/2-1: + head = ' ... ' + start += 5 + break + tail = '' + end = self.pointer + while end < len(self.buffer) and self.buffer[end] not in '\0\r\n\x85\u2028\u2029': + end += 1 + if end-self.pointer > max_length/2-1: + tail = ' ... ' + end -= 5 + break + snippet = self.buffer[start:end] + return ' '*indent + head + snippet + tail + '\n' \ + + ' '*(indent+self.pointer-start+len(head)) + '^' + + def __str__(self): + snippet = self.get_snippet() + where = " in \"%s\", line %d, column %d" \ + % (self.name, self.line+1, self.column+1) + if snippet is not None: + where += ":\n"+snippet + return where + +class YAMLError(Exception): + pass + +class MarkedYAMLError(YAMLError): + + def __init__(self, context=None, context_mark=None, + problem=None, problem_mark=None, note=None): + self.context = context + self.context_mark = context_mark + self.problem = problem + self.problem_mark = problem_mark + self.note = note + + def __str__(self): + lines = [] + if self.context is not None: + lines.append(self.context) + if self.context_mark is not None \ + and (self.problem is None or self.problem_mark is None + or self.context_mark.name != self.problem_mark.name + or self.context_mark.line != self.problem_mark.line + or self.context_mark.column != self.problem_mark.column): + lines.append(str(self.context_mark)) + if self.problem is not None: + lines.append(self.problem) + if self.problem_mark is not None: + lines.append(str(self.problem_mark)) + if self.note is not None: + lines.append(self.note) + return '\n'.join(lines) + diff --git a/coredns/venv/yaml/events.py b/coredns/venv/yaml/events.py new file mode 100644 index 0000000..f79ad38 --- /dev/null +++ b/coredns/venv/yaml/events.py @@ -0,0 +1,86 @@ + +# Abstract classes. + +class Event(object): + def __init__(self, start_mark=None, end_mark=None): + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + attributes = [key for key in ['anchor', 'tag', 'implicit', 'value'] + if hasattr(self, key)] + arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) + for key in attributes]) + return '%s(%s)' % (self.__class__.__name__, arguments) + +class NodeEvent(Event): + def __init__(self, anchor, start_mark=None, end_mark=None): + self.anchor = anchor + self.start_mark = start_mark + self.end_mark = end_mark + +class CollectionStartEvent(NodeEvent): + def __init__(self, anchor, tag, implicit, start_mark=None, end_mark=None, + flow_style=None): + self.anchor = anchor + self.tag = tag + self.implicit = implicit + self.start_mark = start_mark + self.end_mark = end_mark + self.flow_style = flow_style + +class CollectionEndEvent(Event): + pass + +# Implementations. + +class StreamStartEvent(Event): + def __init__(self, start_mark=None, end_mark=None, encoding=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.encoding = encoding + +class StreamEndEvent(Event): + pass + +class DocumentStartEvent(Event): + def __init__(self, start_mark=None, end_mark=None, + explicit=None, version=None, tags=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.explicit = explicit + self.version = version + self.tags = tags + +class DocumentEndEvent(Event): + def __init__(self, start_mark=None, end_mark=None, + explicit=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.explicit = explicit + +class AliasEvent(NodeEvent): + pass + +class ScalarEvent(NodeEvent): + def __init__(self, anchor, tag, implicit, value, + start_mark=None, end_mark=None, style=None): + self.anchor = anchor + self.tag = tag + self.implicit = implicit + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + +class SequenceStartEvent(CollectionStartEvent): + pass + +class SequenceEndEvent(CollectionEndEvent): + pass + +class MappingStartEvent(CollectionStartEvent): + pass + +class MappingEndEvent(CollectionEndEvent): + pass + diff --git a/coredns/venv/yaml/loader.py b/coredns/venv/yaml/loader.py new file mode 100644 index 0000000..e90c112 --- /dev/null +++ b/coredns/venv/yaml/loader.py @@ -0,0 +1,63 @@ + +__all__ = ['BaseLoader', 'FullLoader', 'SafeLoader', 'Loader', 'UnsafeLoader'] + +from .reader import * +from .scanner import * +from .parser import * +from .composer import * +from .constructor import * +from .resolver import * + +class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, BaseResolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + BaseConstructor.__init__(self) + BaseResolver.__init__(self) + +class FullLoader(Reader, Scanner, Parser, Composer, FullConstructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + FullConstructor.__init__(self) + Resolver.__init__(self) + +class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + SafeConstructor.__init__(self) + Resolver.__init__(self) + +class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + Constructor.__init__(self) + Resolver.__init__(self) + +# UnsafeLoader is the same as Loader (which is and was always unsafe on +# untrusted input). Use of either Loader or UnsafeLoader should be rare, since +# FullLoad should be able to load almost all YAML safely. Loader is left intact +# to ensure backwards compatibility. +class UnsafeLoader(Reader, Scanner, Parser, Composer, Constructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + Constructor.__init__(self) + Resolver.__init__(self) diff --git a/coredns/venv/yaml/nodes.py b/coredns/venv/yaml/nodes.py new file mode 100644 index 0000000..c4f070c --- /dev/null +++ b/coredns/venv/yaml/nodes.py @@ -0,0 +1,49 @@ + +class Node(object): + def __init__(self, tag, value, start_mark, end_mark): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + value = self.value + #if isinstance(value, list): + # if len(value) == 0: + # value = '' + # elif len(value) == 1: + # value = '<1 item>' + # else: + # value = '<%d items>' % len(value) + #else: + # if len(value) > 75: + # value = repr(value[:70]+u' ... ') + # else: + # value = repr(value) + value = repr(value) + return '%s(tag=%r, value=%s)' % (self.__class__.__name__, self.tag, value) + +class ScalarNode(Node): + id = 'scalar' + def __init__(self, tag, value, + start_mark=None, end_mark=None, style=None): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + +class CollectionNode(Node): + def __init__(self, tag, value, + start_mark=None, end_mark=None, flow_style=None): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.flow_style = flow_style + +class SequenceNode(CollectionNode): + id = 'sequence' + +class MappingNode(CollectionNode): + id = 'mapping' + diff --git a/coredns/venv/yaml/parser.py b/coredns/venv/yaml/parser.py new file mode 100644 index 0000000..13a5995 --- /dev/null +++ b/coredns/venv/yaml/parser.py @@ -0,0 +1,589 @@ + +# The following YAML grammar is LL(1) and is parsed by a recursive descent +# parser. +# +# stream ::= STREAM-START implicit_document? explicit_document* STREAM-END +# implicit_document ::= block_node DOCUMENT-END* +# explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +# block_node_or_indentless_sequence ::= +# ALIAS +# | properties (block_content | indentless_block_sequence)? +# | block_content +# | indentless_block_sequence +# block_node ::= ALIAS +# | properties block_content? +# | block_content +# flow_node ::= ALIAS +# | properties flow_content? +# | flow_content +# properties ::= TAG ANCHOR? | ANCHOR TAG? +# block_content ::= block_collection | flow_collection | SCALAR +# flow_content ::= flow_collection | SCALAR +# block_collection ::= block_sequence | block_mapping +# flow_collection ::= flow_sequence | flow_mapping +# block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END +# indentless_sequence ::= (BLOCK-ENTRY block_node?)+ +# block_mapping ::= BLOCK-MAPPING_START +# ((KEY block_node_or_indentless_sequence?)? +# (VALUE block_node_or_indentless_sequence?)?)* +# BLOCK-END +# flow_sequence ::= FLOW-SEQUENCE-START +# (flow_sequence_entry FLOW-ENTRY)* +# flow_sequence_entry? +# FLOW-SEQUENCE-END +# flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +# flow_mapping ::= FLOW-MAPPING-START +# (flow_mapping_entry FLOW-ENTRY)* +# flow_mapping_entry? +# FLOW-MAPPING-END +# flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +# +# FIRST sets: +# +# stream: { STREAM-START } +# explicit_document: { DIRECTIVE DOCUMENT-START } +# implicit_document: FIRST(block_node) +# block_node: { ALIAS TAG ANCHOR SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START } +# flow_node: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START } +# block_content: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } +# flow_content: { FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } +# block_collection: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START } +# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } +# block_sequence: { BLOCK-SEQUENCE-START } +# block_mapping: { BLOCK-MAPPING-START } +# block_node_or_indentless_sequence: { ALIAS ANCHOR TAG SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START BLOCK-ENTRY } +# indentless_sequence: { ENTRY } +# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } +# flow_sequence: { FLOW-SEQUENCE-START } +# flow_mapping: { FLOW-MAPPING-START } +# flow_sequence_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } +# flow_mapping_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } + +__all__ = ['Parser', 'ParserError'] + +from .error import MarkedYAMLError +from .tokens import * +from .events import * +from .scanner import * + +class ParserError(MarkedYAMLError): + pass + +class Parser: + # Since writing a recursive-descendant parser is a straightforward task, we + # do not give many comments here. + + DEFAULT_TAGS = { + '!': '!', + '!!': 'tag:yaml.org,2002:', + } + + def __init__(self): + self.current_event = None + self.yaml_version = None + self.tag_handles = {} + self.states = [] + self.marks = [] + self.state = self.parse_stream_start + + def dispose(self): + # Reset the state attributes (to clear self-references) + self.states = [] + self.state = None + + def check_event(self, *choices): + # Check the type of the next event. + if self.current_event is None: + if self.state: + self.current_event = self.state() + if self.current_event is not None: + if not choices: + return True + for choice in choices: + if isinstance(self.current_event, choice): + return True + return False + + def peek_event(self): + # Get the next event. + if self.current_event is None: + if self.state: + self.current_event = self.state() + return self.current_event + + def get_event(self): + # Get the next event and proceed further. + if self.current_event is None: + if self.state: + self.current_event = self.state() + value = self.current_event + self.current_event = None + return value + + # stream ::= STREAM-START implicit_document? explicit_document* STREAM-END + # implicit_document ::= block_node DOCUMENT-END* + # explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* + + def parse_stream_start(self): + + # Parse the stream start. + token = self.get_token() + event = StreamStartEvent(token.start_mark, token.end_mark, + encoding=token.encoding) + + # Prepare the next state. + self.state = self.parse_implicit_document_start + + return event + + def parse_implicit_document_start(self): + + # Parse an implicit document. + if not self.check_token(DirectiveToken, DocumentStartToken, + StreamEndToken): + self.tag_handles = self.DEFAULT_TAGS + token = self.peek_token() + start_mark = end_mark = token.start_mark + event = DocumentStartEvent(start_mark, end_mark, + explicit=False) + + # Prepare the next state. + self.states.append(self.parse_document_end) + self.state = self.parse_block_node + + return event + + else: + return self.parse_document_start() + + def parse_document_start(self): + + # Parse any extra document end indicators. + while self.check_token(DocumentEndToken): + self.get_token() + + # Parse an explicit document. + if not self.check_token(StreamEndToken): + token = self.peek_token() + start_mark = token.start_mark + version, tags = self.process_directives() + if not self.check_token(DocumentStartToken): + raise ParserError(None, None, + "expected '', but found %r" + % self.peek_token().id, + self.peek_token().start_mark) + token = self.get_token() + end_mark = token.end_mark + event = DocumentStartEvent(start_mark, end_mark, + explicit=True, version=version, tags=tags) + self.states.append(self.parse_document_end) + self.state = self.parse_document_content + else: + # Parse the end of the stream. + token = self.get_token() + event = StreamEndEvent(token.start_mark, token.end_mark) + assert not self.states + assert not self.marks + self.state = None + return event + + def parse_document_end(self): + + # Parse the document end. + token = self.peek_token() + start_mark = end_mark = token.start_mark + explicit = False + if self.check_token(DocumentEndToken): + token = self.get_token() + end_mark = token.end_mark + explicit = True + event = DocumentEndEvent(start_mark, end_mark, + explicit=explicit) + + # Prepare the next state. + self.state = self.parse_document_start + + return event + + def parse_document_content(self): + if self.check_token(DirectiveToken, + DocumentStartToken, DocumentEndToken, StreamEndToken): + event = self.process_empty_scalar(self.peek_token().start_mark) + self.state = self.states.pop() + return event + else: + return self.parse_block_node() + + def process_directives(self): + self.yaml_version = None + self.tag_handles = {} + while self.check_token(DirectiveToken): + token = self.get_token() + if token.name == 'YAML': + if self.yaml_version is not None: + raise ParserError(None, None, + "found duplicate YAML directive", token.start_mark) + major, minor = token.value + if major != 1: + raise ParserError(None, None, + "found incompatible YAML document (version 1.* is required)", + token.start_mark) + self.yaml_version = token.value + elif token.name == 'TAG': + handle, prefix = token.value + if handle in self.tag_handles: + raise ParserError(None, None, + "duplicate tag handle %r" % handle, + token.start_mark) + self.tag_handles[handle] = prefix + if self.tag_handles: + value = self.yaml_version, self.tag_handles.copy() + else: + value = self.yaml_version, None + for key in self.DEFAULT_TAGS: + if key not in self.tag_handles: + self.tag_handles[key] = self.DEFAULT_TAGS[key] + return value + + # block_node_or_indentless_sequence ::= ALIAS + # | properties (block_content | indentless_block_sequence)? + # | block_content + # | indentless_block_sequence + # block_node ::= ALIAS + # | properties block_content? + # | block_content + # flow_node ::= ALIAS + # | properties flow_content? + # | flow_content + # properties ::= TAG ANCHOR? | ANCHOR TAG? + # block_content ::= block_collection | flow_collection | SCALAR + # flow_content ::= flow_collection | SCALAR + # block_collection ::= block_sequence | block_mapping + # flow_collection ::= flow_sequence | flow_mapping + + def parse_block_node(self): + return self.parse_node(block=True) + + def parse_flow_node(self): + return self.parse_node() + + def parse_block_node_or_indentless_sequence(self): + return self.parse_node(block=True, indentless_sequence=True) + + def parse_node(self, block=False, indentless_sequence=False): + if self.check_token(AliasToken): + token = self.get_token() + event = AliasEvent(token.value, token.start_mark, token.end_mark) + self.state = self.states.pop() + else: + anchor = None + tag = None + start_mark = end_mark = tag_mark = None + if self.check_token(AnchorToken): + token = self.get_token() + start_mark = token.start_mark + end_mark = token.end_mark + anchor = token.value + if self.check_token(TagToken): + token = self.get_token() + tag_mark = token.start_mark + end_mark = token.end_mark + tag = token.value + elif self.check_token(TagToken): + token = self.get_token() + start_mark = tag_mark = token.start_mark + end_mark = token.end_mark + tag = token.value + if self.check_token(AnchorToken): + token = self.get_token() + end_mark = token.end_mark + anchor = token.value + if tag is not None: + handle, suffix = tag + if handle is not None: + if handle not in self.tag_handles: + raise ParserError("while parsing a node", start_mark, + "found undefined tag handle %r" % handle, + tag_mark) + tag = self.tag_handles[handle]+suffix + else: + tag = suffix + #if tag == '!': + # raise ParserError("while parsing a node", start_mark, + # "found non-specific tag '!'", tag_mark, + # "Please check 'http://pyyaml.org/wiki/YAMLNonSpecificTag' and share your opinion.") + if start_mark is None: + start_mark = end_mark = self.peek_token().start_mark + event = None + implicit = (tag is None or tag == '!') + if indentless_sequence and self.check_token(BlockEntryToken): + end_mark = self.peek_token().end_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark) + self.state = self.parse_indentless_sequence_entry + else: + if self.check_token(ScalarToken): + token = self.get_token() + end_mark = token.end_mark + if (token.plain and tag is None) or tag == '!': + implicit = (True, False) + elif tag is None: + implicit = (False, True) + else: + implicit = (False, False) + event = ScalarEvent(anchor, tag, implicit, token.value, + start_mark, end_mark, style=token.style) + self.state = self.states.pop() + elif self.check_token(FlowSequenceStartToken): + end_mark = self.peek_token().end_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=True) + self.state = self.parse_flow_sequence_first_entry + elif self.check_token(FlowMappingStartToken): + end_mark = self.peek_token().end_mark + event = MappingStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=True) + self.state = self.parse_flow_mapping_first_key + elif block and self.check_token(BlockSequenceStartToken): + end_mark = self.peek_token().start_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=False) + self.state = self.parse_block_sequence_first_entry + elif block and self.check_token(BlockMappingStartToken): + end_mark = self.peek_token().start_mark + event = MappingStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=False) + self.state = self.parse_block_mapping_first_key + elif anchor is not None or tag is not None: + # Empty scalars are allowed even if a tag or an anchor is + # specified. + event = ScalarEvent(anchor, tag, (implicit, False), '', + start_mark, end_mark) + self.state = self.states.pop() + else: + if block: + node = 'block' + else: + node = 'flow' + token = self.peek_token() + raise ParserError("while parsing a %s node" % node, start_mark, + "expected the node content, but found %r" % token.id, + token.start_mark) + return event + + # block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END + + def parse_block_sequence_first_entry(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_block_sequence_entry() + + def parse_block_sequence_entry(self): + if self.check_token(BlockEntryToken): + token = self.get_token() + if not self.check_token(BlockEntryToken, BlockEndToken): + self.states.append(self.parse_block_sequence_entry) + return self.parse_block_node() + else: + self.state = self.parse_block_sequence_entry + return self.process_empty_scalar(token.end_mark) + if not self.check_token(BlockEndToken): + token = self.peek_token() + raise ParserError("while parsing a block collection", self.marks[-1], + "expected , but found %r" % token.id, token.start_mark) + token = self.get_token() + event = SequenceEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + # indentless_sequence ::= (BLOCK-ENTRY block_node?)+ + + def parse_indentless_sequence_entry(self): + if self.check_token(BlockEntryToken): + token = self.get_token() + if not self.check_token(BlockEntryToken, + KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_indentless_sequence_entry) + return self.parse_block_node() + else: + self.state = self.parse_indentless_sequence_entry + return self.process_empty_scalar(token.end_mark) + token = self.peek_token() + event = SequenceEndEvent(token.start_mark, token.start_mark) + self.state = self.states.pop() + return event + + # block_mapping ::= BLOCK-MAPPING_START + # ((KEY block_node_or_indentless_sequence?)? + # (VALUE block_node_or_indentless_sequence?)?)* + # BLOCK-END + + def parse_block_mapping_first_key(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_block_mapping_key() + + def parse_block_mapping_key(self): + if self.check_token(KeyToken): + token = self.get_token() + if not self.check_token(KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_block_mapping_value) + return self.parse_block_node_or_indentless_sequence() + else: + self.state = self.parse_block_mapping_value + return self.process_empty_scalar(token.end_mark) + if not self.check_token(BlockEndToken): + token = self.peek_token() + raise ParserError("while parsing a block mapping", self.marks[-1], + "expected , but found %r" % token.id, token.start_mark) + token = self.get_token() + event = MappingEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_block_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_block_mapping_key) + return self.parse_block_node_or_indentless_sequence() + else: + self.state = self.parse_block_mapping_key + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_block_mapping_key + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + # flow_sequence ::= FLOW-SEQUENCE-START + # (flow_sequence_entry FLOW-ENTRY)* + # flow_sequence_entry? + # FLOW-SEQUENCE-END + # flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + # + # Note that while production rules for both flow_sequence_entry and + # flow_mapping_entry are equal, their interpretations are different. + # For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?` + # generate an inline mapping (set syntax). + + def parse_flow_sequence_first_entry(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_flow_sequence_entry(first=True) + + def parse_flow_sequence_entry(self, first=False): + if not self.check_token(FlowSequenceEndToken): + if not first: + if self.check_token(FlowEntryToken): + self.get_token() + else: + token = self.peek_token() + raise ParserError("while parsing a flow sequence", self.marks[-1], + "expected ',' or ']', but got %r" % token.id, token.start_mark) + + if self.check_token(KeyToken): + token = self.peek_token() + event = MappingStartEvent(None, None, True, + token.start_mark, token.end_mark, + flow_style=True) + self.state = self.parse_flow_sequence_entry_mapping_key + return event + elif not self.check_token(FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry) + return self.parse_flow_node() + token = self.get_token() + event = SequenceEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_flow_sequence_entry_mapping_key(self): + token = self.get_token() + if not self.check_token(ValueToken, + FlowEntryToken, FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry_mapping_value) + return self.parse_flow_node() + else: + self.state = self.parse_flow_sequence_entry_mapping_value + return self.process_empty_scalar(token.end_mark) + + def parse_flow_sequence_entry_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(FlowEntryToken, FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry_mapping_end) + return self.parse_flow_node() + else: + self.state = self.parse_flow_sequence_entry_mapping_end + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_flow_sequence_entry_mapping_end + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + def parse_flow_sequence_entry_mapping_end(self): + self.state = self.parse_flow_sequence_entry + token = self.peek_token() + return MappingEndEvent(token.start_mark, token.start_mark) + + # flow_mapping ::= FLOW-MAPPING-START + # (flow_mapping_entry FLOW-ENTRY)* + # flow_mapping_entry? + # FLOW-MAPPING-END + # flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + + def parse_flow_mapping_first_key(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_flow_mapping_key(first=True) + + def parse_flow_mapping_key(self, first=False): + if not self.check_token(FlowMappingEndToken): + if not first: + if self.check_token(FlowEntryToken): + self.get_token() + else: + token = self.peek_token() + raise ParserError("while parsing a flow mapping", self.marks[-1], + "expected ',' or '}', but got %r" % token.id, token.start_mark) + if self.check_token(KeyToken): + token = self.get_token() + if not self.check_token(ValueToken, + FlowEntryToken, FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_value) + return self.parse_flow_node() + else: + self.state = self.parse_flow_mapping_value + return self.process_empty_scalar(token.end_mark) + elif not self.check_token(FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_empty_value) + return self.parse_flow_node() + token = self.get_token() + event = MappingEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_flow_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(FlowEntryToken, FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_key) + return self.parse_flow_node() + else: + self.state = self.parse_flow_mapping_key + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_flow_mapping_key + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + def parse_flow_mapping_empty_value(self): + self.state = self.parse_flow_mapping_key + return self.process_empty_scalar(self.peek_token().start_mark) + + def process_empty_scalar(self, mark): + return ScalarEvent(None, None, (True, False), '', mark, mark) + diff --git a/coredns/venv/yaml/reader.py b/coredns/venv/yaml/reader.py new file mode 100644 index 0000000..774b021 --- /dev/null +++ b/coredns/venv/yaml/reader.py @@ -0,0 +1,185 @@ +# This module contains abstractions for the input stream. You don't have to +# looks further, there are no pretty code. +# +# We define two classes here. +# +# Mark(source, line, column) +# It's just a record and its only use is producing nice error messages. +# Parser does not use it for any other purposes. +# +# Reader(source, data) +# Reader determines the encoding of `data` and converts it to unicode. +# Reader provides the following methods and attributes: +# reader.peek(length=1) - return the next `length` characters +# reader.forward(length=1) - move the current position to `length` characters. +# reader.index - the number of the current character. +# reader.line, stream.column - the line and the column of the current character. + +__all__ = ['Reader', 'ReaderError'] + +from .error import YAMLError, Mark + +import codecs, re + +class ReaderError(YAMLError): + + def __init__(self, name, position, character, encoding, reason): + self.name = name + self.character = character + self.position = position + self.encoding = encoding + self.reason = reason + + def __str__(self): + if isinstance(self.character, bytes): + return "'%s' codec can't decode byte #x%02x: %s\n" \ + " in \"%s\", position %d" \ + % (self.encoding, ord(self.character), self.reason, + self.name, self.position) + else: + return "unacceptable character #x%04x: %s\n" \ + " in \"%s\", position %d" \ + % (self.character, self.reason, + self.name, self.position) + +class Reader(object): + # Reader: + # - determines the data encoding and converts it to a unicode string, + # - checks if characters are in allowed range, + # - adds '\0' to the end. + + # Reader accepts + # - a `bytes` object, + # - a `str` object, + # - a file-like object with its `read` method returning `str`, + # - a file-like object with its `read` method returning `unicode`. + + # Yeah, it's ugly and slow. + + def __init__(self, stream): + self.name = None + self.stream = None + self.stream_pointer = 0 + self.eof = True + self.buffer = '' + self.pointer = 0 + self.raw_buffer = None + self.raw_decode = None + self.encoding = None + self.index = 0 + self.line = 0 + self.column = 0 + if isinstance(stream, str): + self.name = "" + self.check_printable(stream) + self.buffer = stream+'\0' + elif isinstance(stream, bytes): + self.name = "" + self.raw_buffer = stream + self.determine_encoding() + else: + self.stream = stream + self.name = getattr(stream, 'name', "") + self.eof = False + self.raw_buffer = None + self.determine_encoding() + + def peek(self, index=0): + try: + return self.buffer[self.pointer+index] + except IndexError: + self.update(index+1) + return self.buffer[self.pointer+index] + + def prefix(self, length=1): + if self.pointer+length >= len(self.buffer): + self.update(length) + return self.buffer[self.pointer:self.pointer+length] + + def forward(self, length=1): + if self.pointer+length+1 >= len(self.buffer): + self.update(length+1) + while length: + ch = self.buffer[self.pointer] + self.pointer += 1 + self.index += 1 + if ch in '\n\x85\u2028\u2029' \ + or (ch == '\r' and self.buffer[self.pointer] != '\n'): + self.line += 1 + self.column = 0 + elif ch != '\uFEFF': + self.column += 1 + length -= 1 + + def get_mark(self): + if self.stream is None: + return Mark(self.name, self.index, self.line, self.column, + self.buffer, self.pointer) + else: + return Mark(self.name, self.index, self.line, self.column, + None, None) + + def determine_encoding(self): + while not self.eof and (self.raw_buffer is None or len(self.raw_buffer) < 2): + self.update_raw() + if isinstance(self.raw_buffer, bytes): + if self.raw_buffer.startswith(codecs.BOM_UTF16_LE): + self.raw_decode = codecs.utf_16_le_decode + self.encoding = 'utf-16-le' + elif self.raw_buffer.startswith(codecs.BOM_UTF16_BE): + self.raw_decode = codecs.utf_16_be_decode + self.encoding = 'utf-16-be' + else: + self.raw_decode = codecs.utf_8_decode + self.encoding = 'utf-8' + self.update(1) + + NON_PRINTABLE = re.compile('[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\uD7FF\uE000-\uFFFD\U00010000-\U0010ffff]') + def check_printable(self, data): + match = self.NON_PRINTABLE.search(data) + if match: + character = match.group() + position = self.index+(len(self.buffer)-self.pointer)+match.start() + raise ReaderError(self.name, position, ord(character), + 'unicode', "special characters are not allowed") + + def update(self, length): + if self.raw_buffer is None: + return + self.buffer = self.buffer[self.pointer:] + self.pointer = 0 + while len(self.buffer) < length: + if not self.eof: + self.update_raw() + if self.raw_decode is not None: + try: + data, converted = self.raw_decode(self.raw_buffer, + 'strict', self.eof) + except UnicodeDecodeError as exc: + character = self.raw_buffer[exc.start] + if self.stream is not None: + position = self.stream_pointer-len(self.raw_buffer)+exc.start + else: + position = exc.start + raise ReaderError(self.name, position, character, + exc.encoding, exc.reason) + else: + data = self.raw_buffer + converted = len(data) + self.check_printable(data) + self.buffer += data + self.raw_buffer = self.raw_buffer[converted:] + if self.eof: + self.buffer += '\0' + self.raw_buffer = None + break + + def update_raw(self, size=4096): + data = self.stream.read(size) + if self.raw_buffer is None: + self.raw_buffer = data + else: + self.raw_buffer += data + self.stream_pointer += len(data) + if not data: + self.eof = True diff --git a/coredns/venv/yaml/representer.py b/coredns/venv/yaml/representer.py new file mode 100644 index 0000000..3b0b192 --- /dev/null +++ b/coredns/venv/yaml/representer.py @@ -0,0 +1,389 @@ + +__all__ = ['BaseRepresenter', 'SafeRepresenter', 'Representer', + 'RepresenterError'] + +from .error import * +from .nodes import * + +import datetime, copyreg, types, base64, collections + +class RepresenterError(YAMLError): + pass + +class BaseRepresenter: + + yaml_representers = {} + yaml_multi_representers = {} + + def __init__(self, default_style=None, default_flow_style=False, sort_keys=True): + self.default_style = default_style + self.sort_keys = sort_keys + self.default_flow_style = default_flow_style + self.represented_objects = {} + self.object_keeper = [] + self.alias_key = None + + def represent(self, data): + node = self.represent_data(data) + self.serialize(node) + self.represented_objects = {} + self.object_keeper = [] + self.alias_key = None + + def represent_data(self, data): + if self.ignore_aliases(data): + self.alias_key = None + else: + self.alias_key = id(data) + if self.alias_key is not None: + if self.alias_key in self.represented_objects: + node = self.represented_objects[self.alias_key] + #if node is None: + # raise RepresenterError("recursive objects are not allowed: %r" % data) + return node + #self.represented_objects[alias_key] = None + self.object_keeper.append(data) + data_types = type(data).__mro__ + if data_types[0] in self.yaml_representers: + node = self.yaml_representers[data_types[0]](self, data) + else: + for data_type in data_types: + if data_type in self.yaml_multi_representers: + node = self.yaml_multi_representers[data_type](self, data) + break + else: + if None in self.yaml_multi_representers: + node = self.yaml_multi_representers[None](self, data) + elif None in self.yaml_representers: + node = self.yaml_representers[None](self, data) + else: + node = ScalarNode(None, str(data)) + #if alias_key is not None: + # self.represented_objects[alias_key] = node + return node + + @classmethod + def add_representer(cls, data_type, representer): + if not 'yaml_representers' in cls.__dict__: + cls.yaml_representers = cls.yaml_representers.copy() + cls.yaml_representers[data_type] = representer + + @classmethod + def add_multi_representer(cls, data_type, representer): + if not 'yaml_multi_representers' in cls.__dict__: + cls.yaml_multi_representers = cls.yaml_multi_representers.copy() + cls.yaml_multi_representers[data_type] = representer + + def represent_scalar(self, tag, value, style=None): + if style is None: + style = self.default_style + node = ScalarNode(tag, value, style=style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + return node + + def represent_sequence(self, tag, sequence, flow_style=None): + value = [] + node = SequenceNode(tag, value, flow_style=flow_style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + best_style = True + for item in sequence: + node_item = self.represent_data(item) + if not (isinstance(node_item, ScalarNode) and not node_item.style): + best_style = False + value.append(node_item) + if flow_style is None: + if self.default_flow_style is not None: + node.flow_style = self.default_flow_style + else: + node.flow_style = best_style + return node + + def represent_mapping(self, tag, mapping, flow_style=None): + value = [] + node = MappingNode(tag, value, flow_style=flow_style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + best_style = True + if hasattr(mapping, 'items'): + mapping = list(mapping.items()) + if self.sort_keys: + try: + mapping = sorted(mapping) + except TypeError: + pass + for item_key, item_value in mapping: + node_key = self.represent_data(item_key) + node_value = self.represent_data(item_value) + if not (isinstance(node_key, ScalarNode) and not node_key.style): + best_style = False + if not (isinstance(node_value, ScalarNode) and not node_value.style): + best_style = False + value.append((node_key, node_value)) + if flow_style is None: + if self.default_flow_style is not None: + node.flow_style = self.default_flow_style + else: + node.flow_style = best_style + return node + + def ignore_aliases(self, data): + return False + +class SafeRepresenter(BaseRepresenter): + + def ignore_aliases(self, data): + if data is None: + return True + if isinstance(data, tuple) and data == (): + return True + if isinstance(data, (str, bytes, bool, int, float)): + return True + + def represent_none(self, data): + return self.represent_scalar('tag:yaml.org,2002:null', 'null') + + def represent_str(self, data): + return self.represent_scalar('tag:yaml.org,2002:str', data) + + def represent_binary(self, data): + if hasattr(base64, 'encodebytes'): + data = base64.encodebytes(data).decode('ascii') + else: + data = base64.encodestring(data).decode('ascii') + return self.represent_scalar('tag:yaml.org,2002:binary', data, style='|') + + def represent_bool(self, data): + if data: + value = 'true' + else: + value = 'false' + return self.represent_scalar('tag:yaml.org,2002:bool', value) + + def represent_int(self, data): + return self.represent_scalar('tag:yaml.org,2002:int', str(data)) + + inf_value = 1e300 + while repr(inf_value) != repr(inf_value*inf_value): + inf_value *= inf_value + + def represent_float(self, data): + if data != data or (data == 0.0 and data == 1.0): + value = '.nan' + elif data == self.inf_value: + value = '.inf' + elif data == -self.inf_value: + value = '-.inf' + else: + value = repr(data).lower() + # Note that in some cases `repr(data)` represents a float number + # without the decimal parts. For instance: + # >>> repr(1e17) + # '1e17' + # Unfortunately, this is not a valid float representation according + # to the definition of the `!!float` tag. We fix this by adding + # '.0' before the 'e' symbol. + if '.' not in value and 'e' in value: + value = value.replace('e', '.0e', 1) + return self.represent_scalar('tag:yaml.org,2002:float', value) + + def represent_list(self, data): + #pairs = (len(data) > 0 and isinstance(data, list)) + #if pairs: + # for item in data: + # if not isinstance(item, tuple) or len(item) != 2: + # pairs = False + # break + #if not pairs: + return self.represent_sequence('tag:yaml.org,2002:seq', data) + #value = [] + #for item_key, item_value in data: + # value.append(self.represent_mapping(u'tag:yaml.org,2002:map', + # [(item_key, item_value)])) + #return SequenceNode(u'tag:yaml.org,2002:pairs', value) + + def represent_dict(self, data): + return self.represent_mapping('tag:yaml.org,2002:map', data) + + def represent_set(self, data): + value = {} + for key in data: + value[key] = None + return self.represent_mapping('tag:yaml.org,2002:set', value) + + def represent_date(self, data): + value = data.isoformat() + return self.represent_scalar('tag:yaml.org,2002:timestamp', value) + + def represent_datetime(self, data): + value = data.isoformat(' ') + return self.represent_scalar('tag:yaml.org,2002:timestamp', value) + + def represent_yaml_object(self, tag, data, cls, flow_style=None): + if hasattr(data, '__getstate__'): + state = data.__getstate__() + else: + state = data.__dict__.copy() + return self.represent_mapping(tag, state, flow_style=flow_style) + + def represent_undefined(self, data): + raise RepresenterError("cannot represent an object", data) + +SafeRepresenter.add_representer(type(None), + SafeRepresenter.represent_none) + +SafeRepresenter.add_representer(str, + SafeRepresenter.represent_str) + +SafeRepresenter.add_representer(bytes, + SafeRepresenter.represent_binary) + +SafeRepresenter.add_representer(bool, + SafeRepresenter.represent_bool) + +SafeRepresenter.add_representer(int, + SafeRepresenter.represent_int) + +SafeRepresenter.add_representer(float, + SafeRepresenter.represent_float) + +SafeRepresenter.add_representer(list, + SafeRepresenter.represent_list) + +SafeRepresenter.add_representer(tuple, + SafeRepresenter.represent_list) + +SafeRepresenter.add_representer(dict, + SafeRepresenter.represent_dict) + +SafeRepresenter.add_representer(set, + SafeRepresenter.represent_set) + +SafeRepresenter.add_representer(datetime.date, + SafeRepresenter.represent_date) + +SafeRepresenter.add_representer(datetime.datetime, + SafeRepresenter.represent_datetime) + +SafeRepresenter.add_representer(None, + SafeRepresenter.represent_undefined) + +class Representer(SafeRepresenter): + + def represent_complex(self, data): + if data.imag == 0.0: + data = '%r' % data.real + elif data.real == 0.0: + data = '%rj' % data.imag + elif data.imag > 0: + data = '%r+%rj' % (data.real, data.imag) + else: + data = '%r%rj' % (data.real, data.imag) + return self.represent_scalar('tag:yaml.org,2002:python/complex', data) + + def represent_tuple(self, data): + return self.represent_sequence('tag:yaml.org,2002:python/tuple', data) + + def represent_name(self, data): + name = '%s.%s' % (data.__module__, data.__name__) + return self.represent_scalar('tag:yaml.org,2002:python/name:'+name, '') + + def represent_module(self, data): + return self.represent_scalar( + 'tag:yaml.org,2002:python/module:'+data.__name__, '') + + def represent_object(self, data): + # We use __reduce__ API to save the data. data.__reduce__ returns + # a tuple of length 2-5: + # (function, args, state, listitems, dictitems) + + # For reconstructing, we calls function(*args), then set its state, + # listitems, and dictitems if they are not None. + + # A special case is when function.__name__ == '__newobj__'. In this + # case we create the object with args[0].__new__(*args). + + # Another special case is when __reduce__ returns a string - we don't + # support it. + + # We produce a !!python/object, !!python/object/new or + # !!python/object/apply node. + + cls = type(data) + if cls in copyreg.dispatch_table: + reduce = copyreg.dispatch_table[cls](data) + elif hasattr(data, '__reduce_ex__'): + reduce = data.__reduce_ex__(2) + elif hasattr(data, '__reduce__'): + reduce = data.__reduce__() + else: + raise RepresenterError("cannot represent an object", data) + reduce = (list(reduce)+[None]*5)[:5] + function, args, state, listitems, dictitems = reduce + args = list(args) + if state is None: + state = {} + if listitems is not None: + listitems = list(listitems) + if dictitems is not None: + dictitems = dict(dictitems) + if function.__name__ == '__newobj__': + function = args[0] + args = args[1:] + tag = 'tag:yaml.org,2002:python/object/new:' + newobj = True + else: + tag = 'tag:yaml.org,2002:python/object/apply:' + newobj = False + function_name = '%s.%s' % (function.__module__, function.__name__) + if not args and not listitems and not dictitems \ + and isinstance(state, dict) and newobj: + return self.represent_mapping( + 'tag:yaml.org,2002:python/object:'+function_name, state) + if not listitems and not dictitems \ + and isinstance(state, dict) and not state: + return self.represent_sequence(tag+function_name, args) + value = {} + if args: + value['args'] = args + if state or not isinstance(state, dict): + value['state'] = state + if listitems: + value['listitems'] = listitems + if dictitems: + value['dictitems'] = dictitems + return self.represent_mapping(tag+function_name, value) + + def represent_ordered_dict(self, data): + # Provide uniform representation across different Python versions. + data_type = type(data) + tag = 'tag:yaml.org,2002:python/object/apply:%s.%s' \ + % (data_type.__module__, data_type.__name__) + items = [[key, value] for key, value in data.items()] + return self.represent_sequence(tag, [items]) + +Representer.add_representer(complex, + Representer.represent_complex) + +Representer.add_representer(tuple, + Representer.represent_tuple) + +Representer.add_representer(type, + Representer.represent_name) + +Representer.add_representer(collections.OrderedDict, + Representer.represent_ordered_dict) + +Representer.add_representer(types.FunctionType, + Representer.represent_name) + +Representer.add_representer(types.BuiltinFunctionType, + Representer.represent_name) + +Representer.add_representer(types.ModuleType, + Representer.represent_module) + +Representer.add_multi_representer(object, + Representer.represent_object) + diff --git a/coredns/venv/yaml/resolver.py b/coredns/venv/yaml/resolver.py new file mode 100644 index 0000000..02b82e7 --- /dev/null +++ b/coredns/venv/yaml/resolver.py @@ -0,0 +1,227 @@ + +__all__ = ['BaseResolver', 'Resolver'] + +from .error import * +from .nodes import * + +import re + +class ResolverError(YAMLError): + pass + +class BaseResolver: + + DEFAULT_SCALAR_TAG = 'tag:yaml.org,2002:str' + DEFAULT_SEQUENCE_TAG = 'tag:yaml.org,2002:seq' + DEFAULT_MAPPING_TAG = 'tag:yaml.org,2002:map' + + yaml_implicit_resolvers = {} + yaml_path_resolvers = {} + + def __init__(self): + self.resolver_exact_paths = [] + self.resolver_prefix_paths = [] + + @classmethod + def add_implicit_resolver(cls, tag, regexp, first): + if not 'yaml_implicit_resolvers' in cls.__dict__: + implicit_resolvers = {} + for key in cls.yaml_implicit_resolvers: + implicit_resolvers[key] = cls.yaml_implicit_resolvers[key][:] + cls.yaml_implicit_resolvers = implicit_resolvers + if first is None: + first = [None] + for ch in first: + cls.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp)) + + @classmethod + def add_path_resolver(cls, tag, path, kind=None): + # Note: `add_path_resolver` is experimental. The API could be changed. + # `new_path` is a pattern that is matched against the path from the + # root to the node that is being considered. `node_path` elements are + # tuples `(node_check, index_check)`. `node_check` is a node class: + # `ScalarNode`, `SequenceNode`, `MappingNode` or `None`. `None` + # matches any kind of a node. `index_check` could be `None`, a boolean + # value, a string value, or a number. `None` and `False` match against + # any _value_ of sequence and mapping nodes. `True` matches against + # any _key_ of a mapping node. A string `index_check` matches against + # a mapping value that corresponds to a scalar key which content is + # equal to the `index_check` value. An integer `index_check` matches + # against a sequence value with the index equal to `index_check`. + if not 'yaml_path_resolvers' in cls.__dict__: + cls.yaml_path_resolvers = cls.yaml_path_resolvers.copy() + new_path = [] + for element in path: + if isinstance(element, (list, tuple)): + if len(element) == 2: + node_check, index_check = element + elif len(element) == 1: + node_check = element[0] + index_check = True + else: + raise ResolverError("Invalid path element: %s" % element) + else: + node_check = None + index_check = element + if node_check is str: + node_check = ScalarNode + elif node_check is list: + node_check = SequenceNode + elif node_check is dict: + node_check = MappingNode + elif node_check not in [ScalarNode, SequenceNode, MappingNode] \ + and not isinstance(node_check, str) \ + and node_check is not None: + raise ResolverError("Invalid node checker: %s" % node_check) + if not isinstance(index_check, (str, int)) \ + and index_check is not None: + raise ResolverError("Invalid index checker: %s" % index_check) + new_path.append((node_check, index_check)) + if kind is str: + kind = ScalarNode + elif kind is list: + kind = SequenceNode + elif kind is dict: + kind = MappingNode + elif kind not in [ScalarNode, SequenceNode, MappingNode] \ + and kind is not None: + raise ResolverError("Invalid node kind: %s" % kind) + cls.yaml_path_resolvers[tuple(new_path), kind] = tag + + def descend_resolver(self, current_node, current_index): + if not self.yaml_path_resolvers: + return + exact_paths = {} + prefix_paths = [] + if current_node: + depth = len(self.resolver_prefix_paths) + for path, kind in self.resolver_prefix_paths[-1]: + if self.check_resolver_prefix(depth, path, kind, + current_node, current_index): + if len(path) > depth: + prefix_paths.append((path, kind)) + else: + exact_paths[kind] = self.yaml_path_resolvers[path, kind] + else: + for path, kind in self.yaml_path_resolvers: + if not path: + exact_paths[kind] = self.yaml_path_resolvers[path, kind] + else: + prefix_paths.append((path, kind)) + self.resolver_exact_paths.append(exact_paths) + self.resolver_prefix_paths.append(prefix_paths) + + def ascend_resolver(self): + if not self.yaml_path_resolvers: + return + self.resolver_exact_paths.pop() + self.resolver_prefix_paths.pop() + + def check_resolver_prefix(self, depth, path, kind, + current_node, current_index): + node_check, index_check = path[depth-1] + if isinstance(node_check, str): + if current_node.tag != node_check: + return + elif node_check is not None: + if not isinstance(current_node, node_check): + return + if index_check is True and current_index is not None: + return + if (index_check is False or index_check is None) \ + and current_index is None: + return + if isinstance(index_check, str): + if not (isinstance(current_index, ScalarNode) + and index_check == current_index.value): + return + elif isinstance(index_check, int) and not isinstance(index_check, bool): + if index_check != current_index: + return + return True + + def resolve(self, kind, value, implicit): + if kind is ScalarNode and implicit[0]: + if value == '': + resolvers = self.yaml_implicit_resolvers.get('', []) + else: + resolvers = self.yaml_implicit_resolvers.get(value[0], []) + resolvers += self.yaml_implicit_resolvers.get(None, []) + for tag, regexp in resolvers: + if regexp.match(value): + return tag + implicit = implicit[1] + if self.yaml_path_resolvers: + exact_paths = self.resolver_exact_paths[-1] + if kind in exact_paths: + return exact_paths[kind] + if None in exact_paths: + return exact_paths[None] + if kind is ScalarNode: + return self.DEFAULT_SCALAR_TAG + elif kind is SequenceNode: + return self.DEFAULT_SEQUENCE_TAG + elif kind is MappingNode: + return self.DEFAULT_MAPPING_TAG + +class Resolver(BaseResolver): + pass + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:bool', + re.compile(r'''^(?:yes|Yes|YES|no|No|NO + |true|True|TRUE|false|False|FALSE + |on|On|ON|off|Off|OFF)$''', re.X), + list('yYnNtTfFoO')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:float', + re.compile(r'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)? + |\.[0-9_]+(?:[eE][-+][0-9]+)? + |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]* + |[-+]?\.(?:inf|Inf|INF) + |\.(?:nan|NaN|NAN))$''', re.X), + list('-+0123456789.')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:int', + re.compile(r'''^(?:[-+]?0b[0-1_]+ + |[-+]?0[0-7_]+ + |[-+]?(?:0|[1-9][0-9_]*) + |[-+]?0x[0-9a-fA-F_]+ + |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X), + list('-+0123456789')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:merge', + re.compile(r'^(?:<<)$'), + ['<']) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:null', + re.compile(r'''^(?: ~ + |null|Null|NULL + | )$''', re.X), + ['~', 'n', 'N', '']) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:timestamp', + re.compile(r'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] + |[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]? + (?:[Tt]|[ \t]+)[0-9][0-9]? + :[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)? + (?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X), + list('0123456789')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:value', + re.compile(r'^(?:=)$'), + ['=']) + +# The following resolver is only for documentation purposes. It cannot work +# because plain scalars cannot start with '!', '&', or '*'. +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:yaml', + re.compile(r'^(?:!|&|\*)$'), + list('!&*')) + diff --git a/coredns/venv/yaml/scanner.py b/coredns/venv/yaml/scanner.py new file mode 100644 index 0000000..7437ede --- /dev/null +++ b/coredns/venv/yaml/scanner.py @@ -0,0 +1,1435 @@ + +# Scanner produces tokens of the following types: +# STREAM-START +# STREAM-END +# DIRECTIVE(name, value) +# DOCUMENT-START +# DOCUMENT-END +# BLOCK-SEQUENCE-START +# BLOCK-MAPPING-START +# BLOCK-END +# FLOW-SEQUENCE-START +# FLOW-MAPPING-START +# FLOW-SEQUENCE-END +# FLOW-MAPPING-END +# BLOCK-ENTRY +# FLOW-ENTRY +# KEY +# VALUE +# ALIAS(value) +# ANCHOR(value) +# TAG(value) +# SCALAR(value, plain, style) +# +# Read comments in the Scanner code for more details. +# + +__all__ = ['Scanner', 'ScannerError'] + +from .error import MarkedYAMLError +from .tokens import * + +class ScannerError(MarkedYAMLError): + pass + +class SimpleKey: + # See below simple keys treatment. + + def __init__(self, token_number, required, index, line, column, mark): + self.token_number = token_number + self.required = required + self.index = index + self.line = line + self.column = column + self.mark = mark + +class Scanner: + + def __init__(self): + """Initialize the scanner.""" + # It is assumed that Scanner and Reader will have a common descendant. + # Reader do the dirty work of checking for BOM and converting the + # input data to Unicode. It also adds NUL to the end. + # + # Reader supports the following methods + # self.peek(i=0) # peek the next i-th character + # self.prefix(l=1) # peek the next l characters + # self.forward(l=1) # read the next l characters and move the pointer. + + # Had we reached the end of the stream? + self.done = False + + # The number of unclosed '{' and '['. `flow_level == 0` means block + # context. + self.flow_level = 0 + + # List of processed tokens that are not yet emitted. + self.tokens = [] + + # Add the STREAM-START token. + self.fetch_stream_start() + + # Number of tokens that were emitted through the `get_token` method. + self.tokens_taken = 0 + + # The current indentation level. + self.indent = -1 + + # Past indentation levels. + self.indents = [] + + # Variables related to simple keys treatment. + + # A simple key is a key that is not denoted by the '?' indicator. + # Example of simple keys: + # --- + # block simple key: value + # ? not a simple key: + # : { flow simple key: value } + # We emit the KEY token before all keys, so when we find a potential + # simple key, we try to locate the corresponding ':' indicator. + # Simple keys should be limited to a single line and 1024 characters. + + # Can a simple key start at the current position? A simple key may + # start: + # - at the beginning of the line, not counting indentation spaces + # (in block context), + # - after '{', '[', ',' (in the flow context), + # - after '?', ':', '-' (in the block context). + # In the block context, this flag also signifies if a block collection + # may start at the current position. + self.allow_simple_key = True + + # Keep track of possible simple keys. This is a dictionary. The key + # is `flow_level`; there can be no more that one possible simple key + # for each level. The value is a SimpleKey record: + # (token_number, required, index, line, column, mark) + # A simple key may start with ALIAS, ANCHOR, TAG, SCALAR(flow), + # '[', or '{' tokens. + self.possible_simple_keys = {} + + # Public methods. + + def check_token(self, *choices): + # Check if the next token is one of the given types. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + if not choices: + return True + for choice in choices: + if isinstance(self.tokens[0], choice): + return True + return False + + def peek_token(self): + # Return the next token, but do not delete if from the queue. + # Return None if no more tokens. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + return self.tokens[0] + else: + return None + + def get_token(self): + # Return the next token. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + self.tokens_taken += 1 + return self.tokens.pop(0) + + # Private methods. + + def need_more_tokens(self): + if self.done: + return False + if not self.tokens: + return True + # The current token may be a potential simple key, so we + # need to look further. + self.stale_possible_simple_keys() + if self.next_possible_simple_key() == self.tokens_taken: + return True + + def fetch_more_tokens(self): + + # Eat whitespaces and comments until we reach the next token. + self.scan_to_next_token() + + # Remove obsolete possible simple keys. + self.stale_possible_simple_keys() + + # Compare the current indentation and column. It may add some tokens + # and decrease the current indentation level. + self.unwind_indent(self.column) + + # Peek the next character. + ch = self.peek() + + # Is it the end of stream? + if ch == '\0': + return self.fetch_stream_end() + + # Is it a directive? + if ch == '%' and self.check_directive(): + return self.fetch_directive() + + # Is it the document start? + if ch == '-' and self.check_document_start(): + return self.fetch_document_start() + + # Is it the document end? + if ch == '.' and self.check_document_end(): + return self.fetch_document_end() + + # TODO: support for BOM within a stream. + #if ch == '\uFEFF': + # return self.fetch_bom() <-- issue BOMToken + + # Note: the order of the following checks is NOT significant. + + # Is it the flow sequence start indicator? + if ch == '[': + return self.fetch_flow_sequence_start() + + # Is it the flow mapping start indicator? + if ch == '{': + return self.fetch_flow_mapping_start() + + # Is it the flow sequence end indicator? + if ch == ']': + return self.fetch_flow_sequence_end() + + # Is it the flow mapping end indicator? + if ch == '}': + return self.fetch_flow_mapping_end() + + # Is it the flow entry indicator? + if ch == ',': + return self.fetch_flow_entry() + + # Is it the block entry indicator? + if ch == '-' and self.check_block_entry(): + return self.fetch_block_entry() + + # Is it the key indicator? + if ch == '?' and self.check_key(): + return self.fetch_key() + + # Is it the value indicator? + if ch == ':' and self.check_value(): + return self.fetch_value() + + # Is it an alias? + if ch == '*': + return self.fetch_alias() + + # Is it an anchor? + if ch == '&': + return self.fetch_anchor() + + # Is it a tag? + if ch == '!': + return self.fetch_tag() + + # Is it a literal scalar? + if ch == '|' and not self.flow_level: + return self.fetch_literal() + + # Is it a folded scalar? + if ch == '>' and not self.flow_level: + return self.fetch_folded() + + # Is it a single quoted scalar? + if ch == '\'': + return self.fetch_single() + + # Is it a double quoted scalar? + if ch == '\"': + return self.fetch_double() + + # It must be a plain scalar then. + if self.check_plain(): + return self.fetch_plain() + + # No? It's an error. Let's produce a nice error message. + raise ScannerError("while scanning for the next token", None, + "found character %r that cannot start any token" % ch, + self.get_mark()) + + # Simple keys treatment. + + def next_possible_simple_key(self): + # Return the number of the nearest possible simple key. Actually we + # don't need to loop through the whole dictionary. We may replace it + # with the following code: + # if not self.possible_simple_keys: + # return None + # return self.possible_simple_keys[ + # min(self.possible_simple_keys.keys())].token_number + min_token_number = None + for level in self.possible_simple_keys: + key = self.possible_simple_keys[level] + if min_token_number is None or key.token_number < min_token_number: + min_token_number = key.token_number + return min_token_number + + def stale_possible_simple_keys(self): + # Remove entries that are no longer possible simple keys. According to + # the YAML specification, simple keys + # - should be limited to a single line, + # - should be no longer than 1024 characters. + # Disabling this procedure will allow simple keys of any length and + # height (may cause problems if indentation is broken though). + for level in list(self.possible_simple_keys): + key = self.possible_simple_keys[level] + if key.line != self.line \ + or self.index-key.index > 1024: + if key.required: + raise ScannerError("while scanning a simple key", key.mark, + "could not find expected ':'", self.get_mark()) + del self.possible_simple_keys[level] + + def save_possible_simple_key(self): + # The next token may start a simple key. We check if it's possible + # and save its position. This function is called for + # ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'. + + # Check if a simple key is required at the current position. + required = not self.flow_level and self.indent == self.column + + # The next token might be a simple key. Let's save it's number and + # position. + if self.allow_simple_key: + self.remove_possible_simple_key() + token_number = self.tokens_taken+len(self.tokens) + key = SimpleKey(token_number, required, + self.index, self.line, self.column, self.get_mark()) + self.possible_simple_keys[self.flow_level] = key + + def remove_possible_simple_key(self): + # Remove the saved possible key position at the current flow level. + if self.flow_level in self.possible_simple_keys: + key = self.possible_simple_keys[self.flow_level] + + if key.required: + raise ScannerError("while scanning a simple key", key.mark, + "could not find expected ':'", self.get_mark()) + + del self.possible_simple_keys[self.flow_level] + + # Indentation functions. + + def unwind_indent(self, column): + + ## In flow context, tokens should respect indentation. + ## Actually the condition should be `self.indent >= column` according to + ## the spec. But this condition will prohibit intuitively correct + ## constructions such as + ## key : { + ## } + #if self.flow_level and self.indent > column: + # raise ScannerError(None, None, + # "invalid indentation or unclosed '[' or '{'", + # self.get_mark()) + + # In the flow context, indentation is ignored. We make the scanner less + # restrictive then specification requires. + if self.flow_level: + return + + # In block context, we may need to issue the BLOCK-END tokens. + while self.indent > column: + mark = self.get_mark() + self.indent = self.indents.pop() + self.tokens.append(BlockEndToken(mark, mark)) + + def add_indent(self, column): + # Check if we need to increase indentation. + if self.indent < column: + self.indents.append(self.indent) + self.indent = column + return True + return False + + # Fetchers. + + def fetch_stream_start(self): + # We always add STREAM-START as the first token and STREAM-END as the + # last token. + + # Read the token. + mark = self.get_mark() + + # Add STREAM-START. + self.tokens.append(StreamStartToken(mark, mark, + encoding=self.encoding)) + + + def fetch_stream_end(self): + + # Set the current indentation to -1. + self.unwind_indent(-1) + + # Reset simple keys. + self.remove_possible_simple_key() + self.allow_simple_key = False + self.possible_simple_keys = {} + + # Read the token. + mark = self.get_mark() + + # Add STREAM-END. + self.tokens.append(StreamEndToken(mark, mark)) + + # The steam is finished. + self.done = True + + def fetch_directive(self): + + # Set the current indentation to -1. + self.unwind_indent(-1) + + # Reset simple keys. + self.remove_possible_simple_key() + self.allow_simple_key = False + + # Scan and add DIRECTIVE. + self.tokens.append(self.scan_directive()) + + def fetch_document_start(self): + self.fetch_document_indicator(DocumentStartToken) + + def fetch_document_end(self): + self.fetch_document_indicator(DocumentEndToken) + + def fetch_document_indicator(self, TokenClass): + + # Set the current indentation to -1. + self.unwind_indent(-1) + + # Reset simple keys. Note that there could not be a block collection + # after '---'. + self.remove_possible_simple_key() + self.allow_simple_key = False + + # Add DOCUMENT-START or DOCUMENT-END. + start_mark = self.get_mark() + self.forward(3) + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_sequence_start(self): + self.fetch_flow_collection_start(FlowSequenceStartToken) + + def fetch_flow_mapping_start(self): + self.fetch_flow_collection_start(FlowMappingStartToken) + + def fetch_flow_collection_start(self, TokenClass): + + # '[' and '{' may start a simple key. + self.save_possible_simple_key() + + # Increase the flow level. + self.flow_level += 1 + + # Simple keys are allowed after '[' and '{'. + self.allow_simple_key = True + + # Add FLOW-SEQUENCE-START or FLOW-MAPPING-START. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_sequence_end(self): + self.fetch_flow_collection_end(FlowSequenceEndToken) + + def fetch_flow_mapping_end(self): + self.fetch_flow_collection_end(FlowMappingEndToken) + + def fetch_flow_collection_end(self, TokenClass): + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Decrease the flow level. + self.flow_level -= 1 + + # No simple keys after ']' or '}'. + self.allow_simple_key = False + + # Add FLOW-SEQUENCE-END or FLOW-MAPPING-END. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_entry(self): + + # Simple keys are allowed after ','. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add FLOW-ENTRY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(FlowEntryToken(start_mark, end_mark)) + + def fetch_block_entry(self): + + # Block context needs additional checks. + if not self.flow_level: + + # Are we allowed to start a new entry? + if not self.allow_simple_key: + raise ScannerError(None, None, + "sequence entries are not allowed here", + self.get_mark()) + + # We may need to add BLOCK-SEQUENCE-START. + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockSequenceStartToken(mark, mark)) + + # It's an error for the block entry to occur in the flow context, + # but we let the parser detect this. + else: + pass + + # Simple keys are allowed after '-'. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add BLOCK-ENTRY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(BlockEntryToken(start_mark, end_mark)) + + def fetch_key(self): + + # Block context needs additional checks. + if not self.flow_level: + + # Are we allowed to start a key (not necessary a simple)? + if not self.allow_simple_key: + raise ScannerError(None, None, + "mapping keys are not allowed here", + self.get_mark()) + + # We may need to add BLOCK-MAPPING-START. + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockMappingStartToken(mark, mark)) + + # Simple keys are allowed after '?' in the block context. + self.allow_simple_key = not self.flow_level + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add KEY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(KeyToken(start_mark, end_mark)) + + def fetch_value(self): + + # Do we determine a simple key? + if self.flow_level in self.possible_simple_keys: + + # Add KEY. + key = self.possible_simple_keys[self.flow_level] + del self.possible_simple_keys[self.flow_level] + self.tokens.insert(key.token_number-self.tokens_taken, + KeyToken(key.mark, key.mark)) + + # If this key starts a new block mapping, we need to add + # BLOCK-MAPPING-START. + if not self.flow_level: + if self.add_indent(key.column): + self.tokens.insert(key.token_number-self.tokens_taken, + BlockMappingStartToken(key.mark, key.mark)) + + # There cannot be two simple keys one after another. + self.allow_simple_key = False + + # It must be a part of a complex key. + else: + + # Block context needs additional checks. + # (Do we really need them? They will be caught by the parser + # anyway.) + if not self.flow_level: + + # We are allowed to start a complex value if and only if + # we can start a simple key. + if not self.allow_simple_key: + raise ScannerError(None, None, + "mapping values are not allowed here", + self.get_mark()) + + # If this value starts a new block mapping, we need to add + # BLOCK-MAPPING-START. It will be detected as an error later by + # the parser. + if not self.flow_level: + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockMappingStartToken(mark, mark)) + + # Simple keys are allowed after ':' in the block context. + self.allow_simple_key = not self.flow_level + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add VALUE. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(ValueToken(start_mark, end_mark)) + + def fetch_alias(self): + + # ALIAS could be a simple key. + self.save_possible_simple_key() + + # No simple keys after ALIAS. + self.allow_simple_key = False + + # Scan and add ALIAS. + self.tokens.append(self.scan_anchor(AliasToken)) + + def fetch_anchor(self): + + # ANCHOR could start a simple key. + self.save_possible_simple_key() + + # No simple keys after ANCHOR. + self.allow_simple_key = False + + # Scan and add ANCHOR. + self.tokens.append(self.scan_anchor(AnchorToken)) + + def fetch_tag(self): + + # TAG could start a simple key. + self.save_possible_simple_key() + + # No simple keys after TAG. + self.allow_simple_key = False + + # Scan and add TAG. + self.tokens.append(self.scan_tag()) + + def fetch_literal(self): + self.fetch_block_scalar(style='|') + + def fetch_folded(self): + self.fetch_block_scalar(style='>') + + def fetch_block_scalar(self, style): + + # A simple key may follow a block scalar. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Scan and add SCALAR. + self.tokens.append(self.scan_block_scalar(style)) + + def fetch_single(self): + self.fetch_flow_scalar(style='\'') + + def fetch_double(self): + self.fetch_flow_scalar(style='"') + + def fetch_flow_scalar(self, style): + + # A flow scalar could be a simple key. + self.save_possible_simple_key() + + # No simple keys after flow scalars. + self.allow_simple_key = False + + # Scan and add SCALAR. + self.tokens.append(self.scan_flow_scalar(style)) + + def fetch_plain(self): + + # A plain scalar could be a simple key. + self.save_possible_simple_key() + + # No simple keys after plain scalars. But note that `scan_plain` will + # change this flag if the scan is finished at the beginning of the + # line. + self.allow_simple_key = False + + # Scan and add SCALAR. May change `allow_simple_key`. + self.tokens.append(self.scan_plain()) + + # Checkers. + + def check_directive(self): + + # DIRECTIVE: ^ '%' ... + # The '%' indicator is already checked. + if self.column == 0: + return True + + def check_document_start(self): + + # DOCUMENT-START: ^ '---' (' '|'\n') + if self.column == 0: + if self.prefix(3) == '---' \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return True + + def check_document_end(self): + + # DOCUMENT-END: ^ '...' (' '|'\n') + if self.column == 0: + if self.prefix(3) == '...' \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return True + + def check_block_entry(self): + + # BLOCK-ENTRY: '-' (' '|'\n') + return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' + + def check_key(self): + + # KEY(flow context): '?' + if self.flow_level: + return True + + # KEY(block context): '?' (' '|'\n') + else: + return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' + + def check_value(self): + + # VALUE(flow context): ':' + if self.flow_level: + return True + + # VALUE(block context): ':' (' '|'\n') + else: + return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' + + def check_plain(self): + + # A plain scalar may start with any non-space character except: + # '-', '?', ':', ',', '[', ']', '{', '}', + # '#', '&', '*', '!', '|', '>', '\'', '\"', + # '%', '@', '`'. + # + # It may also start with + # '-', '?', ':' + # if it is followed by a non-space character. + # + # Note that we limit the last rule to the block context (except the + # '-' character) because we want the flow context to be space + # independent. + ch = self.peek() + return ch not in '\0 \t\r\n\x85\u2028\u2029-?:,[]{}#&*!|>\'\"%@`' \ + or (self.peek(1) not in '\0 \t\r\n\x85\u2028\u2029' + and (ch == '-' or (not self.flow_level and ch in '?:'))) + + # Scanners. + + def scan_to_next_token(self): + # We ignore spaces, line breaks and comments. + # If we find a line break in the block context, we set the flag + # `allow_simple_key` on. + # The byte order mark is stripped if it's the first character in the + # stream. We do not yet support BOM inside the stream as the + # specification requires. Any such mark will be considered as a part + # of the document. + # + # TODO: We need to make tab handling rules more sane. A good rule is + # Tabs cannot precede tokens + # BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END, + # KEY(block), VALUE(block), BLOCK-ENTRY + # So the checking code is + # if : + # self.allow_simple_keys = False + # We also need to add the check for `allow_simple_keys == True` to + # `unwind_indent` before issuing BLOCK-END. + # Scanners for block, flow, and plain scalars need to be modified. + + if self.index == 0 and self.peek() == '\uFEFF': + self.forward() + found = False + while not found: + while self.peek() == ' ': + self.forward() + if self.peek() == '#': + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + if self.scan_line_break(): + if not self.flow_level: + self.allow_simple_key = True + else: + found = True + + def scan_directive(self): + # See the specification for details. + start_mark = self.get_mark() + self.forward() + name = self.scan_directive_name(start_mark) + value = None + if name == 'YAML': + value = self.scan_yaml_directive_value(start_mark) + end_mark = self.get_mark() + elif name == 'TAG': + value = self.scan_tag_directive_value(start_mark) + end_mark = self.get_mark() + else: + end_mark = self.get_mark() + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + self.scan_directive_ignored_line(start_mark) + return DirectiveToken(name, value, start_mark, end_mark) + + def scan_directive_name(self, start_mark): + # See the specification for details. + length = 0 + ch = self.peek(length) + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_': + length += 1 + ch = self.peek(length) + if not length: + raise ScannerError("while scanning a directive", start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + value = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + return value + + def scan_yaml_directive_value(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + major = self.scan_yaml_directive_number(start_mark) + if self.peek() != '.': + raise ScannerError("while scanning a directive", start_mark, + "expected a digit or '.', but found %r" % self.peek(), + self.get_mark()) + self.forward() + minor = self.scan_yaml_directive_number(start_mark) + if self.peek() not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected a digit or ' ', but found %r" % self.peek(), + self.get_mark()) + return (major, minor) + + def scan_yaml_directive_number(self, start_mark): + # See the specification for details. + ch = self.peek() + if not ('0' <= ch <= '9'): + raise ScannerError("while scanning a directive", start_mark, + "expected a digit, but found %r" % ch, self.get_mark()) + length = 0 + while '0' <= self.peek(length) <= '9': + length += 1 + value = int(self.prefix(length)) + self.forward(length) + return value + + def scan_tag_directive_value(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + handle = self.scan_tag_directive_handle(start_mark) + while self.peek() == ' ': + self.forward() + prefix = self.scan_tag_directive_prefix(start_mark) + return (handle, prefix) + + def scan_tag_directive_handle(self, start_mark): + # See the specification for details. + value = self.scan_tag_handle('directive', start_mark) + ch = self.peek() + if ch != ' ': + raise ScannerError("while scanning a directive", start_mark, + "expected ' ', but found %r" % ch, self.get_mark()) + return value + + def scan_tag_directive_prefix(self, start_mark): + # See the specification for details. + value = self.scan_tag_uri('directive', start_mark) + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected ' ', but found %r" % ch, self.get_mark()) + return value + + def scan_directive_ignored_line(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + if self.peek() == '#': + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + ch = self.peek() + if ch not in '\0\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected a comment or a line break, but found %r" + % ch, self.get_mark()) + self.scan_line_break() + + def scan_anchor(self, TokenClass): + # The specification does not restrict characters for anchors and + # aliases. This may lead to problems, for instance, the document: + # [ *alias, value ] + # can be interpreted in two ways, as + # [ "value" ] + # and + # [ *alias , "value" ] + # Therefore we restrict aliases to numbers and ASCII letters. + start_mark = self.get_mark() + indicator = self.peek() + if indicator == '*': + name = 'alias' + else: + name = 'anchor' + self.forward() + length = 0 + ch = self.peek(length) + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_': + length += 1 + ch = self.peek(length) + if not length: + raise ScannerError("while scanning an %s" % name, start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + value = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch not in '\0 \t\r\n\x85\u2028\u2029?:,]}%@`': + raise ScannerError("while scanning an %s" % name, start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + end_mark = self.get_mark() + return TokenClass(value, start_mark, end_mark) + + def scan_tag(self): + # See the specification for details. + start_mark = self.get_mark() + ch = self.peek(1) + if ch == '<': + handle = None + self.forward(2) + suffix = self.scan_tag_uri('tag', start_mark) + if self.peek() != '>': + raise ScannerError("while parsing a tag", start_mark, + "expected '>', but found %r" % self.peek(), + self.get_mark()) + self.forward() + elif ch in '\0 \t\r\n\x85\u2028\u2029': + handle = None + suffix = '!' + self.forward() + else: + length = 1 + use_handle = False + while ch not in '\0 \r\n\x85\u2028\u2029': + if ch == '!': + use_handle = True + break + length += 1 + ch = self.peek(length) + handle = '!' + if use_handle: + handle = self.scan_tag_handle('tag', start_mark) + else: + handle = '!' + self.forward() + suffix = self.scan_tag_uri('tag', start_mark) + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a tag", start_mark, + "expected ' ', but found %r" % ch, self.get_mark()) + value = (handle, suffix) + end_mark = self.get_mark() + return TagToken(value, start_mark, end_mark) + + def scan_block_scalar(self, style): + # See the specification for details. + + if style == '>': + folded = True + else: + folded = False + + chunks = [] + start_mark = self.get_mark() + + # Scan the header. + self.forward() + chomping, increment = self.scan_block_scalar_indicators(start_mark) + self.scan_block_scalar_ignored_line(start_mark) + + # Determine the indentation level and go to the first non-empty line. + min_indent = self.indent+1 + if min_indent < 1: + min_indent = 1 + if increment is None: + breaks, max_indent, end_mark = self.scan_block_scalar_indentation() + indent = max(min_indent, max_indent) + else: + indent = min_indent+increment-1 + breaks, end_mark = self.scan_block_scalar_breaks(indent) + line_break = '' + + # Scan the inner part of the block scalar. + while self.column == indent and self.peek() != '\0': + chunks.extend(breaks) + leading_non_space = self.peek() not in ' \t' + length = 0 + while self.peek(length) not in '\0\r\n\x85\u2028\u2029': + length += 1 + chunks.append(self.prefix(length)) + self.forward(length) + line_break = self.scan_line_break() + breaks, end_mark = self.scan_block_scalar_breaks(indent) + if self.column == indent and self.peek() != '\0': + + # Unfortunately, folding rules are ambiguous. + # + # This is the folding according to the specification: + + if folded and line_break == '\n' \ + and leading_non_space and self.peek() not in ' \t': + if not breaks: + chunks.append(' ') + else: + chunks.append(line_break) + + # This is Clark Evans's interpretation (also in the spec + # examples): + # + #if folded and line_break == '\n': + # if not breaks: + # if self.peek() not in ' \t': + # chunks.append(' ') + # else: + # chunks.append(line_break) + #else: + # chunks.append(line_break) + else: + break + + # Chomp the tail. + if chomping is not False: + chunks.append(line_break) + if chomping is True: + chunks.extend(breaks) + + # We are done. + return ScalarToken(''.join(chunks), False, start_mark, end_mark, + style) + + def scan_block_scalar_indicators(self, start_mark): + # See the specification for details. + chomping = None + increment = None + ch = self.peek() + if ch in '+-': + if ch == '+': + chomping = True + else: + chomping = False + self.forward() + ch = self.peek() + if ch in '0123456789': + increment = int(ch) + if increment == 0: + raise ScannerError("while scanning a block scalar", start_mark, + "expected indentation indicator in the range 1-9, but found 0", + self.get_mark()) + self.forward() + elif ch in '0123456789': + increment = int(ch) + if increment == 0: + raise ScannerError("while scanning a block scalar", start_mark, + "expected indentation indicator in the range 1-9, but found 0", + self.get_mark()) + self.forward() + ch = self.peek() + if ch in '+-': + if ch == '+': + chomping = True + else: + chomping = False + self.forward() + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a block scalar", start_mark, + "expected chomping or indentation indicators, but found %r" + % ch, self.get_mark()) + return chomping, increment + + def scan_block_scalar_ignored_line(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + if self.peek() == '#': + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + ch = self.peek() + if ch not in '\0\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a block scalar", start_mark, + "expected a comment or a line break, but found %r" % ch, + self.get_mark()) + self.scan_line_break() + + def scan_block_scalar_indentation(self): + # See the specification for details. + chunks = [] + max_indent = 0 + end_mark = self.get_mark() + while self.peek() in ' \r\n\x85\u2028\u2029': + if self.peek() != ' ': + chunks.append(self.scan_line_break()) + end_mark = self.get_mark() + else: + self.forward() + if self.column > max_indent: + max_indent = self.column + return chunks, max_indent, end_mark + + def scan_block_scalar_breaks(self, indent): + # See the specification for details. + chunks = [] + end_mark = self.get_mark() + while self.column < indent and self.peek() == ' ': + self.forward() + while self.peek() in '\r\n\x85\u2028\u2029': + chunks.append(self.scan_line_break()) + end_mark = self.get_mark() + while self.column < indent and self.peek() == ' ': + self.forward() + return chunks, end_mark + + def scan_flow_scalar(self, style): + # See the specification for details. + # Note that we loose indentation rules for quoted scalars. Quoted + # scalars don't need to adhere indentation because " and ' clearly + # mark the beginning and the end of them. Therefore we are less + # restrictive then the specification requires. We only need to check + # that document separators are not included in scalars. + if style == '"': + double = True + else: + double = False + chunks = [] + start_mark = self.get_mark() + quote = self.peek() + self.forward() + chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) + while self.peek() != quote: + chunks.extend(self.scan_flow_scalar_spaces(double, start_mark)) + chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) + self.forward() + end_mark = self.get_mark() + return ScalarToken(''.join(chunks), False, start_mark, end_mark, + style) + + ESCAPE_REPLACEMENTS = { + '0': '\0', + 'a': '\x07', + 'b': '\x08', + 't': '\x09', + '\t': '\x09', + 'n': '\x0A', + 'v': '\x0B', + 'f': '\x0C', + 'r': '\x0D', + 'e': '\x1B', + ' ': '\x20', + '\"': '\"', + '\\': '\\', + '/': '/', + 'N': '\x85', + '_': '\xA0', + 'L': '\u2028', + 'P': '\u2029', + } + + ESCAPE_CODES = { + 'x': 2, + 'u': 4, + 'U': 8, + } + + def scan_flow_scalar_non_spaces(self, double, start_mark): + # See the specification for details. + chunks = [] + while True: + length = 0 + while self.peek(length) not in '\'\"\\\0 \t\r\n\x85\u2028\u2029': + length += 1 + if length: + chunks.append(self.prefix(length)) + self.forward(length) + ch = self.peek() + if not double and ch == '\'' and self.peek(1) == '\'': + chunks.append('\'') + self.forward(2) + elif (double and ch == '\'') or (not double and ch in '\"\\'): + chunks.append(ch) + self.forward() + elif double and ch == '\\': + self.forward() + ch = self.peek() + if ch in self.ESCAPE_REPLACEMENTS: + chunks.append(self.ESCAPE_REPLACEMENTS[ch]) + self.forward() + elif ch in self.ESCAPE_CODES: + length = self.ESCAPE_CODES[ch] + self.forward() + for k in range(length): + if self.peek(k) not in '0123456789ABCDEFabcdef': + raise ScannerError("while scanning a double-quoted scalar", start_mark, + "expected escape sequence of %d hexdecimal numbers, but found %r" % + (length, self.peek(k)), self.get_mark()) + code = int(self.prefix(length), 16) + chunks.append(chr(code)) + self.forward(length) + elif ch in '\r\n\x85\u2028\u2029': + self.scan_line_break() + chunks.extend(self.scan_flow_scalar_breaks(double, start_mark)) + else: + raise ScannerError("while scanning a double-quoted scalar", start_mark, + "found unknown escape character %r" % ch, self.get_mark()) + else: + return chunks + + def scan_flow_scalar_spaces(self, double, start_mark): + # See the specification for details. + chunks = [] + length = 0 + while self.peek(length) in ' \t': + length += 1 + whitespaces = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch == '\0': + raise ScannerError("while scanning a quoted scalar", start_mark, + "found unexpected end of stream", self.get_mark()) + elif ch in '\r\n\x85\u2028\u2029': + line_break = self.scan_line_break() + breaks = self.scan_flow_scalar_breaks(double, start_mark) + if line_break != '\n': + chunks.append(line_break) + elif not breaks: + chunks.append(' ') + chunks.extend(breaks) + else: + chunks.append(whitespaces) + return chunks + + def scan_flow_scalar_breaks(self, double, start_mark): + # See the specification for details. + chunks = [] + while True: + # Instead of checking indentation, we check for document + # separators. + prefix = self.prefix(3) + if (prefix == '---' or prefix == '...') \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a quoted scalar", start_mark, + "found unexpected document separator", self.get_mark()) + while self.peek() in ' \t': + self.forward() + if self.peek() in '\r\n\x85\u2028\u2029': + chunks.append(self.scan_line_break()) + else: + return chunks + + def scan_plain(self): + # See the specification for details. + # We add an additional restriction for the flow context: + # plain scalars in the flow context cannot contain ',' or '?'. + # We also keep track of the `allow_simple_key` flag here. + # Indentation rules are loosed for the flow context. + chunks = [] + start_mark = self.get_mark() + end_mark = start_mark + indent = self.indent+1 + # We allow zero indentation for scalars, but then we need to check for + # document separators at the beginning of the line. + #if indent == 0: + # indent = 1 + spaces = [] + while True: + length = 0 + if self.peek() == '#': + break + while True: + ch = self.peek(length) + if ch in '\0 \t\r\n\x85\u2028\u2029' \ + or (ch == ':' and + self.peek(length+1) in '\0 \t\r\n\x85\u2028\u2029' + + (u',[]{}' if self.flow_level else u''))\ + or (self.flow_level and ch in ',?[]{}'): + break + length += 1 + if length == 0: + break + self.allow_simple_key = False + chunks.extend(spaces) + chunks.append(self.prefix(length)) + self.forward(length) + end_mark = self.get_mark() + spaces = self.scan_plain_spaces(indent, start_mark) + if not spaces or self.peek() == '#' \ + or (not self.flow_level and self.column < indent): + break + return ScalarToken(''.join(chunks), True, start_mark, end_mark) + + def scan_plain_spaces(self, indent, start_mark): + # See the specification for details. + # The specification is really confusing about tabs in plain scalars. + # We just forbid them completely. Do not use tabs in YAML! + chunks = [] + length = 0 + while self.peek(length) in ' ': + length += 1 + whitespaces = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch in '\r\n\x85\u2028\u2029': + line_break = self.scan_line_break() + self.allow_simple_key = True + prefix = self.prefix(3) + if (prefix == '---' or prefix == '...') \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return + breaks = [] + while self.peek() in ' \r\n\x85\u2028\u2029': + if self.peek() == ' ': + self.forward() + else: + breaks.append(self.scan_line_break()) + prefix = self.prefix(3) + if (prefix == '---' or prefix == '...') \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return + if line_break != '\n': + chunks.append(line_break) + elif not breaks: + chunks.append(' ') + chunks.extend(breaks) + elif whitespaces: + chunks.append(whitespaces) + return chunks + + def scan_tag_handle(self, name, start_mark): + # See the specification for details. + # For some strange reasons, the specification does not allow '_' in + # tag handles. I have allowed it anyway. + ch = self.peek() + if ch != '!': + raise ScannerError("while scanning a %s" % name, start_mark, + "expected '!', but found %r" % ch, self.get_mark()) + length = 1 + ch = self.peek(length) + if ch != ' ': + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_': + length += 1 + ch = self.peek(length) + if ch != '!': + self.forward(length) + raise ScannerError("while scanning a %s" % name, start_mark, + "expected '!', but found %r" % ch, self.get_mark()) + length += 1 + value = self.prefix(length) + self.forward(length) + return value + + def scan_tag_uri(self, name, start_mark): + # See the specification for details. + # Note: we do not check if URI is well-formed. + chunks = [] + length = 0 + ch = self.peek(length) + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-;/?:@&=+$,_.!~*\'()[]%': + if ch == '%': + chunks.append(self.prefix(length)) + self.forward(length) + length = 0 + chunks.append(self.scan_uri_escapes(name, start_mark)) + else: + length += 1 + ch = self.peek(length) + if length: + chunks.append(self.prefix(length)) + self.forward(length) + length = 0 + if not chunks: + raise ScannerError("while parsing a %s" % name, start_mark, + "expected URI, but found %r" % ch, self.get_mark()) + return ''.join(chunks) + + def scan_uri_escapes(self, name, start_mark): + # See the specification for details. + codes = [] + mark = self.get_mark() + while self.peek() == '%': + self.forward() + for k in range(2): + if self.peek(k) not in '0123456789ABCDEFabcdef': + raise ScannerError("while scanning a %s" % name, start_mark, + "expected URI escape sequence of 2 hexdecimal numbers, but found %r" + % self.peek(k), self.get_mark()) + codes.append(int(self.prefix(2), 16)) + self.forward(2) + try: + value = bytes(codes).decode('utf-8') + except UnicodeDecodeError as exc: + raise ScannerError("while scanning a %s" % name, start_mark, str(exc), mark) + return value + + def scan_line_break(self): + # Transforms: + # '\r\n' : '\n' + # '\r' : '\n' + # '\n' : '\n' + # '\x85' : '\n' + # '\u2028' : '\u2028' + # '\u2029 : '\u2029' + # default : '' + ch = self.peek() + if ch in '\r\n\x85': + if self.prefix(2) == '\r\n': + self.forward(2) + else: + self.forward() + return '\n' + elif ch in '\u2028\u2029': + self.forward() + return ch + return '' diff --git a/coredns/venv/yaml/serializer.py b/coredns/venv/yaml/serializer.py new file mode 100644 index 0000000..fe911e6 --- /dev/null +++ b/coredns/venv/yaml/serializer.py @@ -0,0 +1,111 @@ + +__all__ = ['Serializer', 'SerializerError'] + +from .error import YAMLError +from .events import * +from .nodes import * + +class SerializerError(YAMLError): + pass + +class Serializer: + + ANCHOR_TEMPLATE = 'id%03d' + + def __init__(self, encoding=None, + explicit_start=None, explicit_end=None, version=None, tags=None): + self.use_encoding = encoding + self.use_explicit_start = explicit_start + self.use_explicit_end = explicit_end + self.use_version = version + self.use_tags = tags + self.serialized_nodes = {} + self.anchors = {} + self.last_anchor_id = 0 + self.closed = None + + def open(self): + if self.closed is None: + self.emit(StreamStartEvent(encoding=self.use_encoding)) + self.closed = False + elif self.closed: + raise SerializerError("serializer is closed") + else: + raise SerializerError("serializer is already opened") + + def close(self): + if self.closed is None: + raise SerializerError("serializer is not opened") + elif not self.closed: + self.emit(StreamEndEvent()) + self.closed = True + + #def __del__(self): + # self.close() + + def serialize(self, node): + if self.closed is None: + raise SerializerError("serializer is not opened") + elif self.closed: + raise SerializerError("serializer is closed") + self.emit(DocumentStartEvent(explicit=self.use_explicit_start, + version=self.use_version, tags=self.use_tags)) + self.anchor_node(node) + self.serialize_node(node, None, None) + self.emit(DocumentEndEvent(explicit=self.use_explicit_end)) + self.serialized_nodes = {} + self.anchors = {} + self.last_anchor_id = 0 + + def anchor_node(self, node): + if node in self.anchors: + if self.anchors[node] is None: + self.anchors[node] = self.generate_anchor(node) + else: + self.anchors[node] = None + if isinstance(node, SequenceNode): + for item in node.value: + self.anchor_node(item) + elif isinstance(node, MappingNode): + for key, value in node.value: + self.anchor_node(key) + self.anchor_node(value) + + def generate_anchor(self, node): + self.last_anchor_id += 1 + return self.ANCHOR_TEMPLATE % self.last_anchor_id + + def serialize_node(self, node, parent, index): + alias = self.anchors[node] + if node in self.serialized_nodes: + self.emit(AliasEvent(alias)) + else: + self.serialized_nodes[node] = True + self.descend_resolver(parent, index) + if isinstance(node, ScalarNode): + detected_tag = self.resolve(ScalarNode, node.value, (True, False)) + default_tag = self.resolve(ScalarNode, node.value, (False, True)) + implicit = (node.tag == detected_tag), (node.tag == default_tag) + self.emit(ScalarEvent(alias, node.tag, implicit, node.value, + style=node.style)) + elif isinstance(node, SequenceNode): + implicit = (node.tag + == self.resolve(SequenceNode, node.value, True)) + self.emit(SequenceStartEvent(alias, node.tag, implicit, + flow_style=node.flow_style)) + index = 0 + for item in node.value: + self.serialize_node(item, node, index) + index += 1 + self.emit(SequenceEndEvent()) + elif isinstance(node, MappingNode): + implicit = (node.tag + == self.resolve(MappingNode, node.value, True)) + self.emit(MappingStartEvent(alias, node.tag, implicit, + flow_style=node.flow_style)) + for key, value in node.value: + self.serialize_node(key, node, None) + self.serialize_node(value, node, key) + self.emit(MappingEndEvent()) + self.ascend_resolver() + diff --git a/coredns/venv/yaml/tokens.py b/coredns/venv/yaml/tokens.py new file mode 100644 index 0000000..4d0b48a --- /dev/null +++ b/coredns/venv/yaml/tokens.py @@ -0,0 +1,104 @@ + +class Token(object): + def __init__(self, start_mark, end_mark): + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + attributes = [key for key in self.__dict__ + if not key.endswith('_mark')] + attributes.sort() + arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) + for key in attributes]) + return '%s(%s)' % (self.__class__.__name__, arguments) + +#class BOMToken(Token): +# id = '' + +class DirectiveToken(Token): + id = '' + def __init__(self, name, value, start_mark, end_mark): + self.name = name + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class DocumentStartToken(Token): + id = '' + +class DocumentEndToken(Token): + id = '' + +class StreamStartToken(Token): + id = '' + def __init__(self, start_mark=None, end_mark=None, + encoding=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.encoding = encoding + +class StreamEndToken(Token): + id = '' + +class BlockSequenceStartToken(Token): + id = '' + +class BlockMappingStartToken(Token): + id = '' + +class BlockEndToken(Token): + id = '' + +class FlowSequenceStartToken(Token): + id = '[' + +class FlowMappingStartToken(Token): + id = '{' + +class FlowSequenceEndToken(Token): + id = ']' + +class FlowMappingEndToken(Token): + id = '}' + +class KeyToken(Token): + id = '?' + +class ValueToken(Token): + id = ':' + +class BlockEntryToken(Token): + id = '-' + +class FlowEntryToken(Token): + id = ',' + +class AliasToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class AnchorToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class TagToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class ScalarToken(Token): + id = '' + def __init__(self, value, plain, start_mark, end_mark, style=None): + self.value = value + self.plain = plain + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + diff --git a/easyrsa/.build.manifest b/easyrsa/.build.manifest new file mode 100644 index 0000000..2b3616e --- /dev/null +++ b/easyrsa/.build.manifest @@ -0,0 +1,516 @@ +{ + "layers": [ + { + "branch": "refs/heads/master", + "rev": "fcdcea4e5de3e1556c24e6704607862d0ba00a56", + "url": "layer:options" + }, + { + "branch": "refs/heads/master", + "rev": "a3ff62c32c993d80417f6e093e3ef95e42f62083", + "url": "layer:basic" + }, + { + "branch": "refs/heads/master", + "rev": "527dd64fc4b9a6b0f8d80a3c2c0b865155050275", + "url": "layer:debug" + }, + { + "branch": "refs/heads/master", + "rev": "cc5bd3f49b2fa5e6c3ab2336763c313ec8bf083f", + "url": "layer:leadership" + }, + { + "branch": "refs/heads/master", + "rev": "a7d7b6423db37a47611310039e6ed1929c0a2eab", + "url": "layer:status" + }, + { + "branch": "refs/heads/master", + "rev": "44f635b92624be5882c70ca1544d79f5d8483e24", + "url": "easyrsa" + }, + { + "branch": "refs/heads/master", + "rev": "d9850016d930a6d507b9fd45e2598d327922b140", + "url": "interface:tls-certificates" + } + ], + "signatures": { + ".build.manifest": [ + "build", + "dynamic", + "unchecked" + ], + ".github/workflows/main.yml": [ + "layer:basic", + "static", + "96a48a981ceb2a96f427a6b5226d2da6d7191981793804055d70a88ca1987473" + ], + ".github/workflows/tox.yaml": [ + "easyrsa", + "static", + "c323f9ca1fe5bf1369f80d8958be49ad8fd2f6635528865017c357591d31542e" + ], + ".gitignore": [ + "easyrsa", + "static", + "3d3d61b1e6228c5d03ea369331e493d0688f94416a0384c5c0b41194e4297d33" + ], + "CONTRIBUTING.md": [ + "easyrsa", + "static", + "8cb96f21e7dddc5b0f0ee7ced5168566b3534a98dd7dcb8f0b459846ef420e1e" + ], + "LICENSE": [ + "easyrsa", + "static", + "f02fd85a4171482f6bb1d6f87fe0704d3a2da93eca04afe39a0310a00c409902" + ], + "Makefile": [ + "layer:basic", + "static", + "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301" + ], + "README.md": [ + "easyrsa", + "static", + "0face8a003629fcfe025bc339558160b3b5a88f5bfc53c904e931d242c4d0135" + ], + "actions.yaml": [ + "easyrsa", + "dynamic", + "68a27394e89885a22fbe5fe9ea746c3f6354e5883c4b2d70d6c1df1821116037" + ], + "actions/__init__.py": [ + "easyrsa", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "actions/actions.py": [ + "easyrsa", + "static", + "1a7894d5a5bc95629f5447ea3cee52d71682f8b095b0e58fea6fd017e8d1fbc8" + ], + "actions/backup": [ + "easyrsa", + "static", + "1a7894d5a5bc95629f5447ea3cee52d71682f8b095b0e58fea6fd017e8d1fbc8" + ], + "actions/debug": [ + "layer:debug", + "static", + "db0a42dae4c5045b2c06385bf22209dfe0e2ded55822ef847d84b01d9ff2b046" + ], + "actions/delete-backup": [ + "easyrsa", + "static", + "1a7894d5a5bc95629f5447ea3cee52d71682f8b095b0e58fea6fd017e8d1fbc8" + ], + "actions/list-backups": [ + "easyrsa", + "static", + "1a7894d5a5bc95629f5447ea3cee52d71682f8b095b0e58fea6fd017e8d1fbc8" + ], + "actions/restore": [ + "easyrsa", + "static", + "1a7894d5a5bc95629f5447ea3cee52d71682f8b095b0e58fea6fd017e8d1fbc8" + ], + "bin/charm-env": [ + "layer:basic", + "static", + "fb6a20fac4102a6a4b6ffe903fcf666998f9a95a3647e6f9af7a1eeb44e58fd5" + ], + "bin/layer_option": [ + "layer:options", + "static", + "e959bf29da4c5edff28b2602c24113c4df9e25cdc9f2aa3b5d46c8577b2a40cc" + ], + "config.yaml": [ + "easyrsa", + "dynamic", + "d047e48447d374c91de7e90fd46a624f93adf76647a08a78eed40f180d16b29e" + ], + "copyright": [ + "easyrsa", + "static", + "b66cc4de4ddcf30883160876280b9e1f9fbea0ddb4e62cc58685a1913c23f746" + ], + "copyright.layer-basic": [ + "layer:basic", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-leadership": [ + "layer:leadership", + "static", + "8ce407829378fc0f72ce44c7f624e4951c7ccb3db1cfb949bee026b701728cc9" + ], + "copyright.layer-options": [ + "layer:options", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-status": [ + "layer:status", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "debug-scripts/charm-unitdata": [ + "layer:debug", + "static", + "c952b9d31f3942e4e722cb3e70f5119707b69b8e76cc44e2e906bc6d9aef49b7" + ], + "debug-scripts/filesystem": [ + "layer:debug", + "static", + "d29cc8687f4422d024001c91b1ac756ee6bf8a2a125bc98db1199ba775eb8fd7" + ], + "debug-scripts/juju-logs": [ + "layer:debug", + "static", + "d260b35753a917368cb8c64c1312546a0a40ef49cba84c75bc6369549807c55e" + ], + "debug-scripts/juju-network-get": [ + "layer:debug", + "static", + "6d849a1f8e6569bd0d5ea38299f7937cb8b36a5f505e3532f6c756eabeb8b6c5" + ], + "debug-scripts/network": [ + "layer:debug", + "static", + "714afae5dcb45554ff1f05285501e3b7fcc656c8de51217e263b93dab25a9d2e" + ], + "debug-scripts/packages": [ + "layer:debug", + "static", + "e8177102dc2ca853cb9272c1257cf2cfd5253d2a074e602d07c8bc4ea8e27c75" + ], + "debug-scripts/sysctl": [ + "layer:debug", + "static", + "990035b320e09cc2228e1f2f880e795d51118b2959339eacddff9cbb74349c6a" + ], + "debug-scripts/systemd": [ + "layer:debug", + "static", + "23ddf533198bf5b1ce723acde31ada806aab8539292b514c721d8ec08af74106" + ], + "docs/status.md": [ + "layer:status", + "static", + "975dec9f8c938196e102e954a80226bda293407c4e5ae857c118bf692154702a" + ], + "hooks/client-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/client-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/client-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/client-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/client-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/config-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/hook.template": [ + "layer:basic", + "static", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/install": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-elected": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-settings-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/post-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/pre-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/relations/tls-certificates/.gitignore": [ + "interface:tls-certificates", + "static", + "b485e74def213c534676224e655e9276b62d401ebc643508ddc545dd335cb6dc" + ], + "hooks/relations/tls-certificates/README.md": [ + "interface:tls-certificates", + "static", + "6851227de8fcca7edfd504159dbe3e3af31080af64df46f3d3b345da7630827a" + ], + "hooks/relations/tls-certificates/__init__.py": [ + "interface:tls-certificates", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/tls-certificates/docs/common.md": [ + "interface:tls-certificates", + "static", + "5e91d6637fc0ccc50af2776de9e59a0f8098244b627816b2e18fabb266e980ff" + ], + "hooks/relations/tls-certificates/docs/provides.md": [ + "interface:tls-certificates", + "static", + "5c12dfca99b5c15ba10b4e7f7cff4cb4c9b621b198deba5f2397d3c837d035fe" + ], + "hooks/relations/tls-certificates/docs/requires.md": [ + "interface:tls-certificates", + "static", + "148dd1de163d75253f0a9d3c35e108dcaacbc9bdf97e47186743e6c82a67b62e" + ], + "hooks/relations/tls-certificates/interface.yaml": [ + "interface:tls-certificates", + "static", + "e412e54b1d327bad15a882f7f0bf996212090db576b863cc9cff7a68afc0e4fa" + ], + "hooks/relations/tls-certificates/make_docs": [ + "interface:tls-certificates", + "static", + "3671543bddc9d277171263310e404df3f11660429582cb27b39b7e7ec8757a37" + ], + "hooks/relations/tls-certificates/provides.py": [ + "interface:tls-certificates", + "static", + "be2a4b9a411c770989c529fd887070ad91649481a13f5239cfd8751f234b637c" + ], + "hooks/relations/tls-certificates/pydocmd.yml": [ + "interface:tls-certificates", + "static", + "48a233f60a89f87d56e9bc715e05766f5d39bbea2bc8741ed31f67b30c8cfcb8" + ], + "hooks/relations/tls-certificates/requires.py": [ + "interface:tls-certificates", + "static", + "442d773112079bc674d3e6be75b00323fcad7efd2f03613a1972b575dd438dba" + ], + "hooks/relations/tls-certificates/tls_certificates_common.py": [ + "interface:tls-certificates", + "static", + "068bd32ba69bfa514e1da386919d18b348ee678b40c372f275c9110f2cc4677c" + ], + "hooks/start": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/stop": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/update-status": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/upgrade-charm": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "icon.svg": [ + "easyrsa", + "static", + "e5b03f7e2c5f7948e595138412b634a16c45e06b500041f56f6062ab68d11b94" + ], + "layer.yaml": [ + "easyrsa", + "dynamic", + "230791f1d771e8ba06f98403bd1b99996752eb5290cd89b51d4f66fd725e0819" + ], + "lib/charms/layer/__init__.py": [ + "layer:basic", + "static", + "dfe0d26c6bf409767de6e2546bc648f150e1b396243619bad3aa0553ab7e0e6f" + ], + "lib/charms/layer/basic.py": [ + "layer:basic", + "static", + "98b47134770ed6e4c0b2d4aad73cd5bc200bec84aa9c1c4e075fd70c3222a0c9" + ], + "lib/charms/layer/execd.py": [ + "layer:basic", + "static", + "fda8bd491032db1db8ddaf4e99e7cc878c6fb5432efe1f91cadb5b34765d076d" + ], + "lib/charms/layer/options.py": [ + "layer:options", + "static", + "8ae7a07d22542fc964f2d2bee8219d1c78a68dace70a1b38d36d4aea47b1c3b2" + ], + "lib/charms/layer/status.py": [ + "layer:status", + "static", + "d560a5e07b2e5f2b0f25f30e1f0278b06f3f90c01e4dbad5c83d71efc79018c6" + ], + "lib/charms/leadership.py": [ + "layer:leadership", + "static", + "20ffcbbc08147506759726ad51567420659ffb8a2e0121079240b8706658e332" + ], + "lib/debug_script.py": [ + "layer:debug", + "static", + "a4d56f2d3e712b1b5cadb657c7195c6268d0aac6d228991049fd769e0ddaf453" + ], + "make_docs": [ + "layer:status", + "static", + "c990f55c8e879793a62ed8464ee3d7e0d7d2225fdecaf17af24b0df0e2daa8c1" + ], + "metadata.yaml": [ + "easyrsa", + "dynamic", + "0ed64e36422eed5302d8fd152567795c18784d5726f84b5c11080359e169f59a" + ], + "pydocmd.yml": [ + "layer:status", + "static", + "11d9293901f32f75f4256ae4ac2073b92ce1d7ef7b6c892ba9fbb98690a0b330" + ], + "reactive/__init__.py": [ + "layer:leadership", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "reactive/easyrsa.py": [ + "easyrsa", + "static", + "8739e7c51bdda309bab48754a44218a565032aa66137c8b056075c173d07ecc4" + ], + "reactive/leadership.py": [ + "layer:leadership", + "static", + "e2b233cf861adc3b2d9e9c062134ce2f104953f03283cdddd88f49efee652e8f" + ], + "reactive/status.py": [ + "layer:status", + "static", + "30207fc206f24e91def5252f1c7f7c8e23c0aed0e93076babf5e03c05296d207" + ], + "requirements.txt": [ + "layer:basic", + "static", + "a00f75d80849e5b4fc5ad2e7536f947c25b1a4044b341caa8ee87a92d3a4c804" + ], + "tox.ini": [ + "layer:status", + "static", + "2669a78e8e51c1606874e1cc97ca99e660ff547a79592572a38a268d99b25b67" + ], + "version": [ + "easyrsa", + "dynamic", + "851da86e953acd82c0991bb6e68e42819c3a3bce501c177dff2c7cab01794982" + ], + "wheelhouse.txt": [ + "layer:basic", + "dynamic", + "44b8a3ab6ccaf3a81c8a96526a285462e01964e6090fd40104f3a087bab43c0c" + ], + "wheelhouse/Jinja2-2.10.1.tar.gz": [ + "layer:basic", + "dynamic", + "065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013" + ], + "wheelhouse/MarkupSafe-1.1.1.tar.gz": [ + "layer:basic", + "dynamic", + "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b" + ], + "wheelhouse/PyYAML-5.2.tar.gz": [ + "layer:basic", + "dynamic", + "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c" + ], + "wheelhouse/Tempita-0.5.2.tar.gz": [ + "__pip__", + "dynamic", + "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c" + ], + "wheelhouse/charmhelpers-0.20.23.tar.gz": [ + "layer:basic", + "dynamic", + "59a9776594e91cd3e3e000043f8668b4d7b279422dbb17e320f01dc16385b80e" + ], + "wheelhouse/charms.reactive-1.4.1.tar.gz": [ + "layer:basic", + "dynamic", + "bba21b4fd40b26c240c9ef2aa10c6fdf73592031c68591da4e7ccc46ca9cb616" + ], + "wheelhouse/netaddr-0.7.19.tar.gz": [ + "layer:basic", + "dynamic", + "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd" + ], + "wheelhouse/pbr-5.6.0.tar.gz": [ + "__pip__", + "dynamic", + "42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd" + ], + "wheelhouse/pip-18.1.tar.gz": [ + "layer:basic", + "dynamic", + "c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1" + ], + "wheelhouse/pyaml-21.10.1.tar.gz": [ + "__pip__", + "dynamic", + "c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383" + ], + "wheelhouse/setuptools-41.6.0.zip": [ + "layer:basic", + "dynamic", + "6afa61b391dcd16cb8890ec9f66cc4015a8a31a6e1c2b4e0c464514be1a3d722" + ], + "wheelhouse/setuptools_scm-1.17.0.tar.gz": [ + "layer:basic", + "dynamic", + "70a4cf5584e966ae92f54a764e6437af992ba42ac4bca7eb37cc5d02b98ec40a" + ], + "wheelhouse/six-1.16.0.tar.gz": [ + "__pip__", + "dynamic", + "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926" + ], + "wheelhouse/wheel-0.33.6.tar.gz": [ + "layer:basic", + "dynamic", + "10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646" + ] + } +} \ No newline at end of file diff --git a/easyrsa/.github/workflows/main.yml b/easyrsa/.github/workflows/main.yml new file mode 100644 index 0000000..565bfaf --- /dev/null +++ b/easyrsa/.github/workflows/main.yml @@ -0,0 +1,50 @@ +name: Test Suite +on: [pull_request] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.5, 3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + - name: Run lint + run: tox -e flake8 + functional-test: + name: Functional test with LXD + runs-on: ubuntu-latest + timeout-minutes: 360 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install Dependencies + run: | + pip install tox + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@master + - name: Run test + run: tox -e func + - name: Show Status + if: ${{ always() }} + run: | + model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/); + juju status -m "$model" + - name: Show Error Logs + if: ${{ always() }} + run: | + model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/); + juju debug-log -m "$model" --replay --no-tail --level ERROR diff --git a/easyrsa/.github/workflows/tox.yaml b/easyrsa/.github/workflows/tox.yaml new file mode 100644 index 0000000..b07172d --- /dev/null +++ b/easyrsa/.github/workflows/tox.yaml @@ -0,0 +1,22 @@ +name: Run tests with Tox + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + - name: Install Tox and any other packages + run: pip install tox + - name: Run Tox + run: tox -e py # Run tox using the version of Python in `PATH` diff --git a/easyrsa/.gitignore b/easyrsa/.gitignore new file mode 100644 index 0000000..fa25a92 --- /dev/null +++ b/easyrsa/.gitignore @@ -0,0 +1,5 @@ +.tox +.cache +.unit-state.db +*__pycache__ +*.pyc diff --git a/easyrsa/CONTRIBUTING.md b/easyrsa/CONTRIBUTING.md new file mode 100644 index 0000000..0f7bb82 --- /dev/null +++ b/easyrsa/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributor Guide + +This Juju charm is open source ([Apache License 2.0](./LICENSE)) and we actively seek any community contibutions +for code, suggestions and documentation. +This page details a few notes, workflows and suggestions for how to make contributions most effective and help us +all build a better charm - please give them a read before working on any contributions. + +## Licensing + +This charm has been created under the [Apache License 2.0](./LICENSE), which will cover any contributions you may +make to this project. Please familiarise yourself with the terms of the license. + +Additionally, this charm uses the Harmony CLA agreement. It’s the easiest way for you to give us permission to +use your contributions. +In effect, you’re giving us a license, but you still own the copyright — so you retain the right to modify your +code and use it in other projects. Please [sign the CLA here](https://ubuntu.com/legal/contributors/agreement) before +making any contributions. + +## Code of conduct + +We have adopted the Ubuntu code of Conduct. You can read this in full [here](https://ubuntu.com/community/code-of-conduct). + +## Contributing code + +To contribute code to this project, pleas euse the following workflow: + +1. [Submit a bug](https://bugs.launchpad.net/charm-easyrsa/+filebug) to explain the need for and track the change. +2. Create a branch on your fork of the repo with your changes, including a unit test covering the new or modified code. +3. Submit a PR. The PR description should include a link to the bug on Launchpad. +4. Update the Launchpad bug to include a link to the PR and the `review-needed` tag. +5. Once reviewed and merged, the change will become available on the edge channel and assigned to an appropriate milestone + for further release according to priority. + +## Documentation + +Documentation for this charm is currently maintained as part of the Charmed Kubernetes docs. +See [this page](https://github.com/charmed-kubernetes/kubernetes-docs/blob/master/pages/k8s/charm-easyrsa.md) diff --git a/easyrsa/LICENSE b/easyrsa/LICENSE new file mode 100644 index 0000000..0543093 --- /dev/null +++ b/easyrsa/LICENSE @@ -0,0 +1,202 @@ + + 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 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. diff --git a/easyrsa/Makefile b/easyrsa/Makefile new file mode 100644 index 0000000..a1ad3a5 --- /dev/null +++ b/easyrsa/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/easyrsa/README.md b/easyrsa/README.md new file mode 100644 index 0000000..1ad63b2 --- /dev/null +++ b/easyrsa/README.md @@ -0,0 +1,13 @@ +# EasyRSA + +This charm delivers the EasyRSA application to act as a Certificate Authority +(CA) and creates certificates for related charms. + +EasyRSA is a command line utility to build and manage Public Key +Infrastructure (PKI) Certificate Authority (CA). + +The purpose of a Public Key Infrastructure (PKI) is to facilitate the secure +electronic transfer of information. + +This charm is maintained along with the components of Charmed Kubernetes. For full information, +please visit the [official Charmed Kubernetes docs](https://www.ubuntu.com/kubernetes/docs/charm-easyrsa). diff --git a/easyrsa/actions.yaml b/easyrsa/actions.yaml new file mode 100644 index 0000000..35123a8 --- /dev/null +++ b/easyrsa/actions.yaml @@ -0,0 +1,25 @@ +"debug": + "description": "Collect debug data" +"backup": + "description": "Creates a backup bundle containing current easyrsa pki." +"restore": + "description": "Restores a pki previously backed up by 'backup' action" + "params": + "name": + "type": "string" + "description": "Name of the backup file from which the pki will be restored." + "required": + - "name" +"list-backups": + "description": "List all available easyrsa backups." +"delete-backup": + "description": "Delete specified (or all) easyrsa backups." + "params": + "name": + "type": "string" + "description": | + Name of the backup to be deleted. (list of all backups can be + acquired using 'list-backups' action) + "all": + "type": "boolean" + "description": "If set to 'true', all backups on the unit will be deleted." diff --git a/easyrsa/actions/__init__.py b/easyrsa/actions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/easyrsa/actions/actions.py b/easyrsa/actions/actions.py new file mode 100755 index 0000000..91a9e36 --- /dev/null +++ b/easyrsa/actions/actions.py @@ -0,0 +1,362 @@ +#!/usr/local/sbin/charm-env python3 +import os +import pwd +import grp +import sys +import tarfile +import shutil + +from datetime import datetime + +from charms import layer +from charms.reactive.relations import endpoint_from_name +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import ( + function_get, + function_set, + function_fail, + local_unit, + log, + leader_set, + leader_get, +) + +from reactive.easyrsa import ( + easyrsa_directory, + create_client_certificate, + create_server_certificate, +) + +# Import charm layers and start reactive +layer.import_layer_libs() +hookenv._run_atstart() + +PKI_BACKUP = '/home/ubuntu/easyrsa_backup' +# Minimal required contents of the backup tarball +TAR_STRUCTURE = {'pki', + 'pki/ca.crt', + 'pki/issued', + 'pki/issued/client.crt', + 'pki/private', + 'pki/private/ca.key', + 'pki/private/client.key', + 'pki/serial', + } + + +def _check_path_traversal(path_, parent_dir): + """Check that 'path_' does not lie outside of the 'parent_dir'. + + This function takes into account possible '../' in 'path_' and also + any symlinks that could point somewhere outside the expected 'parent_dir' + + NOTE(mkalcok): This implementation could be improved by using + 'os.path.commonpath()'. However it's available only in + py35+. + + :param path_: Path to be tested + :param parent_dir: Directory in which the 'path_' must lie + :raises: RuntimeError if 'path_' is outside of the 'parent_dir' + """ + full_path = os.path.realpath(path_) + parent_dir = os.path.realpath(parent_dir) + if not parent_dir.endswith('/'): + parent_dir += '/' + + if os.path.commonprefix([parent_dir, full_path]) != parent_dir: + err_msg = "Path traversal detected. '{}' tries to travers out " \ + "of {}".format(full_path, parent_dir) + log(err_msg, hookenv.ERROR) + raise RuntimeError(err_msg) + + +def _ensure_backup_dir_exists(): + """Ensure that backup directory exists with proper ownership""" + uid = pwd.getpwnam("ubuntu").pw_uid + gid = grp.getgrnam("ubuntu").gr_gid + try: + os.mkdir(PKI_BACKUP, mode=0o700) + except FileExistsError: + pass + os.chown(PKI_BACKUP, uid, gid) + + if not os.path.isdir(PKI_BACKUP): + log("Backup destination '{}' is not a directory".format(PKI_BACKUP), + hookenv.ERROR) + raise RuntimeError('Backup destination is not a directory.') + + +def _verify_backup(pki_tar): + """ + Verify that backup archive contains expected files + + :param pki_tar: Tarfile object containing easyrsa backup + """ + log("Verifying backup", hookenv.DEBUG) + members = set(pki_tar.getnames()) + + # Check that backup contains all the expected/required files + if not TAR_STRUCTURE.issubset(members): + raise RuntimeError("Backup has unexpected content. Corrupted file?") + log("Check expected files - OK", hookenv.DEBUG) + + # Check for path traversal attempts in tar file + pki_dir = os.path.join(easyrsa_directory, 'pki') + for path_ in members: + destination = os.path.join(pki_dir, path_) + _check_path_traversal(destination, pki_dir) + + +def _replace_pki(pki_tar, pki_dir): + """ + Safely replace easyrsa pki directory. + + If there are any problems during the extraction of the backup, original + pki directory will be brought back and error raised. + + :param pki_tar: Tarfile object containing easyrsa backup + :param pki_dir: Destination for extraction of easyrsa backup + :return: None + """ + safety_backup = os.path.join(easyrsa_directory, 'pki_backup') + shutil.move(pki_dir, safety_backup) + try: + log("Extracting pki from backup", hookenv.DEBUG) + pki_tar.extractall(easyrsa_directory) + except Exception as exc: + log("pki extraction failed: {}".format(exc), + hookenv.WARNING) + log("Restoring original pki.", hookenv.INFO) + shutil.move(safety_backup, pki_dir) + raise RuntimeError('Failed to extract backup bundle. ' + 'Error: {}'.format(exc)) + else: + shutil.rmtree(safety_backup) + + +def _update_leadership_data(pki_dir, cert_dir, key_dir): + """ + Update certificates stored in the leaders database. + + :param pki_dir: location of easyrsa pki (usually /EasyRSA/pki) + :param cert_dir: location of issued certificates (usually /issued) + :param key_dir: location of private keys (usually /private) + :return: None + """ + ca_cert = os.path.join(pki_dir, 'ca.crt') + ca_key = os.path.join(key_dir, 'ca.key') + serial_file = os.path.join(pki_dir, 'serial') + global_client_cert = os.path.join(cert_dir, 'client.crt') + global_client_key = os.path.join(key_dir, 'client.key') + + with open(ca_cert, 'r') as stream: + data = stream.read() + log("Updating CA certificate in leader's database", + hookenv.INFO) + log("CA certificate:\n{}".format(data), hookenv.DEBUG) + leader_set({ + 'certificate_authority': data}) + + with open(ca_key, 'r') as stream: + log("Updating CA key in leader's database", + hookenv.INFO) + leader_set({ + 'certificate_authority_key': stream.read()}) + + with open(serial_file, 'r') as stream: + log("Updating CA serial in leader's database", + hookenv.INFO) + leader_set({ + 'certificate_authority_serial': stream.read()}) + + with open(global_client_cert) as stream: + data = stream.read() + log("Updating (legacy) global client certificate in leader's database", + hookenv.INFO) + log(data, hookenv.DEBUG) + leader_set({'client_certificate': data}) + + with open(global_client_key) as stream: + log("Updating (legacy) global client key in leader's database", + hookenv.INFO) + leader_set({'client_key': stream.read()}) + + +def backup(): + """ + Implementation of easyrsa 'backup' action. + + Currently deployed pki is packed into tarball and stored in the + backups directory. + """ + _ensure_backup_dir_exists() + + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + backup_name = 'easyrsa-{}.tar.gz'.format(timestamp) + backup_path = os.path.join(PKI_BACKUP, backup_name) + with tarfile.open(backup_path, mode='w:gz') as pki_tar: + pki_tar.add(os.path.join(easyrsa_directory, 'pki'), 'pki') + + log("Backup created and saved to '{}'".format(backup_path), hookenv.DEBUG) + function_set({ + 'command': 'juju scp {}:{} .'.format(local_unit(), backup_path), + 'message': 'Backup archive created successfully. Use the juju scp ' + 'command to copy it to your local machine.' + }) + + +def restore(): + """ + Implementation of easyrsa 'restore' action + + Backup restoration process can be summarized as following: + + * Selected backup is scanned and verified + * Contents of the backup are unpacked into /EasyRSA/pki + * Data that are stored in the local database are updated + * All units that have relation with this easyrsa unit will be notified + about the certificate changes. + """ + pki_dir = os.path.join(easyrsa_directory, 'pki') + backup_name = function_get('name') + + if backup_name is None: + raise RuntimeError("Parameter 'name' is required.") + + log("Restoring pki from backup file {}".format(backup_name), hookenv.INFO) + + backup_path = os.path.join(PKI_BACKUP, backup_name) + + if not os.path.isfile(backup_path): + log("Backup file '{}' does not exists.".format(backup_path), + hookenv.ERROR) + raise RuntimeError("Backup with name '{}' does not exist. Use action " + "'list-backups' to list all available " + "backups".format(backup_name)) + + with tarfile.open(backup_path, 'r:gz') as pki_tar: + _verify_backup(pki_tar) + _replace_pki(pki_tar, pki_dir) + + cert_dir = os.path.join(pki_dir, 'issued') + key_dir = os.path.join(pki_dir, 'private') + + # Update CA and global client data stored in the local leader's database + # NOTE(mkalcok): Easyrsa does not really support HA mode, so it's usually + # run as a single unit/model + _update_leadership_data(pki_dir, cert_dir, key_dir) + + ca_cert = leader_get('certificate_authority') + tls = endpoint_from_name('client') + log("Sending CA certificate to all related units", hookenv.INFO) + tls.set_ca(ca_cert) + log("Sending global client certificate and key to all related units", + hookenv.INFO) + tls.set_client_cert(leader_get('client_certificate'), + leader_get('client_key')) + for client in tls.all_requests: + try: + cert_file = os.path.join(cert_dir, + "{}.crt".format(client.common_name)) + key_file = os.path.join(key_dir, + "{}.key".format(client.common_name)) + with open(cert_file, 'r') as file: + cert = file.read() + with open(key_file, 'r') as file: + key = file.read() + log("Sending certificate for '{}' to unit" + "'{}'".format(client.common_name, client.unit_name), + hookenv.INFO) + log(cert, hookenv.DEBUG) + client.set_cert(cert, key) + + except FileNotFoundError: + log("Certificate for '{}' not found in backup. " + "Generating new one.", hookenv.INFO) + if client.cert_type == 'client': + cert, key = create_client_certificate(client.common_name) + elif client.cert_type == 'server': + cert, key = create_server_certificate(client.common_name, + client.sans, + client.common_name) + else: + # This use case should not really happen as easyrsa charm + # does not support Application type certificates + raise RuntimeError('Unrecognized certificate request type ' + '"{}".'.format(client.cert_type)) + log("Sending certificate for '{}' to unit" + "'{}'".format(client.common_name, client.unit_name), + hookenv.INFO) + log(cert, hookenv.DEBUG) + client.set_cert(cert, key) + + hookenv._run_atexit() + + +def list_backups(): + """Implementation of easyrsa 'list-backups' action.""" + file_list = [] + + try: + file_list = os.listdir(PKI_BACKUP) + except FileNotFoundError: + pass + + if file_list: + message = 'Available backup files:' + for file in file_list: + message += '\n{}'.format(file) + else: + message = 'There are no available backup files.' + + function_set({'message': message}) + + +def delete_backup(): + """Implementation of easyrsa 'delete-backup' action""" + backup_name = function_get('name') + delete_all = function_get('all') + + if not delete_all: + if backup_name is None: + raise RuntimeError("Parameter 'name' is required if parameter " + "'all' is False.") + log("Removing backup '{}'".format(backup_name), hookenv.INFO) + delete_file = os.path.join(PKI_BACKUP, backup_name) + _check_path_traversal(delete_file, PKI_BACKUP) + try: + os.remove(delete_file) + except FileNotFoundError: + err_msg = "Backup file '{}' does not exist".format(backup_name) + log(err_msg, hookenv.ERROR) + raise RuntimeError(err_msg) + else: + log("Removing all backup files.", hookenv.INFO) + shutil.rmtree(PKI_BACKUP) + + +ACTIONS = {'backup': backup, + 'restore': restore, + 'list-backups': list_backups, + 'delete-backup': delete_backup + } + + +def main(args): + action_name = os.path.basename(args.pop(0)) + try: + action = ACTIONS[action_name] + except KeyError: + s = "Action {} undefined".format(action_name) + function_fail(s) + return + else: + try: + log("Running action '{}'.".format(action_name)) + action() + except Exception as e: + function_fail("Action {} failed: {}".format(action_name, str(e))) + + +if __name__ == '__main__': + main(sys.argv) diff --git a/easyrsa/actions/backup b/easyrsa/actions/backup new file mode 100755 index 0000000..91a9e36 --- /dev/null +++ b/easyrsa/actions/backup @@ -0,0 +1,362 @@ +#!/usr/local/sbin/charm-env python3 +import os +import pwd +import grp +import sys +import tarfile +import shutil + +from datetime import datetime + +from charms import layer +from charms.reactive.relations import endpoint_from_name +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import ( + function_get, + function_set, + function_fail, + local_unit, + log, + leader_set, + leader_get, +) + +from reactive.easyrsa import ( + easyrsa_directory, + create_client_certificate, + create_server_certificate, +) + +# Import charm layers and start reactive +layer.import_layer_libs() +hookenv._run_atstart() + +PKI_BACKUP = '/home/ubuntu/easyrsa_backup' +# Minimal required contents of the backup tarball +TAR_STRUCTURE = {'pki', + 'pki/ca.crt', + 'pki/issued', + 'pki/issued/client.crt', + 'pki/private', + 'pki/private/ca.key', + 'pki/private/client.key', + 'pki/serial', + } + + +def _check_path_traversal(path_, parent_dir): + """Check that 'path_' does not lie outside of the 'parent_dir'. + + This function takes into account possible '../' in 'path_' and also + any symlinks that could point somewhere outside the expected 'parent_dir' + + NOTE(mkalcok): This implementation could be improved by using + 'os.path.commonpath()'. However it's available only in + py35+. + + :param path_: Path to be tested + :param parent_dir: Directory in which the 'path_' must lie + :raises: RuntimeError if 'path_' is outside of the 'parent_dir' + """ + full_path = os.path.realpath(path_) + parent_dir = os.path.realpath(parent_dir) + if not parent_dir.endswith('/'): + parent_dir += '/' + + if os.path.commonprefix([parent_dir, full_path]) != parent_dir: + err_msg = "Path traversal detected. '{}' tries to travers out " \ + "of {}".format(full_path, parent_dir) + log(err_msg, hookenv.ERROR) + raise RuntimeError(err_msg) + + +def _ensure_backup_dir_exists(): + """Ensure that backup directory exists with proper ownership""" + uid = pwd.getpwnam("ubuntu").pw_uid + gid = grp.getgrnam("ubuntu").gr_gid + try: + os.mkdir(PKI_BACKUP, mode=0o700) + except FileExistsError: + pass + os.chown(PKI_BACKUP, uid, gid) + + if not os.path.isdir(PKI_BACKUP): + log("Backup destination '{}' is not a directory".format(PKI_BACKUP), + hookenv.ERROR) + raise RuntimeError('Backup destination is not a directory.') + + +def _verify_backup(pki_tar): + """ + Verify that backup archive contains expected files + + :param pki_tar: Tarfile object containing easyrsa backup + """ + log("Verifying backup", hookenv.DEBUG) + members = set(pki_tar.getnames()) + + # Check that backup contains all the expected/required files + if not TAR_STRUCTURE.issubset(members): + raise RuntimeError("Backup has unexpected content. Corrupted file?") + log("Check expected files - OK", hookenv.DEBUG) + + # Check for path traversal attempts in tar file + pki_dir = os.path.join(easyrsa_directory, 'pki') + for path_ in members: + destination = os.path.join(pki_dir, path_) + _check_path_traversal(destination, pki_dir) + + +def _replace_pki(pki_tar, pki_dir): + """ + Safely replace easyrsa pki directory. + + If there are any problems during the extraction of the backup, original + pki directory will be brought back and error raised. + + :param pki_tar: Tarfile object containing easyrsa backup + :param pki_dir: Destination for extraction of easyrsa backup + :return: None + """ + safety_backup = os.path.join(easyrsa_directory, 'pki_backup') + shutil.move(pki_dir, safety_backup) + try: + log("Extracting pki from backup", hookenv.DEBUG) + pki_tar.extractall(easyrsa_directory) + except Exception as exc: + log("pki extraction failed: {}".format(exc), + hookenv.WARNING) + log("Restoring original pki.", hookenv.INFO) + shutil.move(safety_backup, pki_dir) + raise RuntimeError('Failed to extract backup bundle. ' + 'Error: {}'.format(exc)) + else: + shutil.rmtree(safety_backup) + + +def _update_leadership_data(pki_dir, cert_dir, key_dir): + """ + Update certificates stored in the leaders database. + + :param pki_dir: location of easyrsa pki (usually /EasyRSA/pki) + :param cert_dir: location of issued certificates (usually /issued) + :param key_dir: location of private keys (usually /private) + :return: None + """ + ca_cert = os.path.join(pki_dir, 'ca.crt') + ca_key = os.path.join(key_dir, 'ca.key') + serial_file = os.path.join(pki_dir, 'serial') + global_client_cert = os.path.join(cert_dir, 'client.crt') + global_client_key = os.path.join(key_dir, 'client.key') + + with open(ca_cert, 'r') as stream: + data = stream.read() + log("Updating CA certificate in leader's database", + hookenv.INFO) + log("CA certificate:\n{}".format(data), hookenv.DEBUG) + leader_set({ + 'certificate_authority': data}) + + with open(ca_key, 'r') as stream: + log("Updating CA key in leader's database", + hookenv.INFO) + leader_set({ + 'certificate_authority_key': stream.read()}) + + with open(serial_file, 'r') as stream: + log("Updating CA serial in leader's database", + hookenv.INFO) + leader_set({ + 'certificate_authority_serial': stream.read()}) + + with open(global_client_cert) as stream: + data = stream.read() + log("Updating (legacy) global client certificate in leader's database", + hookenv.INFO) + log(data, hookenv.DEBUG) + leader_set({'client_certificate': data}) + + with open(global_client_key) as stream: + log("Updating (legacy) global client key in leader's database", + hookenv.INFO) + leader_set({'client_key': stream.read()}) + + +def backup(): + """ + Implementation of easyrsa 'backup' action. + + Currently deployed pki is packed into tarball and stored in the + backups directory. + """ + _ensure_backup_dir_exists() + + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + backup_name = 'easyrsa-{}.tar.gz'.format(timestamp) + backup_path = os.path.join(PKI_BACKUP, backup_name) + with tarfile.open(backup_path, mode='w:gz') as pki_tar: + pki_tar.add(os.path.join(easyrsa_directory, 'pki'), 'pki') + + log("Backup created and saved to '{}'".format(backup_path), hookenv.DEBUG) + function_set({ + 'command': 'juju scp {}:{} .'.format(local_unit(), backup_path), + 'message': 'Backup archive created successfully. Use the juju scp ' + 'command to copy it to your local machine.' + }) + + +def restore(): + """ + Implementation of easyrsa 'restore' action + + Backup restoration process can be summarized as following: + + * Selected backup is scanned and verified + * Contents of the backup are unpacked into /EasyRSA/pki + * Data that are stored in the local database are updated + * All units that have relation with this easyrsa unit will be notified + about the certificate changes. + """ + pki_dir = os.path.join(easyrsa_directory, 'pki') + backup_name = function_get('name') + + if backup_name is None: + raise RuntimeError("Parameter 'name' is required.") + + log("Restoring pki from backup file {}".format(backup_name), hookenv.INFO) + + backup_path = os.path.join(PKI_BACKUP, backup_name) + + if not os.path.isfile(backup_path): + log("Backup file '{}' does not exists.".format(backup_path), + hookenv.ERROR) + raise RuntimeError("Backup with name '{}' does not exist. Use action " + "'list-backups' to list all available " + "backups".format(backup_name)) + + with tarfile.open(backup_path, 'r:gz') as pki_tar: + _verify_backup(pki_tar) + _replace_pki(pki_tar, pki_dir) + + cert_dir = os.path.join(pki_dir, 'issued') + key_dir = os.path.join(pki_dir, 'private') + + # Update CA and global client data stored in the local leader's database + # NOTE(mkalcok): Easyrsa does not really support HA mode, so it's usually + # run as a single unit/model + _update_leadership_data(pki_dir, cert_dir, key_dir) + + ca_cert = leader_get('certificate_authority') + tls = endpoint_from_name('client') + log("Sending CA certificate to all related units", hookenv.INFO) + tls.set_ca(ca_cert) + log("Sending global client certificate and key to all related units", + hookenv.INFO) + tls.set_client_cert(leader_get('client_certificate'), + leader_get('client_key')) + for client in tls.all_requests: + try: + cert_file = os.path.join(cert_dir, + "{}.crt".format(client.common_name)) + key_file = os.path.join(key_dir, + "{}.key".format(client.common_name)) + with open(cert_file, 'r') as file: + cert = file.read() + with open(key_file, 'r') as file: + key = file.read() + log("Sending certificate for '{}' to unit" + "'{}'".format(client.common_name, client.unit_name), + hookenv.INFO) + log(cert, hookenv.DEBUG) + client.set_cert(cert, key) + + except FileNotFoundError: + log("Certificate for '{}' not found in backup. " + "Generating new one.", hookenv.INFO) + if client.cert_type == 'client': + cert, key = create_client_certificate(client.common_name) + elif client.cert_type == 'server': + cert, key = create_server_certificate(client.common_name, + client.sans, + client.common_name) + else: + # This use case should not really happen as easyrsa charm + # does not support Application type certificates + raise RuntimeError('Unrecognized certificate request type ' + '"{}".'.format(client.cert_type)) + log("Sending certificate for '{}' to unit" + "'{}'".format(client.common_name, client.unit_name), + hookenv.INFO) + log(cert, hookenv.DEBUG) + client.set_cert(cert, key) + + hookenv._run_atexit() + + +def list_backups(): + """Implementation of easyrsa 'list-backups' action.""" + file_list = [] + + try: + file_list = os.listdir(PKI_BACKUP) + except FileNotFoundError: + pass + + if file_list: + message = 'Available backup files:' + for file in file_list: + message += '\n{}'.format(file) + else: + message = 'There are no available backup files.' + + function_set({'message': message}) + + +def delete_backup(): + """Implementation of easyrsa 'delete-backup' action""" + backup_name = function_get('name') + delete_all = function_get('all') + + if not delete_all: + if backup_name is None: + raise RuntimeError("Parameter 'name' is required if parameter " + "'all' is False.") + log("Removing backup '{}'".format(backup_name), hookenv.INFO) + delete_file = os.path.join(PKI_BACKUP, backup_name) + _check_path_traversal(delete_file, PKI_BACKUP) + try: + os.remove(delete_file) + except FileNotFoundError: + err_msg = "Backup file '{}' does not exist".format(backup_name) + log(err_msg, hookenv.ERROR) + raise RuntimeError(err_msg) + else: + log("Removing all backup files.", hookenv.INFO) + shutil.rmtree(PKI_BACKUP) + + +ACTIONS = {'backup': backup, + 'restore': restore, + 'list-backups': list_backups, + 'delete-backup': delete_backup + } + + +def main(args): + action_name = os.path.basename(args.pop(0)) + try: + action = ACTIONS[action_name] + except KeyError: + s = "Action {} undefined".format(action_name) + function_fail(s) + return + else: + try: + log("Running action '{}'.".format(action_name)) + action() + except Exception as e: + function_fail("Action {} failed: {}".format(action_name, str(e))) + + +if __name__ == '__main__': + main(sys.argv) diff --git a/easyrsa/actions/debug b/easyrsa/actions/debug new file mode 100755 index 0000000..8ba160e --- /dev/null +++ b/easyrsa/actions/debug @@ -0,0 +1,102 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import tarfile +import tempfile +import traceback +from contextlib import contextmanager +from datetime import datetime +from charmhelpers.core.hookenv import action_set, local_unit + +archive_dir = None +log_file = None + + +@contextmanager +def archive_context(): + """ Open a context with a new temporary directory. + + When the context closes, the directory is archived, and the archive + location is added to Juju action output. """ + global archive_dir + global log_file + with tempfile.TemporaryDirectory() as temp_dir: + name = "debug-" + datetime.now().strftime("%Y%m%d%H%M%S") + archive_dir = os.path.join(temp_dir, name) + os.makedirs(archive_dir) + with open("%s/debug.log" % archive_dir, "w") as log_file: + yield + os.chdir(temp_dir) + tar_path = "/home/ubuntu/%s.tar.gz" % name + with tarfile.open(tar_path, "w:gz") as f: + f.add(name) + action_set({ + "path": tar_path, + "command": "juju scp %s:%s ." % (local_unit(), tar_path), + "message": " ".join([ + "Archive has been created on unit %s." % local_unit(), + "Use the juju scp command to copy it to your local machine." + ]) + }) + + +def log(msg): + """ Log a message that will be included in the debug archive. + + Must be run within archive_context """ + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for line in str(msg).splitlines(): + log_file.write(timestamp + " | " + line.rstrip() + "\n") + + +def run_script(script): + """ Run a single script. Must be run within archive_context """ + log("Running script: " + script) + script_dir = os.path.join(archive_dir, script) + os.makedirs(script_dir) + env = os.environ.copy() + env["PYTHONPATH"] = "lib" # allow same imports as reactive code + env["DEBUG_SCRIPT_DIR"] = script_dir + with open(script_dir + "/stdout", "w") as stdout: + with open(script_dir + "/stderr", "w") as stderr: + process = subprocess.Popen( + "debug-scripts/" + script, + stdout=stdout, stderr=stderr, env=env + ) + try: + exit_code = process.wait(timeout=300) + except subprocess.TimeoutExpired: + log("ERROR: still running, terminating") + process.terminate() + try: + exit_code = process.wait(timeout=10) + except subprocess.TimeoutExpired: + log("ERROR: still running, killing") + process.kill() + exit_code = process.wait(timeout=10) + if exit_code != 0: + log("ERROR: %s failed with exit code %d" % (script, exit_code)) + + +def run_all_scripts(): + """ Run all scripts. For the sake of robustness, log and ignore any + exceptions that occur. + + Must be run within archive_context """ + scripts = os.listdir("debug-scripts") + for script in scripts: + try: + run_script(script) + except: + log(traceback.format_exc()) + + +def main(): + """ Open an archive context and run all scripts. """ + with archive_context(): + run_all_scripts() + + +if __name__ == "__main__": + main() diff --git a/easyrsa/actions/delete-backup b/easyrsa/actions/delete-backup new file mode 100755 index 0000000..91a9e36 --- /dev/null +++ b/easyrsa/actions/delete-backup @@ -0,0 +1,362 @@ +#!/usr/local/sbin/charm-env python3 +import os +import pwd +import grp +import sys +import tarfile +import shutil + +from datetime import datetime + +from charms import layer +from charms.reactive.relations import endpoint_from_name +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import ( + function_get, + function_set, + function_fail, + local_unit, + log, + leader_set, + leader_get, +) + +from reactive.easyrsa import ( + easyrsa_directory, + create_client_certificate, + create_server_certificate, +) + +# Import charm layers and start reactive +layer.import_layer_libs() +hookenv._run_atstart() + +PKI_BACKUP = '/home/ubuntu/easyrsa_backup' +# Minimal required contents of the backup tarball +TAR_STRUCTURE = {'pki', + 'pki/ca.crt', + 'pki/issued', + 'pki/issued/client.crt', + 'pki/private', + 'pki/private/ca.key', + 'pki/private/client.key', + 'pki/serial', + } + + +def _check_path_traversal(path_, parent_dir): + """Check that 'path_' does not lie outside of the 'parent_dir'. + + This function takes into account possible '../' in 'path_' and also + any symlinks that could point somewhere outside the expected 'parent_dir' + + NOTE(mkalcok): This implementation could be improved by using + 'os.path.commonpath()'. However it's available only in + py35+. + + :param path_: Path to be tested + :param parent_dir: Directory in which the 'path_' must lie + :raises: RuntimeError if 'path_' is outside of the 'parent_dir' + """ + full_path = os.path.realpath(path_) + parent_dir = os.path.realpath(parent_dir) + if not parent_dir.endswith('/'): + parent_dir += '/' + + if os.path.commonprefix([parent_dir, full_path]) != parent_dir: + err_msg = "Path traversal detected. '{}' tries to travers out " \ + "of {}".format(full_path, parent_dir) + log(err_msg, hookenv.ERROR) + raise RuntimeError(err_msg) + + +def _ensure_backup_dir_exists(): + """Ensure that backup directory exists with proper ownership""" + uid = pwd.getpwnam("ubuntu").pw_uid + gid = grp.getgrnam("ubuntu").gr_gid + try: + os.mkdir(PKI_BACKUP, mode=0o700) + except FileExistsError: + pass + os.chown(PKI_BACKUP, uid, gid) + + if not os.path.isdir(PKI_BACKUP): + log("Backup destination '{}' is not a directory".format(PKI_BACKUP), + hookenv.ERROR) + raise RuntimeError('Backup destination is not a directory.') + + +def _verify_backup(pki_tar): + """ + Verify that backup archive contains expected files + + :param pki_tar: Tarfile object containing easyrsa backup + """ + log("Verifying backup", hookenv.DEBUG) + members = set(pki_tar.getnames()) + + # Check that backup contains all the expected/required files + if not TAR_STRUCTURE.issubset(members): + raise RuntimeError("Backup has unexpected content. Corrupted file?") + log("Check expected files - OK", hookenv.DEBUG) + + # Check for path traversal attempts in tar file + pki_dir = os.path.join(easyrsa_directory, 'pki') + for path_ in members: + destination = os.path.join(pki_dir, path_) + _check_path_traversal(destination, pki_dir) + + +def _replace_pki(pki_tar, pki_dir): + """ + Safely replace easyrsa pki directory. + + If there are any problems during the extraction of the backup, original + pki directory will be brought back and error raised. + + :param pki_tar: Tarfile object containing easyrsa backup + :param pki_dir: Destination for extraction of easyrsa backup + :return: None + """ + safety_backup = os.path.join(easyrsa_directory, 'pki_backup') + shutil.move(pki_dir, safety_backup) + try: + log("Extracting pki from backup", hookenv.DEBUG) + pki_tar.extractall(easyrsa_directory) + except Exception as exc: + log("pki extraction failed: {}".format(exc), + hookenv.WARNING) + log("Restoring original pki.", hookenv.INFO) + shutil.move(safety_backup, pki_dir) + raise RuntimeError('Failed to extract backup bundle. ' + 'Error: {}'.format(exc)) + else: + shutil.rmtree(safety_backup) + + +def _update_leadership_data(pki_dir, cert_dir, key_dir): + """ + Update certificates stored in the leaders database. + + :param pki_dir: location of easyrsa pki (usually /EasyRSA/pki) + :param cert_dir: location of issued certificates (usually /issued) + :param key_dir: location of private keys (usually /private) + :return: None + """ + ca_cert = os.path.join(pki_dir, 'ca.crt') + ca_key = os.path.join(key_dir, 'ca.key') + serial_file = os.path.join(pki_dir, 'serial') + global_client_cert = os.path.join(cert_dir, 'client.crt') + global_client_key = os.path.join(key_dir, 'client.key') + + with open(ca_cert, 'r') as stream: + data = stream.read() + log("Updating CA certificate in leader's database", + hookenv.INFO) + log("CA certificate:\n{}".format(data), hookenv.DEBUG) + leader_set({ + 'certificate_authority': data}) + + with open(ca_key, 'r') as stream: + log("Updating CA key in leader's database", + hookenv.INFO) + leader_set({ + 'certificate_authority_key': stream.read()}) + + with open(serial_file, 'r') as stream: + log("Updating CA serial in leader's database", + hookenv.INFO) + leader_set({ + 'certificate_authority_serial': stream.read()}) + + with open(global_client_cert) as stream: + data = stream.read() + log("Updating (legacy) global client certificate in leader's database", + hookenv.INFO) + log(data, hookenv.DEBUG) + leader_set({'client_certificate': data}) + + with open(global_client_key) as stream: + log("Updating (legacy) global client key in leader's database", + hookenv.INFO) + leader_set({'client_key': stream.read()}) + + +def backup(): + """ + Implementation of easyrsa 'backup' action. + + Currently deployed pki is packed into tarball and stored in the + backups directory. + """ + _ensure_backup_dir_exists() + + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + backup_name = 'easyrsa-{}.tar.gz'.format(timestamp) + backup_path = os.path.join(PKI_BACKUP, backup_name) + with tarfile.open(backup_path, mode='w:gz') as pki_tar: + pki_tar.add(os.path.join(easyrsa_directory, 'pki'), 'pki') + + log("Backup created and saved to '{}'".format(backup_path), hookenv.DEBUG) + function_set({ + 'command': 'juju scp {}:{} .'.format(local_unit(), backup_path), + 'message': 'Backup archive created successfully. Use the juju scp ' + 'command to copy it to your local machine.' + }) + + +def restore(): + """ + Implementation of easyrsa 'restore' action + + Backup restoration process can be summarized as following: + + * Selected backup is scanned and verified + * Contents of the backup are unpacked into /EasyRSA/pki + * Data that are stored in the local database are updated + * All units that have relation with this easyrsa unit will be notified + about the certificate changes. + """ + pki_dir = os.path.join(easyrsa_directory, 'pki') + backup_name = function_get('name') + + if backup_name is None: + raise RuntimeError("Parameter 'name' is required.") + + log("Restoring pki from backup file {}".format(backup_name), hookenv.INFO) + + backup_path = os.path.join(PKI_BACKUP, backup_name) + + if not os.path.isfile(backup_path): + log("Backup file '{}' does not exists.".format(backup_path), + hookenv.ERROR) + raise RuntimeError("Backup with name '{}' does not exist. Use action " + "'list-backups' to list all available " + "backups".format(backup_name)) + + with tarfile.open(backup_path, 'r:gz') as pki_tar: + _verify_backup(pki_tar) + _replace_pki(pki_tar, pki_dir) + + cert_dir = os.path.join(pki_dir, 'issued') + key_dir = os.path.join(pki_dir, 'private') + + # Update CA and global client data stored in the local leader's database + # NOTE(mkalcok): Easyrsa does not really support HA mode, so it's usually + # run as a single unit/model + _update_leadership_data(pki_dir, cert_dir, key_dir) + + ca_cert = leader_get('certificate_authority') + tls = endpoint_from_name('client') + log("Sending CA certificate to all related units", hookenv.INFO) + tls.set_ca(ca_cert) + log("Sending global client certificate and key to all related units", + hookenv.INFO) + tls.set_client_cert(leader_get('client_certificate'), + leader_get('client_key')) + for client in tls.all_requests: + try: + cert_file = os.path.join(cert_dir, + "{}.crt".format(client.common_name)) + key_file = os.path.join(key_dir, + "{}.key".format(client.common_name)) + with open(cert_file, 'r') as file: + cert = file.read() + with open(key_file, 'r') as file: + key = file.read() + log("Sending certificate for '{}' to unit" + "'{}'".format(client.common_name, client.unit_name), + hookenv.INFO) + log(cert, hookenv.DEBUG) + client.set_cert(cert, key) + + except FileNotFoundError: + log("Certificate for '{}' not found in backup. " + "Generating new one.", hookenv.INFO) + if client.cert_type == 'client': + cert, key = create_client_certificate(client.common_name) + elif client.cert_type == 'server': + cert, key = create_server_certificate(client.common_name, + client.sans, + client.common_name) + else: + # This use case should not really happen as easyrsa charm + # does not support Application type certificates + raise RuntimeError('Unrecognized certificate request type ' + '"{}".'.format(client.cert_type)) + log("Sending certificate for '{}' to unit" + "'{}'".format(client.common_name, client.unit_name), + hookenv.INFO) + log(cert, hookenv.DEBUG) + client.set_cert(cert, key) + + hookenv._run_atexit() + + +def list_backups(): + """Implementation of easyrsa 'list-backups' action.""" + file_list = [] + + try: + file_list = os.listdir(PKI_BACKUP) + except FileNotFoundError: + pass + + if file_list: + message = 'Available backup files:' + for file in file_list: + message += '\n{}'.format(file) + else: + message = 'There are no available backup files.' + + function_set({'message': message}) + + +def delete_backup(): + """Implementation of easyrsa 'delete-backup' action""" + backup_name = function_get('name') + delete_all = function_get('all') + + if not delete_all: + if backup_name is None: + raise RuntimeError("Parameter 'name' is required if parameter " + "'all' is False.") + log("Removing backup '{}'".format(backup_name), hookenv.INFO) + delete_file = os.path.join(PKI_BACKUP, backup_name) + _check_path_traversal(delete_file, PKI_BACKUP) + try: + os.remove(delete_file) + except FileNotFoundError: + err_msg = "Backup file '{}' does not exist".format(backup_name) + log(err_msg, hookenv.ERROR) + raise RuntimeError(err_msg) + else: + log("Removing all backup files.", hookenv.INFO) + shutil.rmtree(PKI_BACKUP) + + +ACTIONS = {'backup': backup, + 'restore': restore, + 'list-backups': list_backups, + 'delete-backup': delete_backup + } + + +def main(args): + action_name = os.path.basename(args.pop(0)) + try: + action = ACTIONS[action_name] + except KeyError: + s = "Action {} undefined".format(action_name) + function_fail(s) + return + else: + try: + log("Running action '{}'.".format(action_name)) + action() + except Exception as e: + function_fail("Action {} failed: {}".format(action_name, str(e))) + + +if __name__ == '__main__': + main(sys.argv) diff --git a/easyrsa/actions/list-backups b/easyrsa/actions/list-backups new file mode 100755 index 0000000..91a9e36 --- /dev/null +++ b/easyrsa/actions/list-backups @@ -0,0 +1,362 @@ +#!/usr/local/sbin/charm-env python3 +import os +import pwd +import grp +import sys +import tarfile +import shutil + +from datetime import datetime + +from charms import layer +from charms.reactive.relations import endpoint_from_name +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import ( + function_get, + function_set, + function_fail, + local_unit, + log, + leader_set, + leader_get, +) + +from reactive.easyrsa import ( + easyrsa_directory, + create_client_certificate, + create_server_certificate, +) + +# Import charm layers and start reactive +layer.import_layer_libs() +hookenv._run_atstart() + +PKI_BACKUP = '/home/ubuntu/easyrsa_backup' +# Minimal required contents of the backup tarball +TAR_STRUCTURE = {'pki', + 'pki/ca.crt', + 'pki/issued', + 'pki/issued/client.crt', + 'pki/private', + 'pki/private/ca.key', + 'pki/private/client.key', + 'pki/serial', + } + + +def _check_path_traversal(path_, parent_dir): + """Check that 'path_' does not lie outside of the 'parent_dir'. + + This function takes into account possible '../' in 'path_' and also + any symlinks that could point somewhere outside the expected 'parent_dir' + + NOTE(mkalcok): This implementation could be improved by using + 'os.path.commonpath()'. However it's available only in + py35+. + + :param path_: Path to be tested + :param parent_dir: Directory in which the 'path_' must lie + :raises: RuntimeError if 'path_' is outside of the 'parent_dir' + """ + full_path = os.path.realpath(path_) + parent_dir = os.path.realpath(parent_dir) + if not parent_dir.endswith('/'): + parent_dir += '/' + + if os.path.commonprefix([parent_dir, full_path]) != parent_dir: + err_msg = "Path traversal detected. '{}' tries to travers out " \ + "of {}".format(full_path, parent_dir) + log(err_msg, hookenv.ERROR) + raise RuntimeError(err_msg) + + +def _ensure_backup_dir_exists(): + """Ensure that backup directory exists with proper ownership""" + uid = pwd.getpwnam("ubuntu").pw_uid + gid = grp.getgrnam("ubuntu").gr_gid + try: + os.mkdir(PKI_BACKUP, mode=0o700) + except FileExistsError: + pass + os.chown(PKI_BACKUP, uid, gid) + + if not os.path.isdir(PKI_BACKUP): + log("Backup destination '{}' is not a directory".format(PKI_BACKUP), + hookenv.ERROR) + raise RuntimeError('Backup destination is not a directory.') + + +def _verify_backup(pki_tar): + """ + Verify that backup archive contains expected files + + :param pki_tar: Tarfile object containing easyrsa backup + """ + log("Verifying backup", hookenv.DEBUG) + members = set(pki_tar.getnames()) + + # Check that backup contains all the expected/required files + if not TAR_STRUCTURE.issubset(members): + raise RuntimeError("Backup has unexpected content. Corrupted file?") + log("Check expected files - OK", hookenv.DEBUG) + + # Check for path traversal attempts in tar file + pki_dir = os.path.join(easyrsa_directory, 'pki') + for path_ in members: + destination = os.path.join(pki_dir, path_) + _check_path_traversal(destination, pki_dir) + + +def _replace_pki(pki_tar, pki_dir): + """ + Safely replace easyrsa pki directory. + + If there are any problems during the extraction of the backup, original + pki directory will be brought back and error raised. + + :param pki_tar: Tarfile object containing easyrsa backup + :param pki_dir: Destination for extraction of easyrsa backup + :return: None + """ + safety_backup = os.path.join(easyrsa_directory, 'pki_backup') + shutil.move(pki_dir, safety_backup) + try: + log("Extracting pki from backup", hookenv.DEBUG) + pki_tar.extractall(easyrsa_directory) + except Exception as exc: + log("pki extraction failed: {}".format(exc), + hookenv.WARNING) + log("Restoring original pki.", hookenv.INFO) + shutil.move(safety_backup, pki_dir) + raise RuntimeError('Failed to extract backup bundle. ' + 'Error: {}'.format(exc)) + else: + shutil.rmtree(safety_backup) + + +def _update_leadership_data(pki_dir, cert_dir, key_dir): + """ + Update certificates stored in the leaders database. + + :param pki_dir: location of easyrsa pki (usually /EasyRSA/pki) + :param cert_dir: location of issued certificates (usually /issued) + :param key_dir: location of private keys (usually /private) + :return: None + """ + ca_cert = os.path.join(pki_dir, 'ca.crt') + ca_key = os.path.join(key_dir, 'ca.key') + serial_file = os.path.join(pki_dir, 'serial') + global_client_cert = os.path.join(cert_dir, 'client.crt') + global_client_key = os.path.join(key_dir, 'client.key') + + with open(ca_cert, 'r') as stream: + data = stream.read() + log("Updating CA certificate in leader's database", + hookenv.INFO) + log("CA certificate:\n{}".format(data), hookenv.DEBUG) + leader_set({ + 'certificate_authority': data}) + + with open(ca_key, 'r') as stream: + log("Updating CA key in leader's database", + hookenv.INFO) + leader_set({ + 'certificate_authority_key': stream.read()}) + + with open(serial_file, 'r') as stream: + log("Updating CA serial in leader's database", + hookenv.INFO) + leader_set({ + 'certificate_authority_serial': stream.read()}) + + with open(global_client_cert) as stream: + data = stream.read() + log("Updating (legacy) global client certificate in leader's database", + hookenv.INFO) + log(data, hookenv.DEBUG) + leader_set({'client_certificate': data}) + + with open(global_client_key) as stream: + log("Updating (legacy) global client key in leader's database", + hookenv.INFO) + leader_set({'client_key': stream.read()}) + + +def backup(): + """ + Implementation of easyrsa 'backup' action. + + Currently deployed pki is packed into tarball and stored in the + backups directory. + """ + _ensure_backup_dir_exists() + + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + backup_name = 'easyrsa-{}.tar.gz'.format(timestamp) + backup_path = os.path.join(PKI_BACKUP, backup_name) + with tarfile.open(backup_path, mode='w:gz') as pki_tar: + pki_tar.add(os.path.join(easyrsa_directory, 'pki'), 'pki') + + log("Backup created and saved to '{}'".format(backup_path), hookenv.DEBUG) + function_set({ + 'command': 'juju scp {}:{} .'.format(local_unit(), backup_path), + 'message': 'Backup archive created successfully. Use the juju scp ' + 'command to copy it to your local machine.' + }) + + +def restore(): + """ + Implementation of easyrsa 'restore' action + + Backup restoration process can be summarized as following: + + * Selected backup is scanned and verified + * Contents of the backup are unpacked into /EasyRSA/pki + * Data that are stored in the local database are updated + * All units that have relation with this easyrsa unit will be notified + about the certificate changes. + """ + pki_dir = os.path.join(easyrsa_directory, 'pki') + backup_name = function_get('name') + + if backup_name is None: + raise RuntimeError("Parameter 'name' is required.") + + log("Restoring pki from backup file {}".format(backup_name), hookenv.INFO) + + backup_path = os.path.join(PKI_BACKUP, backup_name) + + if not os.path.isfile(backup_path): + log("Backup file '{}' does not exists.".format(backup_path), + hookenv.ERROR) + raise RuntimeError("Backup with name '{}' does not exist. Use action " + "'list-backups' to list all available " + "backups".format(backup_name)) + + with tarfile.open(backup_path, 'r:gz') as pki_tar: + _verify_backup(pki_tar) + _replace_pki(pki_tar, pki_dir) + + cert_dir = os.path.join(pki_dir, 'issued') + key_dir = os.path.join(pki_dir, 'private') + + # Update CA and global client data stored in the local leader's database + # NOTE(mkalcok): Easyrsa does not really support HA mode, so it's usually + # run as a single unit/model + _update_leadership_data(pki_dir, cert_dir, key_dir) + + ca_cert = leader_get('certificate_authority') + tls = endpoint_from_name('client') + log("Sending CA certificate to all related units", hookenv.INFO) + tls.set_ca(ca_cert) + log("Sending global client certificate and key to all related units", + hookenv.INFO) + tls.set_client_cert(leader_get('client_certificate'), + leader_get('client_key')) + for client in tls.all_requests: + try: + cert_file = os.path.join(cert_dir, + "{}.crt".format(client.common_name)) + key_file = os.path.join(key_dir, + "{}.key".format(client.common_name)) + with open(cert_file, 'r') as file: + cert = file.read() + with open(key_file, 'r') as file: + key = file.read() + log("Sending certificate for '{}' to unit" + "'{}'".format(client.common_name, client.unit_name), + hookenv.INFO) + log(cert, hookenv.DEBUG) + client.set_cert(cert, key) + + except FileNotFoundError: + log("Certificate for '{}' not found in backup. " + "Generating new one.", hookenv.INFO) + if client.cert_type == 'client': + cert, key = create_client_certificate(client.common_name) + elif client.cert_type == 'server': + cert, key = create_server_certificate(client.common_name, + client.sans, + client.common_name) + else: + # This use case should not really happen as easyrsa charm + # does not support Application type certificates + raise RuntimeError('Unrecognized certificate request type ' + '"{}".'.format(client.cert_type)) + log("Sending certificate for '{}' to unit" + "'{}'".format(client.common_name, client.unit_name), + hookenv.INFO) + log(cert, hookenv.DEBUG) + client.set_cert(cert, key) + + hookenv._run_atexit() + + +def list_backups(): + """Implementation of easyrsa 'list-backups' action.""" + file_list = [] + + try: + file_list = os.listdir(PKI_BACKUP) + except FileNotFoundError: + pass + + if file_list: + message = 'Available backup files:' + for file in file_list: + message += '\n{}'.format(file) + else: + message = 'There are no available backup files.' + + function_set({'message': message}) + + +def delete_backup(): + """Implementation of easyrsa 'delete-backup' action""" + backup_name = function_get('name') + delete_all = function_get('all') + + if not delete_all: + if backup_name is None: + raise RuntimeError("Parameter 'name' is required if parameter " + "'all' is False.") + log("Removing backup '{}'".format(backup_name), hookenv.INFO) + delete_file = os.path.join(PKI_BACKUP, backup_name) + _check_path_traversal(delete_file, PKI_BACKUP) + try: + os.remove(delete_file) + except FileNotFoundError: + err_msg = "Backup file '{}' does not exist".format(backup_name) + log(err_msg, hookenv.ERROR) + raise RuntimeError(err_msg) + else: + log("Removing all backup files.", hookenv.INFO) + shutil.rmtree(PKI_BACKUP) + + +ACTIONS = {'backup': backup, + 'restore': restore, + 'list-backups': list_backups, + 'delete-backup': delete_backup + } + + +def main(args): + action_name = os.path.basename(args.pop(0)) + try: + action = ACTIONS[action_name] + except KeyError: + s = "Action {} undefined".format(action_name) + function_fail(s) + return + else: + try: + log("Running action '{}'.".format(action_name)) + action() + except Exception as e: + function_fail("Action {} failed: {}".format(action_name, str(e))) + + +if __name__ == '__main__': + main(sys.argv) diff --git a/easyrsa/actions/restore b/easyrsa/actions/restore new file mode 100755 index 0000000..91a9e36 --- /dev/null +++ b/easyrsa/actions/restore @@ -0,0 +1,362 @@ +#!/usr/local/sbin/charm-env python3 +import os +import pwd +import grp +import sys +import tarfile +import shutil + +from datetime import datetime + +from charms import layer +from charms.reactive.relations import endpoint_from_name +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import ( + function_get, + function_set, + function_fail, + local_unit, + log, + leader_set, + leader_get, +) + +from reactive.easyrsa import ( + easyrsa_directory, + create_client_certificate, + create_server_certificate, +) + +# Import charm layers and start reactive +layer.import_layer_libs() +hookenv._run_atstart() + +PKI_BACKUP = '/home/ubuntu/easyrsa_backup' +# Minimal required contents of the backup tarball +TAR_STRUCTURE = {'pki', + 'pki/ca.crt', + 'pki/issued', + 'pki/issued/client.crt', + 'pki/private', + 'pki/private/ca.key', + 'pki/private/client.key', + 'pki/serial', + } + + +def _check_path_traversal(path_, parent_dir): + """Check that 'path_' does not lie outside of the 'parent_dir'. + + This function takes into account possible '../' in 'path_' and also + any symlinks that could point somewhere outside the expected 'parent_dir' + + NOTE(mkalcok): This implementation could be improved by using + 'os.path.commonpath()'. However it's available only in + py35+. + + :param path_: Path to be tested + :param parent_dir: Directory in which the 'path_' must lie + :raises: RuntimeError if 'path_' is outside of the 'parent_dir' + """ + full_path = os.path.realpath(path_) + parent_dir = os.path.realpath(parent_dir) + if not parent_dir.endswith('/'): + parent_dir += '/' + + if os.path.commonprefix([parent_dir, full_path]) != parent_dir: + err_msg = "Path traversal detected. '{}' tries to travers out " \ + "of {}".format(full_path, parent_dir) + log(err_msg, hookenv.ERROR) + raise RuntimeError(err_msg) + + +def _ensure_backup_dir_exists(): + """Ensure that backup directory exists with proper ownership""" + uid = pwd.getpwnam("ubuntu").pw_uid + gid = grp.getgrnam("ubuntu").gr_gid + try: + os.mkdir(PKI_BACKUP, mode=0o700) + except FileExistsError: + pass + os.chown(PKI_BACKUP, uid, gid) + + if not os.path.isdir(PKI_BACKUP): + log("Backup destination '{}' is not a directory".format(PKI_BACKUP), + hookenv.ERROR) + raise RuntimeError('Backup destination is not a directory.') + + +def _verify_backup(pki_tar): + """ + Verify that backup archive contains expected files + + :param pki_tar: Tarfile object containing easyrsa backup + """ + log("Verifying backup", hookenv.DEBUG) + members = set(pki_tar.getnames()) + + # Check that backup contains all the expected/required files + if not TAR_STRUCTURE.issubset(members): + raise RuntimeError("Backup has unexpected content. Corrupted file?") + log("Check expected files - OK", hookenv.DEBUG) + + # Check for path traversal attempts in tar file + pki_dir = os.path.join(easyrsa_directory, 'pki') + for path_ in members: + destination = os.path.join(pki_dir, path_) + _check_path_traversal(destination, pki_dir) + + +def _replace_pki(pki_tar, pki_dir): + """ + Safely replace easyrsa pki directory. + + If there are any problems during the extraction of the backup, original + pki directory will be brought back and error raised. + + :param pki_tar: Tarfile object containing easyrsa backup + :param pki_dir: Destination for extraction of easyrsa backup + :return: None + """ + safety_backup = os.path.join(easyrsa_directory, 'pki_backup') + shutil.move(pki_dir, safety_backup) + try: + log("Extracting pki from backup", hookenv.DEBUG) + pki_tar.extractall(easyrsa_directory) + except Exception as exc: + log("pki extraction failed: {}".format(exc), + hookenv.WARNING) + log("Restoring original pki.", hookenv.INFO) + shutil.move(safety_backup, pki_dir) + raise RuntimeError('Failed to extract backup bundle. ' + 'Error: {}'.format(exc)) + else: + shutil.rmtree(safety_backup) + + +def _update_leadership_data(pki_dir, cert_dir, key_dir): + """ + Update certificates stored in the leaders database. + + :param pki_dir: location of easyrsa pki (usually /EasyRSA/pki) + :param cert_dir: location of issued certificates (usually /issued) + :param key_dir: location of private keys (usually /private) + :return: None + """ + ca_cert = os.path.join(pki_dir, 'ca.crt') + ca_key = os.path.join(key_dir, 'ca.key') + serial_file = os.path.join(pki_dir, 'serial') + global_client_cert = os.path.join(cert_dir, 'client.crt') + global_client_key = os.path.join(key_dir, 'client.key') + + with open(ca_cert, 'r') as stream: + data = stream.read() + log("Updating CA certificate in leader's database", + hookenv.INFO) + log("CA certificate:\n{}".format(data), hookenv.DEBUG) + leader_set({ + 'certificate_authority': data}) + + with open(ca_key, 'r') as stream: + log("Updating CA key in leader's database", + hookenv.INFO) + leader_set({ + 'certificate_authority_key': stream.read()}) + + with open(serial_file, 'r') as stream: + log("Updating CA serial in leader's database", + hookenv.INFO) + leader_set({ + 'certificate_authority_serial': stream.read()}) + + with open(global_client_cert) as stream: + data = stream.read() + log("Updating (legacy) global client certificate in leader's database", + hookenv.INFO) + log(data, hookenv.DEBUG) + leader_set({'client_certificate': data}) + + with open(global_client_key) as stream: + log("Updating (legacy) global client key in leader's database", + hookenv.INFO) + leader_set({'client_key': stream.read()}) + + +def backup(): + """ + Implementation of easyrsa 'backup' action. + + Currently deployed pki is packed into tarball and stored in the + backups directory. + """ + _ensure_backup_dir_exists() + + timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + backup_name = 'easyrsa-{}.tar.gz'.format(timestamp) + backup_path = os.path.join(PKI_BACKUP, backup_name) + with tarfile.open(backup_path, mode='w:gz') as pki_tar: + pki_tar.add(os.path.join(easyrsa_directory, 'pki'), 'pki') + + log("Backup created and saved to '{}'".format(backup_path), hookenv.DEBUG) + function_set({ + 'command': 'juju scp {}:{} .'.format(local_unit(), backup_path), + 'message': 'Backup archive created successfully. Use the juju scp ' + 'command to copy it to your local machine.' + }) + + +def restore(): + """ + Implementation of easyrsa 'restore' action + + Backup restoration process can be summarized as following: + + * Selected backup is scanned and verified + * Contents of the backup are unpacked into /EasyRSA/pki + * Data that are stored in the local database are updated + * All units that have relation with this easyrsa unit will be notified + about the certificate changes. + """ + pki_dir = os.path.join(easyrsa_directory, 'pki') + backup_name = function_get('name') + + if backup_name is None: + raise RuntimeError("Parameter 'name' is required.") + + log("Restoring pki from backup file {}".format(backup_name), hookenv.INFO) + + backup_path = os.path.join(PKI_BACKUP, backup_name) + + if not os.path.isfile(backup_path): + log("Backup file '{}' does not exists.".format(backup_path), + hookenv.ERROR) + raise RuntimeError("Backup with name '{}' does not exist. Use action " + "'list-backups' to list all available " + "backups".format(backup_name)) + + with tarfile.open(backup_path, 'r:gz') as pki_tar: + _verify_backup(pki_tar) + _replace_pki(pki_tar, pki_dir) + + cert_dir = os.path.join(pki_dir, 'issued') + key_dir = os.path.join(pki_dir, 'private') + + # Update CA and global client data stored in the local leader's database + # NOTE(mkalcok): Easyrsa does not really support HA mode, so it's usually + # run as a single unit/model + _update_leadership_data(pki_dir, cert_dir, key_dir) + + ca_cert = leader_get('certificate_authority') + tls = endpoint_from_name('client') + log("Sending CA certificate to all related units", hookenv.INFO) + tls.set_ca(ca_cert) + log("Sending global client certificate and key to all related units", + hookenv.INFO) + tls.set_client_cert(leader_get('client_certificate'), + leader_get('client_key')) + for client in tls.all_requests: + try: + cert_file = os.path.join(cert_dir, + "{}.crt".format(client.common_name)) + key_file = os.path.join(key_dir, + "{}.key".format(client.common_name)) + with open(cert_file, 'r') as file: + cert = file.read() + with open(key_file, 'r') as file: + key = file.read() + log("Sending certificate for '{}' to unit" + "'{}'".format(client.common_name, client.unit_name), + hookenv.INFO) + log(cert, hookenv.DEBUG) + client.set_cert(cert, key) + + except FileNotFoundError: + log("Certificate for '{}' not found in backup. " + "Generating new one.", hookenv.INFO) + if client.cert_type == 'client': + cert, key = create_client_certificate(client.common_name) + elif client.cert_type == 'server': + cert, key = create_server_certificate(client.common_name, + client.sans, + client.common_name) + else: + # This use case should not really happen as easyrsa charm + # does not support Application type certificates + raise RuntimeError('Unrecognized certificate request type ' + '"{}".'.format(client.cert_type)) + log("Sending certificate for '{}' to unit" + "'{}'".format(client.common_name, client.unit_name), + hookenv.INFO) + log(cert, hookenv.DEBUG) + client.set_cert(cert, key) + + hookenv._run_atexit() + + +def list_backups(): + """Implementation of easyrsa 'list-backups' action.""" + file_list = [] + + try: + file_list = os.listdir(PKI_BACKUP) + except FileNotFoundError: + pass + + if file_list: + message = 'Available backup files:' + for file in file_list: + message += '\n{}'.format(file) + else: + message = 'There are no available backup files.' + + function_set({'message': message}) + + +def delete_backup(): + """Implementation of easyrsa 'delete-backup' action""" + backup_name = function_get('name') + delete_all = function_get('all') + + if not delete_all: + if backup_name is None: + raise RuntimeError("Parameter 'name' is required if parameter " + "'all' is False.") + log("Removing backup '{}'".format(backup_name), hookenv.INFO) + delete_file = os.path.join(PKI_BACKUP, backup_name) + _check_path_traversal(delete_file, PKI_BACKUP) + try: + os.remove(delete_file) + except FileNotFoundError: + err_msg = "Backup file '{}' does not exist".format(backup_name) + log(err_msg, hookenv.ERROR) + raise RuntimeError(err_msg) + else: + log("Removing all backup files.", hookenv.INFO) + shutil.rmtree(PKI_BACKUP) + + +ACTIONS = {'backup': backup, + 'restore': restore, + 'list-backups': list_backups, + 'delete-backup': delete_backup + } + + +def main(args): + action_name = os.path.basename(args.pop(0)) + try: + action = ACTIONS[action_name] + except KeyError: + s = "Action {} undefined".format(action_name) + function_fail(s) + return + else: + try: + log("Running action '{}'.".format(action_name)) + action() + except Exception as e: + function_fail("Action {} failed: {}".format(action_name, str(e))) + + +if __name__ == '__main__': + main(sys.argv) diff --git a/easyrsa/bin/charm-env b/easyrsa/bin/charm-env new file mode 100755 index 0000000..d211ce9 --- /dev/null +++ b/easyrsa/bin/charm-env @@ -0,0 +1,107 @@ +#!/bin/bash + +VERSION="1.0.0" + + +find_charm_dirs() { + # Hopefully, $JUJU_CHARM_DIR is set so which venv to use in unambiguous. + if [[ -n "$JUJU_CHARM_DIR" || -n "$CHARM_DIR" ]]; then + if [[ -z "$JUJU_CHARM_DIR" ]]; then + # accept $CHARM_DIR to be more forgiving + export JUJU_CHARM_DIR="$CHARM_DIR" + fi + if [[ -z "$CHARM_DIR" ]]; then + # set CHARM_DIR as well to help with backwards compatibility + export CHARM_DIR="$JUJU_CHARM_DIR" + fi + return + fi + # Try to guess the value for JUJU_CHARM_DIR by looking for a non-subordinate + # (because there's got to be at least one principle) charm directory; + # if there are several, pick the first by alpha order. + agents_dir="/var/lib/juju/agents" + if [[ -d "$agents_dir" ]]; then + desired_charm="$1" + found_charm_dir="" + if [[ -n "$desired_charm" ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + charm_name="$(grep -o '^['\''"]\?name['\''"]\?:.*' $charm_dir/metadata.yaml 2> /dev/null | sed -e 's/.*: *//' -e 's/['\''"]//g')" + if [[ "$charm_name" == "$desired_charm" ]]; then + if [[ -n "$found_charm_dir" ]]; then + >&2 echo "Ambiguous possibilities for JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + found_charm_dir="$charm_dir" + fi + done + if [[ -z "$found_charm_dir" ]]; then + >&2 echo "Unable to determine JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + export JUJU_CHARM_DIR="$found_charm_dir" + export CHARM_DIR="$found_charm_dir" + return + fi + # shellcheck disable=SC2126 + non_subordinates="$(grep -L 'subordinate"\?:.*true' "$agents_dir"/unit-*/charm/metadata.yaml | wc -l)" + if [[ "$non_subordinates" -gt 1 ]]; then + >&2 echo 'Ambiguous possibilities for JUJU_CHARM_DIR; please use --charm or run within a Juju hook context' + exit 1 + elif [[ "$non_subordinates" -eq 1 ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + if grep -q 'subordinate"\?:.*true' "$charm_dir/metadata.yaml"; then + continue + fi + export JUJU_CHARM_DIR="$charm_dir" + export CHARM_DIR="$charm_dir" + return + done + fi + fi + >&2 echo 'Unable to determine JUJU_CHARM_DIR; please run within a Juju hook context' + exit 1 +} + +try_activate_venv() { + if [[ -d "$JUJU_CHARM_DIR/../.venv" ]]; then + . "$JUJU_CHARM_DIR/../.venv/bin/activate" + fi +} + +find_wrapped() { + PATH="${PATH/\/usr\/local\/sbin:}" which "$(basename "$0")" +} + + +if [[ "$1" == "--version" || "$1" == "-v" ]]; then + echo "$VERSION" + exit 0 +fi + + +# allow --charm option to hint which JUJU_CHARM_DIR to choose when ambiguous +# NB: --charm option must come first +# NB: option must be processed outside find_charm_dirs to modify $@ +charm_name="" +if [[ "$1" == "--charm" ]]; then + charm_name="$2" + shift; shift +fi + +find_charm_dirs "$charm_name" +try_activate_venv +export PYTHONPATH="$JUJU_CHARM_DIR/lib:$PYTHONPATH" + +if [[ "$(basename "$0")" == "charm-env" ]]; then + # being used as a shebang + exec "$@" +elif [[ "$0" == "$BASH_SOURCE" ]]; then + # being invoked as a symlink wrapping something to find in the venv + exec "$(find_wrapped)" "$@" +elif [[ "$(basename "$BASH_SOURCE")" == "charm-env" ]]; then + # being sourced directly; do nothing + /bin/true +else + # being sourced for wrapped bash helpers + . "$(find_wrapped)" +fi diff --git a/easyrsa/bin/layer_option b/easyrsa/bin/layer_option new file mode 100755 index 0000000..3253ef8 --- /dev/null +++ b/easyrsa/bin/layer_option @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from charms import layer + + +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 = layer.options.get(args.section, 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/easyrsa/config.yaml b/easyrsa/config.yaml new file mode 100644 index 0000000..ffc0186 --- /dev/null +++ b/easyrsa/config.yaml @@ -0,0 +1 @@ +"options": {} diff --git a/easyrsa/copyright b/easyrsa/copyright new file mode 100644 index 0000000..c5e1c8f --- /dev/null +++ b/easyrsa/copyright @@ -0,0 +1,13 @@ +Copyright 2016 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. diff --git a/easyrsa/copyright.layer-basic b/easyrsa/copyright.layer-basic new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/easyrsa/copyright.layer-basic @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/easyrsa/copyright.layer-leadership b/easyrsa/copyright.layer-leadership new file mode 100644 index 0000000..08b983f --- /dev/null +++ b/easyrsa/copyright.layer-leadership @@ -0,0 +1,15 @@ +Copyright 2015-2016 Canonical Ltd. + +This file is part of the Leadership Layer for Juju. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/easyrsa/copyright.layer-options b/easyrsa/copyright.layer-options new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/easyrsa/copyright.layer-options @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/easyrsa/copyright.layer-status b/easyrsa/copyright.layer-status new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/easyrsa/copyright.layer-status @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/easyrsa/debug-scripts/charm-unitdata b/easyrsa/debug-scripts/charm-unitdata new file mode 100755 index 0000000..d2aac60 --- /dev/null +++ b/easyrsa/debug-scripts/charm-unitdata @@ -0,0 +1,12 @@ +#!/usr/local/sbin/charm-env python3 + +import debug_script +import json +from charmhelpers.core import unitdata + +kv = unitdata.kv() +data = kv.getrange("") + +with debug_script.open_file("unitdata.json", "w") as f: + json.dump(data, f, indent=2) + f.write("\n") diff --git a/easyrsa/debug-scripts/filesystem b/easyrsa/debug-scripts/filesystem new file mode 100755 index 0000000..c5ec6d8 --- /dev/null +++ b/easyrsa/debug-scripts/filesystem @@ -0,0 +1,17 @@ +#!/bin/sh +set -ux + +# report file system disk space usage +df -hT > $DEBUG_SCRIPT_DIR/df-hT +# estimate file space usage +du -h / 2>&1 > $DEBUG_SCRIPT_DIR/du-h +# list the mounted filesystems +mount > $DEBUG_SCRIPT_DIR/mount +# list the mounted systems with ascii trees +findmnt -A > $DEBUG_SCRIPT_DIR/findmnt +# list block devices +lsblk > $DEBUG_SCRIPT_DIR/lsblk +# list open files +lsof 2>&1 > $DEBUG_SCRIPT_DIR/lsof +# list local system locks +lslocks > $DEBUG_SCRIPT_DIR/lslocks diff --git a/easyrsa/debug-scripts/juju-logs b/easyrsa/debug-scripts/juju-logs new file mode 100755 index 0000000..d27c458 --- /dev/null +++ b/easyrsa/debug-scripts/juju-logs @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +cp -v /var/log/juju/* $DEBUG_SCRIPT_DIR diff --git a/easyrsa/debug-scripts/juju-network-get b/easyrsa/debug-scripts/juju-network-get new file mode 100755 index 0000000..983c8c4 --- /dev/null +++ b/easyrsa/debug-scripts/juju-network-get @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import yaml +import debug_script + +with open('metadata.yaml') as f: + metadata = yaml.load(f) + +relations = [] +for key in ['requires', 'provides', 'peers']: + relations += list(metadata.get(key, {}).keys()) + +os.mkdir(os.path.join(debug_script.dir, 'relations')) + +for relation in relations: + path = 'relations/' + relation + with debug_script.open_file(path, 'w') as f: + cmd = ['network-get', relation] + subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) diff --git a/easyrsa/debug-scripts/network b/easyrsa/debug-scripts/network new file mode 100755 index 0000000..944a355 --- /dev/null +++ b/easyrsa/debug-scripts/network @@ -0,0 +1,11 @@ +#!/bin/sh +set -ux + +ifconfig -a > $DEBUG_SCRIPT_DIR/ifconfig +cp -v /etc/resolv.conf $DEBUG_SCRIPT_DIR/resolv.conf +cp -v /etc/network/interfaces $DEBUG_SCRIPT_DIR/interfaces +netstat -planut > $DEBUG_SCRIPT_DIR/netstat +route -n > $DEBUG_SCRIPT_DIR/route +iptables-save > $DEBUG_SCRIPT_DIR/iptables-save +dig google.com > $DEBUG_SCRIPT_DIR/dig-google +ping -w 2 -i 0.1 google.com > $DEBUG_SCRIPT_DIR/ping-google diff --git a/easyrsa/debug-scripts/packages b/easyrsa/debug-scripts/packages new file mode 100755 index 0000000..b60a9cf --- /dev/null +++ b/easyrsa/debug-scripts/packages @@ -0,0 +1,7 @@ +#!/bin/sh +set -ux + +dpkg --list > $DEBUG_SCRIPT_DIR/dpkg-list +snap list > $DEBUG_SCRIPT_DIR/snap-list +pip2 list > $DEBUG_SCRIPT_DIR/pip2-list +pip3 list > $DEBUG_SCRIPT_DIR/pip3-list diff --git a/easyrsa/debug-scripts/sysctl b/easyrsa/debug-scripts/sysctl new file mode 100755 index 0000000..a86a6c8 --- /dev/null +++ b/easyrsa/debug-scripts/sysctl @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +sysctl -a > $DEBUG_SCRIPT_DIR/sysctl diff --git a/easyrsa/debug-scripts/systemd b/easyrsa/debug-scripts/systemd new file mode 100755 index 0000000..8bb9b6f --- /dev/null +++ b/easyrsa/debug-scripts/systemd @@ -0,0 +1,9 @@ +#!/bin/sh +set -ux + +systemctl --all > $DEBUG_SCRIPT_DIR/systemctl +journalctl > $DEBUG_SCRIPT_DIR/journalctl +systemd-analyze time > $DEBUG_SCRIPT_DIR/systemd-analyze-time +systemd-analyze blame > $DEBUG_SCRIPT_DIR/systemd-analyze-blame +systemd-analyze critical-chain > $DEBUG_SCRIPT_DIR/systemd-analyze-critical-chain +systemd-analyze dump > $DEBUG_SCRIPT_DIR/systemd-analyze-dump diff --git a/easyrsa/docs/status.md b/easyrsa/docs/status.md new file mode 100644 index 0000000..c6cceab --- /dev/null +++ b/easyrsa/docs/status.md @@ -0,0 +1,91 @@ +

WorkloadState

+ +```python +WorkloadState(self, /, *args, **kwargs) +``` + +Enum of the valid workload states. + +Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + +

maintenance

+ +```python +maintenance(message) +``` + +Set the status to the `MAINTENANCE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

maint

+ +```python +maint(message) +``` + +Shorthand alias for +[maintenance](status.md#charms.layer.status.maintenance). + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

blocked

+ +```python +blocked(message) +``` + +Set the status to the `BLOCKED` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

waiting

+ +```python +waiting(message) +``` + +Set the status to the `WAITING` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

active

+ +```python +active(message) +``` + +Set the status to the `ACTIVE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

status_set

+ +```python +status_set(workload_state, message) +``` + +Set the status to the given workload state with a message. + +__Parameters__ + +- __`workload_state` (WorkloadState or str)__: State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. +- __`message` (str)__: Message to convey to the operator. + diff --git a/easyrsa/hooks/client-relation-broken b/easyrsa/hooks/client-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/client-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/client-relation-changed b/easyrsa/hooks/client-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/client-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/client-relation-created b/easyrsa/hooks/client-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/client-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/client-relation-departed b/easyrsa/hooks/client-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/client-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/client-relation-joined b/easyrsa/hooks/client-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/client-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/config-changed b/easyrsa/hooks/config-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/config-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/hook.template b/easyrsa/hooks/hook.template new file mode 100644 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/hook.template @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/install b/easyrsa/hooks/install new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/install @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/leader-elected b/easyrsa/hooks/leader-elected new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/leader-elected @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/leader-settings-changed b/easyrsa/hooks/leader-settings-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/leader-settings-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/post-series-upgrade b/easyrsa/hooks/post-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/post-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/pre-series-upgrade b/easyrsa/hooks/pre-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/pre-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/relations/tls-certificates/.gitignore b/easyrsa/hooks/relations/tls-certificates/.gitignore new file mode 100644 index 0000000..93813bc --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/.gitignore @@ -0,0 +1,4 @@ +.tox +__pycache__ +*.pyc +_build diff --git a/easyrsa/hooks/relations/tls-certificates/README.md b/easyrsa/hooks/relations/tls-certificates/README.md new file mode 100644 index 0000000..733da6d --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/README.md @@ -0,0 +1,90 @@ +# Interface tls-certificates + +This is a [Juju][] interface layer that enables a charm which requires TLS +certificates to relate to a charm which can provide them, such as [Vault][] or +[EasyRSA][] + +To get started please read the [Introduction to PKI][] which defines some PKI +terms, concepts and processes used in this document. + +# Example Usage + +Let's say you have a charm which needs a server certificate for a service it +provides to other charms and a client certificate for a database it consumes +from another charm. The charm provides its own service on the `clients` +relation endpoint, and it consumes the database on the `db` relation endpoint. + +First, you must define the relation endpoint in your charm's `metadata.yaml`: + +```yaml +requires: + cert-provider: + interface: tls-certificates +``` + +Next, you must ensure the interface layer is included in your `layer.yaml`: + +```yaml +includes: + - interface:tls-certificates +``` + +Then, in your reactive code, add the following, changing `update_certs` to +handle the certificates however your charm needs: + +```python +from charmhelpers.core import hookenv, host +from charms.reactive import endpoint_from_flag + + +@when('cert-provider.ca.changed') +def install_root_ca_cert(): + cert_provider = endpoint_from_flag('cert-provider.ca.available') + host.install_ca_cert(cert_provider.root_ca_cert) + clear_flag('cert-provider.ca.changed') + + +@when('cert-provider.available') +def request_certificates(): + cert_provider = endpoint_from_flag('cert-provider.available') + + # get ingress info + ingress_for_clients = hookenv.network_get('clients')['ingress-addresses'] + ingress_for_db = hookenv.network_get('db')['ingress-addresses'] + + # use first ingress address as primary and any additional as SANs + server_cn, server_sans = ingress_for_clients[0], ingress_for_clients[:1] + client_cn, client_sans = ingress_for_db[0], ingress_for_db[:1] + + # request a single server and single client cert; note that multiple certs + # of either type can be requested as long as they have unique common names + cert_provider.request_server_cert(server_cn, server_sans) + cert_provider.request_client_cert(client_cn, client_sans) + + +@when('cert-provider.certs.changed') +def update_certs(): + cert_provider = endpoint_from_flag('cert-provider.available') + server_cert = cert_provider.server_certs[0] # only requested one + myserver.update_server_cert(server_cert.cert, server_cert.key) + + client_cert = cert_provider.client_certs[0] # only requested one + myclient.update_client_cert(client_cert.cert, client_cert.key) + clear_flag('cert-provider.certs.changed') +``` + + +# Reference + + * [Requires](docs/requires.md) + * [Provides](docs/provides.md) + +# Contact Information + +Maintainer: Cory Johns <Cory.Johns@canonical.com> + + +[Juju]: https://jujucharms.com +[Vault]: https://jujucharms.com/u/openstack-charmers/vault +[EasyRSA]: https://jujucharms.com/u/containers/easyrsa +[Introduction to PKI]: https://github.com/OpenVPN/easy-rsa/blob/master/doc/Intro-To-PKI.md diff --git a/easyrsa/hooks/relations/tls-certificates/__init__.py b/easyrsa/hooks/relations/tls-certificates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/easyrsa/hooks/relations/tls-certificates/docs/common.md b/easyrsa/hooks/relations/tls-certificates/docs/common.md new file mode 100644 index 0000000..25d0e08 --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/docs/common.md @@ -0,0 +1,51 @@ +

CertificateRequest

+ +```python +CertificateRequest(self, unit, cert_type, cert_name, common_name, sans) +``` + +

application_name

+ +Name of the application which the request came from. + +:returns: Name of application +:rtype: str + +

cert

+ + +The cert published for this request, if any. + +

cert_type

+ + +Type of certificate, 'server' or 'client', being requested. + +

resolve_unit_name

+ +```python +CertificateRequest.resolve_unit_name(unit) +``` +Return name of unit associated with this request. + +unit_name should be provided in the relation data to ensure +compatability with cross-model relations. If the unit name +is absent then fall back to unit_name attribute of the +unit associated with this request. + +:param unit: Unit to extract name from +:type unit: charms.reactive.endpoints.RelatedUnit +:returns: Name of unit +:rtype: str + +

Certificate

+ +```python +Certificate(self, cert_type, common_name, cert, key) +``` + +Represents a created certificate and key. + +The ``cert_type``, ``common_name``, ``cert``, and ``key`` values can +be accessed either as properties or as the contents of the dict. + diff --git a/easyrsa/hooks/relations/tls-certificates/docs/provides.md b/easyrsa/hooks/relations/tls-certificates/docs/provides.md new file mode 100644 index 0000000..c213546 --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/docs/provides.md @@ -0,0 +1,212 @@ +

provides

+ + +

TlsProvides

+ +```python +TlsProvides(self, endpoint_name, relation_ids=None) +``` + +The provider's side of the interface protocol. + +The following flags may be set: + + * `{endpoint_name}.available` + Whenever any clients are joined. + + * `{endpoint_name}.certs.requested` + When there are new certificate requests of any kind to be processed. + The requests can be accessed via [new_requests][]. + + * `{endpoint_name}.server.certs.requested` + When there are new server certificate requests to be processed. + The requests can be accessed via [new_server_requests][]. + + * `{endpoint_name}.client.certs.requested` + When there are new client certificate requests to be processed. + The requests can be accessed via [new_client_requests][]. + +[Certificate]: common.md#tls_certificates_common.Certificate +[CertificateRequest]: common.md#tls_certificates_common.CertificateRequest +[all_requests]: provides.md#provides.TlsProvides.all_requests +[new_requests]: provides.md#provides.TlsProvides.new_requests +[new_server_requests]: provides.md#provides.TlsProvides.new_server_requests +[new_client_requests]: provides.md#provides.TlsProvides.new_client_requests + +

all_published_certs

+ + +List of all [Certificate][] instances that this provider has published +for all related applications. + +

all_requests

+ + +List of all requests that have been made. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('certs.regen', + 'tls.certs.available') +def regen_all_certs(): + tls = endpoint_from_flag('tls.certs.available') + for request in tls.all_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_application_requests

+ + +Filtered view of [new_requests][] that only includes application cert +requests. + +Each will be an instance of [ApplicationCertificateRequest][]. + +Example usage: + +```python +@when('tls.application.certs.requested') +def gen_application_certs(): + tls = endpoint_from_flag('tls.application.certs.requested') + for request in tls.new_application_requests: + cert, key = generate_application_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_client_requests

+ + +Filtered view of [new_requests][] that only includes client cert +requests. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('tls.client.certs.requested') +def gen_client_certs(): + tls = endpoint_from_flag('tls.client.certs.requested') + for request in tls.new_client_requests: + cert, key = generate_client_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_requests

+ + +Filtered view of [all_requests][] that only includes requests that +haven't been handled. + +Each will be an instance of [CertificateRequest][]. + +This collection can also be further filtered by request type using +[new_server_requests][] or [new_client_requests][]. + +Example usage: + +```python +@when('tls.certs.requested') +def gen_certs(): + tls = endpoint_from_flag('tls.certs.requested') + for request in tls.new_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_server_requests

+ + +Filtered view of [new_requests][] that only includes server cert +requests. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('tls.server.certs.requested') +def gen_server_certs(): + tls = endpoint_from_flag('tls.server.certs.requested') + for request in tls.new_server_requests: + cert, key = generate_server_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

set_ca

+ +```python +TlsProvides.set_ca(certificate_authority) +``` + +Publish the CA to all related applications. + +

set_chain

+ +```python +TlsProvides.set_chain(chain) +``` + +Publish the chain of trust to all related applications. + +

set_client_cert

+ +```python +TlsProvides.set_client_cert(cert, key) +``` + +Deprecated. This is only for backwards compatibility. + +Publish a globally shared client cert and key. + +

set_server_cert

+ +```python +TlsProvides.set_server_cert(scope, cert, key) +``` + +Deprecated. Use one of the [new_requests][] collections and +`request.set_cert()` instead. + +Set the server cert and key for the request identified by `scope`. + +

set_server_multicerts

+ +```python +TlsProvides.set_server_multicerts(scope) +``` + +Deprecated. Done automatically. + +

add_server_cert

+ +```python +TlsProvides.add_server_cert(scope, cn, cert, key) +``` + +Deprecated. Use `request.set_cert()` instead. + +

get_server_requests

+ +```python +TlsProvides.get_server_requests() +``` + +Deprecated. Use the [new_requests][] or [server_requests][] +collections instead. + +One provider can have many requests to generate server certificates. +Return a map of all server request objects indexed by a unique +identifier. + diff --git a/easyrsa/hooks/relations/tls-certificates/docs/requires.md b/easyrsa/hooks/relations/tls-certificates/docs/requires.md new file mode 100644 index 0000000..fdec902 --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/docs/requires.md @@ -0,0 +1,207 @@ +

requires

+ + +

TlsRequires

+ +```python +TlsRequires(self, endpoint_name, relation_ids=None) +``` + +The client's side of the interface protocol. + +The following flags may be set: + + * `{endpoint_name}.available` + Whenever the relation is joined. + + * `{endpoint_name}.ca.available` + When the root CA information is available via the [root_ca_cert][] and + [root_ca_chain][] properties. + + * `{endpoint_name}.ca.changed` + When the root CA information has changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.certs.available` + When the requested server or client certs are available. + + * `{endpoint_name}.certs.changed` + When the requested server or client certs have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.server.certs.available` + When the server certificates requested by [request_server_cert][] are + available via the [server_certs][] collection. + + * `{endpoint_name}.server.certs.changed` + When the requested server certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.client.certs.available` + When the client certificates requested by [request_client_cert][] are + available via the [client_certs][] collection. + + * `{endpoint_name}.client.certs.changed` + When the requested client certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + +The following flags have been deprecated: + + * `{endpoint_name}.server.cert.available` + * `{endpoint_name}.client.cert.available` + * `{endpoint_name}.batch.cert.available` + +[Certificate]: common.md#tls_certificates_common.Certificate +[CertificateRequest]: common.md#tls_certificates_common.CertificateRequest +[root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert +[root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain +[request_server_cert]: requires.md#requires.TlsRequires.request_server_cert +[request_client_cert]: requires.md#requires.TlsRequires.request_client_cert +[server_certs]: requires.md#requires.TlsRequires.server_certs +[server_certs_map]: requires.md#requires.TlsRequires.server_certs_map +[client_certs]: requires.md#requires.TlsRequires.server_certs + +

application_certs

+ + +List of [Certificate][] instances for all available application certs. + +

client_certs

+ + +List of [Certificate][] instances for all available client certs. + +

client_certs_map

+ + +Mapping of client [Certificate][] instances by their `common_name`. + +

root_ca_cert

+ + +Root CA certificate. + +

root_ca_chain

+ + +The chain of trust for the root CA. + +

server_certs

+ + +List of [Certificate][] instances for all available server certs. + +

server_certs_map

+ + +Mapping of server [Certificate][] instances by their `common_name`. + +

get_ca

+ +```python +TlsRequires.get_ca() +``` + +Return the root CA certificate. + +Same as [root_ca_cert][]. + +

get_chain

+ +```python +TlsRequires.get_chain() +``` + +Return the chain of trust for the root CA. + +Same as [root_ca_chain][]. + +

get_client_cert

+ +```python +TlsRequires.get_client_cert() +``` + +Deprecated. Use [request_client_cert][] and the [client_certs][] +collection instead. + +Return a globally shared client certificate and key. + +

get_server_cert

+ +```python +TlsRequires.get_server_cert() +``` + +Deprecated. Use the [server_certs][] collection instead. + +Return the cert and key of the first server certificate requested. + +

get_batch_requests

+ +```python +TlsRequires.get_batch_requests() +``` + +Deprecated. Use [server_certs_map][] instead. + +Mapping of server [Certificate][] instances by their `common_name`. + +

request_server_cert

+ +```python +TlsRequires.request_server_cert(cn, sans=None, cert_name=None) +``` + +Request a server certificate and key be generated for the given +common name (`cn`) and optional list of alternative names (`sans`). + +The `cert_name` is deprecated and not needed. + +This can be called multiple times to request more than one server +certificate, although the common names must be unique. If called +again with the same common name, it will be ignored. + +

add_request_server_cert

+ +```python +TlsRequires.add_request_server_cert(cn, sans) +``` + +Deprecated. Use [request_server_cert][] instead. + +

request_server_certs

+ +```python +TlsRequires.request_server_certs() +``` + +Deprecated. Just use [request_server_cert][]; this does nothing. + +

request_client_cert

+ +```python +TlsRequires.request_client_cert(cn, sans) +``` + +Request a client certificate and key be generated for the given +common name (`cn`) and list of alternative names (`sans`). + +This can be called multiple times to request more than one client +certificate, although the common names must be unique. If called +again with the same common name, it will be ignored. + +

request_application_cert

+ +```python +TlsRequires.request_application_cert(cn, sans) +``` + +Request an application certificate and key be generated for the given +common name (`cn`) and list of alternative names (`sans` ) of this +unit and all peer units. All units will share a single certificates. + diff --git a/easyrsa/hooks/relations/tls-certificates/interface.yaml b/easyrsa/hooks/relations/tls-certificates/interface.yaml new file mode 100644 index 0000000..beec53b --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/interface.yaml @@ -0,0 +1,6 @@ +name: tls-certificates +summary: | + A Transport Layer Security (TLS) charm layer that uses requires and provides + to exchange certifcates. +version: 1 +repo: https://github.com/juju-solutions/interface-tls-certificates diff --git a/easyrsa/hooks/relations/tls-certificates/make_docs b/easyrsa/hooks/relations/tls-certificates/make_docs new file mode 100644 index 0000000..2f2274a --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/make_docs @@ -0,0 +1,23 @@ +#!.tox/py3/bin/python + +import sys +import importlib +from pathlib import Path +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'cert': {'interface': 'tls-certificates'}}, + 'provides': {'cert': {'interface': 'tls-certificates'}}, + } + sys.path.append('..') + sys.modules[''] = importlib.import_module(Path.cwd().name) + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/easyrsa/hooks/relations/tls-certificates/provides.py b/easyrsa/hooks/relations/tls-certificates/provides.py new file mode 100644 index 0000000..0262baa --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/provides.py @@ -0,0 +1,301 @@ +if not __package__: + # fix relative imports when building docs + import sys + __package__ = sys.modules[''].__name__ + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag + +from .tls_certificates_common import ( + ApplicationCertificateRequest, + CertificateRequest +) + + +class TlsProvides(Endpoint): + """ + The provider's side of the interface protocol. + + The following flags may be set: + + * `{endpoint_name}.available` + Whenever any clients are joined. + + * `{endpoint_name}.certs.requested` + When there are new certificate requests of any kind to be processed. + The requests can be accessed via [new_requests][]. + + * `{endpoint_name}.server.certs.requested` + When there are new server certificate requests to be processed. + The requests can be accessed via [new_server_requests][]. + + * `{endpoint_name}.client.certs.requested` + When there are new client certificate requests to be processed. + The requests can be accessed via [new_client_requests][]. + + [Certificate]: common.md#tls_certificates_common.Certificate + [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest + [all_requests]: provides.md#provides.TlsProvides.all_requests + [new_requests]: provides.md#provides.TlsProvides.new_requests + [new_server_requests]: provides.md#provides.TlsProvides.new_server_requests + [new_client_requests]: provides.md#provides.TlsProvides.new_client_requests + """ + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('{endpoint_name}.available')) + toggle_flag(self.expand_name('{endpoint_name}.certs.requested'), + self.new_requests) + toggle_flag(self.expand_name('{endpoint_name}.server.certs.requested'), + self.new_server_requests) + toggle_flag(self.expand_name('{endpoint_name}.client.certs.requested'), + self.new_client_requests) + toggle_flag( + self.expand_name('{endpoint_name}.application.certs.requested'), + self.new_application_requests) + # For backwards compatibility, set the old "cert" flags as well + toggle_flag(self.expand_name('{endpoint_name}.server.cert.requested'), + self.new_server_requests) + toggle_flag(self.expand_name('{endpoint_name}.client.cert.requested'), + self.new_client_requests) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + clear_flag(self.expand_name('{endpoint_name}.certs.requested')) + clear_flag(self.expand_name('{endpoint_name}.server.certs.requested')) + clear_flag(self.expand_name('{endpoint_name}.client.certs.requested')) + clear_flag( + self.expand_name('{endpoint_name}.application.certs.requested')) + + def set_ca(self, certificate_authority): + """ + Publish the CA to all related applications. + """ + for relation in self.relations: + # All the clients get the same CA, so send it to them. + relation.to_publish_raw['ca'] = certificate_authority + + def set_chain(self, chain): + """ + Publish the chain of trust to all related applications. + """ + for relation in self.relations: + # All the clients get the same chain, so send it to them. + relation.to_publish_raw['chain'] = chain + + def set_client_cert(self, cert, key): + """ + Deprecated. This is only for backwards compatibility. + + Publish a globally shared client cert and key. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'client.cert': cert, + 'client.key': key, + }) + + def set_server_cert(self, scope, cert, key): + """ + Deprecated. Use one of the [new_requests][] collections and + `request.set_cert()` instead. + + Set the server cert and key for the request identified by `scope`. + """ + request = self.get_server_requests()[scope] + request.set_cert(cert, key) + + def set_server_multicerts(self, scope): + """ + Deprecated. Done automatically. + """ + pass + + def add_server_cert(self, scope, cn, cert, key): + ''' + Deprecated. Use `request.set_cert()` instead. + ''' + self.set_server_cert(scope, cert, key) + + def get_server_requests(self): + """ + Deprecated. Use the [new_requests][] or [server_requests][] + collections instead. + + One provider can have many requests to generate server certificates. + Return a map of all server request objects indexed by a unique + identifier. + """ + return {req._key: req for req in self.new_server_requests} + + @property + def all_requests(self): + """ + List of all requests that have been made. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('certs.regen', + 'tls.certs.available') + def regen_all_certs(): + tls = endpoint_from_flag('tls.certs.available') + for request in tls.all_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + requests = [] + for unit in self.all_joined_units: + # handle older single server cert request + if unit.received_raw['common_name']: + requests.append(CertificateRequest( + unit, + 'server', + unit.received_raw['certificate_name'], + unit.received_raw['common_name'], + unit.received['sans'], + )) + + # handle mutli server cert requests + reqs = unit.received['cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(CertificateRequest( + unit, + 'server', + common_name, + common_name, + req['sans'], + )) + + # handle client cert requests + reqs = unit.received['client_cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(CertificateRequest( + unit, + 'client', + common_name, + common_name, + req['sans'], + )) + # handle application cert requests + reqs = unit.received['application_cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(ApplicationCertificateRequest( + unit, + 'application', + common_name, + common_name, + req['sans'] + )) + return requests + + @property + def new_requests(self): + """ + Filtered view of [all_requests][] that only includes requests that + haven't been handled. + + Each will be an instance of [CertificateRequest][]. + + This collection can also be further filtered by request type using + [new_server_requests][] or [new_client_requests][]. + + Example usage: + + ```python + @when('tls.certs.requested') + def gen_certs(): + tls = endpoint_from_flag('tls.certs.requested') + for request in tls.new_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.all_requests if not req.is_handled] + + @property + def new_server_requests(self): + """ + Filtered view of [new_requests][] that only includes server cert + requests. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('tls.server.certs.requested') + def gen_server_certs(): + tls = endpoint_from_flag('tls.server.certs.requested') + for request in tls.new_server_requests: + cert, key = generate_server_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.new_requests if req.cert_type == 'server'] + + @property + def new_client_requests(self): + """ + Filtered view of [new_requests][] that only includes client cert + requests. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('tls.client.certs.requested') + def gen_client_certs(): + tls = endpoint_from_flag('tls.client.certs.requested') + for request in tls.new_client_requests: + cert, key = generate_client_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.new_requests if req.cert_type == 'client'] + + @property + def new_application_requests(self): + """ + Filtered view of [new_requests][] that only includes application cert + requests. + + Each will be an instance of [ApplicationCertificateRequest][]. + + Example usage: + + ```python + @when('tls.application.certs.requested') + def gen_application_certs(): + tls = endpoint_from_flag('tls.application.certs.requested') + for request in tls.new_application_requests: + cert, key = generate_application_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + + :returns: List of certificate requests. + :rtype: [CertificateRequest, ] + """ + return [req for req in self.new_requests + if req.cert_type == 'application'] + + @property + def all_published_certs(self): + """ + List of all [Certificate][] instances that this provider has published + for all related applications. + """ + return [req.cert for req in self.all_requests if req.cert] diff --git a/easyrsa/hooks/relations/tls-certificates/pydocmd.yml b/easyrsa/hooks/relations/tls-certificates/pydocmd.yml new file mode 100644 index 0000000..c568913 --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/pydocmd.yml @@ -0,0 +1,19 @@ +site_name: 'TLS Certificates Interface' + +generate: + - requires.md: + - requires + - requires.TlsRequires+ + - provides.md: + - provides + - provides.TlsProvides+ + - common.md: + - tls_certificates_common.CertificateRequest+ + - tls_certificates_common.Certificate+ + +pages: + - Requires: requires.md + - Provides: provides.md + - Common: common.md + +gens_dir: docs diff --git a/easyrsa/hooks/relations/tls-certificates/requires.py b/easyrsa/hooks/relations/tls-certificates/requires.py new file mode 100644 index 0000000..951f953 --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/requires.py @@ -0,0 +1,342 @@ +if not __package__: + # fix relative imports when building docs + import sys + __package__ = sys.modules[''].__name__ + +import uuid + +from charmhelpers.core import hookenv + +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag +from charms.reactive import Endpoint +from charms.reactive import data_changed + +from .tls_certificates_common import Certificate + + +class TlsRequires(Endpoint): + """ + The client's side of the interface protocol. + + The following flags may be set: + + * `{endpoint_name}.available` + Whenever the relation is joined. + + * `{endpoint_name}.ca.available` + When the root CA information is available via the [root_ca_cert][] and + [root_ca_chain][] properties. + + * `{endpoint_name}.ca.changed` + When the root CA information has changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.certs.available` + When the requested server or client certs are available. + + * `{endpoint_name}.certs.changed` + When the requested server or client certs have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.server.certs.available` + When the server certificates requested by [request_server_cert][] are + available via the [server_certs][] collection. + + * `{endpoint_name}.server.certs.changed` + When the requested server certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.client.certs.available` + When the client certificates requested by [request_client_cert][] are + available via the [client_certs][] collection. + + * `{endpoint_name}.client.certs.changed` + When the requested client certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + The following flags have been deprecated: + + * `{endpoint_name}.server.cert.available` + * `{endpoint_name}.client.cert.available` + * `{endpoint_name}.batch.cert.available` + + [Certificate]: common.md#tls_certificates_common.Certificate + [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest + [root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert + [root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain + [request_server_cert]: requires.md#requires.TlsRequires.request_server_cert + [request_client_cert]: requires.md#requires.TlsRequires.request_client_cert + [server_certs]: requires.md#requires.TlsRequires.server_certs + [server_certs_map]: requires.md#requires.TlsRequires.server_certs_map + [client_certs]: requires.md#requires.TlsRequires.server_certs + """ + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + self.relations[0].to_publish_raw['unit_name'] = self._unit_name + prefix = self.expand_name('{endpoint_name}.') + ca_available = self.root_ca_cert + ca_changed = ca_available and data_changed(prefix + 'ca', + self.root_ca_cert) + server_available = self.server_certs + server_changed = server_available and data_changed(prefix + 'servers', + self.server_certs) + client_available = self.client_certs + client_changed = client_available and data_changed(prefix + 'clients', + self.client_certs) + certs_available = server_available or client_available + certs_changed = server_changed or client_changed + + set_flag(prefix + 'available') + toggle_flag(prefix + 'ca.available', ca_available) + toggle_flag(prefix + 'ca.changed', ca_changed) + toggle_flag(prefix + 'server.certs.available', server_available) + toggle_flag(prefix + 'server.certs.changed', server_changed) + toggle_flag(prefix + 'client.certs.available', client_available) + toggle_flag(prefix + 'client.certs.changed', client_changed) + toggle_flag(prefix + 'certs.available', certs_available) + toggle_flag(prefix + 'certs.changed', certs_changed) + # deprecated + toggle_flag(prefix + 'server.cert.available', self.server_certs) + toggle_flag(prefix + 'client.cert.available', self.get_client_cert()) + toggle_flag(prefix + 'batch.cert.available', self.server_certs) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + prefix = self.expand_name('{endpoint_name}.') + clear_flag(prefix + 'available') + clear_flag(prefix + 'ca.available') + clear_flag(prefix + 'ca.changed') + clear_flag(prefix + 'server.certs.available') + clear_flag(prefix + 'server.certs.changed') + clear_flag(prefix + 'client.certs.available') + clear_flag(prefix + 'client.certs.changed') + clear_flag(prefix + 'certs.available') + clear_flag(prefix + 'certs.changed') + # deprecated + clear_flag(prefix + 'server.cert.available') + clear_flag(prefix + 'client.cert.available') + clear_flag(prefix + 'batch.cert.available') + + @property + def _unit_name(self): + return hookenv.local_unit().replace('/', '_') + + @property + def root_ca_cert(self): + """ + Root CA certificate. + """ + # only the leader of the provider should set the CA, or all units + # had better agree + return self.all_joined_units.received_raw['ca'] + + def get_ca(self): + """ + Return the root CA certificate. + + Same as [root_ca_cert][]. + """ + return self.root_ca_cert + + @property + def root_ca_chain(self): + """ + The chain of trust for the root CA. + """ + # only the leader of the provider should set the CA, or all units + # had better agree + return self.all_joined_units.received_raw['chain'] + + def get_chain(self): + """ + Return the chain of trust for the root CA. + + Same as [root_ca_chain][]. + """ + return self.root_ca_chain + + def get_client_cert(self): + """ + Deprecated. Use [request_client_cert][] and the [client_certs][] + collection instead. + + Return a globally shared client certificate and key. + """ + data = self.all_joined_units.received_raw + return (data['client.cert'], data['client.key']) + + def get_server_cert(self): + """ + Deprecated. Use the [server_certs][] collection instead. + + Return the cert and key of the first server certificate requested. + """ + if not self.server_certs: + return (None, None) + cert = self.server_certs[0] + return (cert.cert, cert.key) + + @property + def server_certs(self): + """ + List of [Certificate][] instances for all available server certs. + """ + certs = [] + raw_data = self.all_joined_units.received_raw + json_data = self.all_joined_units.received + + # for backwards compatibility, the first cert goes in its own fields + if self.relations: + common_name = self.relations[0].to_publish_raw['common_name'] + cert = raw_data['{}.server.cert'.format(self._unit_name)] + key = raw_data['{}.server.key'.format(self._unit_name)] + if cert and key: + certs.append(Certificate('server', + common_name, + cert, + key)) + + # subsequent requests go in the collection + field = '{}.processed_requests'.format(self._unit_name) + certs_data = json_data[field] or {} + certs.extend(Certificate('server', + common_name, + cert['cert'], + cert['key']) + for common_name, cert in certs_data.items()) + return certs + + @property + def application_certs(self): + """ + List containg the application Certificate cert. + + :returns: A list containing one certificate + :rtype: [Certificate()] + """ + certs = [] + json_data = self.all_joined_units.received + field = '{}.processed_application_requests'.format(self._unit_name) + certs_data = json_data[field] or {} + app_cert_data = certs_data.get('app_data') + if app_cert_data: + certs = [Certificate( + 'server', + 'app_data', + app_cert_data['cert'], + app_cert_data['key'])] + return certs + + @property + def server_certs_map(self): + """ + Mapping of server [Certificate][] instances by their `common_name`. + """ + return {cert.common_name: cert for cert in self.server_certs} + + def get_batch_requests(self): + """ + Deprecated. Use [server_certs_map][] instead. + + Mapping of server [Certificate][] instances by their `common_name`. + """ + return self.server_certs_map + + @property + def client_certs(self): + """ + List of [Certificate][] instances for all available client certs. + """ + field = '{}.processed_client_requests'.format(self._unit_name) + certs_data = self.all_joined_units.received[field] or {} + return [Certificate('client', + common_name, + cert['cert'], + cert['key']) + for common_name, cert in certs_data.items()] + + @property + def client_certs_map(self): + """ + Mapping of client [Certificate][] instances by their `common_name`. + """ + return {cert.common_name: cert for cert in self.client_certs} + + def request_server_cert(self, cn, sans=None, cert_name=None): + """ + Request a server certificate and key be generated for the given + common name (`cn`) and optional list of alternative names (`sans`). + + The `cert_name` is deprecated and not needed. + + This can be called multiple times to request more than one server + certificate, although the common names must be unique. If called + again with the same common name, it will be ignored. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + to_publish_raw = self.relations[0].to_publish_raw + if to_publish_raw['common_name'] in (None, '', cn): + # for backwards compatibility, first request goes in its own fields + to_publish_raw['common_name'] = cn + to_publish_json['sans'] = sans or [] + cert_name = to_publish_raw.get('certificate_name') or cert_name + if cert_name is None: + cert_name = str(uuid.uuid4()) + to_publish_raw['certificate_name'] = cert_name + else: + # subsequent requests go in the collection + requests = to_publish_json.get('cert_requests', {}) + requests[cn] = {'sans': sans or []} + to_publish_json['cert_requests'] = requests + + def add_request_server_cert(self, cn, sans): + """ + Deprecated. Use [request_server_cert][] instead. + """ + self.request_server_cert(cn, sans) + + def request_server_certs(self): + """ + Deprecated. Just use [request_server_cert][]; this does nothing. + """ + pass + + def request_client_cert(self, cn, sans): + """ + Request a client certificate and key be generated for the given + common name (`cn`) and list of alternative names (`sans`). + + This can be called multiple times to request more than one client + certificate, although the common names must be unique. If called + again with the same common name, it will be ignored. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + requests = to_publish_json.get('client_cert_requests', {}) + requests[cn] = {'sans': sans} + to_publish_json['client_cert_requests'] = requests + + def request_application_cert(self, cn, sans): + """ + Request an application certificate and key be generated for the given + common name (`cn`) and list of alternative names (`sans` ) of this + unit and all peer units. All units will share a single certificates. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + requests = to_publish_json.get('application_cert_requests', {}) + requests[cn] = {'sans': sans} + to_publish_json['application_cert_requests'] = requests diff --git a/easyrsa/hooks/relations/tls-certificates/tls_certificates_common.py b/easyrsa/hooks/relations/tls-certificates/tls_certificates_common.py new file mode 100644 index 0000000..99a2f8c --- /dev/null +++ b/easyrsa/hooks/relations/tls-certificates/tls_certificates_common.py @@ -0,0 +1,302 @@ +from charms.reactive import clear_flag, is_data_changed, data_changed + + +class CertificateRequest(dict): + def __init__(self, unit, cert_type, cert_name, common_name, sans): + self._unit = unit + self._cert_type = cert_type + super().__init__({ + 'certificate_name': cert_name, + 'common_name': common_name, + 'sans': sans, + }) + + @property + def _key(self): + return '.'.join((self._unit.relation.relation_id, + self.unit_name, + self.common_name)) + + def resolve_unit_name(self, unit): + """Return name of unit associated with this request. + + unit_name should be provided in the relation data to ensure + compatability with cross-model relations. If the unit name + is absent then fall back to unit_name attribute of the + unit associated with this request. + + :param unit: Unit to extract name from + :type unit: charms.reactive.endpoints.RelatedUnit + :returns: Name of unit + :rtype: str + """ + unit_name = unit.received_raw['unit_name'] + if not unit_name: + unit_name = unit.unit_name + return unit_name + + @property + def unit_name(self): + """Name of this unit. + + :returns: Name of unit + :rtype: str + """ + return self.resolve_unit_name(unit=self._unit).replace('/', '_') + + @property + def application_name(self): + """Name of the application which the request came from. + + :returns: Name of application + :rtype: str + """ + return self.resolve_unit_name(unit=self._unit).split('/')[0] + + @property + def cert_type(self): + """ + Type of certificate, 'server' or 'client', being requested. + """ + return self._cert_type + + @property + def cert_name(self): + return self['certificate_name'] + + @property + def common_name(self): + return self['common_name'] + + @property + def sans(self): + return self['sans'] + + @property + def _publish_key(self): + if self.cert_type == 'server': + return '{}.processed_requests'.format(self.unit_name) + elif self.cert_type == 'client': + return '{}.processed_client_requests'.format(self.unit_name) + raise ValueError('Unknown cert_type: {}'.format(self.cert_type)) + + @property + def _server_cert_key(self): + return '{}.server.cert'.format(self.unit_name) + + @property + def _server_key_key(self): + return '{}.server.key'.format(self.unit_name) + + @property + def _is_top_level_server_cert(self): + return (self.cert_type == 'server' and + self.common_name == self._unit.received_raw['common_name']) + + @property + def cert(self): + """ + The cert published for this request, if any. + """ + cert, key = None, None + if self._is_top_level_server_cert: + tpr = self._unit.relation.to_publish_raw + cert = tpr[self._server_cert_key] + key = tpr[self._server_key_key] + else: + tp = self._unit.relation.to_publish + certs_data = tp.get(self._publish_key, {}) + cert_data = certs_data.get(self.common_name, {}) + cert = cert_data.get('cert') + key = cert_data.get('key') + if cert and key: + return Certificate(self.cert_type, self.common_name, cert, key) + return None + + @property + def is_handled(self): + has_cert = self.cert is not None + same_sans = not is_data_changed(self._key, + sorted(set(self.sans or []))) + return has_cert and same_sans + + def set_cert(self, cert, key): + rel = self._unit.relation + if self._is_top_level_server_cert: + # backwards compatibility; if this is the cert that was requested + # as a single server cert, set it in the response as the single + # server cert + rel.to_publish_raw.update({ + self._server_cert_key: cert, + self._server_key_key: key, + }) + else: + data = rel.to_publish.get(self._publish_key, {}) + data[self.common_name] = { + 'cert': cert, + 'key': key, + } + rel.to_publish[self._publish_key] = data + if not rel.endpoint.new_server_requests: + clear_flag(rel.endpoint.expand_name('{endpoint_name}.server' + '.cert.requested')) + if not rel.endpoint.new_requests: + clear_flag(rel.endpoint.expand_name('{endpoint_name}.' + 'certs.requested')) + data_changed(self._key, sorted(set(self.sans or []))) + + +class ApplicationCertificateRequest(CertificateRequest): + """ + A request for an application consistent certificate. + + This is a request for a certificate that works for all units of an + application. All sans and cns are added together to produce one + certificate and the same certificate and key are sent to all the + units of an application. Only one ApplicationCertificateRequest + is needed per application. + """ + + @property + def _key(self): + """Key to identify this cert. + + :returns: cert key + :rtype: str + """ + return '{}.{}'.format(self._unit.relation.relation_id, 'app_cert') + + @property + def cert(self): + """ + The cert published for this request, if any. + + :returns: Certificate + :rtype: Certificate or None + """ + cert, key = None, None + tp = self._unit.relation.to_publish + certs_data = tp.get(self._publish_key, {}) + cert_data = certs_data.get('app_data', {}) + cert = cert_data.get('cert') + key = cert_data.get('key') + if cert and key: + return Certificate(self.cert_type, self.common_name, cert, key) + return None + + @property + def is_handled(self): + """Whether the certificate has been handled. + + :returns: If the cert has been handled + :rtype: bool + """ + has_cert = self.cert is not None + same_sans = not is_data_changed(self._key, + sorted(set(self.sans or []))) + return has_cert and same_sans + + @property + def sans(self): + """Generate a list of all sans from all units of application + + Examine all units of the application and compile a list of + all sans. CNs are treated as addition san entries. + + :returns: List of sans + :rtype: List[str] + """ + _sans = [] + for unit in self._unit.relation.units: + reqs = unit.received['application_cert_requests'] or {} + for cn, req in reqs.items(): + _sans.append(cn) + _sans.extend(req['sans']) + return sorted(list(set(_sans))) + + @property + def _request_key(self): + """Key used to request cert + + :returns: Key used to request cert + :rtype: str + """ + return 'application_cert_requests' + + def derive_publish_key(self, unit=None): + """Derive the application cert publish key for a unit. + + :param unit: Unit to extract name from + :type unit: charms.reactive.endpoints.RelatedUnit + :returns: publish key + :rtype: str + """ + if not unit: + unit = self._unit + unit_name = self.resolve_unit_name(unit).replace('/', '_') + return '{}.processed_application_requests'.format(unit_name) + + @property + def _publish_key(self): + """Key used to publish cert + + :returns: Key used to publish cert + :rtype: str + """ + return self.derive_publish_key(unit=self._unit) + + def set_cert(self, cert, key): + """Send the cert and key to all units of the application + + :param cert: TLS Certificate + :type cert: str + :param key: TLS Private Key + :type cert: str + """ + rel = self._unit.relation + for unit in self._unit.relation.units: + pub_key = self.derive_publish_key(unit=unit) + data = rel.to_publish.get( + pub_key, + {}) + data['app_data'] = { + 'cert': cert, + 'key': key, + } + rel.to_publish[pub_key] = data + if not rel.endpoint.new_application_requests: + clear_flag(rel.endpoint.expand_name( + '{endpoint_name}.application.certs.requested')) + data_changed(self._key, sorted(set(self.sans or []))) + + +class Certificate(dict): + """ + Represents a created certificate and key. + + The ``cert_type``, ``common_name``, ``cert``, and ``key`` values can + be accessed either as properties or as the contents of the dict. + """ + def __init__(self, cert_type, common_name, cert, key): + super().__init__({ + 'cert_type': cert_type, + 'common_name': common_name, + 'cert': cert, + 'key': key, + }) + + @property + def cert_type(self): + return self['cert_type'] + + @property + def common_name(self): + return self['common_name'] + + @property + def cert(self): + return self['cert'] + + @property + def key(self): + return self['key'] diff --git a/easyrsa/hooks/start b/easyrsa/hooks/start new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/start @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/stop b/easyrsa/hooks/stop new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/stop @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/update-status b/easyrsa/hooks/update-status new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/update-status @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/hooks/upgrade-charm b/easyrsa/hooks/upgrade-charm new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/easyrsa/hooks/upgrade-charm @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/easyrsa/icon.svg b/easyrsa/icon.svg new file mode 100644 index 0000000..67aba5a --- /dev/null +++ b/easyrsa/icon.svg @@ -0,0 +1,352 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/easyrsa/layer.yaml b/easyrsa/layer.yaml new file mode 100644 index 0000000..b70ddb8 --- /dev/null +++ b/easyrsa/layer.yaml @@ -0,0 +1,23 @@ +"includes": +- "layer:options" +- "layer:basic" +- "layer:debug" +- "layer:leadership" +- "layer:status" +- "interface:tls-certificates" +"exclude": [".travis.yml", "tests", "tox.ini", "test-requirements.txt", "unit_tests", + ".tox", "__pycache__", "conftest.py"] +"options": + "basic": + "packages": + - "openssl" + "python_packages": [] + "use_venv": !!bool "true" + "include_system_packages": !!bool "false" + "debug": {} + "leadership": {} + "status": + "patch-hookenv": !!bool "true" + "easyrsa": {} +"repo": "http://github.com/juju-solutions/layer-easyrsa.git" +"is": "easyrsa" diff --git a/easyrsa/lib/charms/layer/__init__.py b/easyrsa/lib/charms/layer/__init__.py new file mode 100644 index 0000000..a8e0c64 --- /dev/null +++ b/easyrsa/lib/charms/layer/__init__.py @@ -0,0 +1,60 @@ +import sys +from importlib import import_module +from pathlib import Path + + +def import_layer_libs(): + """ + Ensure that all layer libraries are imported. + + This makes it possible to do the following: + + from charms import layer + + layer.foo.do_foo_thing() + + Note: This function must be called after bootstrap. + """ + for module_file in Path('lib/charms/layer').glob('*'): + module_name = module_file.stem + if module_name in ('__init__', 'basic', 'execd') or not ( + module_file.suffix == '.py' or module_file.is_dir() + ): + continue + import_module('charms.layer.{}'.format(module_name)) + + +# Terrible hack to support the old terrible interface. +# Try to get people to call layer.options.get() instead so +# that we can remove this garbage. +# Cribbed from https://stackoverfLow.com/a/48100440/4941864 +class OptionsBackwardsCompatibilityHack(sys.modules[__name__].__class__): + def __call__(self, section=None, layer_file=None): + if layer_file is None: + return self.get(section=section) + else: + return self.get(section=section, + layer_file=Path(layer_file)) + + +def patch_options_interface(): + from charms.layer import options + if sys.version_info.minor >= 5: + options.__class__ = OptionsBackwardsCompatibilityHack + else: + # Py 3.4 doesn't support changing the __class__, so we have to do it + # another way. The last line is needed because we already have a + # reference that doesn't get updated with sys.modules. + name = options.__name__ + hack = OptionsBackwardsCompatibilityHack(name) + hack.get = options.get + sys.modules[name] = hack + sys.modules[__name__].options = hack + + +try: + patch_options_interface() +except ImportError: + # This may fail if pyyaml hasn't been installed yet. But in that + # case, the bootstrap logic will try it again once it has. + pass diff --git a/easyrsa/lib/charms/layer/basic.py b/easyrsa/lib/charms/layer/basic.py new file mode 100644 index 0000000..bbdd074 --- /dev/null +++ b/easyrsa/lib/charms/layer/basic.py @@ -0,0 +1,501 @@ +import os +import sys +import re +import shutil +from distutils.version import LooseVersion +from pkg_resources import Requirement +from glob import glob +from subprocess import check_call, check_output, CalledProcessError +from time import sleep + +from charms import layer +from charms.layer.execd import execd_preinstall + + +def _get_subprocess_env(): + env = os.environ.copy() + env['LANG'] = env.get('LANG', 'C.UTF-8') + return env + + +def get_series(): + """ + Return series for a few known OS:es. + Tested as of 2019 november: + * centos6, centos7, rhel6. + * bionic + """ + series = "" + + # Looking for content in /etc/os-release + # works for ubuntu + some centos + if os.path.isfile('/etc/os-release'): + d = {} + with open('/etc/os-release', 'r') as rel: + for l in rel: + if not re.match(r'^\s*$', l): + k, v = l.split('=') + d[k.strip()] = v.strip().replace('"', '') + series = "{ID}{VERSION_ID}".format(**d) + + # Looking for content in /etc/redhat-release + # works for redhat enterprise systems + elif os.path.isfile('/etc/redhat-release'): + with open('/etc/redhat-release', 'r') as redhatlsb: + # CentOS Linux release 7.7.1908 (Core) + line = redhatlsb.readline() + release = int(line.split("release")[1].split()[0][0]) + series = "centos" + str(release) + + # Looking for content in /etc/lsb-release + # works for ubuntu + elif os.path.isfile('/etc/lsb-release'): + d = {} + with open('/etc/lsb-release', 'r') as lsb: + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + series = d['DISTRIB_CODENAME'] + + # This is what happens if we cant figure out the OS. + else: + series = "unknown" + return series + + +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 + + series = get_series() + + # OMG?! is build-essentials needed? + ubuntu_packages = ['python3-pip', + 'python3-setuptools', + 'python3-yaml', + 'python3-dev', + 'python3-wheel', + 'build-essential'] + + # I'm not going to "yum group info "Development Tools" + # omitting above madness + centos_packages = ['python3-pip', + 'python3-setuptools', + 'python3-devel', + 'python3-wheel'] + + packages_needed = [] + if 'centos' in series: + packages_needed = centos_packages + else: + packages_needed = ubuntu_packages + + 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') + hook_name = os.path.basename(sys.argv[0]) + is_bootstrapped = os.path.exists('wheelhouse/.bootstrapped') + is_charm_upgrade = hook_name == 'upgrade-charm' + is_series_upgrade = hook_name == 'post-series-upgrade' + is_post_upgrade = os.path.exists('wheelhouse/.upgraded') + is_upgrade = (not is_post_upgrade and + (is_charm_upgrade or is_series_upgrade)) + if is_bootstrapped and not is_upgrade: + # older subordinates might have downgraded charm-env, so we should + # restore it if necessary + install_or_update_charm_env() + activate_venv() + # the .upgrade file prevents us from getting stuck in a loop + # when re-execing to activate the venv; at this point, we've + # activated the venv, so it's safe to clear it + if is_post_upgrade: + os.unlink('wheelhouse/.upgraded') + return + if os.path.exists(venv): + try: + # focal installs or upgrades prior to PR 160 could leave the venv + # in a broken state which would prevent subsequent charm upgrades + _load_installed_versions(vpip) + except CalledProcessError: + is_broken_venv = True + else: + is_broken_venv = False + if is_upgrade or is_broken_venv: + # All upgrades should do a full clear of the venv, rather than + # just updating it, to bring in updates to Python itself + shutil.rmtree(venv) + if is_upgrade: + if os.path.exists('wheelhouse/.bootstrapped'): + os.unlink('wheelhouse/.bootstrapped') + # bootstrap wheelhouse + if os.path.exists('wheelhouse'): + pre_eoan = series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty', + 'ubuntu16.04', 'xenial', + 'ubuntu18.04', 'bionic') + pydistutils_lines = [ + "[easy_install]\n", + "find_links = file://{}/wheelhouse/\n".format(charm_dir), + "no_index=True\n", + "index_url=\n", # deliberately nothing here; disables it. + ] + if pre_eoan: + pydistutils_lines.append("allow_hosts = ''\n") + 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(pydistutils_lines) + if 'centos' in series: + yum_install(packages_needed) + else: + apt_install(packages_needed) + from charms.layer import options + cfg = options.get('basic') + # include packages defined in layer.yaml + if 'centos' in series: + yum_install(cfg.get('packages', [])) + else: + 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 = get_series() + if series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty'): + apt_install(['python-virtualenv']) + elif 'centos' in series: + yum_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, env=_get_subprocess_env()) + 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') + pre_install_pkgs = ['pip', 'setuptools', 'setuptools-scm'] + # we bundle these packages to work around bugs in older versions (such + # as https://github.com/pypa/pip/issues/56), but if the system already + # provided a newer version, downgrading it can cause other problems + _update_if_newer(pip, pre_install_pkgs) + # install the rest of the wheelhouse deps (extract the pkg names into + # a set so that we can ignore the pre-install packages and let pip + # choose the best version in case there are multiple from layer + # conflicts) + _versions = _load_wheelhouse_versions() + _pkgs = _versions.keys() - set(pre_install_pkgs) + # add back the versions such that each package in pkgs is + # ==. + # This ensures that pip 20.3.4+ will install the packages from the + # wheelhouse without (erroneously) flagging an error. + pkgs = _add_back_versions(_pkgs, _versions) + reinstall_flag = '--force-reinstall' + if not cfg.get('use_venv', True) and pre_eoan: + reinstall_flag = '--ignore-installed' + check_call([pip, 'install', '-U', reinstall_flag, '--no-index', + '--no-cache-dir', '-f', 'wheelhouse'] + list(pkgs), + env=_get_subprocess_env()) + # re-enable installation from pypi + os.remove('/root/.pydistutils.cfg') + + # install pyyaml for centos7, since, unlike the ubuntu image, the + # default image for centos doesn't include pyyaml; see the discussion: + # https://discourse.jujucharms.com/t/charms-for-centos-lets-begin + if 'centos' in series: + check_call([pip, 'install', '-U', 'pyyaml'], + env=_get_subprocess_env()) + + # install python packages from layer options + if cfg.get('python_packages'): + check_call([pip, 'install', '-U'] + cfg.get('python_packages'), + env=_get_subprocess_env()) + 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') + # setup wrappers to ensure envs are used for scripts + install_or_update_charm_env() + for wrapper in ('charms.reactive', 'charms.reactive.sh', + 'chlp', 'layer_option'): + src = os.path.join('/usr/local/sbin', 'charm-env') + dst = os.path.join('/usr/local/sbin', wrapper) + if not os.path.exists(dst): + os.symlink(src, dst) + if cfg.get('use_venv'): + shutil.copy2('bin/layer_option', vbin) + else: + shutil.copy2('bin/layer_option', '/usr/local/bin/') + # re-link the charm copy to the wrapper in case charms + # call bin/layer_option directly (as was the old pattern) + os.remove('bin/layer_option') + os.symlink('/usr/local/sbin/layer_option', 'bin/layer_option') + # flag us as having already bootstrapped so we don't do it again + open('wheelhouse/.bootstrapped', 'w').close() + if is_upgrade: + # flag us as having already upgraded so we don't do it again + open('wheelhouse/.upgraded', '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 _load_installed_versions(pip): + pip_freeze = check_output([pip, 'freeze']).decode('utf8') + versions = {} + for pkg_ver in pip_freeze.splitlines(): + try: + req = Requirement.parse(pkg_ver) + except ValueError: + continue + versions.update({ + req.project_name: LooseVersion(ver) + for op, ver in req.specs if op == '==' + }) + return versions + + +def _load_wheelhouse_versions(): + versions = {} + for wheel in glob('wheelhouse/*'): + pkg, ver = os.path.basename(wheel).rsplit('-', 1) + # nb: LooseVersion ignores the file extension + versions[pkg.replace('_', '-')] = LooseVersion(ver) + return versions + + +def _add_back_versions(pkgs, versions): + """Add back the version strings to each of the packages. + + The versions are LooseVersion() from _load_wheelhouse_versions(). This + function strips the ".zip" or ".tar.gz" from the end of the version string + and adds it back to the package in the form of == + + If a package name is not a key in the versions dictionary, then it is + returned in the list unchanged. + + :param pkgs: A list of package names + :type pkgs: List[str] + :param versions: A map of package to LooseVersion + :type versions: Dict[str, LooseVersion] + :returns: A list of (maybe) versioned packages + :rtype: List[str] + """ + def _strip_ext(s): + """Strip an extension (if it exists) from the string + + :param s: the string to strip an extension off if it exists + :type s: str + :returns: string without an extension of .zip or .tar.gz + :rtype: str + """ + for ending in [".zip", ".tar.gz"]: + if s.endswith(ending): + return s[:-len(ending)] + return s + + def _maybe_add_version(pkg): + """Maybe add back the version number to a package if it exists. + + Adds the version number, if the package exists in the lexically + captured `versions` dictionary, in the form ==. Strips + the extension if it exists. + + :param pkg: the package name to (maybe) add the version number to. + :type pkg: str + """ + try: + return "{}=={}".format(pkg, _strip_ext(str(versions[pkg]))) + except KeyError: + pass + return pkg + + return [_maybe_add_version(pkg) for pkg in pkgs] + + +def _update_if_newer(pip, pkgs): + installed = _load_installed_versions(pip) + wheelhouse = _load_wheelhouse_versions() + for pkg in pkgs: + if pkg not in installed or wheelhouse[pkg] > installed[pkg]: + check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', + pkg], env=_get_subprocess_env()) + + +def install_or_update_charm_env(): + # On Trusty python3-pkg-resources is not installed + try: + from pkg_resources import parse_version + except ImportError: + apt_install(['python3-pkg-resources']) + from pkg_resources import parse_version + + try: + installed_version = parse_version( + check_output(['/usr/local/sbin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + installed_version = parse_version('0.0.0') + try: + bundled_version = parse_version( + check_output(['bin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + bundled_version = parse_version('0.0.0') + if installed_version < bundled_version: + shutil.copy2('bin/charm-env', '/usr/local/sbin/') + + +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. + """ + from charms.layer import options + venv = os.path.abspath('../.venv') + vbin = os.path.join(venv, 'bin') + vpy = os.path.join(vbin, 'python') + use_venv = options.get('basic', 'use_venv') + if use_venv and '.venv' not in sys.executable: + # activate the venv + os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) + reload_interpreter(vpy) + layer.patch_options_interface() + layer.import_layer_libs() + + +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 = _get_subprocess_env() + + 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 + try: + # sometimes apt-get update needs to be run + check_call(['apt-get', 'update'], env=env) + except CalledProcessError: + # sometimes it's a dpkg lock issue + pass + sleep(5) + else: + break + + +def yum_install(packages): + """ Installs packages with yum. + This function largely mimics the apt_install function for consistency. + """ + if packages: + env = os.environ.copy() + cmd = ['yum', '-y', 'install'] + for attempt in range(3): + try: + check_call(cmd + packages, env=env) + except CalledProcessError: + if attempt == 2: + raise + try: + check_call(['yum', 'update'], env=env) + except CalledProcessError: + pass + sleep(5) + else: + break + else: + pass + + +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]) + + +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/easyrsa/lib/charms/layer/execd.py b/easyrsa/lib/charms/layer/execd.py new file mode 100644 index 0000000..438d9a1 --- /dev/null +++ b/easyrsa/lib/charms/layer/execd.py @@ -0,0 +1,114 @@ +# 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 . + +# This module may only import from the Python standard library. +import os +import sys +import subprocess +import time + +''' +execd/preinstall + +Read the layer-basic docs for more info on how to use this feature. +https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#exec-d-support +''' + + +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<. + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.reactive import not_unless + + +__all__ = ['leader_get', 'leader_set'] + + +@not_unless('leadership.is_leader') +def leader_set(*args, **kw): + '''Change leadership settings, per charmhelpers.core.hookenv.leader_set. + + Settings may either be passed in as a single dictionary, or using + keyword arguments. All values must be strings. + + The leadership.set.{key} reactive state will be set while the + leadership hook environment setting remains set. + + Changed leadership settings will set the leadership.changed.{key} + and leadership.changed states. These states will remain set until + the following hook. + + These state changes take effect immediately on the leader, and + in future hooks run on non-leaders. In this way both leaders and + non-leaders can share handlers, waiting on these states. + ''' + if args: + if len(args) > 1: + raise TypeError('leader_set() takes 1 positional argument but ' + '{} were given'.format(len(args))) + else: + settings = dict(args[0]) + else: + settings = {} + settings.update(kw) + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + + for key, value in settings.items(): + if value != previous.get(key): + reactive.set_state('leadership.changed.{}'.format(key)) + reactive.set_state('leadership.changed') + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + hookenv.leader_set(settings) + unitdata.kv().update(settings, prefix='leadership.settings.') + + +def leader_get(attribute=None): + '''Return leadership settings, per charmhelpers.core.hookenv.leader_get.''' + return hookenv.leader_get(attribute) diff --git a/easyrsa/lib/debug_script.py b/easyrsa/lib/debug_script.py new file mode 100644 index 0000000..e156924 --- /dev/null +++ b/easyrsa/lib/debug_script.py @@ -0,0 +1,8 @@ +import os + +dir = os.environ["DEBUG_SCRIPT_DIR"] + + +def open_file(path, *args, **kwargs): + """ Open a file within the debug script dir """ + return open(os.path.join(dir, path), *args, **kwargs) diff --git a/easyrsa/make_docs b/easyrsa/make_docs new file mode 100644 index 0000000..dcd4c1f --- /dev/null +++ b/easyrsa/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import os +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + sys.path.insert(0, 'lib') + sys.path.insert(1, 'reactive') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') + if os.path.exists('.unit-state.db'): + os.remove('.unit-state.db') diff --git a/easyrsa/metadata.yaml b/easyrsa/metadata.yaml new file mode 100644 index 0000000..15931aa --- /dev/null +++ b/easyrsa/metadata.yaml @@ -0,0 +1,35 @@ +"name": "easyrsa" +"summary": "Delivers EasyRSA to create a Certificate Authority (CA)." +"maintainers": +- "Tim Van Steenburgh " +- "George Kraft " +- "Rye Terrell " +- "Konstantinos Tsakalozos " +- "Matthew Bruzek " +"description": | + This charm delivers the EasyRSA application and through Juju events creates + a Certificate Authority (CA), server certificates, and client certificates. +"tags": +- "misc" +- "tls" +- "pki" +- "ca" +"series": +- "focal" +- "bionic" +- "xenial" +- "trusty" +"provides": + "client": + "interface": "tls-certificates" +"resources": + "easyrsa": + "type": "file" + "filename": "easyrsa.tgz" + "description": | + The release of the EasyRSA software you would like to use to create + certificate authority (CA) and other Public Key Infrastructure (PKI). + This charm was written using v3.0.1, so earlier versions of EasyRSA may + not work. You can find the releases of EasyRSA at + https://github.com/OpenVPN/easy-rsa/releases +"subordinate": !!bool "false" diff --git a/easyrsa/pydocmd.yml b/easyrsa/pydocmd.yml new file mode 100644 index 0000000..ab3b2ef --- /dev/null +++ b/easyrsa/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'Status Management Layer' + +generate: + - status.md: + - charms.layer.status.WorkloadState + - charms.layer.status.maintenance + - charms.layer.status.maint + - charms.layer.status.blocked + - charms.layer.status.waiting + - charms.layer.status.active + - charms.layer.status.status_set + +pages: + - Status Management Layer: status.md + +gens_dir: docs diff --git a/easyrsa/reactive/__init__.py b/easyrsa/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/easyrsa/reactive/easyrsa.py b/easyrsa/reactive/easyrsa.py new file mode 100644 index 0000000..94ef275 --- /dev/null +++ b/easyrsa/reactive/easyrsa.py @@ -0,0 +1,486 @@ +import os +import shutil + +from shlex import split +from subprocess import check_call +from subprocess import check_output + +from charms.reactive import hook +from charms.reactive import when +from charms.reactive import when_not +from charms.reactive.helpers import data_changed +from charms.reactive.relations import endpoint_from_flag +from charms.reactive.flags import is_flag_set +from charms.reactive.flags import clear_flag +from charms.reactive.flags import set_flag + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata +from charmhelpers.core.host import chdir +from charmhelpers.core.hookenv import resource_get + +from charms.leadership import leader_set +from charms.leadership import leader_get + +from charms.layer import status + + +charm_directory = hookenv.charm_dir() +easyrsa_directory = os.path.join(charm_directory, 'EasyRSA') + + +@when_not('easyrsa.installed') +def install(): + '''Install the easy-rsa software that is used by this layer.''' + easyrsa_resource = None + try: + # Try to get the resource from Juju. + easyrsa_resource = resource_get('easyrsa') + except Exception as e: + message = 'An error occurred fetching the easyrsa resource.' + hookenv.log(message) + hookenv.log(e) + status.blocked(message) + return + + if not easyrsa_resource: + status.blocked('The easyrsa resource is missing.') + return + + # Get the filesize in bytes. + filesize = os.stat(easyrsa_resource).st_size + # When the filesize is less than 10 KB we do not have a real file. + if filesize < 10240: + status.blocked('The easyrsa resource is not complete.') + return + + # Expand the archive in the charm directory creating an EasyRSA directory. + untar = 'tar -xvzf {0} -C {1}'.format(easyrsa_resource, charm_directory) + check_call(split(untar)) + + version = get_version(easyrsa_resource) + # Save the version in the key/value store of the charm. + unitdata.kv().set('easyrsa-version', version) + + if os.path.islink(easyrsa_directory): + check_call(split('rm -v {0}'.format(easyrsa_directory))) + + # Link the EasyRSA version directory to a common name. + link = 'ln -v -s {0}/EasyRSA-{1} {2}'.format(charm_directory, + version, + easyrsa_directory) + check_call(split(link)) + # The charm pki directory contains backup of pki for upgrades. + charm_pki_directory = os.path.join(charm_directory, 'pki') + if os.path.isdir(charm_pki_directory): + new_pki_directory = os.path.join(easyrsa_directory, 'pki') + # Only copy the directory if the new_pki_directory does not exist. + if not os.path.isdir(new_pki_directory): + # Copy the pki to this new directory. + shutil.copytree(charm_pki_directory, new_pki_directory, + symlinks=True) + # We are done with the old charm pki directory, so delete contents. + shutil.rmtree(charm_pki_directory) + else: + # Create new pki. + with chdir(easyrsa_directory): + check_call(split('./easyrsa --batch init-pki 2>&1')) + set_flag('easyrsa.installed') + + +@when('easyrsa.installed') +def set_easyrsa_version(): + '''Find the version of easyrsa and set that on the charm.''' + version = unitdata.kv().get('easyrsa-version') + hookenv.application_version_set(version) + + +@when('easyrsa.installed') +@when_not('easyrsa.configured') +def configure_easyrsa(): + '''A transitional state to allow modifications to configuration before + generating the certificates and working with PKI.''' + hookenv.log('Configuring OpenSSL to copy extensions.') + configure_copy_extensions() + hookenv.log('Configuring X509 server extensions with clientAuth.') + configure_client_authorization() + set_flag('easyrsa.configured') + + +def configure_copy_extensions(): + '''Update the EasyRSA configuration with the capacity to copy the exensions + through to the resulting certificates. ''' + # Create an absolute path to the file which will not be impacted by cwd. + openssl_file = os.path.join(easyrsa_directory, 'openssl-1.0.cnf') + # Update EasyRSA configuration with the capacity to copy CSR Requested + # Extensions through to the resulting certificate. This can be tricky, + # and the implications are not fully clear on this. + with open(openssl_file, 'r') as f: + conf = f.readlines() + # When the copy_extensions key is not in the configuration. + if 'copy_extensions = copy\n' not in conf: + for idx, line in enumerate(conf): + if '[ CA_default ]' in line: + # Insert a new line with the copy_extensions key set to copy. + conf.insert(idx + 1, "copy_extensions = copy\n") + with open(openssl_file, 'w+') as f: + f.writelines(conf) + + +def configure_client_authorization(): + '''easyrsa has a default OpenSSL configuration that does not support + client authentication. Append "clientAuth" to the server ssl certificate + configuration. This is not default, to enable this in your charm set the + reactive state 'tls.client.authorization.required'. + ''' + # Use an absolute path so current directory does not affect the result. + openssl_config = os.path.join(easyrsa_directory, 'x509-types/server') + hookenv.log('Updating {0}'.format(openssl_config)) + + # Read the X509 server extension file in. + with open(openssl_config, 'r') as f: + server_extensions = f.readlines() + + client_server = [] + for line in server_extensions: + # Replace the extendedKeyUsage with clientAuth and serverAuth. + if 'extendedKeyUsage' in line: + line = line.replace('extendedKeyUsage = serverAuth', + 'extendedKeyUsage = clientAuth, serverAuth') + client_server.append(line) + # Write the configuration file back out. + with open(openssl_config, 'w+') as f: + f.writelines(client_server) + + +@when('easyrsa.configured') +@when('leadership.is_leader') +@when_not('easyrsa.certificate.authority.available') +@when_not('upgrade.series.in-progress') +def create_certificate_authority(): + '''Return the CA and server certificates for this system. If the CA is + empty, generate a self signged certificate authority.''' + ca_file = 'pki/ca.crt' + key_file = 'pki/private/ca.key' + serial_file = 'pki/serial' + + with chdir(easyrsa_directory): + if leader_get('certificate_authority') and \ + leader_get('certificate_authority_key') and \ + leader_get('certificate_authority_serial'): + hookenv.log('Recovering CA from controller') + certificate_authority = \ + leader_get('certificate_authority') + certificate_authority_key = \ + leader_get('certificate_authority_key') + certificate_authority_serial = \ + leader_get('certificate_authority_serial') + + # Write the CA from existing relation. + with open(ca_file, 'w') as f_out: + f_out.write(certificate_authority) + + # Write the private key from existing relation. + with open(key_file, 'w') as f_out: + f_out.write(certificate_authority_key) + + # Write the serial from existing relation. + with open(serial_file, 'w') as f_out: + f_out.write(certificate_authority_serial) + + # Bluff required files and folders. + with open('pki/index.txt', 'w') as f_out: + pass + os.makedirs('pki/issued') + os.makedirs('pki/certs_by_serial') + + else: + hookenv.log('Creating new CA') + # The Common Name (CN) for a certificate + # must be an IP or hostname. + cn = hookenv.unit_public_ip() + # Create a self signed CA with the CN, stored pki/ca.crt + build_ca = \ + './easyrsa --batch "--req-cn={0}" build-ca nopass 2>&1' + # Build a self signed Certificate Authority. + check_call(split(build_ca.format(cn))) + + # Read the CA so it can be returned in leader data. + with open(ca_file, 'r') as stream: + certificate_authority = stream.read() + + # Read the private key so it can be set in leader data. + with open(key_file, 'r') as stream: + certificate_authority_key = stream.read() + + with open(serial_file, 'r') as stream: + certificate_authority_serial = stream.read() + + # Set these values on the leadership data. + leader_set({ + 'certificate_authority': certificate_authority}) + leader_set({ + 'certificate_authority_key': certificate_authority_key}) + leader_set({ + 'certificate_authority_serial': certificate_authority_serial}) + + # Install the CA on this system as a trusted CA. + install_ca(certificate_authority) + status.active('Certificiate Authority available') + + set_flag('easyrsa.certificate.authority.available') + + +@when('easyrsa.certificate.authority.available') +@when_not('upgrade.series.in-progress') +def message(): + '''Set a message to notify the user that this charm is ready.''' + if is_flag_set('client.available'): + status.active('Certificate Authority connected.') + else: + status.active('Certificate Authority ready.') + + +@when('client.available', 'easyrsa.certificate.authority.available') +@when('leadership.is_leader') +def send_ca(): + '''The client relationship has been established, read the CA and client + certificate from leadership data to set them on the relationship object.''' + tls = endpoint_from_flag('client.available') + certificate_authority = leader_get('certificate_authority') + tls.set_ca(certificate_authority) + + +@when('leadership.is_leader', + 'easyrsa.certificate.authority.available', + 'client.available') +@when_not('easyrsa.global-client-cert.created') +def create_global_client_cert(): + """ + This is for backwards compatibility with older tls-certificate clients + only. Obviously, it's not good security / design to have clients sharing + a certificate, but it seems that there are clients that depend on this + (though some, like etcd, only block on the flag that it triggers but don't + actually use the cert), so we have to set it for now. + """ + client_cert = leader_get('client_certificate') + client_key = leader_get('client_key') + if not client_cert or not client_key: + hookenv.log("Unable to find global client cert on " + "leadership data, generating...") + client_cert, client_key = create_client_certificate() + # Set the client certificate and key on leadership data. + leader_set({'client_certificate': client_cert}) + leader_set({'client_key': client_key}) + else: + hookenv.log("found global client cert on leadership " + "data, not generating...") + set_flag('easyrsa.global-client-cert.created') + + +@when('leadership.is_leader', + 'easyrsa.global-client-cert.created', + 'client.available') +def publish_global_client_cert(): + # global client cert needs to always be re-published to account for new + # clients joining + tls = endpoint_from_flag('client.available') + tls.set_client_cert(leader_get('client_certificate'), + leader_get('client_key')) + + +@when('client.server.certs.requested', 'easyrsa.configured') +def create_server_cert(): + '''Create server certificates with the request information from the + relation object.''' + + tls = endpoint_from_flag('client.server.certs.requested') + + # Iterate over all new requests + for request in tls.new_server_requests: + cn = request.common_name + sans = request.sans + name = request.common_name + # Create the server certificate based on the information in request. + server_cert, server_key = create_server_certificate(cn, sans, name) + # Set the certificate and key for the unit on the relationship object. + request.set_cert(server_cert, server_key) + + +@when('client.client.certs.requested', 'easyrsa.configured') +def create_client_cert(): + '''Create client certificates with the request information from the + relation object.''' + + tls = endpoint_from_flag('client.client.certs.requested') + + # Iterate over all new requests + for request in tls.new_client_requests: + # Create a client certificate for this request. + name = request.common_name + client_cert, client_key = create_client_certificate(name) + # Set the client certificate and key on the relationship object. + request.set_cert(client_cert, client_key) + + +@hook('upgrade-charm') +def upgrade(): + '''An upgrade has been triggered.''' + pki_directory = os.path.join(easyrsa_directory, 'pki') + if os.path.isdir(pki_directory): + # specific handling if the upgrade is from a previous version + # where certificate_authority_serial is not set at install + serial_file = 'serial' + with chdir(pki_directory): + # if the ca and ca_key are set and serial is not + # set this to serial in the pki directory + if os.path.isfile(serial_file) and \ + leader_get('certificate_authority') and \ + leader_get('certificate_authority_key') and not \ + leader_get('certificate_authority_serial'): + with open(serial_file, 'r') as stream: + ca_serial = stream.read() + # set the previously unset certificate authority serial + leader_set({ + 'certificate_authority_serial': ca_serial}) + + charm_pki_directory = os.path.join(charm_directory, 'pki') + # When the charm pki directory exists, it is stale, remove it. + if os.path.isdir(charm_pki_directory): + shutil.rmtree(charm_pki_directory) + # Copy the EasyRSA/pki to the charm pki directory. + shutil.copytree(pki_directory, charm_pki_directory, symlinks=True) + clear_flag('easyrsa.installed') + clear_flag('easyrsa.configured') + + +@hook('pre-series-upgrade') +def pre_series_upgrade(): + status.blocked('Series upgrade in progress') + + +def remove_file_if_exists(filename): + try: + os.remove(filename) + except FileNotFoundError: + pass + + +def create_server_certificate(cn, san_list, name=None): + '''Return a newly created server certificate and server key from a + common name, list of Subject Alternate Names, and the certificate name.''' + if name is None: + name = 'server' + server_cert = None + server_key = None + with chdir(easyrsa_directory): + # Create the path to the server certificate. + cert_file = 'pki/issued/{0}.crt'.format(name) + # Create the path to the server key. + key_file = 'pki/private/{0}.key'.format(name) + # Create the path to the request file + req_file = 'pki/reqs/{0}.req'.format(name) + # Get a string compatible with easyrsa for the subject-alt-names. + sans = get_sans(san_list) + sans_arg = '--subject-alt-name={}'.format(sans) if sans else '' + this_cert = {'sans': sans, 'cn': cn, 'name': name} + changed = data_changed('server_cert.' + name, this_cert) + cert_exists = os.path.isfile(cert_file) and os.path.isfile(key_file) + # Do not regenerate the server certificate if it already exists + # and the data hasn't changed. + if changed and cert_exists: + # We need to revoke the existing cert and regenerate it + revoke = './easyrsa --batch revoke {0}'.format(name) + check_call(split(revoke)) + # nuke old files if they exist + remove_file_if_exists(cert_file) + remove_file_if_exists(key_file) + remove_file_if_exists(req_file) + if changed or not cert_exists: + # Create a server certificate for the server based on the CN. + server = './easyrsa --batch --req-cn={0} {1} ' \ + 'build-server-full {2} nopass 2>&1'.format(cn, + sans_arg, + name) + check_call(split(server)) + # Read the server certificate from the file system. + with open(cert_file, 'r') as stream: + server_cert = stream.read() + # Read the server key from the file system. + with open(key_file, 'r') as stream: + server_key = stream.read() + return server_cert, server_key + + +def create_client_certificate(name='client'): + '''Return a newly created client certificate and client key, by name.''' + client_cert = None + client_key = None + with chdir(easyrsa_directory): + # Create a path to the client certificate. + cert_file = 'pki/issued/{0}.crt'.format(name) + # Create a path to the client key. + key_file = 'pki/private/{0}.key'.format(name) + # Do not regenerate the client certificate if it already exists. + if not os.path.isfile(cert_file) and not os.path.isfile(key_file): + # Create a client certificate and key. + check_call(['./easyrsa', 'build-client-full', name, 'nopass']) + # Read the client certificate from the file system. + with open(cert_file, 'r') as stream: + client_cert = stream.read() + # Read the client key from the file system. + with open(key_file, 'r') as stream: + client_key = stream.read() + return client_cert, client_key + + +def install_ca(certificate_authority): + '''Install a certificiate authority on the system by calling the + update-ca-certificates command.''' + name = hookenv.service_name() + ca_file = '/usr/local/share/ca-certificates/{0}.crt'.format(name) + hookenv.log('Writing CA to {0}'.format(ca_file)) + # Write the contents of certificate authority to the file. + with open(ca_file, 'w') as fp: + fp.write(certificate_authority) + # Update the trusted CAs on this system. + check_call(['update-ca-certificates']) + message = 'Generated ca-certificates.crt for {0}'.format(name) + hookenv.log(message) + + +def get_sans(address_list=[]): + '''Return a string suitable for the easy-rsa subjectAltNames.''' + sans = [] + for address in address_list: + if _is_ip(address): + sans.append('IP:{0}'.format(address)) + else: + sans.append('DNS:{0}'.format(address)) + return ','.join(sans) + + +def get_version(path): + '''Return the version of EasyRSA by investigating the tar file.''' + # Create a command that lists the tar file. + cmd = 'tar -tf {0}'.format(path) + # Get the listing of the directories and files in the tar file. + output = check_output(split(cmd)).decode('utf-8') + # Get the first listing which is the directory. + directory = output.splitlines()[0] + # Remove the path separator from the string. + if '/' in directory: + directory = directory.replace('/', '') + # Get the version by splitting on the hypen. + return directory.split('-')[1] + + +def _is_ip(address): + '''Return True if the address is an IP address, false otherwise.''' + import ipaddress + try: + # This method will raise a ValueError if argument is not an IP address. + ipaddress.ip_address(address) + return True + except ValueError: + return False diff --git a/easyrsa/reactive/leadership.py b/easyrsa/reactive/leadership.py new file mode 100644 index 0000000..29c6f3a --- /dev/null +++ b/easyrsa/reactive/leadership.py @@ -0,0 +1,68 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Leadership Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.leadership import leader_get, leader_set + + +__all__ = ['leader_get', 'leader_set'] # Backwards compatibility + + +def initialize_leadership_state(): + '''Initialize leadership.* states from the hook environment. + + Invoked by hookenv.atstart() so states are available in + @hook decorated handlers. + ''' + is_leader = hookenv.is_leader() + if is_leader: + hookenv.log('Initializing Leadership Layer (is leader)') + else: + hookenv.log('Initializing Leadership Layer (is follower)') + + reactive.helpers.toggle_state('leadership.is_leader', is_leader) + + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + current = hookenv.leader_get() + + # Handle deletions. + for key in set(previous.keys()) - set(current.keys()): + current[key] = None + + any_changed = False + for key, value in current.items(): + reactive.helpers.toggle_state('leadership.changed.{}'.format(key), + value != previous.get(key)) + if value != previous.get(key): + any_changed = True + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + reactive.helpers.toggle_state('leadership.changed', any_changed) + + unitdata.kv().update(current, prefix='leadership.settings.') + + +# 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, '_leadership_registered'): + hookenv.atstart(initialize_leadership_state) + reactive._leadership_registered = True diff --git a/easyrsa/reactive/status.py b/easyrsa/reactive/status.py new file mode 100644 index 0000000..2f33f3f --- /dev/null +++ b/easyrsa/reactive/status.py @@ -0,0 +1,4 @@ +from charms import layer + + +layer.status._initialize() diff --git a/easyrsa/requirements.txt b/easyrsa/requirements.txt new file mode 100644 index 0000000..55543d9 --- /dev/null +++ b/easyrsa/requirements.txt @@ -0,0 +1,3 @@ +mock +flake8 +pytest diff --git a/easyrsa/revision b/easyrsa/revision new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/easyrsa/revision @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/easyrsa/tox.ini b/easyrsa/tox.ini new file mode 100644 index 0000000..e2b841a --- /dev/null +++ b/easyrsa/tox.ini @@ -0,0 +1,14 @@ +[tox] +envlist = py3 +skipsdist = true + +[testenv] +basepython=python3 +envdir={toxworkdir}/py3 +deps= + pytest + charms.reactive + pydoc-markdown + +[testenv:docs] +commands=python make_docs diff --git a/easyrsa/version b/easyrsa/version new file mode 100644 index 0000000..20817dd --- /dev/null +++ b/easyrsa/version @@ -0,0 +1 @@ +ccfa68be \ No newline at end of file diff --git a/easyrsa/wheelhouse.txt b/easyrsa/wheelhouse.txt new file mode 100644 index 0000000..7197e18 --- /dev/null +++ b/easyrsa/wheelhouse.txt @@ -0,0 +1,18 @@ +# layer:basic +# pip is pinned to <19.0 to avoid https://github.com/pypa/pip/issues/6164 +# even with installing setuptools before upgrading pip ends up with pip seeing +# the older setuptools at the system level if include_system_packages is true +pip>=18.1,<19.0 +# pin Jinja2, PyYAML and MarkupSafe to the last versions supporting python 3.5 +# for trusty +Jinja2<=2.10.1 +PyYAML<=5.2 +MarkupSafe<2.0.0 +setuptools<42 +setuptools-scm<=1.17.0 +charmhelpers>=0.4.0,<1.0.0 +charms.reactive>=0.1.0,<2.0.0 +wheel<0.34 +# pin netaddr to avoid pulling importlib-resources +netaddr<=0.7.19 + diff --git a/easyrsa/wheelhouse/Jinja2-2.10.1.tar.gz b/easyrsa/wheelhouse/Jinja2-2.10.1.tar.gz new file mode 100644 index 0000000..ffd1054 Binary files /dev/null and b/easyrsa/wheelhouse/Jinja2-2.10.1.tar.gz differ diff --git a/easyrsa/wheelhouse/MarkupSafe-1.1.1.tar.gz b/easyrsa/wheelhouse/MarkupSafe-1.1.1.tar.gz new file mode 100644 index 0000000..a6dad8e Binary files /dev/null and b/easyrsa/wheelhouse/MarkupSafe-1.1.1.tar.gz differ diff --git a/easyrsa/wheelhouse/PyYAML-5.2.tar.gz b/easyrsa/wheelhouse/PyYAML-5.2.tar.gz new file mode 100644 index 0000000..666d12a Binary files /dev/null and b/easyrsa/wheelhouse/PyYAML-5.2.tar.gz differ diff --git a/easyrsa/wheelhouse/Tempita-0.5.2.tar.gz b/easyrsa/wheelhouse/Tempita-0.5.2.tar.gz new file mode 100644 index 0000000..755befc Binary files /dev/null and b/easyrsa/wheelhouse/Tempita-0.5.2.tar.gz differ diff --git a/easyrsa/wheelhouse/charmhelpers-0.20.23.tar.gz b/easyrsa/wheelhouse/charmhelpers-0.20.23.tar.gz new file mode 100644 index 0000000..8fbc8ec Binary files /dev/null and b/easyrsa/wheelhouse/charmhelpers-0.20.23.tar.gz differ diff --git a/easyrsa/wheelhouse/charms.reactive-1.4.1.tar.gz b/easyrsa/wheelhouse/charms.reactive-1.4.1.tar.gz new file mode 100644 index 0000000..03bc1fe Binary files /dev/null and b/easyrsa/wheelhouse/charms.reactive-1.4.1.tar.gz differ diff --git a/easyrsa/wheelhouse/netaddr-0.7.19.tar.gz b/easyrsa/wheelhouse/netaddr-0.7.19.tar.gz new file mode 100644 index 0000000..cc31d9d Binary files /dev/null and b/easyrsa/wheelhouse/netaddr-0.7.19.tar.gz differ diff --git a/easyrsa/wheelhouse/pbr-5.6.0.tar.gz b/easyrsa/wheelhouse/pbr-5.6.0.tar.gz new file mode 100644 index 0000000..0d5c965 Binary files /dev/null and b/easyrsa/wheelhouse/pbr-5.6.0.tar.gz differ diff --git a/easyrsa/wheelhouse/pip-18.1.tar.gz b/easyrsa/wheelhouse/pip-18.1.tar.gz new file mode 100644 index 0000000..a18192d Binary files /dev/null and b/easyrsa/wheelhouse/pip-18.1.tar.gz differ diff --git a/easyrsa/wheelhouse/pyaml-21.10.1.tar.gz b/easyrsa/wheelhouse/pyaml-21.10.1.tar.gz new file mode 100644 index 0000000..b19aad3 Binary files /dev/null and b/easyrsa/wheelhouse/pyaml-21.10.1.tar.gz differ diff --git a/easyrsa/wheelhouse/setuptools-41.6.0.zip b/easyrsa/wheelhouse/setuptools-41.6.0.zip new file mode 100644 index 0000000..3345759 Binary files /dev/null and b/easyrsa/wheelhouse/setuptools-41.6.0.zip differ diff --git a/easyrsa/wheelhouse/setuptools_scm-1.17.0.tar.gz b/easyrsa/wheelhouse/setuptools_scm-1.17.0.tar.gz new file mode 100644 index 0000000..43b16c7 Binary files /dev/null and b/easyrsa/wheelhouse/setuptools_scm-1.17.0.tar.gz differ diff --git a/easyrsa/wheelhouse/six-1.16.0.tar.gz b/easyrsa/wheelhouse/six-1.16.0.tar.gz new file mode 100644 index 0000000..5bf3a27 Binary files /dev/null and b/easyrsa/wheelhouse/six-1.16.0.tar.gz differ diff --git a/easyrsa/wheelhouse/wheel-0.33.6.tar.gz b/easyrsa/wheelhouse/wheel-0.33.6.tar.gz new file mode 100644 index 0000000..c922c4e Binary files /dev/null and b/easyrsa/wheelhouse/wheel-0.33.6.tar.gz differ diff --git a/etcd/.build.manifest b/etcd/.build.manifest new file mode 100644 index 0000000..3669718 --- /dev/null +++ b/etcd/.build.manifest @@ -0,0 +1,1156 @@ +{ + "layers": [ + { + "branch": "refs/heads/master", + "rev": "fcdcea4e5de3e1556c24e6704607862d0ba00a56", + "url": "layer:options" + }, + { + "branch": "refs/heads/master", + "rev": "a3ff62c32c993d80417f6e093e3ef95e42f62083", + "url": "layer:basic" + }, + { + "branch": "refs/heads/master", + "rev": "023c67941e18663a4df49f53edba809f43ba5069", + "url": "layer:cis-benchmark" + }, + { + "branch": "refs/heads/master", + "rev": "527dd64fc4b9a6b0f8d80a3c2c0b865155050275", + "url": "layer:debug" + }, + { + "branch": "refs/heads/master", + "rev": "cc5bd3f49b2fa5e6c3ab2336763c313ec8bf083f", + "url": "layer:leadership" + }, + { + "branch": "refs/heads/master", + "rev": "47dfcd4920ef6317850a4837ef0057ab0092a18e", + "url": "layer:nagios" + }, + { + "branch": "refs/heads/master", + "rev": "fb46dec78d390571753d21876bbba689bbbca9e4", + "url": "layer:tls-client" + }, + { + "branch": "refs/heads/master", + "rev": "d3acdf209cbaf5b732e9aba621778a0f56dbaeb9", + "url": "layer:snap" + }, + { + "branch": "refs/heads/master", + "rev": "b60102068c6f0ddbeaf8a308549a3e88cfa35688", + "url": "layer:cdk-service-kicker" + }, + { + "branch": "refs/heads/master", + "rev": "a7d7b6423db37a47611310039e6ed1929c0a2eab", + "url": "layer:status" + }, + { + "branch": "refs/heads/master", + "rev": "77eef0c0a49507b74fc90cec0864fdd85555f982", + "url": "etcd" + }, + { + "branch": "refs/heads/master", + "rev": "2e0e1fdea6d83b55078200aacb537d60013ec5bc", + "url": "interface:nrpe-external-master" + }, + { + "branch": "refs/heads/master", + "rev": "d9850016d930a6d507b9fd45e2598d327922b140", + "url": "interface:tls-certificates" + }, + { + "branch": "refs/heads/master", + "rev": "44f244cbd08b86bf2b68bd71c3fb34c7c070c382", + "url": "interface:etcd" + }, + { + "branch": "refs/heads/master", + "rev": "71b16123e38d9f8e2a38558e4f057f5071e56daa", + "url": "interface:etcd-proxy" + }, + { + "branch": "refs/heads/master", + "rev": "e64261e281f012a00d374c6779ec52e488cb8713", + "url": "interface:grafana-dashboard" + }, + { + "branch": "refs/heads/master", + "rev": "3f775242c16d53243c993d7ba0c896169ad1639e", + "url": "interface:prometheus-manual" + } + ], + "signatures": { + ".build.manifest": [ + "build", + "dynamic", + "unchecked" + ], + ".github/workflows/main.yml": [ + "layer:basic", + "static", + "96a48a981ceb2a96f427a6b5226d2da6d7191981793804055d70a88ca1987473" + ], + ".github/workflows/tox.yaml": [ + "etcd", + "static", + "c323f9ca1fe5bf1369f80d8958be49ad8fd2f6635528865017c357591d31542e" + ], + ".gitignore": [ + "etcd", + "static", + "afd9068d500f350297456d66242056a905510c335ed0e40319b8c0c81eb7cd9f" + ], + ".travis.yml": [ + "layer:cis-benchmark", + "static", + "b6dbe144aa288b8a89caf1119b9835b407b234c9b32a1c81013b12a0593a8be2" + ], + "CONTRIBUTING.md": [ + "etcd", + "static", + "dc83e4e868d1dbe5b1404faf736d556895a7d4ca9be3bff2d1fdebc0036993d6" + ], + "LICENSE": [ + "etcd", + "static", + "58d1e17ffe5109a7ae296caafcadfdbe6a7d176f0bc4ab01e12a689b0499d8bd" + ], + "Makefile": [ + "etcd", + "static", + "4ccdffd0b1232b3451c89c8f48c456a4b831341189e3a913c4ca6ed5f309e261" + ], + "README.md": [ + "etcd", + "static", + "3fa439ec5175369f29cba6dc4624239d615c74cba6933591b900c30c91d7928d" + ], + "actions.yaml": [ + "etcd", + "dynamic", + "492793576dffc7daf5a68f0bb64fc8e12263f64727e3a6ee2da2c87ce528211d" + ], + "actions/actions.py": [ + "etcd", + "static", + "1b339ab870980eb1bcf936849132385fa7922c790daed50a138b2e221544043e" + ], + "actions/alarm-disarm": [ + "etcd", + "static", + "1b339ab870980eb1bcf936849132385fa7922c790daed50a138b2e221544043e" + ], + "actions/alarm-list": [ + "etcd", + "static", + "1b339ab870980eb1bcf936849132385fa7922c790daed50a138b2e221544043e" + ], + "actions/cis-benchmark": [ + "layer:cis-benchmark", + "static", + "fd3c1b8ba478b7f933605897ace8ae9f3ee102d9992f46f1e36d95eb1b094b84" + ], + "actions/compact": [ + "etcd", + "static", + "1b339ab870980eb1bcf936849132385fa7922c790daed50a138b2e221544043e" + ], + "actions/debug": [ + "layer:debug", + "static", + "db0a42dae4c5045b2c06385bf22209dfe0e2ded55822ef847d84b01d9ff2b046" + ], + "actions/defrag": [ + "etcd", + "static", + "1b339ab870980eb1bcf936849132385fa7922c790daed50a138b2e221544043e" + ], + "actions/health": [ + "etcd", + "static", + "1b339ab870980eb1bcf936849132385fa7922c790daed50a138b2e221544043e" + ], + "actions/install": [ + "etcd", + "static", + "21c4fd80371a6141c2f23fad992dc7e22d82ce7ca425d1c7a95539e1ceb87873" + ], + "actions/package-client-credentials": [ + "etcd", + "static", + "8ac1782a8beffb4fa2acc6e7b7766bad020b00e1f68b4005bbfc19871d3a1e8c" + ], + "actions/restore": [ + "etcd", + "static", + "42a3f7920a575d0544172ea3e84d8cbb34472974cde6c57b8b289429f15f0c6b" + ], + "actions/restore.py": [ + "etcd", + "static", + "663d9b6e919d563e925c1123c84e7ff58903e34b8543e073c97cdaf9ede7ecd4" + ], + "actions/snap-upgrade": [ + "etcd", + "static", + "bc24d96c526178136aa9590a87abb019a47d9fecd7b0533fea506a2c3c4fe97b" + ], + "actions/snap-upgrade.py": [ + "etcd", + "static", + "4178275ff6c66fc590d14ff0e219f30d65d6eae6b7d262071ea859ff11ab343d" + ], + "actions/snapshot": [ + "etcd", + "static", + "bcc59597a1cbc4a5d6a7679b154c6e30317853e4ad9fdd09ee0152dda2a9df59" + ], + "bin/charm-env": [ + "layer:basic", + "static", + "fb6a20fac4102a6a4b6ffe903fcf666998f9a95a3647e6f9af7a1eeb44e58fd5" + ], + "bin/layer_option": [ + "layer:options", + "static", + "e959bf29da4c5edff28b2602c24113c4df9e25cdc9f2aa3b5d46c8577b2a40cc" + ], + "config.yaml": [ + "etcd", + "dynamic", + "32906689e87266fb2a85995aeb8ab6120d9735b38a2b1a13f29d9319f1a03854" + ], + "copyright": [ + "etcd", + "static", + "b2c9a95e9606d77b139aed037ad5afd36d0b58268465c0e6b7960a562a322231" + ], + "copyright.layer-basic": [ + "layer:basic", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-leadership": [ + "layer:leadership", + "static", + "8ce407829378fc0f72ce44c7f624e4951c7ccb3db1cfb949bee026b701728cc9" + ], + "copyright.layer-nagios": [ + "layer:nagios", + "static", + "47b2363574909e748bcc471d9004780ac084b301c154905654b5b6f088474749" + ], + "copyright.layer-options": [ + "layer:options", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-snap": [ + "layer:snap", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "copyright.layer-status": [ + "layer:status", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "debug-scripts/charm-unitdata": [ + "layer:debug", + "static", + "c952b9d31f3942e4e722cb3e70f5119707b69b8e76cc44e2e906bc6d9aef49b7" + ], + "debug-scripts/etcd": [ + "etcd", + "static", + "3d6fbb2eba196a9b6c3f76c3f7b22ab10ddd27892501db24cdda01e6cb9afdbf" + ], + "debug-scripts/filesystem": [ + "layer:debug", + "static", + "d29cc8687f4422d024001c91b1ac756ee6bf8a2a125bc98db1199ba775eb8fd7" + ], + "debug-scripts/juju-logs": [ + "layer:debug", + "static", + "d260b35753a917368cb8c64c1312546a0a40ef49cba84c75bc6369549807c55e" + ], + "debug-scripts/juju-network-get": [ + "layer:debug", + "static", + "6d849a1f8e6569bd0d5ea38299f7937cb8b36a5f505e3532f6c756eabeb8b6c5" + ], + "debug-scripts/network": [ + "layer:debug", + "static", + "714afae5dcb45554ff1f05285501e3b7fcc656c8de51217e263b93dab25a9d2e" + ], + "debug-scripts/packages": [ + "layer:debug", + "static", + "e8177102dc2ca853cb9272c1257cf2cfd5253d2a074e602d07c8bc4ea8e27c75" + ], + "debug-scripts/sysctl": [ + "layer:debug", + "static", + "990035b320e09cc2228e1f2f880e795d51118b2959339eacddff9cbb74349c6a" + ], + "debug-scripts/systemd": [ + "layer:debug", + "static", + "23ddf533198bf5b1ce723acde31ada806aab8539292b514c721d8ec08af74106" + ], + "debug-scripts/tls-certs": [ + "layer:tls-client", + "static", + "ebf7f23ef6e39fb8e664bac2e9429e32aaeb673b4a51751724b835c007e85d3b" + ], + "docs/status.md": [ + "layer:status", + "static", + "975dec9f8c938196e102e954a80226bda293407c4e5ae857c118bf692154702a" + ], + "hooks/certificates-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cluster-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cluster-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cluster-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cluster-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cluster-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/config-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/data-storage-attached": [ + "etcd", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/data-storage-detaching": [ + "etcd", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/db-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/db-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/db-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/db-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/db-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/grafana-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/grafana-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/grafana-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/grafana-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/grafana-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/hook.template": [ + "layer:basic", + "static", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/install": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-elected": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-settings-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/post-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/pre-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/prometheus-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/prometheus-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/prometheus-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/prometheus-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/prometheus-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/proxy-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/proxy-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/proxy-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/proxy-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/proxy-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/relations/etcd-proxy/.gitignore": [ + "interface:etcd-proxy", + "static", + "cf237c7aff44efbe6e502e645c3e06da03a69d7bdeb43392108ef3348143417e" + ], + "hooks/relations/etcd-proxy/README.md": [ + "interface:etcd-proxy", + "static", + "b880661604a91c383aacea3289888e30427bb8ec83c5987d2ce7fac364e48cc7" + ], + "hooks/relations/etcd-proxy/__init__.py": [ + "interface:etcd-proxy", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/etcd-proxy/interface.yaml": [ + "interface:etcd-proxy", + "static", + "672792aa20fe5af4a7ba9cbe61f3f40426c783bca5149834e4dc57a636cc1185" + ], + "hooks/relations/etcd-proxy/provides.py": [ + "interface:etcd-proxy", + "static", + "d87de0262566cb4bd66f8fff96d080c76288f5433046ab5af9464cac3f7e111a" + ], + "hooks/relations/etcd-proxy/requires.py": [ + "interface:etcd-proxy", + "static", + "b1535b36ed16e70b39f05753ae2f3f3acaa7f849609ff2e70a143486583e6ee3" + ], + "hooks/relations/etcd/.gitignore": [ + "interface:etcd", + "static", + "cf237c7aff44efbe6e502e645c3e06da03a69d7bdeb43392108ef3348143417e" + ], + "hooks/relations/etcd/README.md": [ + "interface:etcd", + "static", + "93873d073f5f5302d352e09321aaf87458556e9730f89e1c682699c1d0db2386" + ], + "hooks/relations/etcd/__init__.py": [ + "interface:etcd", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/etcd/interface.yaml": [ + "interface:etcd", + "static", + "ba9f723b57a434f7efb2c06abec4167cd412c16da5f496a477dd7691e9a715be" + ], + "hooks/relations/etcd/peers.py": [ + "interface:etcd", + "static", + "99419c3d139fb5bb90021e0482f9e7ac2cfb776fb7af79b46209c6a75b36e834" + ], + "hooks/relations/etcd/provides.py": [ + "interface:etcd", + "static", + "3db1f644ab669e2dec59d59b61de63b721bc05b38fe646e525fff8f0d60982f9" + ], + "hooks/relations/etcd/requires.py": [ + "interface:etcd", + "static", + "8ffc1a094807fd36a1d1428b0a07b2428074134d46086066ecd6c0acd9fcd13e" + ], + "hooks/relations/grafana-dashboard/.gitignore": [ + "interface:grafana-dashboard", + "static", + "5567034242cd31b5fb3a0d7e1f4cee8a2bb7454d4b35d4051f333145b09ff881" + ], + "hooks/relations/grafana-dashboard/LICENSE": [ + "interface:grafana-dashboard", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/grafana-dashboard/README.md": [ + "interface:grafana-dashboard", + "static", + "d46e6c55423b4f0e28f803702632739582f3c0fad5d0427346f210eba8879685" + ], + "hooks/relations/grafana-dashboard/__init__.py": [ + "interface:grafana-dashboard", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/grafana-dashboard/common.py": [ + "interface:grafana-dashboard", + "static", + "965f19c07d3475d7fe5a21235dc0cf1a27f11da9dad498d0cd1a51260b999aa3" + ], + "hooks/relations/grafana-dashboard/copyright": [ + "interface:grafana-dashboard", + "static", + "ee9809231ae81b9efc2b44b52aab2f6c8e4800319fdce5acad537b0eac556de4" + ], + "hooks/relations/grafana-dashboard/docs/common.md": [ + "interface:grafana-dashboard", + "static", + "ab69cc6e293b66175dfeee09707f8d02659ae5ba5b9aa4c441295a1025db12f7" + ], + "hooks/relations/grafana-dashboard/docs/provides.md": [ + "interface:grafana-dashboard", + "static", + "626b5655ce1e9f7733c86379fe67709e840b760046d899e5d761b034f94d939e" + ], + "hooks/relations/grafana-dashboard/docs/requires.md": [ + "interface:grafana-dashboard", + "static", + "4f78cff5a0395aff8477267e925066bfa93654eaeb4ba812c682f968171cca55" + ], + "hooks/relations/grafana-dashboard/interface.yaml": [ + "interface:grafana-dashboard", + "static", + "97e4c9a33360708668aa0330323fe9e9e5e95fa5a1e02d4f6b8e8dc60e155b52" + ], + "hooks/relations/grafana-dashboard/provides.py": [ + "interface:grafana-dashboard", + "static", + "cd63928094e6d34be92944ce65cb5b01ff9ba2bd9646036d006fa743a3c0fdb5" + ], + "hooks/relations/grafana-dashboard/requires.py": [ + "interface:grafana-dashboard", + "static", + "b071b9e66a3206351f563d7a4d160499b13a6af29d80930cb01720b5974e1dd2" + ], + "hooks/relations/nrpe-external-master/README.md": [ + "interface:nrpe-external-master", + "static", + "d8ed3bc7334f6581b12b6091923f58e6f5ef62075a095a4e78fb8f434a948636" + ], + "hooks/relations/nrpe-external-master/__init__.py": [ + "interface:nrpe-external-master", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/nrpe-external-master/interface.yaml": [ + "interface:nrpe-external-master", + "static", + "894f24ba56148044dae5b7febf874b427d199239bcbe1f2f55c3db06bb77b5f0" + ], + "hooks/relations/nrpe-external-master/provides.py": [ + "interface:nrpe-external-master", + "static", + "e6ba708d05b227b139a86be59c83ed95a2bad030bc81e5819167ba5e1e67ecd4" + ], + "hooks/relations/nrpe-external-master/requires.py": [ + "interface:nrpe-external-master", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/prometheus-manual/.gitignore": [ + "interface:prometheus-manual", + "static", + "5567034242cd31b5fb3a0d7e1f4cee8a2bb7454d4b35d4051f333145b09ff881" + ], + "hooks/relations/prometheus-manual/LICENSE": [ + "interface:prometheus-manual", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/prometheus-manual/README.md": [ + "interface:prometheus-manual", + "static", + "506d4a334ebbe40905c76fc74e4ab5285d836ac28c7d1087b85b5a304960be2e" + ], + "hooks/relations/prometheus-manual/__init__.py": [ + "interface:prometheus-manual", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/prometheus-manual/common.py": [ + "interface:prometheus-manual", + "static", + "013107b3bc8f148779ada8097db725ac9c3d22c605a5794cb8bae95cace9fa4c" + ], + "hooks/relations/prometheus-manual/copyright": [ + "interface:prometheus-manual", + "static", + "ee9809231ae81b9efc2b44b52aab2f6c8e4800319fdce5acad537b0eac556de4" + ], + "hooks/relations/prometheus-manual/docs/common.md": [ + "interface:prometheus-manual", + "static", + "91b9e9300a2fef2ce1112cdc57a224ee06ab513ea127edc8a59b6ce9c715cd25" + ], + "hooks/relations/prometheus-manual/docs/provides.md": [ + "interface:prometheus-manual", + "static", + "6b226c2587dbf5b304e6466f2b31bbb208512896b2ab057b11b646cf3501e292" + ], + "hooks/relations/prometheus-manual/docs/requires.md": [ + "interface:prometheus-manual", + "static", + "0100bdc38afd892336747eac005260bc9656ffc1a40f9fb0faef824ab07c1021" + ], + "hooks/relations/prometheus-manual/interface.yaml": [ + "interface:prometheus-manual", + "static", + "4a268318ee2adcc8a5a3482d49595d3805f94bf8976bd1ee4a4f7f9db89e472e" + ], + "hooks/relations/prometheus-manual/provides.py": [ + "interface:prometheus-manual", + "static", + "232917934637d8905ddcd448ce51c2c30dcb9217e043592be356d510c09190c4" + ], + "hooks/relations/prometheus-manual/requires.py": [ + "interface:prometheus-manual", + "static", + "0492a9f1037f39479f2e607162aa48ca67451e00124541a7d56f7e0a920903e0" + ], + "hooks/relations/tls-certificates/.gitignore": [ + "interface:tls-certificates", + "static", + "b485e74def213c534676224e655e9276b62d401ebc643508ddc545dd335cb6dc" + ], + "hooks/relations/tls-certificates/README.md": [ + "interface:tls-certificates", + "static", + "6851227de8fcca7edfd504159dbe3e3af31080af64df46f3d3b345da7630827a" + ], + "hooks/relations/tls-certificates/__init__.py": [ + "interface:tls-certificates", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/tls-certificates/docs/common.md": [ + "interface:tls-certificates", + "static", + "5e91d6637fc0ccc50af2776de9e59a0f8098244b627816b2e18fabb266e980ff" + ], + "hooks/relations/tls-certificates/docs/provides.md": [ + "interface:tls-certificates", + "static", + "5c12dfca99b5c15ba10b4e7f7cff4cb4c9b621b198deba5f2397d3c837d035fe" + ], + "hooks/relations/tls-certificates/docs/requires.md": [ + "interface:tls-certificates", + "static", + "148dd1de163d75253f0a9d3c35e108dcaacbc9bdf97e47186743e6c82a67b62e" + ], + "hooks/relations/tls-certificates/interface.yaml": [ + "interface:tls-certificates", + "static", + "e412e54b1d327bad15a882f7f0bf996212090db576b863cc9cff7a68afc0e4fa" + ], + "hooks/relations/tls-certificates/make_docs": [ + "interface:tls-certificates", + "static", + "3671543bddc9d277171263310e404df3f11660429582cb27b39b7e7ec8757a37" + ], + "hooks/relations/tls-certificates/provides.py": [ + "interface:tls-certificates", + "static", + "be2a4b9a411c770989c529fd887070ad91649481a13f5239cfd8751f234b637c" + ], + "hooks/relations/tls-certificates/pydocmd.yml": [ + "interface:tls-certificates", + "static", + "48a233f60a89f87d56e9bc715e05766f5d39bbea2bc8741ed31f67b30c8cfcb8" + ], + "hooks/relations/tls-certificates/requires.py": [ + "interface:tls-certificates", + "static", + "442d773112079bc674d3e6be75b00323fcad7efd2f03613a1972b575dd438dba" + ], + "hooks/relations/tls-certificates/tls_certificates_common.py": [ + "interface:tls-certificates", + "static", + "068bd32ba69bfa514e1da386919d18b348ee678b40c372f275c9110f2cc4677c" + ], + "hooks/relations/tls-certificates/tox.ini": [ + "interface:tls-certificates", + "static", + "7ab8ab53e5ed98cfa7fb5c1d5009f84077a4bb76640ba64f561ef7ea3a702eab" + ], + "hooks/start": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/stop": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/update-status": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/upgrade-charm": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "icon.svg": [ + "etcd", + "static", + "163dee1040f40db041189c46652ab4e740912b527078995e7ac71bf6c6a08d16" + ], + "layer.yaml": [ + "etcd", + "dynamic", + "c66d59abd20fb4af93d95f2fa5d13ce9eb1693f619c74a2efc5ce2eaa5989f98" + ], + "lib/charms/layer/__init__.py": [ + "layer:basic", + "static", + "dfe0d26c6bf409767de6e2546bc648f150e1b396243619bad3aa0553ab7e0e6f" + ], + "lib/charms/layer/basic.py": [ + "layer:basic", + "static", + "98b47134770ed6e4c0b2d4aad73cd5bc200bec84aa9c1c4e075fd70c3222a0c9" + ], + "lib/charms/layer/execd.py": [ + "layer:basic", + "static", + "fda8bd491032db1db8ddaf4e99e7cc878c6fb5432efe1f91cadb5b34765d076d" + ], + "lib/charms/layer/nagios.py": [ + "layer:nagios", + "static", + "0246710bdbea844356007a64409907d93e6e94a289d83266e8b7c5d921fb3a6c" + ], + "lib/charms/layer/options.py": [ + "layer:options", + "static", + "8ae7a07d22542fc964f2d2bee8219d1c78a68dace70a1b38d36d4aea47b1c3b2" + ], + "lib/charms/layer/snap.py": [ + "layer:snap", + "static", + "f278a3b06a1604e1c59f107d2ff3e9f5705e3c6c7be7a012c1a500d0fc8925df" + ], + "lib/charms/layer/status.py": [ + "layer:status", + "static", + "d560a5e07b2e5f2b0f25f30e1f0278b06f3f90c01e4dbad5c83d71efc79018c6" + ], + "lib/charms/layer/tls_client.py": [ + "layer:tls-client", + "static", + "34531c3980777b661b913d77c432fc371ed10425473c2eb365b1dd5540c2ec6e" + ], + "lib/charms/leadership.py": [ + "layer:leadership", + "static", + "20ffcbbc08147506759726ad51567420659ffb8a2e0121079240b8706658e332" + ], + "lib/debug_script.py": [ + "layer:debug", + "static", + "a4d56f2d3e712b1b5cadb657c7195c6268d0aac6d228991049fd769e0ddaf453" + ], + "lib/etcd_databag.py": [ + "etcd", + "static", + "02063f5d0b869b90b9c13c7e77a8e1772ce4dc1d0be24ddbd1072a4086bd4986" + ], + "lib/etcd_lib.py": [ + "etcd", + "static", + "bffbc6ba8374fbcf7d56b678aee5cabfe935cbbbff6ab1fcaab8da127f25bbf6" + ], + "lib/etcdctl.py": [ + "etcd", + "static", + "26574d901d0bd054c8ff45c3f18e6cbc79b34b61d1ff57165f906c4ed0960700" + ], + "make_docs": [ + "layer:status", + "static", + "c990f55c8e879793a62ed8464ee3d7e0d7d2225fdecaf17af24b0df0e2daa8c1" + ], + "metadata.yaml": [ + "etcd", + "dynamic", + "5b4f1b35359784fb6228d18051ebe1fc4218d757d36affd15a0acecc41bdcccd" + ], + "pydocmd.yml": [ + "layer:status", + "static", + "11d9293901f32f75f4256ae4ac2073b92ce1d7ef7b6c892ba9fbb98690a0b330" + ], + "reactive/__init__.py": [ + "layer:snap", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "reactive/cdk_service_kicker.py": [ + "layer:cdk-service-kicker", + "static", + "cc2648443016a18324ecb26acb71d69c71610ba23df235f280383552136f7efc" + ], + "reactive/etcd.py": [ + "etcd", + "static", + "82533bea8ce1a7201bd2a6f4b6bd1351370a9c84957d813274e143ea908c2999" + ], + "reactive/leadership.py": [ + "layer:leadership", + "static", + "e2b233cf861adc3b2d9e9c062134ce2f104953f03283cdddd88f49efee652e8f" + ], + "reactive/snap.py": [ + "layer:snap", + "static", + "e4625ff4190ed33625f50d94343eda100871052ef133028f5f0ff1edfa5a23c3" + ], + "reactive/status.py": [ + "layer:status", + "static", + "30207fc206f24e91def5252f1c7f7c8e23c0aed0e93076babf5e03c05296d207" + ], + "reactive/tls_client.py": [ + "layer:tls-client", + "static", + "08e850e401d2004523dca6b5e6bc47c33d558bf575dd55969491e11cd3ed98c8" + ], + "requirements.txt": [ + "layer:basic", + "static", + "a00f75d80849e5b4fc5ad2e7536f947c25b1a4044b341caa8ee87a92d3a4c804" + ], + "setup.py": [ + "layer:snap", + "static", + "b219c8c6cb138a2f70a8ef9136d1cc3fe6210bd1e28c99fccb5e7ae90d547164" + ], + "templates/cdk-service-kicker": [ + "layer:cdk-service-kicker", + "static", + "b17adff995310e14d1b510337efa0af0531b55e2c487210168829e0dc1a6f99b" + ], + "templates/cdk-service-kicker.service": [ + "layer:cdk-service-kicker", + "static", + "c2d3977fa89d453f0f13a8a823621c44bb642ec7392d8b7462b631864f665029" + ], + "templates/check_etcd-alarms.cron": [ + "etcd", + "static", + "a208def35e82ffde80ddd328d75261cfd3f3fc26cab436c4a2ba35823337a65b" + ], + "templates/check_etcd-alarms.py": [ + "etcd", + "static", + "a5b5e4efa0c59fba1941b6bfb480538bf7988e7c5eded7598f208cb57df42594" + ], + "templates/etcd2.conf": [ + "etcd", + "static", + "5fd82b1b95f337406f12133b82c705ec278696915cf298c2a0256f42647af43a" + ], + "templates/etcd3.conf": [ + "etcd", + "static", + "3ab6570d48daaa95ef87f28db1d333177fb7942f31e8157b3ac71c1ea319b108" + ], + "templates/grafana_dashboard.json.j2": [ + "etcd", + "static", + "4d60e5e6211aa609f271567efa7fcbdc1dc25ca10d41b68fd473916b35f5a0a4" + ], + "templates/service-always-restart.systemd-229.conf": [ + "etcd", + "static", + "516958fbf8b9a05cc86f6700d0de7bdc6b2ba1847d69fbe1214e23b52e00b064" + ], + "templates/service-always-restart.systemd-latest.conf": [ + "etcd", + "static", + "37de98817682363d48b3dd2b635f5cfb281533aaa9d3836d1af44f9d6a59984c" + ], + "tests/10-deploy.py": [ + "etcd", + "static", + "5984efd977a45d99f3618037737cf6b4ed0316ab7cc7a84be7f3a70c168d9373" + ], + "tests/20-actions.py": [ + "etcd", + "static", + "4161748b29273598c6aff480853d11fa887d077e20639400e40398c94f5d5505" + ], + "tests/30-deb-bundle.yml": [ + "etcd", + "static", + "58c5118376f1086b3842a6eca01cb7adae8ee983a6ca7f28cb3624aa4a51170b" + ], + "tests/30-deb-snap-migrate.py": [ + "etcd", + "static", + "8f287e2869e3998dc778f1b1cdb7ed6bc7f98a9b58e59379e8e62c5d7f4ff81a" + ], + "tests/conftest.py": [ + "etcd", + "static", + "a9dd13c52cdca36d842eadcbbd4c56c5ca7cbbd298cae97fe866b2f604949d34" + ], + "tests/snap-upgrade.yaml": [ + "etcd", + "static", + "5aaa91cbec3d9ff0024ba90f080a761d26578c30a2c88c6fc76de73c7a4b11ba" + ], + "tests/tests.yaml": [ + "etcd", + "static", + "974fc387c55354f6d9d412c5676a1d990e4d0a7aa79dcc5422ba671631ae75a6" + ], + "tox.ini": [ + "etcd", + "static", + "0c893707ff1ee537da640b538dadd1dd9d3cfe8f886c1e3ed165c40ae7c21c4b" + ], + "unit_tests/lib/test_etcd_lib.py": [ + "etcd", + "static", + "74daf7645e3c172106f3aded3995ad32ce7ac32aede9afab52b52f898bf617bb" + ], + "unit_tests/test_etcdctl.py": [ + "etcd", + "static", + "bc3c259b337fd9064c0ac7ce7f15e56e39d81b8cc186024be5ef46e8e48dea91" + ], + "version": [ + "etcd", + "dynamic", + "dade3247ceda164d3855d5984b15d394cff71eb8eafa1e202327576145f0ad6e" + ], + "wheelhouse.txt": [ + "etcd", + "dynamic", + "57f8b4334d2be2b03a58c29f42ad8394c179f4ff85153e7feddd4d157644e5e5" + ], + "wheelhouse/Jinja2-2.10.1.tar.gz": [ + "layer:basic", + "dynamic", + "065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013" + ], + "wheelhouse/MarkupSafe-1.1.1.tar.gz": [ + "layer:basic", + "dynamic", + "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b" + ], + "wheelhouse/PyYAML-5.2.tar.gz": [ + "layer:basic", + "dynamic", + "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c" + ], + "wheelhouse/Tempita-0.5.2.tar.gz": [ + "__pip__", + "dynamic", + "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c" + ], + "wheelhouse/charmhelpers-0.20.23.tar.gz": [ + "layer:basic", + "dynamic", + "59a9776594e91cd3e3e000043f8668b4d7b279422dbb17e320f01dc16385b80e" + ], + "wheelhouse/charms.reactive-1.4.1.tar.gz": [ + "layer:basic", + "dynamic", + "bba21b4fd40b26c240c9ef2aa10c6fdf73592031c68591da4e7ccc46ca9cb616" + ], + "wheelhouse/charms.templating.jinja2-1.0.2.tar.gz": [ + "etcd", + "dynamic", + "8193c6a1d40bdb66fe272c359b4e4780501c658acfaf2b1118c4230927815fe2" + ], + "wheelhouse/netaddr-0.7.19.tar.gz": [ + "etcd", + "dynamic", + "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd" + ], + "wheelhouse/pbr-5.6.0.tar.gz": [ + "__pip__", + "dynamic", + "42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd" + ], + "wheelhouse/pip-18.1.tar.gz": [ + "layer:basic", + "dynamic", + "c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1" + ], + "wheelhouse/pyaml-21.10.1.tar.gz": [ + "__pip__", + "dynamic", + "c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383" + ], + "wheelhouse/setuptools-41.6.0.zip": [ + "layer:basic", + "dynamic", + "6afa61b391dcd16cb8890ec9f66cc4015a8a31a6e1c2b4e0c464514be1a3d722" + ], + "wheelhouse/setuptools_scm-1.17.0.tar.gz": [ + "layer:basic", + "dynamic", + "70a4cf5584e966ae92f54a764e6437af992ba42ac4bca7eb37cc5d02b98ec40a" + ], + "wheelhouse/six-1.16.0.tar.gz": [ + "__pip__", + "dynamic", + "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926" + ], + "wheelhouse/tenacity-5.0.3.tar.gz": [ + "layer:snap", + "dynamic", + "24b7f302a1caa1801e58b39ea557129c095966e64e5b1ddad3c93a6cb033e38b" + ], + "wheelhouse/wheel-0.33.6.tar.gz": [ + "layer:basic", + "dynamic", + "10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646" + ] + } +} \ No newline at end of file diff --git a/etcd/.github/workflows/main.yml b/etcd/.github/workflows/main.yml new file mode 100644 index 0000000..565bfaf --- /dev/null +++ b/etcd/.github/workflows/main.yml @@ -0,0 +1,50 @@ +name: Test Suite +on: [pull_request] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.5, 3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + - name: Run lint + run: tox -e flake8 + functional-test: + name: Functional test with LXD + runs-on: ubuntu-latest + timeout-minutes: 360 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install Dependencies + run: | + pip install tox + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@master + - name: Run test + run: tox -e func + - name: Show Status + if: ${{ always() }} + run: | + model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/); + juju status -m "$model" + - name: Show Error Logs + if: ${{ always() }} + run: | + model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/); + juju debug-log -m "$model" --replay --no-tail --level ERROR diff --git a/etcd/.github/workflows/tox.yaml b/etcd/.github/workflows/tox.yaml new file mode 100644 index 0000000..b07172d --- /dev/null +++ b/etcd/.github/workflows/tox.yaml @@ -0,0 +1,22 @@ +name: Run tests with Tox + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + - name: Install Tox and any other packages + run: pip install tox + - name: Run Tox + run: tox -e py # Run tox using the version of Python in `PATH` diff --git a/etcd/.gitignore b/etcd/.gitignore new file mode 100644 index 0000000..447a867 --- /dev/null +++ b/etcd/.gitignore @@ -0,0 +1,9 @@ +resources +.cache +.tox +.DS_Store +__pycache__ +.coverage +.unit-state.db +*.swp +.cache/* diff --git a/etcd/.travis.yml b/etcd/.travis.yml new file mode 100644 index 0000000..66d8e1f --- /dev/null +++ b/etcd/.travis.yml @@ -0,0 +1,7 @@ +language: python +python: + - "3.5" +install: + - pip install tox-travis +script: + - tox diff --git a/etcd/CONTRIBUTING.md b/etcd/CONTRIBUTING.md new file mode 100644 index 0000000..f0d8d31 --- /dev/null +++ b/etcd/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributor Guide + +This Juju charm is open source ([Apache License 2.0](./LICENSE)) and we actively seek any community contibutions +for code, suggestions and documentation. +This page details a few notes, workflows and suggestions for how to make contributions most effective and help us +all build a better charm - please give them a read before working on any contributions. + +## Licensing + +This charm has been created under the [Apache License 2.0](./LICENSE), which will cover any contributions you may +make to this project. Please familiarise yourself with the terms of the license. + +Additionally, this charm uses the Harmony CLA agreement. It’s the easiest way for you to give us permission to +use your contributions. +In effect, you’re giving us a license, but you still own the copyright — so you retain the right to modify your +code and use it in other projects. Please [sign the CLA here](https://ubuntu.com/legal/contributors/agreement) before +making any contributions. + +## Code of conduct + +We have adopted the Ubuntu code of Conduct. You can read this in full [here](https://ubuntu.com/community/code-of-conduct). + +## Contributing code + +To contribute code to this project, pleas euse the following workflow: + +1. [Submit a bug](https://bugs.launchpad.net/charm-etcd/+filebug) to explain the need for and track the change. +2. Create a branch on your fork of the repo with your changes, including a unit test covering the new or modified code. +3. Submit a PR. The PR description should include a link to the bug on Launchpad. +4. Update the Launchpad bug to include a link to the PR and the `review-needed` tag. +5. Once reviewed and merged, the change will become available on the edge channel and assigned to an appropriate milestone + for further release according to priority. + +## Documentation + +Documentation for this charm is currently maintained as part of the Charmed Kubernetes docs. +See [this page](https://github.com/charmed-kubernetes/kubernetes-docs/blob/master/pages/k8s/charm-etcd.md) diff --git a/etcd/LICENSE b/etcd/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/etcd/LICENSE @@ -0,0 +1,202 @@ + + 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. \ No newline at end of file diff --git a/etcd/Makefile b/etcd/Makefile new file mode 100644 index 0000000..eac207f --- /dev/null +++ b/etcd/Makefile @@ -0,0 +1,29 @@ + +build: + charm build -r --no-local-layers + +deploy: build + juju deploy ${JUJU_REPOSITORY}/builds/etcd + juju deploy cs:~containers/easyrsa + juju add-relation etcd easyrsa + +lint: + tox --notest + PATH=.tox/py34/bin:.tox/py35/bin flake8 reactive lib + +upgrade: build + juju upgrade-charm etcd --path=${JUJU_REPOSITORY}/builds/etcd + +force: build + juju upgrade-charm etcd --path=${JUJU_REPOSITORY}/builds/etcd --force-units + +clean: + @echo "Cleaning files" + @rm -f .coverage .unit-state.db + @find . -name "*.pyc" -type f -exec rm -f '{}' \; + @find . -name "__pycache__" -type d -prune -exec rm -rf '{}' \; + @rm -rf ./.tox + @rm -rf ./.pytest_cache + +clean-all: clean + rm -rf ${JUJU_REPOSITORY}/builds/etcd diff --git a/etcd/README.md b/etcd/README.md new file mode 100644 index 0000000..df46bdf --- /dev/null +++ b/etcd/README.md @@ -0,0 +1,21 @@ +# Etcd + +Etcd is a highly available distributed key value store that provides a reliable +way to store data across a cluster of machines. Etcd gracefully handles master +elections during network partitions and will tolerate machine failure, +including the master. + +Your applications can read and write data into etcd. A simple use-case is to +store database connection details or feature flags in etcd as key value pairs. +These values can be watched, allowing your app to reconfigure itself when they +change. + +Advanced uses take advantage of the consistency guarantees to implement +database master elections or do distributed locking across a cluster of +workers. + +Etcd allows storing data in a distributed hierarchical database with +observation. + +This charm is maintained along with the components of Charmed Kubernetes. For full information, +please visit the [official Charmed Kubernetes docs](https://www.ubuntu.com/kubernetes/docs/charm-etcd). diff --git a/etcd/actions.yaml b/etcd/actions.yaml new file mode 100644 index 0000000..b5c2f44 --- /dev/null +++ b/etcd/actions.yaml @@ -0,0 +1,99 @@ +"cis-benchmark": + "description": | + Run the CIS Kubernetes Benchmark against snap-based components. + "params": + "apply": + "type": "string" + "default": "none" + "description": | + Apply remediations to address benchmark failures. The default, 'none', + will not attempt to fix any reported failures. Set to 'conservative' + to resolve simple failures. Set to 'dangerous' to attempt to resolve + all failures. + + Note: Applying any remediation may result in an unusable cluster. + "config": + "type": "string" + "default": "https://github.com/charmed-kubernetes/kube-bench-config/archive/cis-1.5.zip#sha1=811f21dbf6c841bafdbfbd8a21f912ad67582f46" + "description": | + Archive containing configuration files to use when running kube-bench. + The default value is known to be compatible with snap components. When + using a custom URL, append '#=' to verify the + archive integrity when downloaded. + "release": + "type": "string" + "default": "https://github.com/aquasecurity/kube-bench/releases/download/v0.3.1/kube-bench_0.3.1_linux_amd64.tar.gz#sha256=6616f1373987259285e2f676a225d4a3885cd62b7e7a116102ff2fb445724281" + "description": | + Archive containing the 'kube-bench' binary to run. The default value + points to a stable upstream release. When using a custom URL, append + '#=' to verify the archive integrity when + downloaded. + + This may also be set to the special keyword 'upstream'. In this case, + the action will compile and use a local kube-bench binary built from + the master branch of the upstream repository: + https://github.com/aquasecurity/kube-bench + +"debug": + "description": "Collect debug data" +"alarm-disarm": + "description": | + Disarm all alarms. +"alarm-list": + "description": | + List all alarms. +"compact": + "description": | + Compact etcd event history. + "params": + "revision": + "type": "string" + "default": "" + "description": | + Revision to compact to. Leave blank to compact to the latest revision. + "physical": + "type": "boolean" + "default": !!bool "false" + "description": | + Setting to True will cause the compaction process to exit only after + all revisions have been physically removed from the database. +"defrag": + "description": | + Defragment the storage of the local etcd member. +"health": + "description": "Report the health of the cluster." +"package-client-credentials": + "description": | + Generate a tarball of the client certificates to connect to the cluster + remotely. +"snap-upgrade": + "description": | + Execute a migration from the apt package to a snap package format. + "params": + "use-resource": + "type": "boolean" + "default": !!bool "false" + "description": "Default to using the resource (offline environments)" +"snapshot": + "description": "Export and compress a backup of the data in the Etcd cluster." + "params": + "target": + "type": "string" + "default": "/home/ubuntu/etcd-snapshots" + "description": "Location to save the etcd snapshot." + "keys-version": + "type": "string" + "default": "v3" + "description": "Version of keys to snapshoot. Allowed values 'v3' or 'v2'." +"restore": + "description": "Restore an etcd cluster's data from a snapshot tarball." + "params": + "target": + "type": "string" + "default": "/home/ubuntu" + "description": "Path on disk to save any pre-existing data." + "skip-backup": + "type": "boolean" + "default": !!bool "true" + "description": | + Dont backup any existing data, and skip directly to data restoration. diff --git a/etcd/actions/actions.py b/etcd/actions/actions.py new file mode 100755 index 0000000..c1f4834 --- /dev/null +++ b/etcd/actions/actions.py @@ -0,0 +1,144 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import re +import shlex +import subprocess +import sys + +from charms import layer + +from etcdctl import EtcdCtl + +from charmhelpers.core.hookenv import ( + action_get, + action_set, + action_fail, + action_name +) + + +CTL = EtcdCtl() + + +def action_fail_now(*args, **kw): + '''Call action_fail() and exit immediately. + + ''' + action_fail(*args, **kw) + sys.exit(0) + + +def requires_etcd_version(version_regex, human_version=None): + '''Decorator that enforces a specific version of etcdctl be present. + + The decorated function will only be executed if the required version + of etcdctl is present. Otherwise, action_fail() will be called and + the process will exit immediately. + + ''' + def wrap(f): + def wrapped_f(*args): + version = CTL.version() + if not re.match(version_regex, version): + required_version = human_version or version_regex + action_fail_now( + 'This action requires etcd version {}'.format( + required_version)) + f(*args) + return wrapped_f + return wrap + + +requires_etcd_v2 = requires_etcd_version(r'2\..*', human_version='2.x') +requires_etcd_v3 = requires_etcd_version(r'3\..*', human_version='3.x') + + +@requires_etcd_v3 +def alarm_disarm(): + '''Call `etcdctl alarm disarm`. + + ''' + try: + output = CTL.run('alarm disarm') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def alarm_list(): + '''Call `etcdctl alarm list`. + + ''' + try: + output = CTL.run('alarm list') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def compact(): + '''Call `etcdctl compact`. + + ''' + def get_latest_revision(): + try: + output = CTL.run('endpoint status --write-out json') + except subprocess.CalledProcessError as e: + action_fail_now( + 'Failed to determine latest revision for ' + 'compaction: {}'.format(e)) + + m = re.search(r'"revision":(\d*)', output) + if not m: + action_fail_now( + "Failed to get revision from 'endpoint status' " + "output: {}".format(output)) + return m.group(1) + + revision = action_get('revision') or get_latest_revision() + physical = 'true' if action_get('physical') else 'false' + command = 'compact {} --physical={}'.format(revision, physical) + try: + output = CTL.run(command) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def defrag(): + '''Call `etcdctl defrag`. + + ''' + try: + output = CTL.run('defrag') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +def health(): + '''Call etcdctl cluster-health + + ''' + try: + output = CTL.cluster_health(True) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +if __name__ == '__main__': + ACTIONS = { + 'alarm-disarm': alarm_disarm, + 'alarm-list': alarm_list, + 'compact': compact, + 'defrag': defrag, + 'health': health, + } + + action = action_name() + ACTIONS[action]() diff --git a/etcd/actions/alarm-disarm b/etcd/actions/alarm-disarm new file mode 100755 index 0000000..c1f4834 --- /dev/null +++ b/etcd/actions/alarm-disarm @@ -0,0 +1,144 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import re +import shlex +import subprocess +import sys + +from charms import layer + +from etcdctl import EtcdCtl + +from charmhelpers.core.hookenv import ( + action_get, + action_set, + action_fail, + action_name +) + + +CTL = EtcdCtl() + + +def action_fail_now(*args, **kw): + '''Call action_fail() and exit immediately. + + ''' + action_fail(*args, **kw) + sys.exit(0) + + +def requires_etcd_version(version_regex, human_version=None): + '''Decorator that enforces a specific version of etcdctl be present. + + The decorated function will only be executed if the required version + of etcdctl is present. Otherwise, action_fail() will be called and + the process will exit immediately. + + ''' + def wrap(f): + def wrapped_f(*args): + version = CTL.version() + if not re.match(version_regex, version): + required_version = human_version or version_regex + action_fail_now( + 'This action requires etcd version {}'.format( + required_version)) + f(*args) + return wrapped_f + return wrap + + +requires_etcd_v2 = requires_etcd_version(r'2\..*', human_version='2.x') +requires_etcd_v3 = requires_etcd_version(r'3\..*', human_version='3.x') + + +@requires_etcd_v3 +def alarm_disarm(): + '''Call `etcdctl alarm disarm`. + + ''' + try: + output = CTL.run('alarm disarm') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def alarm_list(): + '''Call `etcdctl alarm list`. + + ''' + try: + output = CTL.run('alarm list') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def compact(): + '''Call `etcdctl compact`. + + ''' + def get_latest_revision(): + try: + output = CTL.run('endpoint status --write-out json') + except subprocess.CalledProcessError as e: + action_fail_now( + 'Failed to determine latest revision for ' + 'compaction: {}'.format(e)) + + m = re.search(r'"revision":(\d*)', output) + if not m: + action_fail_now( + "Failed to get revision from 'endpoint status' " + "output: {}".format(output)) + return m.group(1) + + revision = action_get('revision') or get_latest_revision() + physical = 'true' if action_get('physical') else 'false' + command = 'compact {} --physical={}'.format(revision, physical) + try: + output = CTL.run(command) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def defrag(): + '''Call `etcdctl defrag`. + + ''' + try: + output = CTL.run('defrag') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +def health(): + '''Call etcdctl cluster-health + + ''' + try: + output = CTL.cluster_health(True) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +if __name__ == '__main__': + ACTIONS = { + 'alarm-disarm': alarm_disarm, + 'alarm-list': alarm_list, + 'compact': compact, + 'defrag': defrag, + 'health': health, + } + + action = action_name() + ACTIONS[action]() diff --git a/etcd/actions/alarm-list b/etcd/actions/alarm-list new file mode 100755 index 0000000..c1f4834 --- /dev/null +++ b/etcd/actions/alarm-list @@ -0,0 +1,144 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import re +import shlex +import subprocess +import sys + +from charms import layer + +from etcdctl import EtcdCtl + +from charmhelpers.core.hookenv import ( + action_get, + action_set, + action_fail, + action_name +) + + +CTL = EtcdCtl() + + +def action_fail_now(*args, **kw): + '''Call action_fail() and exit immediately. + + ''' + action_fail(*args, **kw) + sys.exit(0) + + +def requires_etcd_version(version_regex, human_version=None): + '''Decorator that enforces a specific version of etcdctl be present. + + The decorated function will only be executed if the required version + of etcdctl is present. Otherwise, action_fail() will be called and + the process will exit immediately. + + ''' + def wrap(f): + def wrapped_f(*args): + version = CTL.version() + if not re.match(version_regex, version): + required_version = human_version or version_regex + action_fail_now( + 'This action requires etcd version {}'.format( + required_version)) + f(*args) + return wrapped_f + return wrap + + +requires_etcd_v2 = requires_etcd_version(r'2\..*', human_version='2.x') +requires_etcd_v3 = requires_etcd_version(r'3\..*', human_version='3.x') + + +@requires_etcd_v3 +def alarm_disarm(): + '''Call `etcdctl alarm disarm`. + + ''' + try: + output = CTL.run('alarm disarm') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def alarm_list(): + '''Call `etcdctl alarm list`. + + ''' + try: + output = CTL.run('alarm list') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def compact(): + '''Call `etcdctl compact`. + + ''' + def get_latest_revision(): + try: + output = CTL.run('endpoint status --write-out json') + except subprocess.CalledProcessError as e: + action_fail_now( + 'Failed to determine latest revision for ' + 'compaction: {}'.format(e)) + + m = re.search(r'"revision":(\d*)', output) + if not m: + action_fail_now( + "Failed to get revision from 'endpoint status' " + "output: {}".format(output)) + return m.group(1) + + revision = action_get('revision') or get_latest_revision() + physical = 'true' if action_get('physical') else 'false' + command = 'compact {} --physical={}'.format(revision, physical) + try: + output = CTL.run(command) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def defrag(): + '''Call `etcdctl defrag`. + + ''' + try: + output = CTL.run('defrag') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +def health(): + '''Call etcdctl cluster-health + + ''' + try: + output = CTL.cluster_health(True) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +if __name__ == '__main__': + ACTIONS = { + 'alarm-disarm': alarm_disarm, + 'alarm-list': alarm_list, + 'compact': compact, + 'defrag': defrag, + 'health': health, + } + + action = action_name() + ACTIONS[action]() diff --git a/etcd/actions/cis-benchmark b/etcd/actions/cis-benchmark new file mode 100755 index 0000000..3f91dea --- /dev/null +++ b/etcd/actions/cis-benchmark @@ -0,0 +1,371 @@ +#!/usr/local/sbin/charm-env python3 +import os +import json +import shlex +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +import charms.layer +import charms.reactive +from charmhelpers.core import hookenv, unitdata +from charmhelpers.fetch.archiveurl import ArchiveUrlFetchHandler +from charms.layer import snap +from charms.reactive import clear_flag, is_flag_set, set_flag + + +BENCH_HOME = '/home/ubuntu/kube-bench' +BENCH_BIN = '{}/kube-bench'.format(BENCH_HOME) +BENCH_CFG = '{}/cfg-ck'.format(BENCH_HOME) +GO_PKG = 'github.com/aquasecurity/kube-bench' +RESULTS_DIR = '/home/ubuntu/kube-bench-results' + +# Remediation dicts associate a failing test with a tuple to fix it. +# Conservative fixes will probably leave the cluster in a good state. +# Dangerous fixes will likely break the cluster. +# Tuple examples: +# {'1.2.3': ('manual -- we don't know how to auto fix this', None, None)} +# {'1.2.3': ('cli', 'command to run', None)} +# {'1.2.3': ('kv', 'snap', {cfg_key: value})} +CONSERVATIVE = { + '0.0.0': ('cli', 'echo "this is fine"', None), + + # etcd (no known failures with a default install) + + # k8s-master + '1.2.21': ('kv', 'kube-apiserver', {'profiling': 'false'}), + '1.2.23': ('kv', 'kube-apiserver', {'audit-log-maxage': '30'}), + '1.2.24': ('kv', 'kube-apiserver', {'audit-log-maxbackup': '10'}), + '1.3.1': ('kv', 'kube-controller-manager', {'terminated-pod-gc-threshold': '500'}), + '1.3.2': ('kv', 'kube-controller-manager', {'profiling': 'false'}), + '1.4.1': ('kv', 'kube-scheduler', {'profiling': 'false'}), + + # k8s-worker + '4.2.2': ('kv', 'kubelet', {'authorization-mode': 'Webhook'}), + '4.2.4': ('kv', 'kubelet', {'read-only-port': '0'}), + '4.2.6': ('kv', 'kubelet', {'protect-kernel-defaults': 'true'}), +} +ADMISSION_PLUGINS = {'enable-admission-plugins': ('PersistentVolumeLabel', + 'PodSecurityPolicy,' + 'ServiceAccount,' + 'NodeRestriction')} +DANGEROUS = { + '0.0.0': ('cli', 'echo "this is fine"', None), + + # etcd (no known failures with a default install) + + # k8s-master + '1.2.2': ('kv', 'kube-apiserver', {'basic-auth-file': None}), + '1.2.3': ('kv', 'kube-apiserver', {'token-auth-file': None}), + '1.2.7': ('kv', 'kube-apiserver', {'authorization-mode': 'RBAC,Node'}), + '1.2.8': ('kv', 'kube-apiserver', {'authorization-mode': 'RBAC,Node'}), + '1.2.9': ('kv', 'kube-apiserver', {'authorization-mode': 'RBAC,Node'}), + '1.2.14': ('kv', 'kube-apiserver', ADMISSION_PLUGINS), + '1.2.16': ('kv', 'kube-apiserver', ADMISSION_PLUGINS), + '1.2.17': ('kv', 'kube-apiserver', ADMISSION_PLUGINS), + '1.2.18': ('kv', 'kube-apiserver', {'insecure-bind-address': None}), + '1.2.19': ('kv', 'kube-apiserver', {'insecure-port': '0'}), + '1.2.33': ('manual', None, None), + '1.3.6': ('kv', 'kube-controller-manager', + {'feature-gates': 'RotateKubeletServerCertificate=true'}), + + # k8s-worker + '4.2.12': ('kv', 'kubelet', + {'feature-gates': 'RotateKubeletServerCertificate=true'}), +} + + +def _fail(msg): + '''Fail the action with a given message.''' + hookenv.action_fail(msg) + sys.exit() + + +def _move_matching_parent(dirpath, filename, dest): + '''Move a parent directory that contains a specific file. + + Helper function that walks a directory looking for a given file. If found, + the file's parent directory is moved to the given destination. + + :param: dirpath: String path to search + :param: filename: String file to find + :param: dest: String destination of the found parent directory + ''' + for root, _, files in os.walk(dirpath): + for name in files: + if name == filename: + hookenv.log('Moving {} to {}'.format(root, dest)) + shutil.move(root, dest) + return + else: + _fail('Could not find {} in {}'.format(filename, dirpath)) + + +def _restart_charm(): + '''Set charm-specific flags and call reactive.main().''' + app = hookenv.charm_name() or 'unknown' + if 'master' in app: + hookenv.log('Restarting master') + clear_flag('kubernetes-master.components.started') + elif 'worker' in app: + hookenv.log('Restarting worker') + set_flag('kubernetes-worker.restart-needed') + elif 'etcd' in app: + hookenv.log('No-op: etcd does not need to be restarted') + return + else: + _fail('Unable to determine the charm to restart: {}'.format(app)) + + # Invoke reactive so the charm will react to the flags we just managed + charms.layer.import_layer_libs() + charms.reactive.main() + + +def install(release, config): + '''Install kube-bench and related configuration. + + Release and configuration are set via action params. If installing an + upstream release, this method will also install 'go' if needed. + + :param: release: Archive URI or 'upstream' + :param: config: Archive URI of configuration files + ''' + if Path(BENCH_HOME).exists(): + shutil.rmtree(BENCH_HOME) + fetcher = ArchiveUrlFetchHandler() + + if release == 'upstream': + Path(BENCH_HOME).mkdir(parents=True, exist_ok=True) + + # Setup the 'go' environment + env = os.environ.copy() + go_bin = shutil.which('go', path='{}:/snap/bin'.format(env['PATH'])) + if not go_bin: + snap.install('go', channel='stable', classic=True) + go_bin = '/snap/bin/go' + go_cache = os.getenv('GOCACHE', '/var/snap/go/common/cache') + go_path = os.getenv('GOPATH', '/var/snap/go/common') + env['GOCACHE'] = go_cache + env['GOPATH'] = go_path + Path(go_path).mkdir(parents=True, exist_ok=True) + + # From https://github.com/aquasecurity/kube-bench#installing-from-sources + go_cmd = ('{bin} get {pkg} ' + 'github.com/golang/dep/cmd/dep'.format(bin=go_bin, pkg=GO_PKG)) + try: + subprocess.check_call(shlex.split(go_cmd), cwd=go_path, env=env) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(go_cmd)) + + go_cmd = ('{bin} build -o {out} {base}/src/{pkg}'.format( + bin=go_bin, out=BENCH_BIN, base=go_path, pkg=GO_PKG)) + try: + subprocess.check_call(shlex.split(go_cmd), cwd=go_path, env=env) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(go_cmd)) + else: + # Fetch the release URI and put it in the right place. + archive_path = fetcher.install(source=release) + # NB: We may not know the structure of the archive, but we know the + # directory containing 'kube-bench' belongs in our BENCH_HOME. + _move_matching_parent( + dirpath=archive_path, filename='kube-bench', dest=BENCH_HOME) + + # Fetch the config URI and put it in the right place. + archive_dir = fetcher.install(source=config) + # NB: We may not know the structure of the archive, but we know the + # directory containing 'config.yaml' belongs in our BENCH_CFG. + _move_matching_parent( + dirpath=archive_dir, filename='config.yaml', dest=BENCH_CFG) + + +def apply(remediations=None): + '''Apply remediations to address benchmark failures. + + :param: remediations: either 'conservative' or 'dangerous' + ''' + applied_fixes = 0 + danger = True if remediations == 'dangerous' else False + db = unitdata.kv() + + json_log = report(log_format='json') + hookenv.log('Loading JSON from: {}'.format(json_log)) + try: + with open(json_log, 'r') as f: + full_json = json.load(f) + except Exception: + _fail('Failed to load: {}'.format(json_log)) + + for test in full_json.get('tests', {}): + for result in test.get('results', {}): + test_num = result.get('test_number') + test_remediation = result.get('remediation') + test_status = result.get('status', '') + + if test_status.lower() == 'fail': + test_remedy = CONSERVATIVE.get(test_num) + if not test_remedy and danger: + # no conservative remedy, check dangerous if user wants + test_remedy = DANGEROUS.get(test_num) + if isinstance(test_remedy, tuple): + if test_remedy[0] == 'manual': + # we don't know how to autofix; log remediation text + hookenv.log('Test {}: unable to auto-apply remedy.\n' + 'Manual steps:\n{}'.format(test_num, + test_remediation)) + elif test_remedy[0] == 'cli': + cmd = shlex.split(test_remedy[1]) + try: + out = subprocess.check_output(cmd) + except subprocess.CalledProcessError: + _fail('Test {}: failed to run: {}'.format(test_num, cmd)) + else: + hookenv.log('Test {}: applied remedy: {}\n' + 'Output: {}'.format(test_num, cmd, out)) + applied_fixes += 1 + elif test_remedy[0] == 'kv': + cfg_key = 'cis-' + test_remedy[1] + cfg = db.get(cfg_key) or {} + cfg.update(test_remedy[2]) + db.set(cfg_key, cfg) + + hookenv.log('Test {}: updated configuration: {}\n'.format( + test_num, cfg)) + applied_fixes += 1 + else: + hookenv.log('Test {}: remediation is missing'.format(test_num)) + + # CLI and KV changes will require a charm restart; do it. + if applied_fixes > 0: + _restart_charm() + + msg = ('Applied {} remediations. Re-run with "apply=none" to generate a ' + 'new report.').format(applied_fixes) + hookenv.action_set({'summary': msg}) + + +def reset(): + '''Reset any remediations we applied to unitdata.kv(). + + This action does not track individual remediations to reset. Therefore, + this function unconditionally unsets all 'cis-' prefixed arguments that + this action may have set and restarts the relevant charm. + ''' + db = unitdata.kv() + + db.unset('cis-kube-apiserver') + db.unset('cis-kube-scheduler') + db.unset('cis-kube-controller-manager') + db.unset('cis-kubelet') + _restart_charm() + + hookenv.action_set({'summary': ('Reset is complete. Re-run with ' + '"apply=none" to generate a new report.')}) + + +def report(log_format='text'): + '''Run kube-bench and report results. + + By default, save the full plain-text results to our RESULTS_DIR and set + action output with a summary. This function can also save full results in + a machine-friendly json format. + + :param: log_format: String determines if output is text or json + :returns: Path to results log + ''' + Path(RESULTS_DIR).mkdir(parents=True, exist_ok=True) + + # Node type is different depending on the charm + app = hookenv.charm_name() or 'unknown' + version = 'cis-1.5' + if 'master' in app: + target = 'master' + elif 'worker' in app: + target = 'node' + elif 'etcd' in app: + target = 'etcd' + else: + _fail('Unable to determine the target to benchmark: {}'.format(app)) + + # Commands and log names are different depending on the format + if log_format == 'json': + log_prefix = 'results-json-' + verbose_cmd = ('{bin} -D {cfg} --benchmark {ver} --json run ' + '--targets {target}').format( + bin=BENCH_BIN, cfg=BENCH_CFG, ver=version, target=target) + else: + log_prefix = 'results-text-' + verbose_cmd = ('{bin} -D {cfg} --benchmark {ver} run ' + '--targets {target}').format( + bin=BENCH_BIN, cfg=BENCH_CFG, ver=version, target=target) + + summary_cmd = ('{bin} -D {cfg} --benchmark {ver} ' + '--noremediations --noresults run --targets {target}').format( + bin=BENCH_BIN, cfg=BENCH_CFG, ver=version, target=target) + + # Store full results for future consumption + with tempfile.NamedTemporaryFile(mode='w+b', prefix=log_prefix, + dir=RESULTS_DIR, delete=False) as res_file: + try: + subprocess.call(shlex.split(verbose_cmd), stdout=res_file) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(verbose_cmd)) + else: + # remember the filename for later (and make it readable, why not?) + Path(res_file.name).chmod(0o644) + log = res_file.name + + # When making a summary, we also have a verbose report. Set action output + # so operators can see everything related to this run. + try: + out = subprocess.check_output(shlex.split(summary_cmd), + universal_newlines=True) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(summary_cmd)) + else: + fetch_cmd = 'juju scp {unit}:{file} .'.format(unit=hookenv.local_unit(), + file=log) + hookenv.action_set({'cmd': summary_cmd, + 'report': fetch_cmd, + 'summary': out}) + + return log or None + + +if __name__ == '__main__': + if not (is_flag_set('snap.installed.etcd') or + is_flag_set('kubernetes-master.snaps.installed') or + is_flag_set('kubernetes-worker.snaps.installed')): + msg = 'Snaps are not yet installed on this unit.' + _fail(msg) + + # Validate action params + release = hookenv.action_get('release') or 'upstream' + config = hookenv.action_get('config') + if not config: + msg = 'Missing "config" parameter' + _fail(msg) + remediations = hookenv.action_get('apply') + if remediations not in ['none', 'conservative', 'dangerous', 'reset']: + msg = 'Invalid "apply" parameter: {}'.format(remediations) + _fail(msg) + + # TODO: may want an option to overwrite an existing install + if Path(BENCH_BIN).exists() and Path(BENCH_CFG).exists(): + hookenv.log('{} exists; skipping install'.format(BENCH_HOME)) + else: + hookenv.log('Installing benchmark from: {}'.format(release)) + install(release, config) + + # Reset, remediate, or report + if remediations == 'reset': + hookenv.log('Attempting to remove all remediations') + reset() + elif remediations != 'none': + hookenv.log('Applying "{}" remediations'.format(remediations)) + apply(remediations) + else: + hookenv.log('Report only; no remediations were requested') + report(log_format='text') diff --git a/etcd/actions/compact b/etcd/actions/compact new file mode 100755 index 0000000..c1f4834 --- /dev/null +++ b/etcd/actions/compact @@ -0,0 +1,144 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import re +import shlex +import subprocess +import sys + +from charms import layer + +from etcdctl import EtcdCtl + +from charmhelpers.core.hookenv import ( + action_get, + action_set, + action_fail, + action_name +) + + +CTL = EtcdCtl() + + +def action_fail_now(*args, **kw): + '''Call action_fail() and exit immediately. + + ''' + action_fail(*args, **kw) + sys.exit(0) + + +def requires_etcd_version(version_regex, human_version=None): + '''Decorator that enforces a specific version of etcdctl be present. + + The decorated function will only be executed if the required version + of etcdctl is present. Otherwise, action_fail() will be called and + the process will exit immediately. + + ''' + def wrap(f): + def wrapped_f(*args): + version = CTL.version() + if not re.match(version_regex, version): + required_version = human_version or version_regex + action_fail_now( + 'This action requires etcd version {}'.format( + required_version)) + f(*args) + return wrapped_f + return wrap + + +requires_etcd_v2 = requires_etcd_version(r'2\..*', human_version='2.x') +requires_etcd_v3 = requires_etcd_version(r'3\..*', human_version='3.x') + + +@requires_etcd_v3 +def alarm_disarm(): + '''Call `etcdctl alarm disarm`. + + ''' + try: + output = CTL.run('alarm disarm') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def alarm_list(): + '''Call `etcdctl alarm list`. + + ''' + try: + output = CTL.run('alarm list') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def compact(): + '''Call `etcdctl compact`. + + ''' + def get_latest_revision(): + try: + output = CTL.run('endpoint status --write-out json') + except subprocess.CalledProcessError as e: + action_fail_now( + 'Failed to determine latest revision for ' + 'compaction: {}'.format(e)) + + m = re.search(r'"revision":(\d*)', output) + if not m: + action_fail_now( + "Failed to get revision from 'endpoint status' " + "output: {}".format(output)) + return m.group(1) + + revision = action_get('revision') or get_latest_revision() + physical = 'true' if action_get('physical') else 'false' + command = 'compact {} --physical={}'.format(revision, physical) + try: + output = CTL.run(command) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def defrag(): + '''Call `etcdctl defrag`. + + ''' + try: + output = CTL.run('defrag') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +def health(): + '''Call etcdctl cluster-health + + ''' + try: + output = CTL.cluster_health(True) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +if __name__ == '__main__': + ACTIONS = { + 'alarm-disarm': alarm_disarm, + 'alarm-list': alarm_list, + 'compact': compact, + 'defrag': defrag, + 'health': health, + } + + action = action_name() + ACTIONS[action]() diff --git a/etcd/actions/debug b/etcd/actions/debug new file mode 100755 index 0000000..8ba160e --- /dev/null +++ b/etcd/actions/debug @@ -0,0 +1,102 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import tarfile +import tempfile +import traceback +from contextlib import contextmanager +from datetime import datetime +from charmhelpers.core.hookenv import action_set, local_unit + +archive_dir = None +log_file = None + + +@contextmanager +def archive_context(): + """ Open a context with a new temporary directory. + + When the context closes, the directory is archived, and the archive + location is added to Juju action output. """ + global archive_dir + global log_file + with tempfile.TemporaryDirectory() as temp_dir: + name = "debug-" + datetime.now().strftime("%Y%m%d%H%M%S") + archive_dir = os.path.join(temp_dir, name) + os.makedirs(archive_dir) + with open("%s/debug.log" % archive_dir, "w") as log_file: + yield + os.chdir(temp_dir) + tar_path = "/home/ubuntu/%s.tar.gz" % name + with tarfile.open(tar_path, "w:gz") as f: + f.add(name) + action_set({ + "path": tar_path, + "command": "juju scp %s:%s ." % (local_unit(), tar_path), + "message": " ".join([ + "Archive has been created on unit %s." % local_unit(), + "Use the juju scp command to copy it to your local machine." + ]) + }) + + +def log(msg): + """ Log a message that will be included in the debug archive. + + Must be run within archive_context """ + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for line in str(msg).splitlines(): + log_file.write(timestamp + " | " + line.rstrip() + "\n") + + +def run_script(script): + """ Run a single script. Must be run within archive_context """ + log("Running script: " + script) + script_dir = os.path.join(archive_dir, script) + os.makedirs(script_dir) + env = os.environ.copy() + env["PYTHONPATH"] = "lib" # allow same imports as reactive code + env["DEBUG_SCRIPT_DIR"] = script_dir + with open(script_dir + "/stdout", "w") as stdout: + with open(script_dir + "/stderr", "w") as stderr: + process = subprocess.Popen( + "debug-scripts/" + script, + stdout=stdout, stderr=stderr, env=env + ) + try: + exit_code = process.wait(timeout=300) + except subprocess.TimeoutExpired: + log("ERROR: still running, terminating") + process.terminate() + try: + exit_code = process.wait(timeout=10) + except subprocess.TimeoutExpired: + log("ERROR: still running, killing") + process.kill() + exit_code = process.wait(timeout=10) + if exit_code != 0: + log("ERROR: %s failed with exit code %d" % (script, exit_code)) + + +def run_all_scripts(): + """ Run all scripts. For the sake of robustness, log and ignore any + exceptions that occur. + + Must be run within archive_context """ + scripts = os.listdir("debug-scripts") + for script in scripts: + try: + run_script(script) + except: + log(traceback.format_exc()) + + +def main(): + """ Open an archive context and run all scripts. """ + with archive_context(): + run_all_scripts() + + +if __name__ == "__main__": + main() diff --git a/etcd/actions/defrag b/etcd/actions/defrag new file mode 100755 index 0000000..c1f4834 --- /dev/null +++ b/etcd/actions/defrag @@ -0,0 +1,144 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import re +import shlex +import subprocess +import sys + +from charms import layer + +from etcdctl import EtcdCtl + +from charmhelpers.core.hookenv import ( + action_get, + action_set, + action_fail, + action_name +) + + +CTL = EtcdCtl() + + +def action_fail_now(*args, **kw): + '''Call action_fail() and exit immediately. + + ''' + action_fail(*args, **kw) + sys.exit(0) + + +def requires_etcd_version(version_regex, human_version=None): + '''Decorator that enforces a specific version of etcdctl be present. + + The decorated function will only be executed if the required version + of etcdctl is present. Otherwise, action_fail() will be called and + the process will exit immediately. + + ''' + def wrap(f): + def wrapped_f(*args): + version = CTL.version() + if not re.match(version_regex, version): + required_version = human_version or version_regex + action_fail_now( + 'This action requires etcd version {}'.format( + required_version)) + f(*args) + return wrapped_f + return wrap + + +requires_etcd_v2 = requires_etcd_version(r'2\..*', human_version='2.x') +requires_etcd_v3 = requires_etcd_version(r'3\..*', human_version='3.x') + + +@requires_etcd_v3 +def alarm_disarm(): + '''Call `etcdctl alarm disarm`. + + ''' + try: + output = CTL.run('alarm disarm') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def alarm_list(): + '''Call `etcdctl alarm list`. + + ''' + try: + output = CTL.run('alarm list') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def compact(): + '''Call `etcdctl compact`. + + ''' + def get_latest_revision(): + try: + output = CTL.run('endpoint status --write-out json') + except subprocess.CalledProcessError as e: + action_fail_now( + 'Failed to determine latest revision for ' + 'compaction: {}'.format(e)) + + m = re.search(r'"revision":(\d*)', output) + if not m: + action_fail_now( + "Failed to get revision from 'endpoint status' " + "output: {}".format(output)) + return m.group(1) + + revision = action_get('revision') or get_latest_revision() + physical = 'true' if action_get('physical') else 'false' + command = 'compact {} --physical={}'.format(revision, physical) + try: + output = CTL.run(command) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def defrag(): + '''Call `etcdctl defrag`. + + ''' + try: + output = CTL.run('defrag') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +def health(): + '''Call etcdctl cluster-health + + ''' + try: + output = CTL.cluster_health(True) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +if __name__ == '__main__': + ACTIONS = { + 'alarm-disarm': alarm_disarm, + 'alarm-list': alarm_list, + 'compact': compact, + 'defrag': defrag, + 'health': health, + } + + action = action_name() + ACTIONS[action]() diff --git a/etcd/actions/health b/etcd/actions/health new file mode 100755 index 0000000..c1f4834 --- /dev/null +++ b/etcd/actions/health @@ -0,0 +1,144 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import re +import shlex +import subprocess +import sys + +from charms import layer + +from etcdctl import EtcdCtl + +from charmhelpers.core.hookenv import ( + action_get, + action_set, + action_fail, + action_name +) + + +CTL = EtcdCtl() + + +def action_fail_now(*args, **kw): + '''Call action_fail() and exit immediately. + + ''' + action_fail(*args, **kw) + sys.exit(0) + + +def requires_etcd_version(version_regex, human_version=None): + '''Decorator that enforces a specific version of etcdctl be present. + + The decorated function will only be executed if the required version + of etcdctl is present. Otherwise, action_fail() will be called and + the process will exit immediately. + + ''' + def wrap(f): + def wrapped_f(*args): + version = CTL.version() + if not re.match(version_regex, version): + required_version = human_version or version_regex + action_fail_now( + 'This action requires etcd version {}'.format( + required_version)) + f(*args) + return wrapped_f + return wrap + + +requires_etcd_v2 = requires_etcd_version(r'2\..*', human_version='2.x') +requires_etcd_v3 = requires_etcd_version(r'3\..*', human_version='3.x') + + +@requires_etcd_v3 +def alarm_disarm(): + '''Call `etcdctl alarm disarm`. + + ''' + try: + output = CTL.run('alarm disarm') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def alarm_list(): + '''Call `etcdctl alarm list`. + + ''' + try: + output = CTL.run('alarm list') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def compact(): + '''Call `etcdctl compact`. + + ''' + def get_latest_revision(): + try: + output = CTL.run('endpoint status --write-out json') + except subprocess.CalledProcessError as e: + action_fail_now( + 'Failed to determine latest revision for ' + 'compaction: {}'.format(e)) + + m = re.search(r'"revision":(\d*)', output) + if not m: + action_fail_now( + "Failed to get revision from 'endpoint status' " + "output: {}".format(output)) + return m.group(1) + + revision = action_get('revision') or get_latest_revision() + physical = 'true' if action_get('physical') else 'false' + command = 'compact {} --physical={}'.format(revision, physical) + try: + output = CTL.run(command) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +@requires_etcd_v3 +def defrag(): + '''Call `etcdctl defrag`. + + ''' + try: + output = CTL.run('defrag') + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +def health(): + '''Call etcdctl cluster-health + + ''' + try: + output = CTL.cluster_health(True) + action_set(dict(output=output)) + except subprocess.CalledProcessError as e: + action_fail_now(e.output) + + +if __name__ == '__main__': + ACTIONS = { + 'alarm-disarm': alarm_disarm, + 'alarm-list': alarm_list, + 'compact': compact, + 'defrag': defrag, + 'health': health, + } + + action = action_name() + ACTIONS[action]() diff --git a/etcd/actions/install b/etcd/actions/install new file mode 100755 index 0000000..c4e2fe6 --- /dev/null +++ b/etcd/actions/install @@ -0,0 +1,8 @@ +#!/bin/bash +set -eux + +# Install a snap attached from the `etcd` resource. + +RESOURCE_PATH=$(resource-get etcd) +snap install --dangerous $RESOURCE_PATH + diff --git a/etcd/actions/package-client-credentials b/etcd/actions/package-client-credentials new file mode 100755 index 0000000..ed85f93 --- /dev/null +++ b/etcd/actions/package-client-credentials @@ -0,0 +1,47 @@ +#!/bin/bash + +# The certificates live in leader-data. Grab them from there, always + +source ~/.bash_aliases +mkdir -p etcd_credentials + +if [ -z ${ETCDCTL_CERT_FILE} ]; then + cp $ETCDCTL_CERT etcd_credentials/client.crt + cp $ETCDCTL_KEY etcd_credentials/client.key + cp $ETCDCTL_CACERT etcd_credentials/ca.crt +else + cp $ETCDCTL_CERT_FILE etcd_credentials/client.crt + cp $ETCDCTL_KEY_FILE etcd_credentials/client.key + cp $ETCDCTL_CA_FILE etcd_credentials/ca.crt +fi +# Render a README heredoc +cat << EOF > etcd_credentials/README.txt +# ETCD Credentials Package + +Greetings! This credentials package was generated for you by Juju. In order +to consume these keys, you will need to do a few things first: + +Untarball the archive somewhere you wish to keep your sensitive client +credentials. + +Export those locations as environment variables, set the etcdctl endpoint, +and expose the etcd service. Even though Etcd is currently configured to +validate SSL certificates before a connection can be established, it's best +practice to leave it firewalled from the world unless you have need of an +exposed etcd endpoint. + + juju expose etcd + export ETCDCTL_KEY=$(pwd)/client.key + export ETCDCTL_CERT=$(pwd)/client.crt + export ETCDCTL_CACERT=$(pwd)/ca.crt + export ETCDCTL_ENDPOINTS=https://$(unit-get public-address):2379 + etcdctl member list + +If you have any trouble regarding connecting to your Etcd cluster, don't +hesitate to reach out over the juju mailing list: juju@lists.ubuntu.com + +EOF + +tar cfz etcd_credentials.tar.gz etcd_credentials +cp etcd_credentials.tar.gz /home/ubuntu/ +rm -rf etcd_credentials diff --git a/etcd/actions/restore b/etcd/actions/restore new file mode 100755 index 0000000..32fa764 --- /dev/null +++ b/etcd/actions/restore @@ -0,0 +1,5 @@ +#!/bin/bash + +# This runs with the assumption that the code is being executed from $CHARM_DIR +# and is also working around a stray issue where actions aren't loading $LIB +PYTHONPATH=$PYTHONPATH:$CHARM_DIR/lib actions/restore.py diff --git a/etcd/actions/restore.py b/etcd/actions/restore.py new file mode 100755 index 0000000..0a2aba7 --- /dev/null +++ b/etcd/actions/restore.py @@ -0,0 +1,261 @@ +#!/usr/local/sbin/charm-env python3 + +from charms import layer +from charms.templating.jinja2 import render +from charmhelpers.core import unitdata +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import function_fail +from charmhelpers.core.hookenv import action_get +from charmhelpers.core.hookenv import action_set +from charmhelpers.core.hookenv import config +from charmhelpers.core.hookenv import log +from charmhelpers.core.hookenv import resource_get +from charmhelpers.core.hookenv import is_leader +from charmhelpers.core.hookenv import _run_atstart +from charmhelpers.core.hookenv import _run_atexit +from charmhelpers.core.host import chdir +from charmhelpers.core.host import service_start +from charmhelpers.core.host import service_stop +from etcd_lib import get_ingress_address +from etcdctl import EtcdCtl +from etcd_databag import EtcdDatabag +from shlex import split +from subprocess import check_call +from subprocess import check_output +from subprocess import CalledProcessError +from subprocess import Popen +from subprocess import PIPE +from datetime import datetime +from uuid import uuid4 +import hashlib +import os +import sys +import time +import yaml + +# Import charm layers and start reactive +layer.import_layer_libs() +_run_atstart() + +opts = layer.options('etcd') + +DATESTAMP = datetime.strftime(datetime.now(), '%Y%m%d-%H%M%S') +ARCHIVE = "etcd-data-{}.tar.gz".format(DATESTAMP) + +unit_name = os.getenv('JUJU_UNIT_NAME').replace('/', '') +ETCD_DATA_DIR = '{}/{}.etcd'.format(opts['etcd_data_dir'], unit_name) +if not os.path.isdir(ETCD_DATA_DIR): + ETCD_DATA_DIR = opts['etcd_data_dir'] + +ETCD_PORT = config('management_port') +CLUSTER_ADDRESS = get_ingress_address('cluster') +SKIP_BACKUP = action_get('skip-backup') +SNAPSHOT_ARCHIVE = resource_get('snapshot') +TARGET_PATH = action_get('target') + + +def preflight_check(): + ''' Check preconditions for data restoration ''' + if not is_leader(): + function_fail('This action can only be run on the leader unit') + sys.exit(0) + if not SNAPSHOT_ARCHIVE: + function_fail({'result.failed': 'Missing snapshot. See: README.md'}) + sys.exit(0) + + +def render_backup(): + ''' Backup existing data in the event of restoration on a dirty unit. ''' + if not os.path.isdir(ETCD_DATA_DIR) and SKIP_BACKUP: + msg = "Backup set to True, but no data found to backup" + action_set({'backup.error': msg}) + if not os.path.isdir(ETCD_DATA_DIR): + return + + with chdir(ETCD_DATA_DIR): + if not SKIP_BACKUP: + log('Backing up existing data found in {}'.format(ETCD_DATA_DIR)) + archive_path = "{}/{}".format(TARGET_PATH, ARCHIVE) + cmd = 'tar cvf {0} {1}'.format(archive_path, '.') + check_call(split(cmd)) + backup_sum = shasum_file(archive_path) + action_set({'backup.path': archive_path, + 'backup.sha256sum': backup_sum}) + + +def unpack_resource(): + ''' Grab the resource path, and unpack it into $PATH ''' + cmd = "tar xvf {0} -C {1}".format(SNAPSHOT_ARCHIVE, ETCD_DATA_DIR) + check_call(split(cmd)) + + +def is_v3_backup(): + ''' See if the backup file contains a db file indicating a v3 backup ''' + cmd = "tar -tvf {0} --wildcards '*/db'".format(SNAPSHOT_ARCHIVE) + try: + check_call(split(cmd)) + except CalledProcessError: + return False + return True + + +def restore_v3_backup(): + ''' Apply a v3 backup ''' + cmd = "mkdir -p /root/tmp/restore-v3" + check_call(split(cmd)) + + cmd = "tar xvf {0} -C /root/tmp/restore-v3".format(SNAPSHOT_ARCHIVE) + check_call(split(cmd)) + + configfile = open('/var/snap/etcd/common/etcd.conf.yml', "r") + config = yaml.safe_load(configfile) + # Use the insecure 4001 port we have open in our deployment + environ = dict(os.environ, ETCDCTL_API="3") + cmd = "/snap/bin/etcdctl --endpoints=http://localhost:4001 snapshot " \ + "restore /root/tmp/restore-v3/db --skip-hash-check " \ + "--data-dir='/root/tmp/restore-v3/etcd' " \ + "--initial-cluster='{}' --initial-cluster-token='{}' " \ + "--initial-advertise-peer-urls='{}' --name='{}'" + + if 'initial-cluster' in config and config['initial-cluster']: + # configuration contains initilization params + cmd = cmd.format(config['initial-cluster'], + config['initial-cluster-token'], + config['initial-advertise-peer-urls'], + config['name']) + else: + # configuration does not contain initilization params + # probably coming from an etcd upgrades from etcd2 + initial_cluster = '{}=https://{}:2380'.format(config['name'], CLUSTER_ADDRESS) + initial_cluster_token = CLUSTER_ADDRESS + initial_urls = 'https://{}:2380'.format(CLUSTER_ADDRESS) + cmd = cmd.format(initial_cluster, + initial_cluster_token, + initial_urls, + config['name']) + + configfile.close() + check_call(split(cmd), env=environ) + + # Make sure we do not have anything left from any old deployments + cmd = "rm -rf {}/member".format(config['data-dir']) + check_call(split(cmd)) + + cmd = "cp -r /root/tmp/restore-v3/etcd/member {}".format(config['data-dir']) + check_call(split(cmd)) + + # Clean up + cmd = "rm -rf /root/tmp/restore-v3" + check_call(split(cmd)) + + +def start_etcd_forked(): + ''' Start the etcd daemon temporarily to initiate new cluster details ''' + raw = "/snap/etcd/current/bin/etcd -data-dir={0} -force-new-cluster" + cmd = raw.format(ETCD_DATA_DIR) + proc = Popen(split(cmd), stdout=PIPE, stderr=PIPE) + return proc.pid + + +def pkill_etcd(pid=''): + ''' Kill the temporary forked etcd daemon ''' + # cmd = 'pkill etcd' + if pid: + cmd = 'kill -9 {}'.format(pid) + else: + cmd = 'pkill etcd' + + check_call(split(cmd)) + + +def probe_forked_etcd(): + ''' Block until the forked etcd instance has started and return''' + output = b"" + loop = 0 + MAX_WAIT = 10 + + while b"http://localhost" not in output: + try: + output = check_output(split('/snap/bin/etcd.etcdctl member list')) + loop = loop + 1 + except: + log('Still waiting on forked etcd instance...') + output = b"" + loop = loop + 1 + time.sleep(1) + if loop > MAX_WAIT: + raise TimeoutError("Timed out waiting for forked etcd.") + + +def reconfigure_client_advertise(): + ''' Reconfigure the backup to use host network addresses for client advertise + instead of the assumed localhost addressing ''' + cmd = "/snap/bin/etcd.etcdctl member list" + members = check_output(split(cmd)) + member_id = members.split(b':')[0].decode('utf-8') + + raw_update = "/snap/bin/etcd.etcdctl member update {0} http://{1}:{2}" + update_cmd = raw_update.format(member_id, CLUSTER_ADDRESS, ETCD_PORT) + check_call(split(update_cmd)) + + +def shasum_file(filepath): + ''' Compute the SHA256sum of a file for verification purposes ''' + BUF_SIZE = 65536 # 64kb chunk size + shasum = hashlib.sha256() + with open(filepath, 'rb') as fp: + while True: + data = fp.read(BUF_SIZE) + if not data: + break + shasum.update(data) + return shasum.hexdigest() + + +def dismantle_cluster(): + """Disconnect other cluster members. + + This is a preparation step before restoring snapshot on the cluster. + """ + log('Disconnecting cluster members') + etcdctl = EtcdCtl() + etcd_conf = EtcdDatabag() + + my_name = etcd_conf.unit_name + endpoint = 'https://{}:{}'.format(etcd_conf.cluster_address, + etcd_conf.port) + for name, data in etcdctl.member_list(endpoint).items(): + if name != my_name: + log('Disconnecting {}'.format(name), hookenv.DEBUG) + etcdctl.unregister(data['unit_id'], endpoint) + + etcd_conf.cluster_state = 'new' + conf_path = os.path.join(etcd_conf.etcd_conf_dir, "etcd.conf.yml") + render('etcd3.conf', conf_path, etcd_conf.__dict__, owner='root', + group='root') + + +def rebuild_cluster(): + """Signal other etcd units to rejoin new cluster.""" + log('Requesting peer members to rejoin cluster') + rejoin_request = uuid4().hex + hookenv.leader_set(force_rejoin=rejoin_request) + + +if __name__ == '__main__': + log('Performing etcd snapshot restore') + preflight_check() + render_backup() + dismantle_cluster() + service_stop(opts['etcd_daemon_process']) + if is_v3_backup(): + restore_v3_backup() + else: + unpack_resource() + pid = start_etcd_forked() + probe_forked_etcd() + reconfigure_client_advertise() + pkill_etcd(pid) + service_start(opts['etcd_daemon_process']) + rebuild_cluster() + _run_atexit() diff --git a/etcd/actions/snap-upgrade b/etcd/actions/snap-upgrade new file mode 100755 index 0000000..1da3eab --- /dev/null +++ b/etcd/actions/snap-upgrade @@ -0,0 +1,5 @@ +#!/bin/bash + +# This runs with the assumption that the code is being executed from $CHARM_DIR +# and is also working around a stray issue where actions aren't loading $LIB +PYTHONPATH=$PYTHONPATH:$CHARM_DIR/lib actions/snap-upgrade.py diff --git a/etcd/actions/snap-upgrade.py b/etcd/actions/snap-upgrade.py new file mode 100755 index 0000000..6bfdd6a --- /dev/null +++ b/etcd/actions/snap-upgrade.py @@ -0,0 +1,178 @@ +#!/usr/local/sbin/charm-env python3 + +from charms.layer import snap +from charmhelpers.core import unitdata +from charmhelpers.core.hookenv import action_get +from charmhelpers.core.hookenv import action_set +from charmhelpers.core.hookenv import action_fail +from charmhelpers.core.hookenv import config +from charmhelpers.core.hookenv import log +from charms.reactive import is_state +from charms.reactive import remove_state +from charms.reactive import set_state + +# from charmhelpers.core.host import chdir + +from datetime import datetime +from subprocess import call +from subprocess import check_call +from subprocess import CalledProcessError + +from shlex import split + +import os +import shutil +import sys +import tempfile + + +# Define some dict's containing paths of files we expect to see in +# scenarios + +deb_paths = {'config': ['/etc/ssl/etcd/ca.crt', + '/etc/ssl/etcd/server.crt', + '/etc/ssl/etcd/server.key', + '/etc/ssl/etcd/client.crt', + '/etc/ssl/etcd/client.key', + '/etc/default/etcd'], + 'data': ['/var/lib/etcd/default']} + +# Snappy only cares about the config objects. Data validation will come +# at a later date. We can etcdctl ls / and then verify the data made it +# post migration. +snap_paths = {'config': ['/var/snap/etcd/common/etcd.conf', + '/var/snap/etcd/common/server.crt', + '/var/snap/etcd/common/server.key', + '/var/snap/etcd/common/ca.crt'], + 'client': ['/var/snap/etcd/common/client.crt', + '/var/snap/etcd/common/client.key'], + 'common': '/var/snap/etcd/common'} + + +def create_migration_backup(backup_package=''): + ''' Backup existing Etcd config/data paths if found and create a + tarball consisting of that discovered configuration ''' + + datestring = datetime.strftime(datetime.now(), '%Y%m%d_%H%M%S') + + if not backup_package: + pkg = '/home/ubuntu/etcd_migration_{}' + backup_package = pkg.format(datestring) + + if os.path.exists(backup_package): + msg = 'Backup package exists: {}'.format(backup_package) + action_set({'fail.message': msg}) + return False + + with tempfile.TemporaryDirectory() as tmpdir: + # Create a temporary path to perform the backup, and date the contents. + dated_path = "{0}/etcd_migration_{1}".format(tmpdir, datestring) + os.makedirs(dated_path) + + # backup all the configuration data + for p in deb_paths['config']: + if os.path.exists(p): + shutil.copy(p, dated_path) + else: + log('Skipping copy for: {} - file not found'.format(p), 'WARN') + + # backup the actual state of etcd's data + for p in deb_paths['data']: + if os.path.exists(p): + cmd = 'rsync -avzp {} {}'.format(p, dated_path) + check_call(split(cmd)) + + try: + # Create the tarball in its final location + shutil.make_archive(backup_package, 'gztar', tmpdir) + except Exception as ex: + action_set({'fail.message': ex.message}) + return False + log('Created backup {}'.format(backup_package)) + return True + + +def install_snap(channel, classic=False): + ''' Handle installation of snaps, both from resources and from the snap + store. The only indicator we need is classic mode and the channel ''' + snap.install('etcd', channel=channel, classic=classic) + + +def deb_to_snap_migration(): + has_migrated = has_migrated_from_deb() + if not has_migrated: + try: + cmd = '/snap/bin/etcd.ingest' + check_call(split(cmd)) + except CalledProcessError as cpe: + log('Error encountered during ingest.', 'ERROR') + log('Error message: {}'.format(cpe.message)) + action_fail('Migration failed') + + for key_path in snap_paths['client']: + chmod = "chmod 644 {}".format(key_path) + call(split(chmod)) + cmod = "chmod 755 {}".format(snap_paths['common']) + call(split(cmod)) + + +def purge_deb_files(): + probe_package_command = 'dpkg --list etcd' + return_code = call(split(probe_package_command)) + if return_code != 0: + # The return code from dpkg --list when the package is + # non existant + action_set({'dpkg.list.message': 'dpkg probe return_code > 0', + 'skip.package.purge': 'True'}) + return + log('Purging deb configuration files post migration', 'INFO') + cmd = 'apt-get purge -y etcd' + try: + check_call(split(cmd)) + except CalledProcessError as cpe: + action_fail({'apt.purge.message': cpe.message}) + + for f in deb_paths['config']: + try: + log('Removing file {}'.format(f), 'INFO') + os.remove(f) + except FileNotFoundError: + k = 'purge.missing.{}'.format(os.path.basename(f)) + msg = 'Did not purge {}. File not found.'.format(f) + action_set({k: msg}) + except: + k = 'purge.error.{}'.format(f) + msg = 'Failed to purge {}. Manual removal required.'.format(k) + action_set({k: msg}) + + +def has_migrated_from_deb(): + for p in snap_paths['config']: + # helpful when debugging + log("Scanning for file: {} {}".format(p, os.path.exists(p)), 'DEBUG') + if not os.path.exists(p): + return False + return True + +if __name__ == '__main__': + # Control flow of the action + backup_package = action_get('target') + backup = action_get('backup') + channel = config('channel') + + if backup: + backup_status = create_migration_backup(backup_package) + if not backup_status: + action_fail('Failed creating the backup. Refusing to proceed.') + sys.exit(0) + + if not is_state('etcd.deb.migrated'): + install_snap('ingest/stable', True) + deb_to_snap_migration() + install_snap(channel, False) + purge_deb_files() + remove_state('etcd.installed') + set_state('snap.installed.etcd') + remove_state('etcd.pillowmints') + unitdata.kv().flush() + call(['hooks/config-changed']) diff --git a/etcd/actions/snapshot b/etcd/actions/snapshot new file mode 100755 index 0000000..35ce3cf --- /dev/null +++ b/etcd/actions/snapshot @@ -0,0 +1,52 @@ +#!/bin/bash + +set -ex + +# Snippet from: https://coreos.com/etcd/docs/latest/admin_guide.html +# Snapshot a running etcd cluster data. +# This command will rewrite some of the metadata contained in the backup +# (specifically, the node ID and cluster ID), which means that the node will +# lose its former identity. In order to recreate a cluster from the backup, you +# will need to start a new, single-node cluster. The metadata is rewritten to +# prevent the new node from inadvertently being joined onto an existing cluster. + +ETCD_BACKUP_TARGET_DIR=$(action-get target) +ETCD_KEYS_VERSION=$(action-get keys-version) +#ETCD_DATA_DIR=/var/lib/etcd/default +UNIT_NAME=${JUJU_UNIT_NAME%%/*} +UNIT_NUM=${JUJU_UNIT_NAME#*/} +ETCD_DATA_DIR=/var/snap/etcd/current/$UNIT_NAME$UNIT_NUM.etcd/ +if [ ! -d "$ETCD_DATA_DIR" ]; then + ETCD_DATA_DIR=/var/snap/etcd/current/ +fi + +DATE_STAMP=$(date +%Y-%m-%d-%H.%M.%S) +ARCHIVE=etcd-snapshot-$DATE_STAMP.tar.gz + +# Ensure the backupd target exists +mkdir -p $ETCD_BACKUP_TARGET_DIR/$JUJU_ACTION_UUID + +if [ "${ETCD_KEYS_VERSION}" == "v2" ]; then + # Dump the data currently in the cluster + /snap/bin/etcd.etcdctl backup --data-dir $ETCD_DATA_DIR --backup-dir $ETCD_BACKUP_TARGET_DIR/$JUJU_ACTION_UUID +elif [ "${ETCD_KEYS_VERSION}" == "v3" ]; then + mkdir -p $ETCD_BACKUP_TARGET_DIR/$JUJU_ACTION_UUID + cp $ETCD_DATA_DIR/member/snap/db $ETCD_BACKUP_TARGET_DIR/$JUJU_ACTION_UUID +else + action-fail "keys-version must be either v2 or v3" + exit +fi + +# Create the backup archive +cd $ETCD_BACKUP_TARGET_DIR/$JUJU_ACTION_UUID +tar cvfz ../$ARCHIVE . + +# keep things tidy +cd .. +rm -rf $JUJU_ACTION_UUID + +action-set snapshot.path="$ETCD_BACKUP_TARGET_DIR/$ARCHIVE" +action-set snapshot.size="$(du -h $ETCD_BACKUP_TARGET_DIR/$ARCHIVE | cut -d$'\t' -f1)" +action-set snapshot.sha256="$(sha256sum $ETCD_BACKUP_TARGET_DIR/$ARCHIVE | cut -d' ' -f1)" +action-set copy.cmd="juju scp $JUJU_UNIT_NAME:$ETCD_BACKUP_TARGET_DIR/$ARCHIVE ." +action-set snapshot.version="$(/snap/bin/etcd.etcdctl version)" diff --git a/etcd/bin/charm-env b/etcd/bin/charm-env new file mode 100755 index 0000000..d211ce9 --- /dev/null +++ b/etcd/bin/charm-env @@ -0,0 +1,107 @@ +#!/bin/bash + +VERSION="1.0.0" + + +find_charm_dirs() { + # Hopefully, $JUJU_CHARM_DIR is set so which venv to use in unambiguous. + if [[ -n "$JUJU_CHARM_DIR" || -n "$CHARM_DIR" ]]; then + if [[ -z "$JUJU_CHARM_DIR" ]]; then + # accept $CHARM_DIR to be more forgiving + export JUJU_CHARM_DIR="$CHARM_DIR" + fi + if [[ -z "$CHARM_DIR" ]]; then + # set CHARM_DIR as well to help with backwards compatibility + export CHARM_DIR="$JUJU_CHARM_DIR" + fi + return + fi + # Try to guess the value for JUJU_CHARM_DIR by looking for a non-subordinate + # (because there's got to be at least one principle) charm directory; + # if there are several, pick the first by alpha order. + agents_dir="/var/lib/juju/agents" + if [[ -d "$agents_dir" ]]; then + desired_charm="$1" + found_charm_dir="" + if [[ -n "$desired_charm" ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + charm_name="$(grep -o '^['\''"]\?name['\''"]\?:.*' $charm_dir/metadata.yaml 2> /dev/null | sed -e 's/.*: *//' -e 's/['\''"]//g')" + if [[ "$charm_name" == "$desired_charm" ]]; then + if [[ -n "$found_charm_dir" ]]; then + >&2 echo "Ambiguous possibilities for JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + found_charm_dir="$charm_dir" + fi + done + if [[ -z "$found_charm_dir" ]]; then + >&2 echo "Unable to determine JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + export JUJU_CHARM_DIR="$found_charm_dir" + export CHARM_DIR="$found_charm_dir" + return + fi + # shellcheck disable=SC2126 + non_subordinates="$(grep -L 'subordinate"\?:.*true' "$agents_dir"/unit-*/charm/metadata.yaml | wc -l)" + if [[ "$non_subordinates" -gt 1 ]]; then + >&2 echo 'Ambiguous possibilities for JUJU_CHARM_DIR; please use --charm or run within a Juju hook context' + exit 1 + elif [[ "$non_subordinates" -eq 1 ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + if grep -q 'subordinate"\?:.*true' "$charm_dir/metadata.yaml"; then + continue + fi + export JUJU_CHARM_DIR="$charm_dir" + export CHARM_DIR="$charm_dir" + return + done + fi + fi + >&2 echo 'Unable to determine JUJU_CHARM_DIR; please run within a Juju hook context' + exit 1 +} + +try_activate_venv() { + if [[ -d "$JUJU_CHARM_DIR/../.venv" ]]; then + . "$JUJU_CHARM_DIR/../.venv/bin/activate" + fi +} + +find_wrapped() { + PATH="${PATH/\/usr\/local\/sbin:}" which "$(basename "$0")" +} + + +if [[ "$1" == "--version" || "$1" == "-v" ]]; then + echo "$VERSION" + exit 0 +fi + + +# allow --charm option to hint which JUJU_CHARM_DIR to choose when ambiguous +# NB: --charm option must come first +# NB: option must be processed outside find_charm_dirs to modify $@ +charm_name="" +if [[ "$1" == "--charm" ]]; then + charm_name="$2" + shift; shift +fi + +find_charm_dirs "$charm_name" +try_activate_venv +export PYTHONPATH="$JUJU_CHARM_DIR/lib:$PYTHONPATH" + +if [[ "$(basename "$0")" == "charm-env" ]]; then + # being used as a shebang + exec "$@" +elif [[ "$0" == "$BASH_SOURCE" ]]; then + # being invoked as a symlink wrapping something to find in the venv + exec "$(find_wrapped)" "$@" +elif [[ "$(basename "$BASH_SOURCE")" == "charm-env" ]]; then + # being sourced directly; do nothing + /bin/true +else + # being sourced for wrapped bash helpers + . "$(find_wrapped)" +fi diff --git a/etcd/bin/layer_option b/etcd/bin/layer_option new file mode 100755 index 0000000..3253ef8 --- /dev/null +++ b/etcd/bin/layer_option @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from charms import layer + + +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 = layer.options.get(args.section, 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/etcd/config.yaml b/etcd/config.yaml new file mode 100644 index 0000000..8e06437 --- /dev/null +++ b/etcd/config.yaml @@ -0,0 +1,48 @@ +"options": + "nagios_context": + "default": "juju" + "type": "string" + "description": | + Used by the nrpe subordinate charms. + A string that will be prepended to instance name to set the host name + in nagios. So for instance the hostname would be something like: + juju-myservice-0 + If you're running multiple environments with the same services in them + this allows you to differentiate between them. + "nagios_servicegroups": + "default": "" + "type": "string" + "description": | + A comma-separated list of nagios servicegroups. + If left empty, the nagios_context will be used as the servicegroup + "snapd_refresh": + "default": "max" + "type": "string" + "description": | + How often snapd handles updates for installed snaps. Set to an empty + string to check 4x per day. Set to "max" (the default) to check once per + month based on the charm deployment date. You may also set a custom + string as described in the 'refresh.timer' section here: + https://forum.snapcraft.io/t/system-options/87 + "port": + "type": "int" + "default": !!int "2379" + "description": "Port to run the public ETCD service on" + "management_port": + "type": "int" + "default": !!int "2380" + "description": "Port to run the ETCD Management service" + "channel": + "type": "string" + "default": "auto" + "description": | + The snap channel from which to install etcd (e.g. '3.3/stable'), or 'auto' + to accept the charm default. Choosing 'auto' will install the latest + supported version of etcd at deploy time, but will not automatically upgrade + to a newer version thereafter. + "bind_to_all_interfaces": + "type": "boolean" + "default": !!bool "true" + "description": | + The service binds to all network interfaces if true. The service binds + only to the first found bind address of each relation if false diff --git a/etcd/copyright b/etcd/copyright new file mode 100644 index 0000000..3306630 --- /dev/null +++ b/etcd/copyright @@ -0,0 +1,13 @@ +Copyright 2015 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. diff --git a/etcd/copyright.layer-basic b/etcd/copyright.layer-basic new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/etcd/copyright.layer-basic @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/etcd/copyright.layer-leadership b/etcd/copyright.layer-leadership new file mode 100644 index 0000000..08b983f --- /dev/null +++ b/etcd/copyright.layer-leadership @@ -0,0 +1,15 @@ +Copyright 2015-2016 Canonical Ltd. + +This file is part of the Leadership Layer for Juju. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/etcd/copyright.layer-nagios b/etcd/copyright.layer-nagios new file mode 100644 index 0000000..c80db95 --- /dev/null +++ b/etcd/copyright.layer-nagios @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2016, Canonical Ltd. +License: GPL-3 + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranties of + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . diff --git a/etcd/copyright.layer-options b/etcd/copyright.layer-options new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/etcd/copyright.layer-options @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/etcd/copyright.layer-snap b/etcd/copyright.layer-snap new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/etcd/copyright.layer-snap @@ -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/etcd/copyright.layer-status b/etcd/copyright.layer-status new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/etcd/copyright.layer-status @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/etcd/debug-scripts/charm-unitdata b/etcd/debug-scripts/charm-unitdata new file mode 100755 index 0000000..d2aac60 --- /dev/null +++ b/etcd/debug-scripts/charm-unitdata @@ -0,0 +1,12 @@ +#!/usr/local/sbin/charm-env python3 + +import debug_script +import json +from charmhelpers.core import unitdata + +kv = unitdata.kv() +data = kv.getrange("") + +with debug_script.open_file("unitdata.json", "w") as f: + json.dump(data, f, indent=2) + f.write("\n") diff --git a/etcd/debug-scripts/etcd b/etcd/debug-scripts/etcd new file mode 100755 index 0000000..45e355d --- /dev/null +++ b/etcd/debug-scripts/etcd @@ -0,0 +1,11 @@ +#!/bin/sh +set -ux + +# TODO: Link these explicit system calls against the layer.yaml configuration +systemctl status snap.etcd.etcd > $DEBUG_SCRIPT_DIR/etcd-systemctl-status +journalctl -u snap.etcd.etcd > $DEBUG_SCRIPT_DIR/etcd-journal + +alias etcdctl="/snap/bin/etcd.etcdctl --cert-file /var/snap/etcd/common/client.crt --key-file /var/snap/etcd/common/client.key --ca-file /var/snap/etcd/common/ca.crt" +etcdctl cluster-health > $DEBUG_SCRIPT_DIR/etcdctl-cluster-health +etcdctl ls --recursive > $DEBUG_SCRIPT_DIR/etcdctl-ls +etcdctl member list > $DEBUG_SCRIPT_DIR/etcdctl-member-list diff --git a/etcd/debug-scripts/filesystem b/etcd/debug-scripts/filesystem new file mode 100755 index 0000000..c5ec6d8 --- /dev/null +++ b/etcd/debug-scripts/filesystem @@ -0,0 +1,17 @@ +#!/bin/sh +set -ux + +# report file system disk space usage +df -hT > $DEBUG_SCRIPT_DIR/df-hT +# estimate file space usage +du -h / 2>&1 > $DEBUG_SCRIPT_DIR/du-h +# list the mounted filesystems +mount > $DEBUG_SCRIPT_DIR/mount +# list the mounted systems with ascii trees +findmnt -A > $DEBUG_SCRIPT_DIR/findmnt +# list block devices +lsblk > $DEBUG_SCRIPT_DIR/lsblk +# list open files +lsof 2>&1 > $DEBUG_SCRIPT_DIR/lsof +# list local system locks +lslocks > $DEBUG_SCRIPT_DIR/lslocks diff --git a/etcd/debug-scripts/juju-logs b/etcd/debug-scripts/juju-logs new file mode 100755 index 0000000..d27c458 --- /dev/null +++ b/etcd/debug-scripts/juju-logs @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +cp -v /var/log/juju/* $DEBUG_SCRIPT_DIR diff --git a/etcd/debug-scripts/juju-network-get b/etcd/debug-scripts/juju-network-get new file mode 100755 index 0000000..983c8c4 --- /dev/null +++ b/etcd/debug-scripts/juju-network-get @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import yaml +import debug_script + +with open('metadata.yaml') as f: + metadata = yaml.load(f) + +relations = [] +for key in ['requires', 'provides', 'peers']: + relations += list(metadata.get(key, {}).keys()) + +os.mkdir(os.path.join(debug_script.dir, 'relations')) + +for relation in relations: + path = 'relations/' + relation + with debug_script.open_file(path, 'w') as f: + cmd = ['network-get', relation] + subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) diff --git a/etcd/debug-scripts/network b/etcd/debug-scripts/network new file mode 100755 index 0000000..944a355 --- /dev/null +++ b/etcd/debug-scripts/network @@ -0,0 +1,11 @@ +#!/bin/sh +set -ux + +ifconfig -a > $DEBUG_SCRIPT_DIR/ifconfig +cp -v /etc/resolv.conf $DEBUG_SCRIPT_DIR/resolv.conf +cp -v /etc/network/interfaces $DEBUG_SCRIPT_DIR/interfaces +netstat -planut > $DEBUG_SCRIPT_DIR/netstat +route -n > $DEBUG_SCRIPT_DIR/route +iptables-save > $DEBUG_SCRIPT_DIR/iptables-save +dig google.com > $DEBUG_SCRIPT_DIR/dig-google +ping -w 2 -i 0.1 google.com > $DEBUG_SCRIPT_DIR/ping-google diff --git a/etcd/debug-scripts/packages b/etcd/debug-scripts/packages new file mode 100755 index 0000000..b60a9cf --- /dev/null +++ b/etcd/debug-scripts/packages @@ -0,0 +1,7 @@ +#!/bin/sh +set -ux + +dpkg --list > $DEBUG_SCRIPT_DIR/dpkg-list +snap list > $DEBUG_SCRIPT_DIR/snap-list +pip2 list > $DEBUG_SCRIPT_DIR/pip2-list +pip3 list > $DEBUG_SCRIPT_DIR/pip3-list diff --git a/etcd/debug-scripts/sysctl b/etcd/debug-scripts/sysctl new file mode 100755 index 0000000..a86a6c8 --- /dev/null +++ b/etcd/debug-scripts/sysctl @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +sysctl -a > $DEBUG_SCRIPT_DIR/sysctl diff --git a/etcd/debug-scripts/systemd b/etcd/debug-scripts/systemd new file mode 100755 index 0000000..8bb9b6f --- /dev/null +++ b/etcd/debug-scripts/systemd @@ -0,0 +1,9 @@ +#!/bin/sh +set -ux + +systemctl --all > $DEBUG_SCRIPT_DIR/systemctl +journalctl > $DEBUG_SCRIPT_DIR/journalctl +systemd-analyze time > $DEBUG_SCRIPT_DIR/systemd-analyze-time +systemd-analyze blame > $DEBUG_SCRIPT_DIR/systemd-analyze-blame +systemd-analyze critical-chain > $DEBUG_SCRIPT_DIR/systemd-analyze-critical-chain +systemd-analyze dump > $DEBUG_SCRIPT_DIR/systemd-analyze-dump diff --git a/etcd/debug-scripts/tls-certs b/etcd/debug-scripts/tls-certs new file mode 100755 index 0000000..2692e51 --- /dev/null +++ b/etcd/debug-scripts/tls-certs @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import shutil +import traceback +import debug_script +from charms import layer + +options = layer.options.get('tls-client') + +def copy_cert(source_key, name): + try: + source = options[source_key] + dest = os.path.join(debug_script.dir, name) + shutil.copy(source, dest) + except Exception: + traceback.print_exc() + +copy_cert('client_certificate_path', 'client.crt') +copy_cert('server_certificate_path', 'server.crt') +copy_cert('ca_certificate_path', 'ca.crt') diff --git a/etcd/docs/status.md b/etcd/docs/status.md new file mode 100644 index 0000000..c6cceab --- /dev/null +++ b/etcd/docs/status.md @@ -0,0 +1,91 @@ +

WorkloadState

+ +```python +WorkloadState(self, /, *args, **kwargs) +``` + +Enum of the valid workload states. + +Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + +

maintenance

+ +```python +maintenance(message) +``` + +Set the status to the `MAINTENANCE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

maint

+ +```python +maint(message) +``` + +Shorthand alias for +[maintenance](status.md#charms.layer.status.maintenance). + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

blocked

+ +```python +blocked(message) +``` + +Set the status to the `BLOCKED` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

waiting

+ +```python +waiting(message) +``` + +Set the status to the `WAITING` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

active

+ +```python +active(message) +``` + +Set the status to the `ACTIVE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

status_set

+ +```python +status_set(workload_state, message) +``` + +Set the status to the given workload state with a message. + +__Parameters__ + +- __`workload_state` (WorkloadState or str)__: State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. +- __`message` (str)__: Message to convey to the operator. + diff --git a/etcd/hooks/certificates-relation-broken b/etcd/hooks/certificates-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/certificates-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/certificates-relation-changed b/etcd/hooks/certificates-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/certificates-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/certificates-relation-created b/etcd/hooks/certificates-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/certificates-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/certificates-relation-departed b/etcd/hooks/certificates-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/certificates-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/certificates-relation-joined b/etcd/hooks/certificates-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/certificates-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/cluster-relation-broken b/etcd/hooks/cluster-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/cluster-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/cluster-relation-changed b/etcd/hooks/cluster-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/cluster-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/cluster-relation-created b/etcd/hooks/cluster-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/cluster-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/cluster-relation-departed b/etcd/hooks/cluster-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/cluster-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/cluster-relation-joined b/etcd/hooks/cluster-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/cluster-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/config-changed b/etcd/hooks/config-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/config-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/data-storage-attached b/etcd/hooks/data-storage-attached new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/data-storage-attached @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/data-storage-detaching b/etcd/hooks/data-storage-detaching new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/data-storage-detaching @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/db-relation-broken b/etcd/hooks/db-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/db-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/db-relation-changed b/etcd/hooks/db-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/db-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/db-relation-created b/etcd/hooks/db-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/db-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/db-relation-departed b/etcd/hooks/db-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/db-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/db-relation-joined b/etcd/hooks/db-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/db-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/grafana-relation-broken b/etcd/hooks/grafana-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/grafana-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/grafana-relation-changed b/etcd/hooks/grafana-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/grafana-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/grafana-relation-created b/etcd/hooks/grafana-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/grafana-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/grafana-relation-departed b/etcd/hooks/grafana-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/grafana-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/grafana-relation-joined b/etcd/hooks/grafana-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/grafana-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/hook.template b/etcd/hooks/hook.template new file mode 100644 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/hook.template @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/install b/etcd/hooks/install new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/install @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/leader-elected b/etcd/hooks/leader-elected new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/leader-elected @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/leader-settings-changed b/etcd/hooks/leader-settings-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/leader-settings-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/nrpe-external-master-relation-broken b/etcd/hooks/nrpe-external-master-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/nrpe-external-master-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/nrpe-external-master-relation-changed b/etcd/hooks/nrpe-external-master-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/nrpe-external-master-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/nrpe-external-master-relation-created b/etcd/hooks/nrpe-external-master-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/nrpe-external-master-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/nrpe-external-master-relation-departed b/etcd/hooks/nrpe-external-master-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/nrpe-external-master-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/nrpe-external-master-relation-joined b/etcd/hooks/nrpe-external-master-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/nrpe-external-master-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/post-series-upgrade b/etcd/hooks/post-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/post-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/pre-series-upgrade b/etcd/hooks/pre-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/pre-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/prometheus-relation-broken b/etcd/hooks/prometheus-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/prometheus-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/prometheus-relation-changed b/etcd/hooks/prometheus-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/prometheus-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/prometheus-relation-created b/etcd/hooks/prometheus-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/prometheus-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/prometheus-relation-departed b/etcd/hooks/prometheus-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/prometheus-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/prometheus-relation-joined b/etcd/hooks/prometheus-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/prometheus-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/proxy-relation-broken b/etcd/hooks/proxy-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/proxy-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/proxy-relation-changed b/etcd/hooks/proxy-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/proxy-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/proxy-relation-created b/etcd/hooks/proxy-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/proxy-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/proxy-relation-departed b/etcd/hooks/proxy-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/proxy-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/proxy-relation-joined b/etcd/hooks/proxy-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/proxy-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/relations/etcd-proxy/.gitignore b/etcd/hooks/relations/etcd-proxy/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/etcd/hooks/relations/etcd-proxy/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/etcd/hooks/relations/etcd-proxy/README.md b/etcd/hooks/relations/etcd-proxy/README.md new file mode 100644 index 0000000..48d6a91 --- /dev/null +++ b/etcd/hooks/relations/etcd-proxy/README.md @@ -0,0 +1,110 @@ +# Overview + +This interface layer handles the communication with Etcd via the `etcd-proxy` interface. + +# Usage + +## Requires + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.connected` The relation is established, but Etcd may not + yet have provided any connection or service information. + + * `{relation_name}.available` Etcd has provided its cluster string + information, and is ready to handle incoming connections. + The provided information can be accessed via the following methods: + * `cluster_string()` + + * `{relation_name}.tls.available` Etcd has provided client + connection credentials for TLS communication. + * `client_ca` - CA certificate + * `client_cert` - Client Cert + * `client_key` - Client Key + + +For example, a common application for this is configuring an +applications backend kv storage, like Docker. + +```python +@when('proxy.available') +def prepare_etcd_proxy(proxy): + con_string = proxy.cluster_string() + # Save certificates to disk + proxy.save_client_credentials('/etc/ssl/etcd') + opts = {} + opts['cluster_string'] = con_string + opts['client_ca'] = '/etc/ssl/etcd/client-ca.pem' + opts['client_cert'] = '/etc/ssl/etcd/client-cert.pem' + opts['client_key'] = '/etc/ssl/etcd/client-key.pem' + render('proxy_systemd_template', '/etc/systemd/system/etcd-proxy.service', opts) + +``` + + +## Provides + +A charm providing this interface is providing the Etcd cluster management +connection string. This is similar to what ETCD requires when peering, declared as: + +```shell +etcd0=https://192.168.1.2:2380,etcd1=https://192.168.2.22:2380 +``` + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.connected` One or more clients of any type + have been related. The charm should call the following + methods to provide the appropriate information to the clients: + + * `{relation_name}.set_cluster_string()` + + * Additionally to secure the Etcd network connections, All of + the client certificate keys must be set, which is conveniently + enabled as a method on the interface: + + +#### Example: + +```python +from charmhelpers.core import hookenv +# this module lives in the etcd charm in lib/etcdctl.py +import etcdctl + +@when('proxy.connected') +def send_cluster_details(proxy): + # ETCD charm provides client keys via leader_data + cert = hookenv.leader_get('client_certificate') + key = hookenv.leader_get('client_key') + ca = hookenv.leader_get('certificate_authority') + # set the certificates on the conversation + proxy.set_client_credentials(key, cert, ca) + + # format a list of cluster participants + etcdctl = etcdctl.EtcdCtl() + peers = etcdctl.member_list() + cluster = [] + for peer in peers: + # Potential member doing registration. Default to skip + if 'peer_urls' not in peer.keys() or not peer['peer_urls']: + continue + peer_string = "{}={}".format(peer['name'], peer['peer_urls']) + cluster.append(peer_string) + # set the cluster string on the conversation + proxy.set_cluster_string(','.join(cluster)) +``` + + +# Contact Information + +### Maintainer +- Charles Butler <[charles.butler@canonical.com](mailto:charles.butler@canonical.com)> + +### Contributors +- Mathew Bruzek <[mathew.bruzek@canonical.com](mailto:mathew.bruzek@canonical.com)> + +# Etcd + +- [Etcd](https://coreos.com/etcd/) home page +- [Etcd bug trackers](https://github.com/coreos/etcd/issues) +- [Etcd Juju Charm](http://github.com/juju-solutions/layer-etcd) diff --git a/etcd/hooks/relations/etcd-proxy/__init__.py b/etcd/hooks/relations/etcd-proxy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/etcd/hooks/relations/etcd-proxy/interface.yaml b/etcd/hooks/relations/etcd-proxy/interface.yaml new file mode 100644 index 0000000..9ed36a8 --- /dev/null +++ b/etcd/hooks/relations/etcd-proxy/interface.yaml @@ -0,0 +1,4 @@ +name: etcd-proxy +summary: Interface for relating to ETCD +version: 1 +maintainer: "Charles Butler " diff --git a/etcd/hooks/relations/etcd-proxy/provides.py b/etcd/hooks/relations/etcd-proxy/provides.py new file mode 100644 index 0000000..6d36d3b --- /dev/null +++ b/etcd/hooks/relations/etcd-proxy/provides.py @@ -0,0 +1,51 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdProvider(RelationBase): + scope = scopes.GLOBAL + + @hook('{provides:etcd-proxy}-relation-{joined,changed}') + def joined_or_changed(self): + ''' Set state so the unit can identify it is connecting ''' + self.set_state('{relation_name}.connected') + + @hook('{provides:etcd-proxy}-relation-{broken,departed}') + def broken_or_departed(self): + ''' Set state so the unit can identify it is departing ''' + self.remove_state('{relation_name}.connected') + + def set_client_credentials(self, key, cert, ca): + ''' Set the client credentials on the global conversation for this + relation. ''' + self.set_remote('client_key', key) + self.set_remote('client_ca', ca) + self.set_remote('client_cert', cert) + + def set_cluster_string(self, cluster_string): + ''' Set the cluster string on the convsersation ''' + self.set_remote('cluster', cluster_string) + + # Kept for backwords compatibility + def provide_cluster_string(self, cluster_string): + ''' + @params cluster_string - fully formed etcd cluster string. + This is akin to the --initial-cluster-string setting to the + etcd-daemon. Proxy's will need to know each declared member of + the cluster to effectively proxy. + ''' + self.set_remote('cluster', cluster_string) diff --git a/etcd/hooks/relations/etcd-proxy/requires.py b/etcd/hooks/relations/etcd-proxy/requires.py new file mode 100644 index 0000000..1fdcced --- /dev/null +++ b/etcd/hooks/relations/etcd-proxy/requires.py @@ -0,0 +1,72 @@ +#!/usr/bin/python +# 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 + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdClientProxy(RelationBase): + scope = scopes.GLOBAL + + @hook('{requires:etcd-proxy}-relation-{joined,changed}') + def changed(self): + self.set_state('{relation_name}.connected') + if self.get_cluster_string(): + self.set_state('{relation_name}.available') + # Get the ca, key, cert from the relation data. + cert = self.get_client_credentials() + # The tls state depends on the existance of the ca, key and cert. + if cert['client_cert'] and cert['client_key'] and cert['client_ca']: # noqa + self.set_state('{relation_name}.tls.available') + + @hook('{requires:etcd-proxy}-relation-{broken, departed}') + def broken(self): + self.remove_state('{relation_name}.available') + self.remove_state('{relation_name}.connected') + self.remove_state('{relation_name}.tls.available') + + def get_cluster_string(self): + ''' Return the connection string, if available, or None. ''' + return self.get_remote('cluster') + + def get_client_credentials(self): + ''' Return a dict with the client certificate, ca and key to + communicate with etcd using tls. ''' + return {'client_cert': self.get_remote('client_cert'), + 'client_key': self.get_remote('client_key'), + 'client_ca': self.get_remote('client_ca')} + + def cluster_string(self): + """ + Get the cluster string, if available, or None. + """ + return self.get_cluster_string() + + def save_client_credentials(self, key, cert, ca): + ''' Save all the client certificates for etcd to local files. ''' + self._save_remote_data('client_cert', cert) + self._save_remote_data('client_key', key) + self._save_remote_data('client_ca', ca) + + def _save_remote_data(self, key, path): + ''' Save the remote data to a file indicated by path creating the + parent directory if needed.''' + value = self.get_remote(key) + if value: + parent = os.path.dirname(path) + if not os.path.isdir(parent): + os.makedirs(parent) + with open(path, 'w') as stream: + stream.write(value) diff --git a/etcd/hooks/relations/etcd/.gitignore b/etcd/hooks/relations/etcd/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/etcd/hooks/relations/etcd/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/etcd/hooks/relations/etcd/README.md b/etcd/hooks/relations/etcd/README.md new file mode 100644 index 0000000..9ed51dd --- /dev/null +++ b/etcd/hooks/relations/etcd/README.md @@ -0,0 +1,89 @@ +# Overview + +This interface layer handles the communication with Etcd via the `etcd` +interface. + +# Usage + +## Requires + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.connected` The relation is established, but Etcd may not + yet have provided any connection or service information. + + * `{relation_name}.available` Etcd has provided its connection string + information, and is ready to serve as a KV store. + The provided information can be accessed via the following methods: + * `etcd.get_connection_string()` + * `etcd.get_version()` + * `{relation_name}.tls.available` Etcd has provided the connection string + information, and the tls client credentials to communicate with it. + The client credentials can be accessed via: + * `{relation_name}.get_client_credentials()` returning a dictionary of + the clinet certificate, key and CA. + * `{relation_name}.save_client_credentials(key, cert, ca)` is a convenience + method to save the client certificate, key and CA to files of your + choosing. + + +For example, a common application for this is configuring an applications +backend key/value storage, like Docker. + +```python +@when('etcd.available', 'docker.available') +def swarm_etcd_cluster_setup(etcd): + con_string = etcd.connection_string().replace('http', 'etcd') + opts = {} + opts['connection_string'] = con_string + render('docker-compose.yml', 'files/swarm/docker-compose.yml', opts) + +``` + + +## Provides + +A charm providing this interface is providing the Etcd rest api service. + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.connected` One or more clients of any type have + been related. The charm should call the following methods to provide the + appropriate information to the clients: + + * `{relation_name}.set_connection_string(string, version)` + * `{relation_name}.set_client_credentials(key, cert, ca)` + +Example: + +```python +@when('db.connected') +def send_connection_details(db): + cert = leader_get('client_certificate') + key = leader_get('client_key') + ca = leader_get('certificate_authority') + # Set the key, cert, and ca on the db relation + db.set_client_credentials(key, cert, ca) + + port = hookenv.config().get('port') + # Get all the peers participating in the cluster relation. + addresses = cluster.get_peer_addresses() + connections = [] + for address in addresses: + connections.append('http://{0}:{1}'.format(address, port)) + # Set the connection string on the db relation. + db.set_connection_string(','.join(conections)) +``` + + +# Contact Information + +### Maintainer +- Charles Butler + + +# Etcd + +- [Etcd](https://coreos.com/etcd/) home page +- [Etcd bug trackers](https://github.com/coreos/etcd/issues) +- [Etcd Juju Charm](http://jujucharms.com/?text=etcd) diff --git a/etcd/hooks/relations/etcd/__init__.py b/etcd/hooks/relations/etcd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/etcd/hooks/relations/etcd/interface.yaml b/etcd/hooks/relations/etcd/interface.yaml new file mode 100644 index 0000000..929b1d5 --- /dev/null +++ b/etcd/hooks/relations/etcd/interface.yaml @@ -0,0 +1,4 @@ +name: etcd +summary: Interface for relating to ETCD +version: 2 +maintainer: "Charles Butler " diff --git a/etcd/hooks/relations/etcd/peers.py b/etcd/hooks/relations/etcd/peers.py new file mode 100644 index 0000000..90980d1 --- /dev/null +++ b/etcd/hooks/relations/etcd/peers.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdPeer(RelationBase): + '''This class handles peer relation communication by setting states that + the reactive code can respond to. ''' + + scope = scopes.UNIT + + @hook('{peers:etcd}-relation-joined') + def peer_joined(self): + '''A new peer has joined, set the state on the unit so we can track + when they are departed. ''' + conv = self.conversation() + conv.set_state('{relation_name}.joined') + + @hook('{peers:etcd}-relation-departed') + def peers_going_away(self): + '''Trigger a state on the unit that it is leaving. We can use this + state in conjunction with the joined state to determine which unit to + unregister from the etcd cluster. ''' + conv = self.conversation() + conv.remove_state('{relation_name}.joined') + conv.set_state('{relation_name}.departing') + + def dismiss(self): + '''Remove the departing state from all other units in the conversation, + and we can resume normal operation. + ''' + for conv in self.conversations(): + conv.remove_state('{relation_name}.departing') + + def get_peers(self): + '''Return a list of names for the peers participating in this + conversation scope. ''' + peers = [] + # Iterate over all the conversations of this type. + for conversation in self.conversations(): + peers.append(conversation.scope) + return peers + + def set_db_ingress_address(self, address): + '''Set the ingress address belonging to the db relation.''' + for conversation in self.conversations(): + conversation.set_remote('db-ingress-address', address) + + def get_db_ingress_addresses(self): + '''Return a list of db ingress addresses''' + addresses = [] + # Iterate over all the conversations of this type. + for conversation in self.conversations(): + address = conversation.get_remote('db-ingress-address') + if address: + addresses.append(address) + return addresses diff --git a/etcd/hooks/relations/etcd/provides.py b/etcd/hooks/relations/etcd/provides.py new file mode 100644 index 0000000..3cfc174 --- /dev/null +++ b/etcd/hooks/relations/etcd/provides.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdProvider(RelationBase): + scope = scopes.GLOBAL + + @hook('{provides:etcd}-relation-{joined,changed}') + def joined_or_changed(self): + ''' Set the connected state from the provides side of the relation. ''' + self.set_state('{relation_name}.connected') + + @hook('{provides:etcd}-relation-{broken,departed}') + def broken_or_departed(self): + '''Remove connected state from the provides side of the relation. ''' + conv = self.conversation() + if len(conv.units) == 1: + conv.remove_state('{relation_name}.connected') + + def set_client_credentials(self, key, cert, ca): + ''' Set the client credentials on the global conversation for this + relation. ''' + self.set_remote('client_key', key) + self.set_remote('client_ca', ca) + self.set_remote('client_cert', cert) + + def set_connection_string(self, connection_string, version=''): + ''' Set the connection string on the global conversation for this + relation. ''' + # Note: Version added as a late-dependency for 2 => 3 migration + # If no version is specified, consumers should presume etcd 2.x + self.set_remote('connection_string', connection_string) + self.set_remote('version', version) diff --git a/etcd/hooks/relations/etcd/requires.py b/etcd/hooks/relations/etcd/requires.py new file mode 100644 index 0000000..435532f --- /dev/null +++ b/etcd/hooks/relations/etcd/requires.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# 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 + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdClient(RelationBase): + scope = scopes.GLOBAL + + @hook('{requires:etcd}-relation-{joined,changed}') + def changed(self): + ''' Indicate the relation is connected, and if the relation data is + set it is also available. ''' + self.set_state('{relation_name}.connected') + + if self.get_connection_string(): + self.set_state('{relation_name}.available') + # Get the ca, key, cert from the relation data. + cert = self.get_client_credentials() + # The tls state depends on the existance of the ca, key and cert. + if cert['client_cert'] and cert['client_key'] and cert['client_ca']: # noqa + self.set_state('{relation_name}.tls.available') + + @hook('{requires:etcd}-relation-{broken, departed}') + def broken(self): + ''' Indicate the relation is no longer available and not connected. ''' + self.remove_state('{relation_name}.available') + self.remove_state('{relation_name}.connected') + self.remove_state('{relation_name}.tls.available') + + def connection_string(self): + ''' This method is depreciated but ensures backward compatibility + @see get_connection_string(self). ''' + return self.get_connection_string() + + def get_connection_string(self): + ''' Return the connection string, if available, or None. ''' + return self.get_remote('connection_string') + + def get_version(self): + ''' Return the version of the etd protocol being used, or None. ''' + return self.get_remote('version') + + def get_client_credentials(self): + ''' Return a dict with the client certificate, ca and key to + communicate with etcd using tls. ''' + return {'client_cert': self.get_remote('client_cert'), + 'client_key': self.get_remote('client_key'), + 'client_ca': self.get_remote('client_ca')} + + def save_client_credentials(self, key, cert, ca): + ''' Save all the client certificates for etcd to local files. ''' + self._save_remote_data('client_cert', cert) + self._save_remote_data('client_key', key) + self._save_remote_data('client_ca', ca) + + def _save_remote_data(self, key, path): + ''' Save the remote data to a file indicated by path creating the + parent directory if needed.''' + value = self.get_remote(key) + if value: + parent = os.path.dirname(path) + if not os.path.isdir(parent): + os.makedirs(parent) + with open(path, 'w') as stream: + stream.write(value) diff --git a/etcd/hooks/relations/grafana-dashboard/.gitignore b/etcd/hooks/relations/grafana-dashboard/.gitignore new file mode 100644 index 0000000..01a6a44 --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/.gitignore @@ -0,0 +1,3 @@ +.docs +__pycache__ +*.pyc diff --git a/etcd/hooks/relations/grafana-dashboard/LICENSE b/etcd/hooks/relations/grafana-dashboard/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/LICENSE @@ -0,0 +1,202 @@ + + 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/etcd/hooks/relations/grafana-dashboard/README.md b/etcd/hooks/relations/grafana-dashboard/README.md new file mode 100644 index 0000000..b111350 --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/README.md @@ -0,0 +1,92 @@ +# Interface grafana-dashboard + +This is a [Juju][] interface layer that enables a charm which provides +dashboards to be imported into Grafana. + +You can download existing [Grafana Dashboards][] or use the [Grafana Dashboard +Reference][] to create your own. + +# Example Usage + +First, you must define the relation endpoint in your charm's `metadata.yaml`: + +```yaml +provides: + grafana: + interface: grafana-dashboard +``` + +Next, you must ensure the interface layer is included in your `layer.yaml`: + +```yaml +includes: + - interface:grafana-dashboard +``` + +Then, in your reactive code, add the following, modifying the dashboard data as +your charm needs: + +```python +import json +from charms.reactive import endpoint_from_flag + + +@when('endpoint.grafana.joined') +def register_grafana_dashboards(): + grafana = endpoint_from_flag('endpoint.grafana.joined') + for dashboard_file in Path('files/grafana').glob('*.json'): + dashboard = json.loads(dashboard_file.read_text()) + grafana.register_dashboard(name=dashboard_file.stem, + dashboard=dashboard) +``` + + + +# Reference + +* [common.md](common.md) + * [ImportRequest](docs/common.md#importrequest) + * [egress_subnets](docs/common.md#importrequest-egress_subnets) + * [ingress_address](docs/common.md#importrequest-ingress_address) + * [is_created](docs/common.md#importrequest-is_created) + * [is_received](docs/common.md#importrequest-is_received) + * [respond](docs/common.md#importrequest-respond) + * [ImportResponse](docs/common.md#importresponse) + * [name](docs/common.md#importresponse-name) +* [provides.md](provides.md) + * [GrafanaDashboardProvides](docs/provides.md#grafanadashboardprovides) + * [all_departed_units](docs/provides.md#grafanadashboardprovides-all_departed_units) + * [all_joined_units](docs/provides.md#grafanadashboardprovides-all_joined_units) + * [all_units](docs/provides.md#grafanadashboardprovides-all_units) + * [endpoint_name](docs/provides.md#grafanadashboardprovides-endpoint_name) + * [failed_imports](docs/provides.md#grafanadashboardprovides-failed_imports) + * [is_joined](docs/provides.md#grafanadashboardprovides-is_joined) + * [joined](docs/provides.md#grafanadashboardprovides-joined) + * [manage_flags](docs/provides.md#grafanadashboardprovides-manage_flags) + * [register_dashboard](docs/provides.md#grafanadashboardprovides-register_dashboard) + * [relations](docs/provides.md#grafanadashboardprovides-relations) + * [requests](docs/provides.md#grafanadashboardprovides-requests) + * [responses](docs/provides.md#grafanadashboardprovides-responses) +* [requires.md](requires.md) + * [GrafanaDashboardRequires](docs/requires.md#grafanadashboardrequires) + * [all_departed_units](docs/requires.md#grafanadashboardrequires-all_departed_units) + * [all_joined_units](docs/requires.md#grafanadashboardrequires-all_joined_units) + * [all_requests](docs/requires.md#grafanadashboardrequires-all_requests) + * [all_units](docs/requires.md#grafanadashboardrequires-all_units) + * [endpoint_name](docs/requires.md#grafanadashboardrequires-endpoint_name) + * [is_joined](docs/requires.md#grafanadashboardrequires-is_joined) + * [joined](docs/requires.md#grafanadashboardrequires-joined) + * [manage_flags](docs/requires.md#grafanadashboardrequires-manage_flags) + * [new_requests](docs/requires.md#grafanadashboardrequires-new_requests) + * [relations](docs/requires.md#grafanadashboardrequires-relations) + + + +# Contact Information + +Maintainer: Cory Johns <Cory.Johns@canonical.com> + + +[Juju]: https://jujucharms.com +[Grafana Dashboards]: https://grafana.com/grafana/dashboards +[Grafana Dashboard Reference]: https://grafana.com/docs/reference/dashboard/ diff --git a/etcd/hooks/relations/grafana-dashboard/__init__.py b/etcd/hooks/relations/grafana-dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/etcd/hooks/relations/grafana-dashboard/common.py b/etcd/hooks/relations/grafana-dashboard/common.py new file mode 100644 index 0000000..99db2d8 --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/common.py @@ -0,0 +1,38 @@ +from charms.reactive import BaseRequest, BaseResponse, Field + + +class ImportResponse(BaseResponse): + success = Field(description='Whether or not the import succeeded') + reason = Field(description='If failed, a description of why') + + @property + def name(self): + """ + The name given when the import was requested. + """ + return self.request.name + + +class ImportRequest(BaseRequest): + RESPONSE_CLASS = ImportResponse + + name = Field(description=""" + Name of the dashboard to import. Informational only, so that + you can tell which dashboard request this was, e.g. to check + for success or failure. + """) + + dashboard = Field(description=""" + Data structure defining the dashboard. Must be JSON + serializable. (Note: This should *not* be pre-serialized + JSON.) + """) + + def respond(self, success, reason=None): + """ + Acknowledge this request, and indicate success or failure with an + optional explanation. + """ + # wrap the base respond method to make the success field required and + # positional, as well as to provide a better doc string + super().respond(success=success, reason=reason) diff --git a/etcd/hooks/relations/grafana-dashboard/copyright b/etcd/hooks/relations/grafana-dashboard/copyright new file mode 100644 index 0000000..69768db --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2019, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/etcd/hooks/relations/grafana-dashboard/docs/common.md b/etcd/hooks/relations/grafana-dashboard/docs/common.md new file mode 100644 index 0000000..ab7de1d --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/docs/common.md @@ -0,0 +1,50 @@ +# `class ImportRequest(BaseRequest)` + +Base class for requests using the request / response pattern. + +Subclasses **must** set the ``RESPONSE_CLASS`` attribute to a subclass of +the :class:`BaseResponse` which defines the fields that the response will +use. They must also define additional attributes as :class:`Field`s. + +For example:: + + class TLSResponse(BaseResponse): + key = Field('Private key for the cert') + cert = Field('Public cert info') + + + class TLSRequest(BaseRequest): + RESPONSE_CLASS = TLSResponse + + common_name = Field('Common Name (CN) for the cert to be created') + sans = Field('List of Subject Alternative Names (SANs)') + +## `egress_subnets` + +Subnets over which network traffic to the requester will flow. + +## `ingress_address` + +Address to use if a connection to the requester is required. + +## `is_created` + +Whether this request was created by this side of the relation. + +## `is_received` + +Whether this request was received by the other side of the relation. + +## `def respond(self, success, reason=None)` + +Acknowledge this request, and indicate success or failure with an +optional explanation. + +# `class ImportResponse(BaseResponse)` + +Base class for responses using the request / response pattern. + +## `name` + +The name given when the import was requested. + diff --git a/etcd/hooks/relations/grafana-dashboard/docs/provides.md b/etcd/hooks/relations/grafana-dashboard/docs/provides.md new file mode 100644 index 0000000..cc1f3dc --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/docs/provides.md @@ -0,0 +1,120 @@ +# `class GrafanaDashboardProvides(RequesterEndpoint)` + +Base class for Endpoints that create requests in the request / response +pattern. + +Subclasses **must** set the ``REQUEST_CLASS`` attribute to a subclass +of :class:`BaseRequest` which defines the fields the request will use. + +## `all_departed_units` + +Collection of all units that were previously part of any relation on +this endpoint but which have since departed. + +This collection is persistent and mutable. The departed units will +be kept until they are explicitly removed, to allow for reasonable +cleanup of units that have left. + +Example: You need to run a command each time a unit departs the relation. + +.. code-block:: python + + @when('endpoint.{endpoint_name}.departed') + def handle_departed_unit(self): + for name, unit in self.all_departed_units.items(): + # run the command to remove `unit` from the cluster + # .. + self.all_departed_units.clear() + clear_flag(self.expand_name('departed')) + +Once a unit is departed, it will no longer show up in +:attr:`all_joined_units`. Note that units are considered departed as +soon as the departed hook is entered, which differs slightly from how +the Juju primitives behave (departing units are still returned from +``related-units`` until after the departed hook is complete). + +This collection is a :class:`KeyList`, so can be used as a mapping to +look up units by their unit name, or iterated or accessed by index. + +## `all_joined_units` + +A list view of all the units of all relations attached to this +:class:`~charms.reactive.endpoints.Endpoint`. + +This is actually a +:class:`~charms.reactive.endpoints.CombinedUnitsView`, so the units +will be in order by relation ID and then unit name, and you can access a +merged view of all the units' data as a single mapping. You should be +very careful when using the merged data collections, however, and +consider carefully what will happen when the endpoint has multiple +relations and multiple remote units on each. It is probably better to +iterate over each unit and handle its data individually. See +:class:`~charms.reactive.endpoints.CombinedUnitsView` for an +explanation of how the merged data collections work. + +Note that, because a given application might be related multiple times +on a given endpoint, units may show up in this collection more than +once. + +## `all_units` + +.. deprecated:: 0.6.1 + Use :attr:`all_joined_units` instead + +## `endpoint_name` + +Relation name of this endpoint. + +## `failed_imports` + +A list of requests that failed to import. + +## `is_joined` + +Whether this endpoint has remote applications attached to it. + +## `joined` + +.. deprecated:: 0.6.3 + Use :attr:`is_joined` instead + +## `def manage_flags(self)` + +Method that subclasses can override to perform any flag management +needed during startup. + +This will be called automatically after the framework-managed automatic +flags have been updated. + +## `def register_dashboard(self, name, dashboard)` + +Request a dashboard to be imported. + +:param name: Name of dashboard. Informational only, so that you can + tell which dashboard request this was, e.g. to check for success or + failure. +:param dashboard: Data structure defining the dashboard. Must be JSON + serializable. (Note: This should *not* be pre-serialized JSON.) + +## `relations` + +Collection of :class:`Relation` instances that are established for +this :class:`Endpoint`. + +This is a :class:`KeyList`, so it can be iterated and indexed as a list, +or you can look up relations by their ID. For example:: + + rel0 = endpoint.relations[0] + assert rel0 is endpoint.relations[rel0.relation_id] + assert all(rel is endpoint.relations[rel.relation_id] + for rel in endpoint.relations) + print(', '.join(endpoint.relations.keys())) + +## `requests` + +A list of all requests which have been submitted. + +## `responses` + +A list of all responses which have been received. + diff --git a/etcd/hooks/relations/grafana-dashboard/docs/requires.md b/etcd/hooks/relations/grafana-dashboard/docs/requires.md new file mode 100644 index 0000000..c84f1bc --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/docs/requires.md @@ -0,0 +1,109 @@ +# `class GrafanaDashboardRequires(ResponderEndpoint)` + +Base class for Endpoints that respond to requests in the request / response +pattern. + +Subclasses **must** set the ``REQUEST_CLASS`` attribute to a subclass +of :class:`BaseRequest` which defines the fields the request will use. + +## `all_departed_units` + +Collection of all units that were previously part of any relation on +this endpoint but which have since departed. + +This collection is persistent and mutable. The departed units will +be kept until they are explicitly removed, to allow for reasonable +cleanup of units that have left. + +Example: You need to run a command each time a unit departs the relation. + +.. code-block:: python + + @when('endpoint.{endpoint_name}.departed') + def handle_departed_unit(self): + for name, unit in self.all_departed_units.items(): + # run the command to remove `unit` from the cluster + # .. + self.all_departed_units.clear() + clear_flag(self.expand_name('departed')) + +Once a unit is departed, it will no longer show up in +:attr:`all_joined_units`. Note that units are considered departed as +soon as the departed hook is entered, which differs slightly from how +the Juju primitives behave (departing units are still returned from +``related-units`` until after the departed hook is complete). + +This collection is a :class:`KeyList`, so can be used as a mapping to +look up units by their unit name, or iterated or accessed by index. + +## `all_joined_units` + +A list view of all the units of all relations attached to this +:class:`~charms.reactive.endpoints.Endpoint`. + +This is actually a +:class:`~charms.reactive.endpoints.CombinedUnitsView`, so the units +will be in order by relation ID and then unit name, and you can access a +merged view of all the units' data as a single mapping. You should be +very careful when using the merged data collections, however, and +consider carefully what will happen when the endpoint has multiple +relations and multiple remote units on each. It is probably better to +iterate over each unit and handle its data individually. See +:class:`~charms.reactive.endpoints.CombinedUnitsView` for an +explanation of how the merged data collections work. + +Note that, because a given application might be related multiple times +on a given endpoint, units may show up in this collection more than +once. + +## `all_requests` + +A list of all requests, including ones which have been responded to. + +## `all_units` + +.. deprecated:: 0.6.1 + Use :attr:`all_joined_units` instead + +## `endpoint_name` + +Relation name of this endpoint. + +## `is_joined` + +Whether this endpoint has remote applications attached to it. + +## `joined` + +.. deprecated:: 0.6.3 + Use :attr:`is_joined` instead + +## `def manage_flags(self)` + +Method that subclasses can override to perform any flag management +needed during startup. + +This will be called automatically after the framework-managed automatic +flags have been updated. + +## `new_requests` + +A list of requests which have not been responded. + +Requests should be handled by the charm and then responded to by +calling ``request.respond(...)``. + +## `relations` + +Collection of :class:`Relation` instances that are established for +this :class:`Endpoint`. + +This is a :class:`KeyList`, so it can be iterated and indexed as a list, +or you can look up relations by their ID. For example:: + + rel0 = endpoint.relations[0] + assert rel0 is endpoint.relations[rel0.relation_id] + assert all(rel is endpoint.relations[rel.relation_id] + for rel in endpoint.relations) + print(', '.join(endpoint.relations.keys())) + diff --git a/etcd/hooks/relations/grafana-dashboard/interface.yaml b/etcd/hooks/relations/grafana-dashboard/interface.yaml new file mode 100644 index 0000000..0ee9ef8 --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/interface.yaml @@ -0,0 +1,6 @@ +name: grafana-dashboard +summary: Interface for importing dashboards into Grafana +version: 1 +maintainer: "Cory Johns " +exclude: + - .docs diff --git a/etcd/hooks/relations/grafana-dashboard/provides.py b/etcd/hooks/relations/grafana-dashboard/provides.py new file mode 100644 index 0000000..670ded9 --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/provides.py @@ -0,0 +1,42 @@ +from charms.reactive import ( + toggle_flag, + RequesterEndpoint, +) + +from .common import ImportRequest + + +class GrafanaDashboardProvides(RequesterEndpoint): + REQUEST_CLASS = ImportRequest + + def manage_flags(self): + super().manage_flags() + toggle_flag(self.expand_name('endpoint.{endpoint_name}.failed'), + self.is_joined and self.failed_imports) + + @property + def failed_imports(self): + """ + A list of requests that failed to import. + """ + return [response + for response in self.responses + if not response.success] + + def register_dashboard(self, name, dashboard): + """ + Request a dashboard to be imported. + + :param name: Name of dashboard. Informational only, so that you can + tell which dashboard request this was, e.g. to check for success or + failure. + :param dashboard: Data structure defining the dashboard. Must be JSON + serializable. (Note: This should *not* be pre-serialized JSON.) + """ + # we might be connected to multiple grafanas for some strange + # reason, so just send the dashboard to all of them + for relation in self.relations: + ImportRequest.create_or_update(match_fields=['name'], + relation=relation, + name=name, + dashboard=dashboard) diff --git a/etcd/hooks/relations/grafana-dashboard/requires.py b/etcd/hooks/relations/grafana-dashboard/requires.py new file mode 100644 index 0000000..de696c1 --- /dev/null +++ b/etcd/hooks/relations/grafana-dashboard/requires.py @@ -0,0 +1,15 @@ +from charms.reactive import ( + toggle_flag, + ResponderEndpoint, +) + +from .common import ImportRequest + + +class GrafanaDashboardRequires(ResponderEndpoint): + REQUEST_CLASS = ImportRequest + + def manage_flags(self): + super().manage_flags() + toggle_flag(self.expand_name('endpoint.{endpoint_name}.requests'), + self.is_joined and self.new_requests) diff --git a/etcd/hooks/relations/nrpe-external-master/README.md b/etcd/hooks/relations/nrpe-external-master/README.md new file mode 100644 index 0000000..e33deb8 --- /dev/null +++ b/etcd/hooks/relations/nrpe-external-master/README.md @@ -0,0 +1,66 @@ +# nrpe-external-master interface + +Use this interface to register nagios checks in your charm layers. + +## Purpose + +This interface is designed to interoperate with the +[nrpe-external-master](https://jujucharms.com/nrpe-external-master) subordinate charm. + +## How to use in your layers + +The event handler for `nrpe-external-master.available` is called with an object +through which you can register your own custom nagios checks, when a relation +is established with `nrpe-external-master:nrpe-external-master`. + +This object provides a method, + +_add_check_(args, name=_check_name_, description=_description_, context=_context_, unit=_unit_) + +which is called to register a nagios plugin check for your service. + +All arguments are required. + +*args* is a list of nagios plugin command line arguments, starting with the path to the plugin executable. + +*name* is the name of the check registered in nagios + +*description* is some text that describes what the check is for and what it does + +*context* is the nagios context name, something that identifies your application + +*unit* is `hookenv.local_unit()` + +The nrpe subordinate installs `check_http`, so you can use it like this: + +``` +@when('nrpe-external-master.available') +def setup_nagios(nagios): + config = hookenv.config() + unit_name = hookenv.local_unit() + nagios.add_check(['/usr/lib/nagios/plugins/check_http', + '-I', '127.0.0.1', '-p', str(config['port']), + '-e', " 200 OK", '-u', '/publickey'], + name="check_http", + description="Verify my awesome service is responding", + context=config["nagios_context"], + unit=unit_name, + ) +``` +If your `nagios.add_check` defines a custom plugin, you will also need to restart the `nagios-nrpe-server` service. + +Consult the nagios documentation for more information on [how to write your own +plugins](https://assets.nagios.com/downloads/nagioscore/docs/nagioscore/4/en/pluginapi.html) +or [find one](https://www.nagios.org/projects/nagios-plugins/) that does what you need. + +## Example deployment + +``` +$ juju deploy your-awesome-charm +$ juju deploy nrpe-external-master --config site-nagios.yaml +$ juju add-relation your-awesome-charm nrpe-external-master +``` + +where `site-nagios.yaml` has the necessary configuration settings for the +subordinate to connect to nagios. + diff --git a/etcd/hooks/relations/nrpe-external-master/__init__.py b/etcd/hooks/relations/nrpe-external-master/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/etcd/hooks/relations/nrpe-external-master/interface.yaml b/etcd/hooks/relations/nrpe-external-master/interface.yaml new file mode 100644 index 0000000..859a423 --- /dev/null +++ b/etcd/hooks/relations/nrpe-external-master/interface.yaml @@ -0,0 +1,3 @@ +name: nrpe-external-master +summary: Nagios interface +version: 1 diff --git a/etcd/hooks/relations/nrpe-external-master/provides.py b/etcd/hooks/relations/nrpe-external-master/provides.py new file mode 100644 index 0000000..b6c7f0d --- /dev/null +++ b/etcd/hooks/relations/nrpe-external-master/provides.py @@ -0,0 +1,91 @@ +import datetime +import os + +from charmhelpers.core import hookenv + +from charms.reactive import hook +from charms.reactive import RelationBase +from charms.reactive import scopes + + +class NrpeExternalMasterProvides(RelationBase): + scope = scopes.GLOBAL + + @hook('{provides:nrpe-external-master}-relation-{joined,changed}') + def changed_nrpe(self): + self.set_state('{relation_name}.available') + + @hook('{provides:nrpe-external-master}-relation-{broken,departed}') + def broken_nrpe(self): + self.remove_state('{relation_name}.available') + + def add_check(self, args, name=None, description=None, context=None, + servicegroups=None, unit=None): + nagios_files = self.get_local('nagios.check.files', []) + + if not unit: + unit = hookenv.local_unit() + unit = unit.replace('/', '-') + context = self.get_remote('nagios_host_context', context) + host_name = self.get_remote('nagios_hostname', + '%s-%s' % (context, unit)) + + check_tmpl = """ +#--------------------------------------------------- +# This file is Juju managed +#--------------------------------------------------- +command[%(check_name)s]=%(check_args)s +""" + service_tmpl = """ +#--------------------------------------------------- +# This file is Juju managed +#--------------------------------------------------- +define service { + use active-service + host_name %(host_name)s + service_description %(description)s + check_command check_nrpe!%(check_name)s + servicegroups %(servicegroups)s +} +""" + check_filename = "/etc/nagios/nrpe.d/check_%s.cfg" % (name) + with open(check_filename, "w") as fh: + fh.write(check_tmpl % { + 'check_args': ' '.join(args), + 'check_name': name, + }) + nagios_files.append(check_filename) + + service_filename = "/var/lib/nagios/export/service__%s_%s.cfg" % ( + unit, name) + with open(service_filename, "w") as fh: + fh.write(service_tmpl % { + 'servicegroups': servicegroups or context, + 'context': context, + 'description': description, + 'check_name': name, + 'host_name': host_name, + 'unit_name': unit, + }) + nagios_files.append(service_filename) + + self.set_local('nagios.check.files', nagios_files) + + def removed(self): + files = self.get_local('nagios.check.files', []) + for f in files: + try: + os.unlink(f) + except Exception as e: + hookenv.log("failed to remove %s: %s" % (f, e)) + self.set_local('nagios.check.files', []) + self.remove_state('{relation_name}.removed') + + def added(self): + self.updated() + + def updated(self): + relation_info = { + 'timestamp': datetime.datetime.now().isoformat(), + } + self.set_remote(**relation_info) diff --git a/etcd/hooks/relations/nrpe-external-master/requires.py b/etcd/hooks/relations/nrpe-external-master/requires.py new file mode 100644 index 0000000..e69de29 diff --git a/etcd/hooks/relations/prometheus-manual/.gitignore b/etcd/hooks/relations/prometheus-manual/.gitignore new file mode 100644 index 0000000..01a6a44 --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/.gitignore @@ -0,0 +1,3 @@ +.docs +__pycache__ +*.pyc diff --git a/etcd/hooks/relations/prometheus-manual/LICENSE b/etcd/hooks/relations/prometheus-manual/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/LICENSE @@ -0,0 +1,202 @@ + + 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/etcd/hooks/relations/prometheus-manual/README.md b/etcd/hooks/relations/prometheus-manual/README.md new file mode 100644 index 0000000..4ff5c83 --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/README.md @@ -0,0 +1,113 @@ +# Interface prometheus-manual + +This is a [Juju][] interface layer that enables a charm which provides manual +or raw metric scraper job configuration stanzas for Prometheus 2. + +The format for the job configuration data can be found in the [Prometheus +Configuration Docs][]. The job configuration will be included as an item +under `scrape_configs` largely unchanged, except for two things: + +* To ensure uniqueness, the provided job name will have a UUID appended to it. +* Because the CA cert must be written to disk separately from the config, any + `tls_config` sections will have their `ca_file` field values replaced with + the path to the file where the provided `ca_cert` data is written. + +# Example Usage + +First, you must define the relation endpoint in your charm's `metadata.yaml`: + +```yaml +provides: + prometheus: + interface: prometheus-manual +``` + +Next, you must ensure the interface layer is included in your `layer.yaml`: + +```yaml +includes: + - interface:prometheus-manual +``` + +Then, in your reactive code, add the following, modifying the job data as +your charm needs: + +```python +from charms.reactive import endpoint_from_flag + + +@when('endpoint.prometheus.joined', + 'tls.ca.available') +def register_prometheus_jobs(): + prometheus = endpoint_from_flag('endpoint.prometheus.joined') + tls = endpoint_from_flag('tls.ca.available') + prometheus.register_job(job_name='kubernetes-apiservers', + ca_cert=tls.root_ca_cert, + job_data={ + 'kubernetes_sd_configs': [{'role': 'endpoints'}], + 'scheme': 'https', + 'tls_config': {'ca_file': '__ca_file__'}, # placeholder for saved filename + 'bearer_token': get_token('system:prometheus'), + }) + prometheus.register_job(job_name='kubernetes-nodes', + ca_cert=tls.root_ca_cert, + job_data={ + 'kubernetes_sd_configs': [{'role': 'node'}], + 'scheme': 'https', + 'tls_config': {'ca_file': '__ca_file__'}, # placeholder for saved filename + 'bearer_token': get_token('system:prometheus'), + }) +``` + + + +# Reference + +* [common.md](common.md) + * [JobRequest](docs/common.md#jobrequest) + * [egress_subnets](docs/common.md#jobrequest-egress_subnets) + * [fromkeys](docs/common.md#jobrequest-fromkeys) + * [ingress_address](docs/common.md#jobrequest-ingress_address) + * [is_created](docs/common.md#jobrequest-is_created) + * [is_received](docs/common.md#jobrequest-is_received) + * [respond](docs/common.md#jobrequest-respond) + * [to_json](docs/common.md#jobrequest-to_json) + * [JobResponse](docs/common.md#jobresponse) + * [fromkeys](docs/common.md#jobresponse-fromkeys) +* [provides.md](provides.md) + * [PrometheusManualProvides](docs/provides.md#prometheusmanualprovides) + * [all_departed_units](docs/provides.md#prometheusmanualprovides-all_departed_units) + * [all_joined_units](docs/provides.md#prometheusmanualprovides-all_joined_units) + * [all_units](docs/provides.md#prometheusmanualprovides-all_units) + * [endpoint_name](docs/provides.md#prometheusmanualprovides-endpoint_name) + * [is_joined](docs/provides.md#prometheusmanualprovides-is_joined) + * [joined](docs/provides.md#prometheusmanualprovides-joined) + * [manage_flags](docs/provides.md#prometheusmanualprovides-manage_flags) + * [register_job](docs/provides.md#prometheusmanualprovides-register_job) + * [relations](docs/provides.md#prometheusmanualprovides-relations) + * [requests](docs/provides.md#prometheusmanualprovides-requests) + * [responses](docs/provides.md#prometheusmanualprovides-responses) +* [requires.md](requires.md) + * [PrometheusManualRequires](docs/requires.md#prometheusmanualrequires) + * [all_departed_units](docs/requires.md#prometheusmanualrequires-all_departed_units) + * [all_joined_units](docs/requires.md#prometheusmanualrequires-all_joined_units) + * [all_requests](docs/requires.md#prometheusmanualrequires-all_requests) + * [all_units](docs/requires.md#prometheusmanualrequires-all_units) + * [endpoint_name](docs/requires.md#prometheusmanualrequires-endpoint_name) + * [is_joined](docs/requires.md#prometheusmanualrequires-is_joined) + * [jobs](docs/requires.md#prometheusmanualrequires-jobs) + * [joined](docs/requires.md#prometheusmanualrequires-joined) + * [manage_flags](docs/requires.md#prometheusmanualrequires-manage_flags) + * [new_jobs](docs/requires.md#prometheusmanualrequires-new_jobs) + * [new_requests](docs/requires.md#prometheusmanualrequires-new_requests) + * [relations](docs/requires.md#prometheusmanualrequires-relations) + + + +# Contact Information + +Maintainer: Cory Johns <Cory.Johns@canonical.com> + + +[Juju]: https://jujucharms.com +[Prometheus Configuration Docs]: https://prometheus.io/docs/prometheus/latest/configuration/configuration/ diff --git a/etcd/hooks/relations/prometheus-manual/__init__.py b/etcd/hooks/relations/prometheus-manual/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/etcd/hooks/relations/prometheus-manual/common.py b/etcd/hooks/relations/prometheus-manual/common.py new file mode 100644 index 0000000..530f965 --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/common.py @@ -0,0 +1,57 @@ +import json +from copy import deepcopy + +from charms.reactive import BaseRequest, BaseResponse, Field + + +class JobResponse(BaseResponse): + success = Field('Whether or not the registration succeeded') + reason = Field('If failed, a description of why') + + +class JobRequest(BaseRequest): + RESPONSE_CLASS = JobResponse + + job_name = Field('Desired name for the job. To ensure uniqueness, the ' + 'the request ID will be appended to the final job name.') + + job_data = Field('Config data for the job.') + + ca_cert = Field('Cert data for the CA used to validate connections.') + + def to_json(self, ca_file=None): + """ + Render the job request to JSON string which can be included directly + into Prometheus config. + + Keys will be sorted in the rendering to ensure a stable ordering for + comparisons to detect changes. + + If `ca_file` is given, it will be used to replace the value of any + `ca_file` fields in the job. The charm should ensure that the + request's `ca_cert` data is writen to that path prior to calling this + method. + """ + job_data = deepcopy(self.job_data) # make a copy we can modify + job_data['job_name'] = '{}-{}'.format(self.job_name, self.request_id) + + if ca_file: + for key, value in job_data.items(): + # update the cert path at the job level + if key == 'tls_config': + value['ca_file'] = str(ca_file) + + # update the cert path at the SD config level + if key.endswith('_sd_configs'): + for sd_config in value: + if 'ca_file' in sd_config.get('tls_config', {}): + sd_config['tls_config']['ca_file'] = str(ca_file) + + return json.dumps(job_data, sort_keys=True) + + def respond(self, success, reason=None): + """ + Acknowledge this request, and indicate success or failure with an + optional explanation. + """ + super().respond(success=success, reason=reason) diff --git a/etcd/hooks/relations/prometheus-manual/copyright b/etcd/hooks/relations/prometheus-manual/copyright new file mode 100644 index 0000000..69768db --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2019, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/etcd/hooks/relations/prometheus-manual/docs/common.md b/etcd/hooks/relations/prometheus-manual/docs/common.md new file mode 100644 index 0000000..a97d54b --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/docs/common.md @@ -0,0 +1,62 @@ +# `class JobRequest(BaseRequest)` + +Base class for requests using the request / response pattern. + +Subclasses **must** set the ``RESPONSE_CLASS`` attribute to a subclass of +the :class:`BaseResponse` which defines the fields that the response will +use. They must also define additional attributes as :class:`Field`s. + +For example:: + + class TLSResponse(BaseResponse): + key = Field('Private key for the cert') + cert = Field('Public cert info') + + + class TLSRequest(BaseRequest): + RESPONSE_CLASS = TLSResponse + + common_name = Field('Common Name (CN) for the cert to be created') + sans = Field('List of Subject Alternative Names (SANs)') + +## `egress_subnets` + +Subnets over which network traffic to the requester will flow. + +## `None` + +Returns a new dict with keys from iterable and values equal to value. + +## `ingress_address` + +Address to use if a connection to the requester is required. + +## `is_created` + +Whether this request was created by this side of the relation. + +## `is_received` + +Whether this request was received by the other side of the relation. + +## `def respond(self, success, reason=None)` + +Acknowledge this request, and indicate success or failure with an +optional explanation. + +## `def to_json(self)` + +Render the job request to JSON string which can be included directly +into Prometheus config. + +Keys will be sorted in the rendering to ensure a stable ordering for +comparisons to detect changes. + +# `class JobResponse(BaseResponse)` + +Base class for responses using the request / response pattern. + +## `None` + +Returns a new dict with keys from iterable and values equal to value. + diff --git a/etcd/hooks/relations/prometheus-manual/docs/provides.md b/etcd/hooks/relations/prometheus-manual/docs/provides.md new file mode 100644 index 0000000..439b4e5 --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/docs/provides.md @@ -0,0 +1,119 @@ +# `class PrometheusManualProvides(RequesterEndpoint)` + +Base class for Endpoints that create requests in the request / response +pattern. + +Subclasses **must** set the ``REQUEST_CLASS`` attribute to a subclass +of :class:`BaseRequest` which defines the fields the request will use. + +## `all_departed_units` + +Collection of all units that were previously part of any relation on +this endpoint but which have since departed. + +This collection is persistent and mutable. The departed units will +be kept until they are explicitly removed, to allow for reasonable +cleanup of units that have left. + +Example: You need to run a command each time a unit departs the relation. + +.. code-block:: python + + @when('endpoint.{endpoint_name}.departed') + def handle_departed_unit(self): + for name, unit in self.all_departed_units.items(): + # run the command to remove `unit` from the cluster + # .. + self.all_departed_units.clear() + clear_flag(self.expand_name('departed')) + +Once a unit is departed, it will no longer show up in +:attr:`all_joined_units`. Note that units are considered departed as +soon as the departed hook is entered, which differs slightly from how +the Juju primitives behave (departing units are still returned from +``related-units`` until after the departed hook is complete). + +This collection is a :class:`KeyList`, so can be used as a mapping to +look up units by their unit name, or iterated or accessed by index. + +## `all_joined_units` + +A list view of all the units of all relations attached to this +:class:`~charms.reactive.endpoints.Endpoint`. + +This is actually a +:class:`~charms.reactive.endpoints.CombinedUnitsView`, so the units +will be in order by relation ID and then unit name, and you can access a +merged view of all the units' data as a single mapping. You should be +very careful when using the merged data collections, however, and +consider carefully what will happen when the endpoint has multiple +relations and multiple remote units on each. It is probably better to +iterate over each unit and handle its data individually. See +:class:`~charms.reactive.endpoints.CombinedUnitsView` for an +explanation of how the merged data collections work. + +Note that, because a given application might be related multiple times +on a given endpoint, units may show up in this collection more than +once. + +## `all_units` + +.. deprecated:: 0.6.1 + Use :attr:`all_joined_units` instead + +## `endpoint_name` + +Relation name of this endpoint. + +## `is_joined` + +Whether this endpoint has remote applications attached to it. + +## `joined` + +.. deprecated:: 0.6.3 + Use :attr:`is_joined` instead + +## `def manage_flags(self)` + +Method that subclasses can override to perform any flag management +needed during startup. + +This will be called automatically after the framework-managed automatic +flags have been updated. + +## `def register_job(self, job_name, job_data, ca_cert=None)` + +Register a manual job. + +The job data should be the (unserialized) data defining the job. + +To ensure uniqueness, a UUID will be added to the job name, and it will +be injected into the job data. + +If a CA cert is given, the value of any ca_file field in the job data +will be replaced with a filename after the CA cert data is written, so +a placeholder value should be used. + +## `relations` + +Collection of :class:`Relation` instances that are established for +this :class:`Endpoint`. + +This is a :class:`KeyList`, so it can be iterated and indexed as a list, +or you can look up relations by their ID. For example:: + + rel0 = endpoint.relations[0] + assert rel0 is endpoint.relations[rel0.relation_id] + assert all(rel is endpoint.relations[rel.relation_id] + for rel in endpoint.relations) + print(', '.join(endpoint.relations.keys())) + +## `requests` + +A list of all requests which have been submitted. + +## `responses` + +A list of all responses which have been received. + diff --git a/etcd/hooks/relations/prometheus-manual/docs/requires.md b/etcd/hooks/relations/prometheus-manual/docs/requires.md new file mode 100644 index 0000000..31a7e8f --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/docs/requires.md @@ -0,0 +1,117 @@ +# `class PrometheusManualRequires(ResponderEndpoint)` + +Base class for Endpoints that respond to requests in the request / response +pattern. + +Subclasses **must** set the ``REQUEST_CLASS`` attribute to a subclass +of :class:`BaseRequest` which defines the fields the request will use. + +## `all_departed_units` + +Collection of all units that were previously part of any relation on +this endpoint but which have since departed. + +This collection is persistent and mutable. The departed units will +be kept until they are explicitly removed, to allow for reasonable +cleanup of units that have left. + +Example: You need to run a command each time a unit departs the relation. + +.. code-block:: python + + @when('endpoint.{endpoint_name}.departed') + def handle_departed_unit(self): + for name, unit in self.all_departed_units.items(): + # run the command to remove `unit` from the cluster + # .. + self.all_departed_units.clear() + clear_flag(self.expand_name('departed')) + +Once a unit is departed, it will no longer show up in +:attr:`all_joined_units`. Note that units are considered departed as +soon as the departed hook is entered, which differs slightly from how +the Juju primitives behave (departing units are still returned from +``related-units`` until after the departed hook is complete). + +This collection is a :class:`KeyList`, so can be used as a mapping to +look up units by their unit name, or iterated or accessed by index. + +## `all_joined_units` + +A list view of all the units of all relations attached to this +:class:`~charms.reactive.endpoints.Endpoint`. + +This is actually a +:class:`~charms.reactive.endpoints.CombinedUnitsView`, so the units +will be in order by relation ID and then unit name, and you can access a +merged view of all the units' data as a single mapping. You should be +very careful when using the merged data collections, however, and +consider carefully what will happen when the endpoint has multiple +relations and multiple remote units on each. It is probably better to +iterate over each unit and handle its data individually. See +:class:`~charms.reactive.endpoints.CombinedUnitsView` for an +explanation of how the merged data collections work. + +Note that, because a given application might be related multiple times +on a given endpoint, units may show up in this collection more than +once. + +## `all_requests` + +A list of all requests, including ones which have been responded to. + +## `all_units` + +.. deprecated:: 0.6.1 + Use :attr:`all_joined_units` instead + +## `endpoint_name` + +Relation name of this endpoint. + +## `is_joined` + +Whether this endpoint has remote applications attached to it. + +## `jobs` + +Return a list of all jobs to be registered. + +## `joined` + +.. deprecated:: 0.6.3 + Use :attr:`is_joined` instead + +## `def manage_flags(self)` + +Method that subclasses can override to perform any flag management +needed during startup. + +This will be called automatically after the framework-managed automatic +flags have been updated. + +## `new_jobs` + +Return a list of new jobs to be registered. + +## `new_requests` + +A list of requests which have not been responded. + +Requests should be handled by the charm and then responded to by +calling ``request.respond(...)``. + +## `relations` + +Collection of :class:`Relation` instances that are established for +this :class:`Endpoint`. + +This is a :class:`KeyList`, so it can be iterated and indexed as a list, +or you can look up relations by their ID. For example:: + + rel0 = endpoint.relations[0] + assert rel0 is endpoint.relations[rel0.relation_id] + assert all(rel is endpoint.relations[rel.relation_id] + for rel in endpoint.relations) + print(', '.join(endpoint.relations.keys())) + diff --git a/etcd/hooks/relations/prometheus-manual/interface.yaml b/etcd/hooks/relations/prometheus-manual/interface.yaml new file mode 100644 index 0000000..5c324c6 --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/interface.yaml @@ -0,0 +1,6 @@ +name: prometheus-manual +summary: Interface for registering manual job definitions with Prometheus +version: 1 +maintainer: "Cory Johns " +exclude: + - .docs diff --git a/etcd/hooks/relations/prometheus-manual/provides.py b/etcd/hooks/relations/prometheus-manual/provides.py new file mode 100644 index 0000000..884629c --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/provides.py @@ -0,0 +1,41 @@ +from charms.reactive import ( + toggle_flag, + RequesterEndpoint, +) + +from .common import JobRequest + + +class PrometheusManualProvides(RequesterEndpoint): + REQUEST_CLASS = JobRequest + + def manage_flags(self): + super().manage_flags() + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined and self.requests) + + def register_job(self, job_name, job_data, ca_cert=None, relation=None): + """ + Register a manual job. + + The job data should be the (unserialized) data defining the job. + + To ensure uniqueness, a UUID will be added to the job name, and it will + be injected into the job data. + + If a CA cert is given, the value of any ca_file field in the job data + will be replaced with a filename after the CA cert data is written, so + a placeholder value should be used. + + If a specific relation is not given, the job will be registered with + every related Prometheus. + """ + # we might be connected to multiple prometheuses for some strange + # reason, so just send the job to all of them + relations = [relation] if relation is not None else self.relations + for relation in relations: + JobRequest.create_or_update(match_fields=['job_name'], + relation=relation, + job_name=job_name, + job_data=job_data, + ca_cert=ca_cert) diff --git a/etcd/hooks/relations/prometheus-manual/requires.py b/etcd/hooks/relations/prometheus-manual/requires.py new file mode 100644 index 0000000..a8d1acb --- /dev/null +++ b/etcd/hooks/relations/prometheus-manual/requires.py @@ -0,0 +1,31 @@ +from charms.reactive import ( + toggle_flag, + ResponderEndpoint, +) + +from .common import JobRequest + + +class PrometheusManualRequires(ResponderEndpoint): + REQUEST_CLASS = JobRequest + + def manage_flags(self): + super().manage_flags() + toggle_flag(self.expand_name('endpoint.{endpoint_name}.has_jobs'), + self.is_joined and self.jobs) + toggle_flag(self.expand_name('endpoint.{endpoint_name}.new_jobs'), + self.is_joined and self.new_jobs) + + @property + def jobs(self): + """ + Return a list of all jobs to be registered. + """ + return self.all_requests + + @property + def new_jobs(self): + """ + Return a list of new jobs to be registered. + """ + return self.new_requests diff --git a/etcd/hooks/relations/tls-certificates/.gitignore b/etcd/hooks/relations/tls-certificates/.gitignore new file mode 100644 index 0000000..93813bc --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/.gitignore @@ -0,0 +1,4 @@ +.tox +__pycache__ +*.pyc +_build diff --git a/etcd/hooks/relations/tls-certificates/README.md b/etcd/hooks/relations/tls-certificates/README.md new file mode 100644 index 0000000..733da6d --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/README.md @@ -0,0 +1,90 @@ +# Interface tls-certificates + +This is a [Juju][] interface layer that enables a charm which requires TLS +certificates to relate to a charm which can provide them, such as [Vault][] or +[EasyRSA][] + +To get started please read the [Introduction to PKI][] which defines some PKI +terms, concepts and processes used in this document. + +# Example Usage + +Let's say you have a charm which needs a server certificate for a service it +provides to other charms and a client certificate for a database it consumes +from another charm. The charm provides its own service on the `clients` +relation endpoint, and it consumes the database on the `db` relation endpoint. + +First, you must define the relation endpoint in your charm's `metadata.yaml`: + +```yaml +requires: + cert-provider: + interface: tls-certificates +``` + +Next, you must ensure the interface layer is included in your `layer.yaml`: + +```yaml +includes: + - interface:tls-certificates +``` + +Then, in your reactive code, add the following, changing `update_certs` to +handle the certificates however your charm needs: + +```python +from charmhelpers.core import hookenv, host +from charms.reactive import endpoint_from_flag + + +@when('cert-provider.ca.changed') +def install_root_ca_cert(): + cert_provider = endpoint_from_flag('cert-provider.ca.available') + host.install_ca_cert(cert_provider.root_ca_cert) + clear_flag('cert-provider.ca.changed') + + +@when('cert-provider.available') +def request_certificates(): + cert_provider = endpoint_from_flag('cert-provider.available') + + # get ingress info + ingress_for_clients = hookenv.network_get('clients')['ingress-addresses'] + ingress_for_db = hookenv.network_get('db')['ingress-addresses'] + + # use first ingress address as primary and any additional as SANs + server_cn, server_sans = ingress_for_clients[0], ingress_for_clients[:1] + client_cn, client_sans = ingress_for_db[0], ingress_for_db[:1] + + # request a single server and single client cert; note that multiple certs + # of either type can be requested as long as they have unique common names + cert_provider.request_server_cert(server_cn, server_sans) + cert_provider.request_client_cert(client_cn, client_sans) + + +@when('cert-provider.certs.changed') +def update_certs(): + cert_provider = endpoint_from_flag('cert-provider.available') + server_cert = cert_provider.server_certs[0] # only requested one + myserver.update_server_cert(server_cert.cert, server_cert.key) + + client_cert = cert_provider.client_certs[0] # only requested one + myclient.update_client_cert(client_cert.cert, client_cert.key) + clear_flag('cert-provider.certs.changed') +``` + + +# Reference + + * [Requires](docs/requires.md) + * [Provides](docs/provides.md) + +# Contact Information + +Maintainer: Cory Johns <Cory.Johns@canonical.com> + + +[Juju]: https://jujucharms.com +[Vault]: https://jujucharms.com/u/openstack-charmers/vault +[EasyRSA]: https://jujucharms.com/u/containers/easyrsa +[Introduction to PKI]: https://github.com/OpenVPN/easy-rsa/blob/master/doc/Intro-To-PKI.md diff --git a/etcd/hooks/relations/tls-certificates/__init__.py b/etcd/hooks/relations/tls-certificates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/etcd/hooks/relations/tls-certificates/docs/common.md b/etcd/hooks/relations/tls-certificates/docs/common.md new file mode 100644 index 0000000..25d0e08 --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/docs/common.md @@ -0,0 +1,51 @@ +

CertificateRequest

+ +```python +CertificateRequest(self, unit, cert_type, cert_name, common_name, sans) +``` + +

application_name

+ +Name of the application which the request came from. + +:returns: Name of application +:rtype: str + +

cert

+ + +The cert published for this request, if any. + +

cert_type

+ + +Type of certificate, 'server' or 'client', being requested. + +

resolve_unit_name

+ +```python +CertificateRequest.resolve_unit_name(unit) +``` +Return name of unit associated with this request. + +unit_name should be provided in the relation data to ensure +compatability with cross-model relations. If the unit name +is absent then fall back to unit_name attribute of the +unit associated with this request. + +:param unit: Unit to extract name from +:type unit: charms.reactive.endpoints.RelatedUnit +:returns: Name of unit +:rtype: str + +

Certificate

+ +```python +Certificate(self, cert_type, common_name, cert, key) +``` + +Represents a created certificate and key. + +The ``cert_type``, ``common_name``, ``cert``, and ``key`` values can +be accessed either as properties or as the contents of the dict. + diff --git a/etcd/hooks/relations/tls-certificates/docs/provides.md b/etcd/hooks/relations/tls-certificates/docs/provides.md new file mode 100644 index 0000000..c213546 --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/docs/provides.md @@ -0,0 +1,212 @@ +

provides

+ + +

TlsProvides

+ +```python +TlsProvides(self, endpoint_name, relation_ids=None) +``` + +The provider's side of the interface protocol. + +The following flags may be set: + + * `{endpoint_name}.available` + Whenever any clients are joined. + + * `{endpoint_name}.certs.requested` + When there are new certificate requests of any kind to be processed. + The requests can be accessed via [new_requests][]. + + * `{endpoint_name}.server.certs.requested` + When there are new server certificate requests to be processed. + The requests can be accessed via [new_server_requests][]. + + * `{endpoint_name}.client.certs.requested` + When there are new client certificate requests to be processed. + The requests can be accessed via [new_client_requests][]. + +[Certificate]: common.md#tls_certificates_common.Certificate +[CertificateRequest]: common.md#tls_certificates_common.CertificateRequest +[all_requests]: provides.md#provides.TlsProvides.all_requests +[new_requests]: provides.md#provides.TlsProvides.new_requests +[new_server_requests]: provides.md#provides.TlsProvides.new_server_requests +[new_client_requests]: provides.md#provides.TlsProvides.new_client_requests + +

all_published_certs

+ + +List of all [Certificate][] instances that this provider has published +for all related applications. + +

all_requests

+ + +List of all requests that have been made. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('certs.regen', + 'tls.certs.available') +def regen_all_certs(): + tls = endpoint_from_flag('tls.certs.available') + for request in tls.all_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_application_requests

+ + +Filtered view of [new_requests][] that only includes application cert +requests. + +Each will be an instance of [ApplicationCertificateRequest][]. + +Example usage: + +```python +@when('tls.application.certs.requested') +def gen_application_certs(): + tls = endpoint_from_flag('tls.application.certs.requested') + for request in tls.new_application_requests: + cert, key = generate_application_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_client_requests

+ + +Filtered view of [new_requests][] that only includes client cert +requests. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('tls.client.certs.requested') +def gen_client_certs(): + tls = endpoint_from_flag('tls.client.certs.requested') + for request in tls.new_client_requests: + cert, key = generate_client_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_requests

+ + +Filtered view of [all_requests][] that only includes requests that +haven't been handled. + +Each will be an instance of [CertificateRequest][]. + +This collection can also be further filtered by request type using +[new_server_requests][] or [new_client_requests][]. + +Example usage: + +```python +@when('tls.certs.requested') +def gen_certs(): + tls = endpoint_from_flag('tls.certs.requested') + for request in tls.new_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_server_requests

+ + +Filtered view of [new_requests][] that only includes server cert +requests. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('tls.server.certs.requested') +def gen_server_certs(): + tls = endpoint_from_flag('tls.server.certs.requested') + for request in tls.new_server_requests: + cert, key = generate_server_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

set_ca

+ +```python +TlsProvides.set_ca(certificate_authority) +``` + +Publish the CA to all related applications. + +

set_chain

+ +```python +TlsProvides.set_chain(chain) +``` + +Publish the chain of trust to all related applications. + +

set_client_cert

+ +```python +TlsProvides.set_client_cert(cert, key) +``` + +Deprecated. This is only for backwards compatibility. + +Publish a globally shared client cert and key. + +

set_server_cert

+ +```python +TlsProvides.set_server_cert(scope, cert, key) +``` + +Deprecated. Use one of the [new_requests][] collections and +`request.set_cert()` instead. + +Set the server cert and key for the request identified by `scope`. + +

set_server_multicerts

+ +```python +TlsProvides.set_server_multicerts(scope) +``` + +Deprecated. Done automatically. + +

add_server_cert

+ +```python +TlsProvides.add_server_cert(scope, cn, cert, key) +``` + +Deprecated. Use `request.set_cert()` instead. + +

get_server_requests

+ +```python +TlsProvides.get_server_requests() +``` + +Deprecated. Use the [new_requests][] or [server_requests][] +collections instead. + +One provider can have many requests to generate server certificates. +Return a map of all server request objects indexed by a unique +identifier. + diff --git a/etcd/hooks/relations/tls-certificates/docs/requires.md b/etcd/hooks/relations/tls-certificates/docs/requires.md new file mode 100644 index 0000000..fdec902 --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/docs/requires.md @@ -0,0 +1,207 @@ +

requires

+ + +

TlsRequires

+ +```python +TlsRequires(self, endpoint_name, relation_ids=None) +``` + +The client's side of the interface protocol. + +The following flags may be set: + + * `{endpoint_name}.available` + Whenever the relation is joined. + + * `{endpoint_name}.ca.available` + When the root CA information is available via the [root_ca_cert][] and + [root_ca_chain][] properties. + + * `{endpoint_name}.ca.changed` + When the root CA information has changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.certs.available` + When the requested server or client certs are available. + + * `{endpoint_name}.certs.changed` + When the requested server or client certs have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.server.certs.available` + When the server certificates requested by [request_server_cert][] are + available via the [server_certs][] collection. + + * `{endpoint_name}.server.certs.changed` + When the requested server certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.client.certs.available` + When the client certificates requested by [request_client_cert][] are + available via the [client_certs][] collection. + + * `{endpoint_name}.client.certs.changed` + When the requested client certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + +The following flags have been deprecated: + + * `{endpoint_name}.server.cert.available` + * `{endpoint_name}.client.cert.available` + * `{endpoint_name}.batch.cert.available` + +[Certificate]: common.md#tls_certificates_common.Certificate +[CertificateRequest]: common.md#tls_certificates_common.CertificateRequest +[root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert +[root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain +[request_server_cert]: requires.md#requires.TlsRequires.request_server_cert +[request_client_cert]: requires.md#requires.TlsRequires.request_client_cert +[server_certs]: requires.md#requires.TlsRequires.server_certs +[server_certs_map]: requires.md#requires.TlsRequires.server_certs_map +[client_certs]: requires.md#requires.TlsRequires.server_certs + +

application_certs

+ + +List of [Certificate][] instances for all available application certs. + +

client_certs

+ + +List of [Certificate][] instances for all available client certs. + +

client_certs_map

+ + +Mapping of client [Certificate][] instances by their `common_name`. + +

root_ca_cert

+ + +Root CA certificate. + +

root_ca_chain

+ + +The chain of trust for the root CA. + +

server_certs

+ + +List of [Certificate][] instances for all available server certs. + +

server_certs_map

+ + +Mapping of server [Certificate][] instances by their `common_name`. + +

get_ca

+ +```python +TlsRequires.get_ca() +``` + +Return the root CA certificate. + +Same as [root_ca_cert][]. + +

get_chain

+ +```python +TlsRequires.get_chain() +``` + +Return the chain of trust for the root CA. + +Same as [root_ca_chain][]. + +

get_client_cert

+ +```python +TlsRequires.get_client_cert() +``` + +Deprecated. Use [request_client_cert][] and the [client_certs][] +collection instead. + +Return a globally shared client certificate and key. + +

get_server_cert

+ +```python +TlsRequires.get_server_cert() +``` + +Deprecated. Use the [server_certs][] collection instead. + +Return the cert and key of the first server certificate requested. + +

get_batch_requests

+ +```python +TlsRequires.get_batch_requests() +``` + +Deprecated. Use [server_certs_map][] instead. + +Mapping of server [Certificate][] instances by their `common_name`. + +

request_server_cert

+ +```python +TlsRequires.request_server_cert(cn, sans=None, cert_name=None) +``` + +Request a server certificate and key be generated for the given +common name (`cn`) and optional list of alternative names (`sans`). + +The `cert_name` is deprecated and not needed. + +This can be called multiple times to request more than one server +certificate, although the common names must be unique. If called +again with the same common name, it will be ignored. + +

add_request_server_cert

+ +```python +TlsRequires.add_request_server_cert(cn, sans) +``` + +Deprecated. Use [request_server_cert][] instead. + +

request_server_certs

+ +```python +TlsRequires.request_server_certs() +``` + +Deprecated. Just use [request_server_cert][]; this does nothing. + +

request_client_cert

+ +```python +TlsRequires.request_client_cert(cn, sans) +``` + +Request a client certificate and key be generated for the given +common name (`cn`) and list of alternative names (`sans`). + +This can be called multiple times to request more than one client +certificate, although the common names must be unique. If called +again with the same common name, it will be ignored. + +

request_application_cert

+ +```python +TlsRequires.request_application_cert(cn, sans) +``` + +Request an application certificate and key be generated for the given +common name (`cn`) and list of alternative names (`sans` ) of this +unit and all peer units. All units will share a single certificates. + diff --git a/etcd/hooks/relations/tls-certificates/interface.yaml b/etcd/hooks/relations/tls-certificates/interface.yaml new file mode 100644 index 0000000..beec53b --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/interface.yaml @@ -0,0 +1,6 @@ +name: tls-certificates +summary: | + A Transport Layer Security (TLS) charm layer that uses requires and provides + to exchange certifcates. +version: 1 +repo: https://github.com/juju-solutions/interface-tls-certificates diff --git a/etcd/hooks/relations/tls-certificates/make_docs b/etcd/hooks/relations/tls-certificates/make_docs new file mode 100644 index 0000000..2f2274a --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/make_docs @@ -0,0 +1,23 @@ +#!.tox/py3/bin/python + +import sys +import importlib +from pathlib import Path +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'cert': {'interface': 'tls-certificates'}}, + 'provides': {'cert': {'interface': 'tls-certificates'}}, + } + sys.path.append('..') + sys.modules[''] = importlib.import_module(Path.cwd().name) + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/etcd/hooks/relations/tls-certificates/provides.py b/etcd/hooks/relations/tls-certificates/provides.py new file mode 100644 index 0000000..0262baa --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/provides.py @@ -0,0 +1,301 @@ +if not __package__: + # fix relative imports when building docs + import sys + __package__ = sys.modules[''].__name__ + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag + +from .tls_certificates_common import ( + ApplicationCertificateRequest, + CertificateRequest +) + + +class TlsProvides(Endpoint): + """ + The provider's side of the interface protocol. + + The following flags may be set: + + * `{endpoint_name}.available` + Whenever any clients are joined. + + * `{endpoint_name}.certs.requested` + When there are new certificate requests of any kind to be processed. + The requests can be accessed via [new_requests][]. + + * `{endpoint_name}.server.certs.requested` + When there are new server certificate requests to be processed. + The requests can be accessed via [new_server_requests][]. + + * `{endpoint_name}.client.certs.requested` + When there are new client certificate requests to be processed. + The requests can be accessed via [new_client_requests][]. + + [Certificate]: common.md#tls_certificates_common.Certificate + [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest + [all_requests]: provides.md#provides.TlsProvides.all_requests + [new_requests]: provides.md#provides.TlsProvides.new_requests + [new_server_requests]: provides.md#provides.TlsProvides.new_server_requests + [new_client_requests]: provides.md#provides.TlsProvides.new_client_requests + """ + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('{endpoint_name}.available')) + toggle_flag(self.expand_name('{endpoint_name}.certs.requested'), + self.new_requests) + toggle_flag(self.expand_name('{endpoint_name}.server.certs.requested'), + self.new_server_requests) + toggle_flag(self.expand_name('{endpoint_name}.client.certs.requested'), + self.new_client_requests) + toggle_flag( + self.expand_name('{endpoint_name}.application.certs.requested'), + self.new_application_requests) + # For backwards compatibility, set the old "cert" flags as well + toggle_flag(self.expand_name('{endpoint_name}.server.cert.requested'), + self.new_server_requests) + toggle_flag(self.expand_name('{endpoint_name}.client.cert.requested'), + self.new_client_requests) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + clear_flag(self.expand_name('{endpoint_name}.certs.requested')) + clear_flag(self.expand_name('{endpoint_name}.server.certs.requested')) + clear_flag(self.expand_name('{endpoint_name}.client.certs.requested')) + clear_flag( + self.expand_name('{endpoint_name}.application.certs.requested')) + + def set_ca(self, certificate_authority): + """ + Publish the CA to all related applications. + """ + for relation in self.relations: + # All the clients get the same CA, so send it to them. + relation.to_publish_raw['ca'] = certificate_authority + + def set_chain(self, chain): + """ + Publish the chain of trust to all related applications. + """ + for relation in self.relations: + # All the clients get the same chain, so send it to them. + relation.to_publish_raw['chain'] = chain + + def set_client_cert(self, cert, key): + """ + Deprecated. This is only for backwards compatibility. + + Publish a globally shared client cert and key. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'client.cert': cert, + 'client.key': key, + }) + + def set_server_cert(self, scope, cert, key): + """ + Deprecated. Use one of the [new_requests][] collections and + `request.set_cert()` instead. + + Set the server cert and key for the request identified by `scope`. + """ + request = self.get_server_requests()[scope] + request.set_cert(cert, key) + + def set_server_multicerts(self, scope): + """ + Deprecated. Done automatically. + """ + pass + + def add_server_cert(self, scope, cn, cert, key): + ''' + Deprecated. Use `request.set_cert()` instead. + ''' + self.set_server_cert(scope, cert, key) + + def get_server_requests(self): + """ + Deprecated. Use the [new_requests][] or [server_requests][] + collections instead. + + One provider can have many requests to generate server certificates. + Return a map of all server request objects indexed by a unique + identifier. + """ + return {req._key: req for req in self.new_server_requests} + + @property + def all_requests(self): + """ + List of all requests that have been made. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('certs.regen', + 'tls.certs.available') + def regen_all_certs(): + tls = endpoint_from_flag('tls.certs.available') + for request in tls.all_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + requests = [] + for unit in self.all_joined_units: + # handle older single server cert request + if unit.received_raw['common_name']: + requests.append(CertificateRequest( + unit, + 'server', + unit.received_raw['certificate_name'], + unit.received_raw['common_name'], + unit.received['sans'], + )) + + # handle mutli server cert requests + reqs = unit.received['cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(CertificateRequest( + unit, + 'server', + common_name, + common_name, + req['sans'], + )) + + # handle client cert requests + reqs = unit.received['client_cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(CertificateRequest( + unit, + 'client', + common_name, + common_name, + req['sans'], + )) + # handle application cert requests + reqs = unit.received['application_cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(ApplicationCertificateRequest( + unit, + 'application', + common_name, + common_name, + req['sans'] + )) + return requests + + @property + def new_requests(self): + """ + Filtered view of [all_requests][] that only includes requests that + haven't been handled. + + Each will be an instance of [CertificateRequest][]. + + This collection can also be further filtered by request type using + [new_server_requests][] or [new_client_requests][]. + + Example usage: + + ```python + @when('tls.certs.requested') + def gen_certs(): + tls = endpoint_from_flag('tls.certs.requested') + for request in tls.new_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.all_requests if not req.is_handled] + + @property + def new_server_requests(self): + """ + Filtered view of [new_requests][] that only includes server cert + requests. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('tls.server.certs.requested') + def gen_server_certs(): + tls = endpoint_from_flag('tls.server.certs.requested') + for request in tls.new_server_requests: + cert, key = generate_server_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.new_requests if req.cert_type == 'server'] + + @property + def new_client_requests(self): + """ + Filtered view of [new_requests][] that only includes client cert + requests. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('tls.client.certs.requested') + def gen_client_certs(): + tls = endpoint_from_flag('tls.client.certs.requested') + for request in tls.new_client_requests: + cert, key = generate_client_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.new_requests if req.cert_type == 'client'] + + @property + def new_application_requests(self): + """ + Filtered view of [new_requests][] that only includes application cert + requests. + + Each will be an instance of [ApplicationCertificateRequest][]. + + Example usage: + + ```python + @when('tls.application.certs.requested') + def gen_application_certs(): + tls = endpoint_from_flag('tls.application.certs.requested') + for request in tls.new_application_requests: + cert, key = generate_application_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + + :returns: List of certificate requests. + :rtype: [CertificateRequest, ] + """ + return [req for req in self.new_requests + if req.cert_type == 'application'] + + @property + def all_published_certs(self): + """ + List of all [Certificate][] instances that this provider has published + for all related applications. + """ + return [req.cert for req in self.all_requests if req.cert] diff --git a/etcd/hooks/relations/tls-certificates/pydocmd.yml b/etcd/hooks/relations/tls-certificates/pydocmd.yml new file mode 100644 index 0000000..c568913 --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/pydocmd.yml @@ -0,0 +1,19 @@ +site_name: 'TLS Certificates Interface' + +generate: + - requires.md: + - requires + - requires.TlsRequires+ + - provides.md: + - provides + - provides.TlsProvides+ + - common.md: + - tls_certificates_common.CertificateRequest+ + - tls_certificates_common.Certificate+ + +pages: + - Requires: requires.md + - Provides: provides.md + - Common: common.md + +gens_dir: docs diff --git a/etcd/hooks/relations/tls-certificates/requires.py b/etcd/hooks/relations/tls-certificates/requires.py new file mode 100644 index 0000000..951f953 --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/requires.py @@ -0,0 +1,342 @@ +if not __package__: + # fix relative imports when building docs + import sys + __package__ = sys.modules[''].__name__ + +import uuid + +from charmhelpers.core import hookenv + +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag +from charms.reactive import Endpoint +from charms.reactive import data_changed + +from .tls_certificates_common import Certificate + + +class TlsRequires(Endpoint): + """ + The client's side of the interface protocol. + + The following flags may be set: + + * `{endpoint_name}.available` + Whenever the relation is joined. + + * `{endpoint_name}.ca.available` + When the root CA information is available via the [root_ca_cert][] and + [root_ca_chain][] properties. + + * `{endpoint_name}.ca.changed` + When the root CA information has changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.certs.available` + When the requested server or client certs are available. + + * `{endpoint_name}.certs.changed` + When the requested server or client certs have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.server.certs.available` + When the server certificates requested by [request_server_cert][] are + available via the [server_certs][] collection. + + * `{endpoint_name}.server.certs.changed` + When the requested server certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.client.certs.available` + When the client certificates requested by [request_client_cert][] are + available via the [client_certs][] collection. + + * `{endpoint_name}.client.certs.changed` + When the requested client certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + The following flags have been deprecated: + + * `{endpoint_name}.server.cert.available` + * `{endpoint_name}.client.cert.available` + * `{endpoint_name}.batch.cert.available` + + [Certificate]: common.md#tls_certificates_common.Certificate + [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest + [root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert + [root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain + [request_server_cert]: requires.md#requires.TlsRequires.request_server_cert + [request_client_cert]: requires.md#requires.TlsRequires.request_client_cert + [server_certs]: requires.md#requires.TlsRequires.server_certs + [server_certs_map]: requires.md#requires.TlsRequires.server_certs_map + [client_certs]: requires.md#requires.TlsRequires.server_certs + """ + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + self.relations[0].to_publish_raw['unit_name'] = self._unit_name + prefix = self.expand_name('{endpoint_name}.') + ca_available = self.root_ca_cert + ca_changed = ca_available and data_changed(prefix + 'ca', + self.root_ca_cert) + server_available = self.server_certs + server_changed = server_available and data_changed(prefix + 'servers', + self.server_certs) + client_available = self.client_certs + client_changed = client_available and data_changed(prefix + 'clients', + self.client_certs) + certs_available = server_available or client_available + certs_changed = server_changed or client_changed + + set_flag(prefix + 'available') + toggle_flag(prefix + 'ca.available', ca_available) + toggle_flag(prefix + 'ca.changed', ca_changed) + toggle_flag(prefix + 'server.certs.available', server_available) + toggle_flag(prefix + 'server.certs.changed', server_changed) + toggle_flag(prefix + 'client.certs.available', client_available) + toggle_flag(prefix + 'client.certs.changed', client_changed) + toggle_flag(prefix + 'certs.available', certs_available) + toggle_flag(prefix + 'certs.changed', certs_changed) + # deprecated + toggle_flag(prefix + 'server.cert.available', self.server_certs) + toggle_flag(prefix + 'client.cert.available', self.get_client_cert()) + toggle_flag(prefix + 'batch.cert.available', self.server_certs) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + prefix = self.expand_name('{endpoint_name}.') + clear_flag(prefix + 'available') + clear_flag(prefix + 'ca.available') + clear_flag(prefix + 'ca.changed') + clear_flag(prefix + 'server.certs.available') + clear_flag(prefix + 'server.certs.changed') + clear_flag(prefix + 'client.certs.available') + clear_flag(prefix + 'client.certs.changed') + clear_flag(prefix + 'certs.available') + clear_flag(prefix + 'certs.changed') + # deprecated + clear_flag(prefix + 'server.cert.available') + clear_flag(prefix + 'client.cert.available') + clear_flag(prefix + 'batch.cert.available') + + @property + def _unit_name(self): + return hookenv.local_unit().replace('/', '_') + + @property + def root_ca_cert(self): + """ + Root CA certificate. + """ + # only the leader of the provider should set the CA, or all units + # had better agree + return self.all_joined_units.received_raw['ca'] + + def get_ca(self): + """ + Return the root CA certificate. + + Same as [root_ca_cert][]. + """ + return self.root_ca_cert + + @property + def root_ca_chain(self): + """ + The chain of trust for the root CA. + """ + # only the leader of the provider should set the CA, or all units + # had better agree + return self.all_joined_units.received_raw['chain'] + + def get_chain(self): + """ + Return the chain of trust for the root CA. + + Same as [root_ca_chain][]. + """ + return self.root_ca_chain + + def get_client_cert(self): + """ + Deprecated. Use [request_client_cert][] and the [client_certs][] + collection instead. + + Return a globally shared client certificate and key. + """ + data = self.all_joined_units.received_raw + return (data['client.cert'], data['client.key']) + + def get_server_cert(self): + """ + Deprecated. Use the [server_certs][] collection instead. + + Return the cert and key of the first server certificate requested. + """ + if not self.server_certs: + return (None, None) + cert = self.server_certs[0] + return (cert.cert, cert.key) + + @property + def server_certs(self): + """ + List of [Certificate][] instances for all available server certs. + """ + certs = [] + raw_data = self.all_joined_units.received_raw + json_data = self.all_joined_units.received + + # for backwards compatibility, the first cert goes in its own fields + if self.relations: + common_name = self.relations[0].to_publish_raw['common_name'] + cert = raw_data['{}.server.cert'.format(self._unit_name)] + key = raw_data['{}.server.key'.format(self._unit_name)] + if cert and key: + certs.append(Certificate('server', + common_name, + cert, + key)) + + # subsequent requests go in the collection + field = '{}.processed_requests'.format(self._unit_name) + certs_data = json_data[field] or {} + certs.extend(Certificate('server', + common_name, + cert['cert'], + cert['key']) + for common_name, cert in certs_data.items()) + return certs + + @property + def application_certs(self): + """ + List containg the application Certificate cert. + + :returns: A list containing one certificate + :rtype: [Certificate()] + """ + certs = [] + json_data = self.all_joined_units.received + field = '{}.processed_application_requests'.format(self._unit_name) + certs_data = json_data[field] or {} + app_cert_data = certs_data.get('app_data') + if app_cert_data: + certs = [Certificate( + 'server', + 'app_data', + app_cert_data['cert'], + app_cert_data['key'])] + return certs + + @property + def server_certs_map(self): + """ + Mapping of server [Certificate][] instances by their `common_name`. + """ + return {cert.common_name: cert for cert in self.server_certs} + + def get_batch_requests(self): + """ + Deprecated. Use [server_certs_map][] instead. + + Mapping of server [Certificate][] instances by their `common_name`. + """ + return self.server_certs_map + + @property + def client_certs(self): + """ + List of [Certificate][] instances for all available client certs. + """ + field = '{}.processed_client_requests'.format(self._unit_name) + certs_data = self.all_joined_units.received[field] or {} + return [Certificate('client', + common_name, + cert['cert'], + cert['key']) + for common_name, cert in certs_data.items()] + + @property + def client_certs_map(self): + """ + Mapping of client [Certificate][] instances by their `common_name`. + """ + return {cert.common_name: cert for cert in self.client_certs} + + def request_server_cert(self, cn, sans=None, cert_name=None): + """ + Request a server certificate and key be generated for the given + common name (`cn`) and optional list of alternative names (`sans`). + + The `cert_name` is deprecated and not needed. + + This can be called multiple times to request more than one server + certificate, although the common names must be unique. If called + again with the same common name, it will be ignored. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + to_publish_raw = self.relations[0].to_publish_raw + if to_publish_raw['common_name'] in (None, '', cn): + # for backwards compatibility, first request goes in its own fields + to_publish_raw['common_name'] = cn + to_publish_json['sans'] = sans or [] + cert_name = to_publish_raw.get('certificate_name') or cert_name + if cert_name is None: + cert_name = str(uuid.uuid4()) + to_publish_raw['certificate_name'] = cert_name + else: + # subsequent requests go in the collection + requests = to_publish_json.get('cert_requests', {}) + requests[cn] = {'sans': sans or []} + to_publish_json['cert_requests'] = requests + + def add_request_server_cert(self, cn, sans): + """ + Deprecated. Use [request_server_cert][] instead. + """ + self.request_server_cert(cn, sans) + + def request_server_certs(self): + """ + Deprecated. Just use [request_server_cert][]; this does nothing. + """ + pass + + def request_client_cert(self, cn, sans): + """ + Request a client certificate and key be generated for the given + common name (`cn`) and list of alternative names (`sans`). + + This can be called multiple times to request more than one client + certificate, although the common names must be unique. If called + again with the same common name, it will be ignored. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + requests = to_publish_json.get('client_cert_requests', {}) + requests[cn] = {'sans': sans} + to_publish_json['client_cert_requests'] = requests + + def request_application_cert(self, cn, sans): + """ + Request an application certificate and key be generated for the given + common name (`cn`) and list of alternative names (`sans` ) of this + unit and all peer units. All units will share a single certificates. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + requests = to_publish_json.get('application_cert_requests', {}) + requests[cn] = {'sans': sans} + to_publish_json['application_cert_requests'] = requests diff --git a/etcd/hooks/relations/tls-certificates/tls_certificates_common.py b/etcd/hooks/relations/tls-certificates/tls_certificates_common.py new file mode 100644 index 0000000..99a2f8c --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/tls_certificates_common.py @@ -0,0 +1,302 @@ +from charms.reactive import clear_flag, is_data_changed, data_changed + + +class CertificateRequest(dict): + def __init__(self, unit, cert_type, cert_name, common_name, sans): + self._unit = unit + self._cert_type = cert_type + super().__init__({ + 'certificate_name': cert_name, + 'common_name': common_name, + 'sans': sans, + }) + + @property + def _key(self): + return '.'.join((self._unit.relation.relation_id, + self.unit_name, + self.common_name)) + + def resolve_unit_name(self, unit): + """Return name of unit associated with this request. + + unit_name should be provided in the relation data to ensure + compatability with cross-model relations. If the unit name + is absent then fall back to unit_name attribute of the + unit associated with this request. + + :param unit: Unit to extract name from + :type unit: charms.reactive.endpoints.RelatedUnit + :returns: Name of unit + :rtype: str + """ + unit_name = unit.received_raw['unit_name'] + if not unit_name: + unit_name = unit.unit_name + return unit_name + + @property + def unit_name(self): + """Name of this unit. + + :returns: Name of unit + :rtype: str + """ + return self.resolve_unit_name(unit=self._unit).replace('/', '_') + + @property + def application_name(self): + """Name of the application which the request came from. + + :returns: Name of application + :rtype: str + """ + return self.resolve_unit_name(unit=self._unit).split('/')[0] + + @property + def cert_type(self): + """ + Type of certificate, 'server' or 'client', being requested. + """ + return self._cert_type + + @property + def cert_name(self): + return self['certificate_name'] + + @property + def common_name(self): + return self['common_name'] + + @property + def sans(self): + return self['sans'] + + @property + def _publish_key(self): + if self.cert_type == 'server': + return '{}.processed_requests'.format(self.unit_name) + elif self.cert_type == 'client': + return '{}.processed_client_requests'.format(self.unit_name) + raise ValueError('Unknown cert_type: {}'.format(self.cert_type)) + + @property + def _server_cert_key(self): + return '{}.server.cert'.format(self.unit_name) + + @property + def _server_key_key(self): + return '{}.server.key'.format(self.unit_name) + + @property + def _is_top_level_server_cert(self): + return (self.cert_type == 'server' and + self.common_name == self._unit.received_raw['common_name']) + + @property + def cert(self): + """ + The cert published for this request, if any. + """ + cert, key = None, None + if self._is_top_level_server_cert: + tpr = self._unit.relation.to_publish_raw + cert = tpr[self._server_cert_key] + key = tpr[self._server_key_key] + else: + tp = self._unit.relation.to_publish + certs_data = tp.get(self._publish_key, {}) + cert_data = certs_data.get(self.common_name, {}) + cert = cert_data.get('cert') + key = cert_data.get('key') + if cert and key: + return Certificate(self.cert_type, self.common_name, cert, key) + return None + + @property + def is_handled(self): + has_cert = self.cert is not None + same_sans = not is_data_changed(self._key, + sorted(set(self.sans or []))) + return has_cert and same_sans + + def set_cert(self, cert, key): + rel = self._unit.relation + if self._is_top_level_server_cert: + # backwards compatibility; if this is the cert that was requested + # as a single server cert, set it in the response as the single + # server cert + rel.to_publish_raw.update({ + self._server_cert_key: cert, + self._server_key_key: key, + }) + else: + data = rel.to_publish.get(self._publish_key, {}) + data[self.common_name] = { + 'cert': cert, + 'key': key, + } + rel.to_publish[self._publish_key] = data + if not rel.endpoint.new_server_requests: + clear_flag(rel.endpoint.expand_name('{endpoint_name}.server' + '.cert.requested')) + if not rel.endpoint.new_requests: + clear_flag(rel.endpoint.expand_name('{endpoint_name}.' + 'certs.requested')) + data_changed(self._key, sorted(set(self.sans or []))) + + +class ApplicationCertificateRequest(CertificateRequest): + """ + A request for an application consistent certificate. + + This is a request for a certificate that works for all units of an + application. All sans and cns are added together to produce one + certificate and the same certificate and key are sent to all the + units of an application. Only one ApplicationCertificateRequest + is needed per application. + """ + + @property + def _key(self): + """Key to identify this cert. + + :returns: cert key + :rtype: str + """ + return '{}.{}'.format(self._unit.relation.relation_id, 'app_cert') + + @property + def cert(self): + """ + The cert published for this request, if any. + + :returns: Certificate + :rtype: Certificate or None + """ + cert, key = None, None + tp = self._unit.relation.to_publish + certs_data = tp.get(self._publish_key, {}) + cert_data = certs_data.get('app_data', {}) + cert = cert_data.get('cert') + key = cert_data.get('key') + if cert and key: + return Certificate(self.cert_type, self.common_name, cert, key) + return None + + @property + def is_handled(self): + """Whether the certificate has been handled. + + :returns: If the cert has been handled + :rtype: bool + """ + has_cert = self.cert is not None + same_sans = not is_data_changed(self._key, + sorted(set(self.sans or []))) + return has_cert and same_sans + + @property + def sans(self): + """Generate a list of all sans from all units of application + + Examine all units of the application and compile a list of + all sans. CNs are treated as addition san entries. + + :returns: List of sans + :rtype: List[str] + """ + _sans = [] + for unit in self._unit.relation.units: + reqs = unit.received['application_cert_requests'] or {} + for cn, req in reqs.items(): + _sans.append(cn) + _sans.extend(req['sans']) + return sorted(list(set(_sans))) + + @property + def _request_key(self): + """Key used to request cert + + :returns: Key used to request cert + :rtype: str + """ + return 'application_cert_requests' + + def derive_publish_key(self, unit=None): + """Derive the application cert publish key for a unit. + + :param unit: Unit to extract name from + :type unit: charms.reactive.endpoints.RelatedUnit + :returns: publish key + :rtype: str + """ + if not unit: + unit = self._unit + unit_name = self.resolve_unit_name(unit).replace('/', '_') + return '{}.processed_application_requests'.format(unit_name) + + @property + def _publish_key(self): + """Key used to publish cert + + :returns: Key used to publish cert + :rtype: str + """ + return self.derive_publish_key(unit=self._unit) + + def set_cert(self, cert, key): + """Send the cert and key to all units of the application + + :param cert: TLS Certificate + :type cert: str + :param key: TLS Private Key + :type cert: str + """ + rel = self._unit.relation + for unit in self._unit.relation.units: + pub_key = self.derive_publish_key(unit=unit) + data = rel.to_publish.get( + pub_key, + {}) + data['app_data'] = { + 'cert': cert, + 'key': key, + } + rel.to_publish[pub_key] = data + if not rel.endpoint.new_application_requests: + clear_flag(rel.endpoint.expand_name( + '{endpoint_name}.application.certs.requested')) + data_changed(self._key, sorted(set(self.sans or []))) + + +class Certificate(dict): + """ + Represents a created certificate and key. + + The ``cert_type``, ``common_name``, ``cert``, and ``key`` values can + be accessed either as properties or as the contents of the dict. + """ + def __init__(self, cert_type, common_name, cert, key): + super().__init__({ + 'cert_type': cert_type, + 'common_name': common_name, + 'cert': cert, + 'key': key, + }) + + @property + def cert_type(self): + return self['cert_type'] + + @property + def common_name(self): + return self['common_name'] + + @property + def cert(self): + return self['cert'] + + @property + def key(self): + return self['key'] diff --git a/etcd/hooks/relations/tls-certificates/tox.ini b/etcd/hooks/relations/tls-certificates/tox.ini new file mode 100644 index 0000000..90de9d3 --- /dev/null +++ b/etcd/hooks/relations/tls-certificates/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = py3 +skipsdist = true + +[testenv] +basepython=python3 +envdir={toxworkdir}/py3 +deps= + pytest + charms.reactive + pydoc-markdown + +[testenv:docs] +commands=python make_docs + +[flake8] +ignore=E402 diff --git a/etcd/hooks/start b/etcd/hooks/start new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/start @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/stop b/etcd/hooks/stop new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/stop @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/update-status b/etcd/hooks/update-status new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/update-status @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/hooks/upgrade-charm b/etcd/hooks/upgrade-charm new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/etcd/hooks/upgrade-charm @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/etcd/icon.svg b/etcd/icon.svg new file mode 100644 index 0000000..a6dddda --- /dev/null +++ b/etcd/icon.svg @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/etcd/layer.yaml b/etcd/layer.yaml new file mode 100644 index 0000000..8e820b6 --- /dev/null +++ b/etcd/layer.yaml @@ -0,0 +1,49 @@ +"includes": +- "layer:options" +- "layer:basic" +- "interface:nrpe-external-master" +- "layer:debug" +- "interface:tls-certificates" +- "layer:cis-benchmark" +- "layer:leadership" +- "layer:nagios" +- "layer:tls-client" +- "layer:snap" +- "layer:cdk-service-kicker" +- "layer:status" +- "interface:etcd" +- "interface:etcd-proxy" +- "interface:grafana-dashboard" +- "interface:prometheus-manual" +"exclude": [".travis.yml", "tests", "tox.ini", "test-requirements.txt", "unit_tests", + ".tox", "__pycache__", "Makefile", ".coverage"] +"options": + "basic": + "packages": ["rsync"] + "python_packages": [] + "use_venv": !!bool "true" + "include_system_packages": !!bool "false" +# These options are mirrored in the test suite as hard-coded values. +# If these cert locations change, please update the test suite accordingly + "tls-client": + "ca_certificate_path": "/var/snap/etcd/common/ca.crt" + "server_certificate_path": "/var/snap/etcd/common/server.crt" + "server_key_path": "/var/snap/etcd/common/server.key" + "client_certificate_path": "/var/snap/etcd/common/client.crt" + "client_key_path": "/var/snap/etcd/common/client.key" + "cdk-service-kicker": + "services": + - "snap.etcd.etcd" + "cis-benchmark": {} + "debug": {} + "leadership": {} + "nagios": {} + "snap": {} + "status": + "patch-hookenv": !!bool "true" + "etcd": + "etcd_conf_dir": "/var/snap/etcd/common" + "etcd_data_dir": "/var/snap/etcd/current" + "etcd_daemon_process": "snap.etcd.etcd" +"repo": "https://github.com/juju-solutions/layer-etcd.git" +"is": "etcd" diff --git a/etcd/lib/charms/layer/__init__.py b/etcd/lib/charms/layer/__init__.py new file mode 100644 index 0000000..a8e0c64 --- /dev/null +++ b/etcd/lib/charms/layer/__init__.py @@ -0,0 +1,60 @@ +import sys +from importlib import import_module +from pathlib import Path + + +def import_layer_libs(): + """ + Ensure that all layer libraries are imported. + + This makes it possible to do the following: + + from charms import layer + + layer.foo.do_foo_thing() + + Note: This function must be called after bootstrap. + """ + for module_file in Path('lib/charms/layer').glob('*'): + module_name = module_file.stem + if module_name in ('__init__', 'basic', 'execd') or not ( + module_file.suffix == '.py' or module_file.is_dir() + ): + continue + import_module('charms.layer.{}'.format(module_name)) + + +# Terrible hack to support the old terrible interface. +# Try to get people to call layer.options.get() instead so +# that we can remove this garbage. +# Cribbed from https://stackoverfLow.com/a/48100440/4941864 +class OptionsBackwardsCompatibilityHack(sys.modules[__name__].__class__): + def __call__(self, section=None, layer_file=None): + if layer_file is None: + return self.get(section=section) + else: + return self.get(section=section, + layer_file=Path(layer_file)) + + +def patch_options_interface(): + from charms.layer import options + if sys.version_info.minor >= 5: + options.__class__ = OptionsBackwardsCompatibilityHack + else: + # Py 3.4 doesn't support changing the __class__, so we have to do it + # another way. The last line is needed because we already have a + # reference that doesn't get updated with sys.modules. + name = options.__name__ + hack = OptionsBackwardsCompatibilityHack(name) + hack.get = options.get + sys.modules[name] = hack + sys.modules[__name__].options = hack + + +try: + patch_options_interface() +except ImportError: + # This may fail if pyyaml hasn't been installed yet. But in that + # case, the bootstrap logic will try it again once it has. + pass diff --git a/etcd/lib/charms/layer/basic.py b/etcd/lib/charms/layer/basic.py new file mode 100644 index 0000000..bbdd074 --- /dev/null +++ b/etcd/lib/charms/layer/basic.py @@ -0,0 +1,501 @@ +import os +import sys +import re +import shutil +from distutils.version import LooseVersion +from pkg_resources import Requirement +from glob import glob +from subprocess import check_call, check_output, CalledProcessError +from time import sleep + +from charms import layer +from charms.layer.execd import execd_preinstall + + +def _get_subprocess_env(): + env = os.environ.copy() + env['LANG'] = env.get('LANG', 'C.UTF-8') + return env + + +def get_series(): + """ + Return series for a few known OS:es. + Tested as of 2019 november: + * centos6, centos7, rhel6. + * bionic + """ + series = "" + + # Looking for content in /etc/os-release + # works for ubuntu + some centos + if os.path.isfile('/etc/os-release'): + d = {} + with open('/etc/os-release', 'r') as rel: + for l in rel: + if not re.match(r'^\s*$', l): + k, v = l.split('=') + d[k.strip()] = v.strip().replace('"', '') + series = "{ID}{VERSION_ID}".format(**d) + + # Looking for content in /etc/redhat-release + # works for redhat enterprise systems + elif os.path.isfile('/etc/redhat-release'): + with open('/etc/redhat-release', 'r') as redhatlsb: + # CentOS Linux release 7.7.1908 (Core) + line = redhatlsb.readline() + release = int(line.split("release")[1].split()[0][0]) + series = "centos" + str(release) + + # Looking for content in /etc/lsb-release + # works for ubuntu + elif os.path.isfile('/etc/lsb-release'): + d = {} + with open('/etc/lsb-release', 'r') as lsb: + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + series = d['DISTRIB_CODENAME'] + + # This is what happens if we cant figure out the OS. + else: + series = "unknown" + return series + + +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 + + series = get_series() + + # OMG?! is build-essentials needed? + ubuntu_packages = ['python3-pip', + 'python3-setuptools', + 'python3-yaml', + 'python3-dev', + 'python3-wheel', + 'build-essential'] + + # I'm not going to "yum group info "Development Tools" + # omitting above madness + centos_packages = ['python3-pip', + 'python3-setuptools', + 'python3-devel', + 'python3-wheel'] + + packages_needed = [] + if 'centos' in series: + packages_needed = centos_packages + else: + packages_needed = ubuntu_packages + + 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') + hook_name = os.path.basename(sys.argv[0]) + is_bootstrapped = os.path.exists('wheelhouse/.bootstrapped') + is_charm_upgrade = hook_name == 'upgrade-charm' + is_series_upgrade = hook_name == 'post-series-upgrade' + is_post_upgrade = os.path.exists('wheelhouse/.upgraded') + is_upgrade = (not is_post_upgrade and + (is_charm_upgrade or is_series_upgrade)) + if is_bootstrapped and not is_upgrade: + # older subordinates might have downgraded charm-env, so we should + # restore it if necessary + install_or_update_charm_env() + activate_venv() + # the .upgrade file prevents us from getting stuck in a loop + # when re-execing to activate the venv; at this point, we've + # activated the venv, so it's safe to clear it + if is_post_upgrade: + os.unlink('wheelhouse/.upgraded') + return + if os.path.exists(venv): + try: + # focal installs or upgrades prior to PR 160 could leave the venv + # in a broken state which would prevent subsequent charm upgrades + _load_installed_versions(vpip) + except CalledProcessError: + is_broken_venv = True + else: + is_broken_venv = False + if is_upgrade or is_broken_venv: + # All upgrades should do a full clear of the venv, rather than + # just updating it, to bring in updates to Python itself + shutil.rmtree(venv) + if is_upgrade: + if os.path.exists('wheelhouse/.bootstrapped'): + os.unlink('wheelhouse/.bootstrapped') + # bootstrap wheelhouse + if os.path.exists('wheelhouse'): + pre_eoan = series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty', + 'ubuntu16.04', 'xenial', + 'ubuntu18.04', 'bionic') + pydistutils_lines = [ + "[easy_install]\n", + "find_links = file://{}/wheelhouse/\n".format(charm_dir), + "no_index=True\n", + "index_url=\n", # deliberately nothing here; disables it. + ] + if pre_eoan: + pydistutils_lines.append("allow_hosts = ''\n") + 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(pydistutils_lines) + if 'centos' in series: + yum_install(packages_needed) + else: + apt_install(packages_needed) + from charms.layer import options + cfg = options.get('basic') + # include packages defined in layer.yaml + if 'centos' in series: + yum_install(cfg.get('packages', [])) + else: + 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 = get_series() + if series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty'): + apt_install(['python-virtualenv']) + elif 'centos' in series: + yum_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, env=_get_subprocess_env()) + 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') + pre_install_pkgs = ['pip', 'setuptools', 'setuptools-scm'] + # we bundle these packages to work around bugs in older versions (such + # as https://github.com/pypa/pip/issues/56), but if the system already + # provided a newer version, downgrading it can cause other problems + _update_if_newer(pip, pre_install_pkgs) + # install the rest of the wheelhouse deps (extract the pkg names into + # a set so that we can ignore the pre-install packages and let pip + # choose the best version in case there are multiple from layer + # conflicts) + _versions = _load_wheelhouse_versions() + _pkgs = _versions.keys() - set(pre_install_pkgs) + # add back the versions such that each package in pkgs is + # ==. + # This ensures that pip 20.3.4+ will install the packages from the + # wheelhouse without (erroneously) flagging an error. + pkgs = _add_back_versions(_pkgs, _versions) + reinstall_flag = '--force-reinstall' + if not cfg.get('use_venv', True) and pre_eoan: + reinstall_flag = '--ignore-installed' + check_call([pip, 'install', '-U', reinstall_flag, '--no-index', + '--no-cache-dir', '-f', 'wheelhouse'] + list(pkgs), + env=_get_subprocess_env()) + # re-enable installation from pypi + os.remove('/root/.pydistutils.cfg') + + # install pyyaml for centos7, since, unlike the ubuntu image, the + # default image for centos doesn't include pyyaml; see the discussion: + # https://discourse.jujucharms.com/t/charms-for-centos-lets-begin + if 'centos' in series: + check_call([pip, 'install', '-U', 'pyyaml'], + env=_get_subprocess_env()) + + # install python packages from layer options + if cfg.get('python_packages'): + check_call([pip, 'install', '-U'] + cfg.get('python_packages'), + env=_get_subprocess_env()) + 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') + # setup wrappers to ensure envs are used for scripts + install_or_update_charm_env() + for wrapper in ('charms.reactive', 'charms.reactive.sh', + 'chlp', 'layer_option'): + src = os.path.join('/usr/local/sbin', 'charm-env') + dst = os.path.join('/usr/local/sbin', wrapper) + if not os.path.exists(dst): + os.symlink(src, dst) + if cfg.get('use_venv'): + shutil.copy2('bin/layer_option', vbin) + else: + shutil.copy2('bin/layer_option', '/usr/local/bin/') + # re-link the charm copy to the wrapper in case charms + # call bin/layer_option directly (as was the old pattern) + os.remove('bin/layer_option') + os.symlink('/usr/local/sbin/layer_option', 'bin/layer_option') + # flag us as having already bootstrapped so we don't do it again + open('wheelhouse/.bootstrapped', 'w').close() + if is_upgrade: + # flag us as having already upgraded so we don't do it again + open('wheelhouse/.upgraded', '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 _load_installed_versions(pip): + pip_freeze = check_output([pip, 'freeze']).decode('utf8') + versions = {} + for pkg_ver in pip_freeze.splitlines(): + try: + req = Requirement.parse(pkg_ver) + except ValueError: + continue + versions.update({ + req.project_name: LooseVersion(ver) + for op, ver in req.specs if op == '==' + }) + return versions + + +def _load_wheelhouse_versions(): + versions = {} + for wheel in glob('wheelhouse/*'): + pkg, ver = os.path.basename(wheel).rsplit('-', 1) + # nb: LooseVersion ignores the file extension + versions[pkg.replace('_', '-')] = LooseVersion(ver) + return versions + + +def _add_back_versions(pkgs, versions): + """Add back the version strings to each of the packages. + + The versions are LooseVersion() from _load_wheelhouse_versions(). This + function strips the ".zip" or ".tar.gz" from the end of the version string + and adds it back to the package in the form of == + + If a package name is not a key in the versions dictionary, then it is + returned in the list unchanged. + + :param pkgs: A list of package names + :type pkgs: List[str] + :param versions: A map of package to LooseVersion + :type versions: Dict[str, LooseVersion] + :returns: A list of (maybe) versioned packages + :rtype: List[str] + """ + def _strip_ext(s): + """Strip an extension (if it exists) from the string + + :param s: the string to strip an extension off if it exists + :type s: str + :returns: string without an extension of .zip or .tar.gz + :rtype: str + """ + for ending in [".zip", ".tar.gz"]: + if s.endswith(ending): + return s[:-len(ending)] + return s + + def _maybe_add_version(pkg): + """Maybe add back the version number to a package if it exists. + + Adds the version number, if the package exists in the lexically + captured `versions` dictionary, in the form ==. Strips + the extension if it exists. + + :param pkg: the package name to (maybe) add the version number to. + :type pkg: str + """ + try: + return "{}=={}".format(pkg, _strip_ext(str(versions[pkg]))) + except KeyError: + pass + return pkg + + return [_maybe_add_version(pkg) for pkg in pkgs] + + +def _update_if_newer(pip, pkgs): + installed = _load_installed_versions(pip) + wheelhouse = _load_wheelhouse_versions() + for pkg in pkgs: + if pkg not in installed or wheelhouse[pkg] > installed[pkg]: + check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', + pkg], env=_get_subprocess_env()) + + +def install_or_update_charm_env(): + # On Trusty python3-pkg-resources is not installed + try: + from pkg_resources import parse_version + except ImportError: + apt_install(['python3-pkg-resources']) + from pkg_resources import parse_version + + try: + installed_version = parse_version( + check_output(['/usr/local/sbin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + installed_version = parse_version('0.0.0') + try: + bundled_version = parse_version( + check_output(['bin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + bundled_version = parse_version('0.0.0') + if installed_version < bundled_version: + shutil.copy2('bin/charm-env', '/usr/local/sbin/') + + +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. + """ + from charms.layer import options + venv = os.path.abspath('../.venv') + vbin = os.path.join(venv, 'bin') + vpy = os.path.join(vbin, 'python') + use_venv = options.get('basic', 'use_venv') + if use_venv and '.venv' not in sys.executable: + # activate the venv + os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) + reload_interpreter(vpy) + layer.patch_options_interface() + layer.import_layer_libs() + + +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 = _get_subprocess_env() + + 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 + try: + # sometimes apt-get update needs to be run + check_call(['apt-get', 'update'], env=env) + except CalledProcessError: + # sometimes it's a dpkg lock issue + pass + sleep(5) + else: + break + + +def yum_install(packages): + """ Installs packages with yum. + This function largely mimics the apt_install function for consistency. + """ + if packages: + env = os.environ.copy() + cmd = ['yum', '-y', 'install'] + for attempt in range(3): + try: + check_call(cmd + packages, env=env) + except CalledProcessError: + if attempt == 2: + raise + try: + check_call(['yum', 'update'], env=env) + except CalledProcessError: + pass + sleep(5) + else: + break + else: + pass + + +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]) + + +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/etcd/lib/charms/layer/execd.py b/etcd/lib/charms/layer/execd.py new file mode 100644 index 0000000..438d9a1 --- /dev/null +++ b/etcd/lib/charms/layer/execd.py @@ -0,0 +1,114 @@ +# 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 . + +# This module may only import from the Python standard library. +import os +import sys +import subprocess +import time + +''' +execd/preinstall + +Read the layer-basic docs for more info on how to use this feature. +https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#exec-d-support +''' + + +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<. + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.reactive import not_unless + + +__all__ = ['leader_get', 'leader_set'] + + +@not_unless('leadership.is_leader') +def leader_set(*args, **kw): + '''Change leadership settings, per charmhelpers.core.hookenv.leader_set. + + Settings may either be passed in as a single dictionary, or using + keyword arguments. All values must be strings. + + The leadership.set.{key} reactive state will be set while the + leadership hook environment setting remains set. + + Changed leadership settings will set the leadership.changed.{key} + and leadership.changed states. These states will remain set until + the following hook. + + These state changes take effect immediately on the leader, and + in future hooks run on non-leaders. In this way both leaders and + non-leaders can share handlers, waiting on these states. + ''' + if args: + if len(args) > 1: + raise TypeError('leader_set() takes 1 positional argument but ' + '{} were given'.format(len(args))) + else: + settings = dict(args[0]) + else: + settings = {} + settings.update(kw) + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + + for key, value in settings.items(): + if value != previous.get(key): + reactive.set_state('leadership.changed.{}'.format(key)) + reactive.set_state('leadership.changed') + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + hookenv.leader_set(settings) + unitdata.kv().update(settings, prefix='leadership.settings.') + + +def leader_get(attribute=None): + '''Return leadership settings, per charmhelpers.core.hookenv.leader_get.''' + return hookenv.leader_get(attribute) diff --git a/etcd/lib/debug_script.py b/etcd/lib/debug_script.py new file mode 100644 index 0000000..e156924 --- /dev/null +++ b/etcd/lib/debug_script.py @@ -0,0 +1,8 @@ +import os + +dir = os.environ["DEBUG_SCRIPT_DIR"] + + +def open_file(path, *args, **kwargs): + """ Open a file within the debug script dir """ + return open(os.path.join(dir, path), *args, **kwargs) diff --git a/etcd/lib/etcd_databag.py b/etcd/lib/etcd_databag.py new file mode 100644 index 0000000..40207e8 --- /dev/null +++ b/etcd/lib/etcd_databag.py @@ -0,0 +1,123 @@ +from charms import layer +from charmhelpers.core.hookenv import unit_get +from charmhelpers.core.hookenv import config +from charmhelpers.core.hookenv import is_leader +from charmhelpers.core.hookenv import leader_get, leader_set +from charmhelpers.core import unitdata +from charms.reactive import is_state +from etcd_lib import get_ingress_address +from etcd_lib import get_bind_address + +import string +import random +import os + + +class EtcdDatabag: + ''' + This class represents a configuration object to ease configuration of an + etcd unit during deployment and reconfiguration. The full dict of data + when expanded looks like the following: + + {'public_address': '127.0.0.1', + 'cluster_bind_address': '127.0.0.1', + 'db_bind_address': '127.0.0.1', + 'cluster_address': '127.0.0.1', + 'db_address': '127.0.0.1', + 'unit_name': 'etcd0', + 'port': '2380', + 'management_port': '2379', + 'ca_certificate': '/etc/ssl/etcd/ca.crt', + 'server_certificate': '/etc/ssl/etcd/server.crt', + 'server_key': '/etc/ssl/etcd/server.key', + 'token': '8XG27B', + 'cluster_state': 'existing'} + ''' + + def __init__(self): + self.db = unitdata.kv() + self.cluster_bind_address = self.get_bind_address('cluster') + self.db_bind_address = self.get_bind_address('db') + self.port = config('port') + self.management_port = config('management_port') + # Live polled properties + self.public_address = unit_get('public-address') + self.cluster_address = get_ingress_address('cluster') + self.db_address = get_ingress_address('db') + self.unit_name = os.getenv('JUJU_UNIT_NAME').replace('/', '') + + # Pull the TLS certificate paths from layer data + tls_opts = layer.options('tls-client') + ca_path = tls_opts['ca_certificate_path'] + crt_path = tls_opts['server_certificate_path'] + key_path = tls_opts['server_key_path'] + + # Pull the static etcd configuration from layer-data + etcd_opts = layer.options('etcd') + self.etcd_conf_dir = etcd_opts['etcd_conf_dir'] + # This getter determines the current context of the storage path + # depending on if durable storage is mounted. + self.etcd_data_dir = self.storage_path() + self.etcd_daemon = etcd_opts['etcd_daemon_process'] + + self.ca_certificate = ca_path + self.server_certificate = crt_path + self.server_key = key_path + + # Cluster concerns + self.cluster = self.db.get('etcd.cluster', '') + self.token = self.cluster_token() + self.cluster_state = self.db.get('etcd.cluster-state', 'existing') + + def set_cluster(self, value): + ''' Set the cluster string for peer registration ''' + self.cluster = value + self.db.set('etcd.cluster', value) + + def set_cluster_state(self, value): + ''' Set the cluster state ''' + self.cluster_state = value + self.db.set('etcd.cluster-state', value) + + def cluster_token(self): + ''' Getter to return the unique cluster token. ''' + token = leader_get('token') + if not token and is_leader(): + token = self.id_generator() + leader_set({'token': token}) + return token + + def id_generator(self, size=6): + ''' Return a random 6 character string for use in cluster init. + + @params size - The size of the string to return in characters + ''' + chars = string.ascii_uppercase + string.digits + return ''.join(random.choice(chars) for _ in range(size)) + + def storage_path(self): + ''' Storage mounts are limited in snap confinement. Default behavior + is to version the database files in $SNAP_DATA. However the user can + attach durable storage, which is mounted in /media. We need a common + method to determine which storage path we are concerned with ''' + + etcd_opts = layer.options('etcd') + + if is_state('data.volume.attached'): + return "/media/etcd/data" + else: + return etcd_opts['etcd_data_dir'] + + def get_bind_address(self, endpoint_name): + ''' Returns the address that the service binds to. If the config + parameter 'bind_to_all_interfaces' is set to true, it returns 0.0.0.0 + If 'bind_to_all_interfaces' is set to false, it returns the + bind address of the endpoint_name received as parameter + + @param endpoint_name name the endpoint from where the + bind address is obtained + ''' + if bool(config('bind_to_all_interfaces')): + return '0.0.0.0' + + return get_bind_address(endpoint_name) diff --git a/etcd/lib/etcd_lib.py b/etcd/lib/etcd_lib.py new file mode 100644 index 0000000..ec1a5d8 --- /dev/null +++ b/etcd/lib/etcd_lib.py @@ -0,0 +1,90 @@ +from charmhelpers.contrib.templating.jinja import render +from charmhelpers.core.hookenv import ( + network_get, + unit_private_ip, +) + +import json + +GRAFANA_DASHBOARD_FILE = 'grafana_dashboard.json.j2' + + +def get_ingress_addresses(endpoint_name): + ''' Returns all ingress-addresses belonging to the named endpoint, if + available. Falls back to private-address if necessary. ''' + try: + data = network_get(endpoint_name) + except NotImplementedError: + return [unit_private_ip()] + + if 'ingress-addresses' in data: + return data['ingress-addresses'] + else: + return [unit_private_ip()] + + +def get_ingress_address(endpoint_name): + ''' Returns an ingress-address belonging to the named endpoint, if + available. Falls back to private-address if necessary. ''' + return get_ingress_addresses(endpoint_name)[0] + + +def get_bind_address(endpoint_name): + ''' Returns the first bind-address found in network info + belonging to the named endpoint, if available. + Falls back to private-address if necessary. + + @param endpoint_name the endpoint from where taking the + bind address + ''' + try: + data = network_get(endpoint_name) + except NotImplementedError: + return unit_private_ip() + + # Consider that network-get returns something like: + # + # bind-addresses: + # - macaddress: 02:d0:9e:31:d9:e0 + # interfacename: ens5 + # addresses: + # - hostname: "" + # address: 172.31.5.4 + # cidr: 172.31.0.0/20 + # - hostname: "" + # address: 172.31.5.4 + # cidr: 172.31.0.0/20 + # - macaddress: 8a:32:d7:8d:f6:9a + # interfacename: fan-252 + # addresses: + # - hostname: "" + # address: 252.5.4.1 + # cidr: 252.0.0.0/12 + # egress-subnets: + # - 172.31.5.4/32 + # ingress-addresses: + # - 172.31.5.4 + # - 172.31.5.4 + # - 252.5.4.1 + if 'bind-addresses' in data: + bind_addresses = data['bind-addresses'] + if len(bind_addresses) > 0: + if 'addresses' in bind_addresses[0]: + if len(bind_addresses[0]['addresses']) > 0: + return bind_addresses[0]['addresses'][0]['address'] + + return unit_private_ip() + + +def render_grafana_dashboard(datasource): + """Load grafana dashboard json model and insert prometheus datasource. + + :param datasource: name of the 'prometheus' application that will be used + as datasource in grafana dashboard + :return: Grafana dashboard json model as a dict. + """ + datasource = "{} - Juju generated source".format(datasource) + jinja_args = {'variable_start_string': '<<', 'variable_end_string': '>>'} + return json.loads(render(GRAFANA_DASHBOARD_FILE, + {'datasource': datasource}, + jinja_env_args=jinja_args)) diff --git a/etcd/lib/etcdctl.py b/etcd/lib/etcdctl.py new file mode 100644 index 0000000..b903217 --- /dev/null +++ b/etcd/lib/etcdctl.py @@ -0,0 +1,214 @@ +from charms import layer +from charmhelpers.core.hookenv import log +from subprocess import CalledProcessError +from subprocess import check_output +import os + + +def etcdctl_command(): + if os.path.isfile('/snap/bin/etcd.etcdctl'): + return '/snap/bin/etcd.etcdctl' + return 'etcdctl' + + +class EtcdCtl: + ''' etcdctl modeled as a python class. This python wrapper consumes + and exposes some of the commands contained in etcdctl. Related to unit + registration, cluster health, and other operations ''' + class CommandFailed(Exception): + pass + + def register(self, cluster_data): + ''' Perform self registration against the etcd leader and returns the + raw output response. + + @params cluster_data - a dict of data to fill out the request to + push our registration to the leader + requires keys: leader_address, port, unit_name, cluster_address, + management_port + ''' + # Build a connection string for the cluster data. + connection = get_connection_string([cluster_data['cluster_address']], + cluster_data['management_port']) + + command = 'member add {} {}'.format( + cluster_data['unit_name'], + connection + ) + + try: + result = self.run(command, endpoints=cluster_data['leader_address'], api=2) + except EtcdCtl.CommandFailed: + log('Notice: Unit failed self registration', 'WARNING') + raise + + # ['Added member named etcd12 with ID b9ab5b5a2e4baec5 to cluster', + # '', 'ETCD_NAME="etcd12"', + # 'ETCD_INITIAL_CLUSTER="etcd11=https://10.113.96.26:2380,etcd12=https://10.113.96.206:2380"', # noqa + # 'ETCD_INITIAL_CLUSTER_STATE="existing"', ''] + + reg = {} + + for line in result.split('\n'): + if 'ETCD_INITIAL_CLUSTER=' in line: + reg['cluster'] = line.split('="')[-1].rstrip('"') + return reg + + def unregister(self, unit_id, leader_address=None): + ''' Perform self deregistration during unit teardown + + @params unit_id - the ID for the unit assigned by etcd. Obtainable from + member_list method. + + @params leader_address - The endpoint to communicate with the leader in + the event of self deregistration. + ''' + return self.run(['member', 'remove', unit_id], endpoints=leader_address, api=2) + + def member_list(self, leader_address=False): + ''' Returns the output from `etcdctl member list` as a python dict + organized by unit_name, containing all the data-points in the resulting + response. ''' + command = 'member list' + + members = {} + out = self.run(command, endpoints=leader_address, api=2) + raw_member_list = out.strip('\n').split('\n') + # Expect output like this: + # 4f24ee16c889f6c1: name=etcd20 peerURLs=https://10.113.96.197:2380 clientURLs=https://10.113.96.197:2379 # noqa + # edc04bb81479d7e8: name=etcd21 peerURLs=https://10.113.96.243:2380 clientURLs=https://10.113.96.243:2379 # noqa + # edc0dsa81479d7e8[unstarted]: peerURLs=https://10.113.96.124:2380 # noqa + + for unit in raw_member_list: + if '[unstarted]' in unit: + unit_guid = unit.split('[')[0] + members['unstarted'] = {'unit_id': unit_guid} + if 'peerURLs=' in unit: + peer_urls = unit.split(' ')[1].split("=")[-1] + members['unstarted']['peer_urls'] = peer_urls + continue + unit_guid = unit.split(':')[0] + unit_name = unit.split(' ')[1].split("=")[-1] + peer_urls = unit.split(' ')[2].split("=")[-1] + client_urls = unit.split(' ')[3].split("=")[-1] + + members[unit_name] = {'unit_id': unit_guid, + 'name': unit_name, + 'peer_urls': peer_urls, + 'client_urls': client_urls} + return members + + def member_update(self, unit_id, uri): + ''' Update the etcd cluster member by unit_id with a new uri. This + allows us to change protocol, address or port. + @params unit_id: The string ID of the unit in the cluster. + @params uri: The string universal resource indicator of where to + contact the peer. ''' + out = '' + try: + command = 'member update {} {}'.format(unit_id, uri) + log(command) + # Run the member update command for the existing unit_id. + out = self.run(command) + except EtcdCtl.CommandFailed: + log('Failed to update member {}'.format(unit_id), 'WARNING') + return out + + def cluster_health(self, output_only=False): + ''' Returns the output of etcdctl cluster-health as a python dict + organized by topical information with detailed unit output ''' + health = {} + try: + out = self.run('cluster-health', endpoints=False, api=2) + if output_only: + return out + health_output = out.strip('\n').split('\n') + health['status'] = health_output[-1] + health['units'] = health_output[0:-2] + except EtcdCtl.CommandFailed: + log('Notice: Unit failed cluster-health check', 'WARNING') + health['status'] = 'cluster is unhealthy see log file for details.' + health['units'] = [] + return health + + def run(self, arguments, endpoints=None, api=3): + ''' Wrapper to subprocess calling output. This is a convenience + method to clean up the calls to subprocess and append TLS data''' + env = {} + command = [etcdctl_command()] + opts = layer.options('tls-client') + ca_path = opts['ca_certificate_path'] + crt_path = opts['server_certificate_path'] + key_path = opts['server_key_path'] + + if api == 3: + env['ETCDCTL_API'] = '3' + env['ETCDCTL_CACERT'] = ca_path + env['ETCDCTL_CERT'] = crt_path + env['ETCDCTL_KEY'] = key_path + if endpoints is None: + endpoints = 'http://127.0.0.1:4001' + + elif api == 2: + env['ETCDCTL_API'] = '2' + env['ETCDCTL_CA_FILE'] = ca_path + env['ETCDCTL_CERT_FILE'] = crt_path + env['ETCDCTL_KEY_FILE'] = key_path + if endpoints is None: + endpoints = ':4001' + + else: + raise NotImplementedError( + 'etcd api version {} not supported'.format(api)) + + if isinstance(arguments, str): + command.extend(arguments.split()) + elif isinstance(arguments, list) or isinstance(arguments, tuple): + command.extend(arguments) + else: + raise RuntimeError( + 'arguments not correct type; must be string, list or tuple') + + if endpoints is not False: + if api == 3: + command.extend(['--endpoints', endpoints]) + elif api == 2: + command.insert(1, '--endpoint') + command.insert(2, endpoints) + + try: + return check_output( + command, + env=env + ).decode('utf-8') + except CalledProcessError as e: + log(command, 'ERROR') + log(env, 'ERROR') + log(e.stdout, 'ERROR') + log(e.stderr, 'ERROR') + raise EtcdCtl.CommandFailed() from e + + def version(self): + ''' Return the version of etcdctl ''' + out = check_output( + [etcdctl_command(), 'version'], + env={'ETCDCTL_API': '3'} + ).decode('utf-8') + + if out == "No help topic for 'version'\n": + # Probably on etcd2 + out = check_output( + [etcdctl_command(), '--version'] + ).decode('utf-8') + + return out.split('\n')[0].split()[2] + + +def get_connection_string(members, port, protocol='https'): + ''' Return a connection string for the list of members using the provided + port and protocol (defaults to https)''' + connections = [] + for address in members: + connections.append('{}://{}:{}'.format(protocol, address, port)) + connection_string = ','.join(connections) + return connection_string diff --git a/etcd/make_docs b/etcd/make_docs new file mode 100644 index 0000000..dcd4c1f --- /dev/null +++ b/etcd/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import os +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + sys.path.insert(0, 'lib') + sys.path.insert(1, 'reactive') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') + if os.path.exists('.unit-state.db'): + os.remove('.unit-state.db') diff --git a/etcd/metadata.yaml b/etcd/metadata.yaml new file mode 100644 index 0000000..b333b27 --- /dev/null +++ b/etcd/metadata.yaml @@ -0,0 +1,65 @@ +"name": "etcd" +"summary": "Deploy a TLS terminated ETCD Cluster" +"maintainers": +- "Tim Van Steenburgh " +- "George Kraft " +- "Rye Terrell " +- "Konstantinos Tsakalozos " +- "Charles Butler " +"description": | + This charm supports deploying Etcd from the upstream binaries with resources. + It will also TLS wrap your service, and distribute client keys to any service + connecting. Etcd is a highly available key/value store. +"tags": +- "misc" +- "database" +- "keystore" +"series": +- "focal" +- "bionic" +- "xenial" +"requires": + "certificates": + "interface": "tls-certificates" +"provides": + "nrpe-external-master": + "interface": "nrpe-external-master" + "scope": "container" + "db": + "interface": "etcd" + "proxy": + "interface": "etcd-proxy" + "prometheus": + "interface": "prometheus-manual" + "grafana": + "interface": "grafana-dashboard" +"peers": + "cluster": + "interface": "etcd" +"resources": + "snapshot": + "type": "file" + "filename": "snapshot.tar.gz" + "description": "Tarball snapshot of an etcd clusters data." + "core": + "type": "file" + "filename": "core.snap" + "description": | + Snap package of core + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. + "etcd": + "type": "file" + "filename": "etcd.snap" + "description": | + Snap package of etcd + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. +"storage": + "data": + "type": "block" + "description": "Data volume to store the Etcd wal file and data." + "multiple": + "range": "0-1" + "minimum-size": "1G" +"subordinate": !!bool "false" diff --git a/etcd/pydocmd.yml b/etcd/pydocmd.yml new file mode 100644 index 0000000..ab3b2ef --- /dev/null +++ b/etcd/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'Status Management Layer' + +generate: + - status.md: + - charms.layer.status.WorkloadState + - charms.layer.status.maintenance + - charms.layer.status.maint + - charms.layer.status.blocked + - charms.layer.status.waiting + - charms.layer.status.active + - charms.layer.status.status_set + +pages: + - Status Management Layer: status.md + +gens_dir: docs diff --git a/etcd/reactive/__init__.py b/etcd/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/etcd/reactive/cdk_service_kicker.py b/etcd/reactive/cdk_service_kicker.py new file mode 100644 index 0000000..f7fd33a --- /dev/null +++ b/etcd/reactive/cdk_service_kicker.py @@ -0,0 +1,32 @@ +import os +import subprocess +from charms import layer +from charms.reactive import hook, when_not, remove_state, set_state +from charmhelpers.core.templating import render + + +@hook('upgrade-charm') +def upgrade_charm(): + remove_state('cdk-service-kicker.installed') + + +@when_not('cdk-service-kicker.installed') +def install_cdk_service_kicker(): + ''' Installs the cdk-service-kicker service. Workaround for + https://github.com/juju-solutions/bundle-canonical-kubernetes/issues/357 + ''' + source = 'cdk-service-kicker' + dest = '/usr/bin/cdk-service-kicker' + services = layer.options('cdk-service-kicker').get('services') + context = {'services': ' '.join(services)} + render(source, dest, context) + os.chmod('/usr/bin/cdk-service-kicker', 0o775) + + source = 'cdk-service-kicker.service' + dest = '/etc/systemd/system/cdk-service-kicker.service' + context = {} + render(source, dest, context) + command = ['systemctl', 'enable', 'cdk-service-kicker'] + subprocess.check_call(command) + + set_state('cdk-service-kicker.installed') diff --git a/etcd/reactive/etcd.py b/etcd/reactive/etcd.py new file mode 100644 index 0000000..30ca1be --- /dev/null +++ b/etcd/reactive/etcd.py @@ -0,0 +1,1018 @@ +#!/usr/bin/python3 + +from charms import layer + +from charms.layer import snap + +from charms.reactive import endpoint_from_flag +from charms.reactive import when +from charms.reactive import when_any +from charms.reactive import when_not +from charms.reactive import is_state +from charms.reactive import set_state +from charms.reactive import is_flag_set +from charms.reactive import remove_state +from charms.reactive import set_flag +from charms.reactive import clear_flag +from charms.reactive import hook +from charms.reactive import register_trigger +from charms.reactive.helpers import data_changed + +from charms.templating.jinja2 import render + +from charmhelpers.core.hookenv import config +from charmhelpers.core.hookenv import log +from charmhelpers.core.hookenv import DEBUG + +from charmhelpers.core.hookenv import leader_set +from charmhelpers.core.hookenv import leader_get +from charmhelpers.core.hookenv import storage_get + +from charmhelpers.core.hookenv import application_version_set +from charmhelpers.core.hookenv import open_port +from charmhelpers.core.hookenv import close_port +from charmhelpers.core.host import write_file +from charmhelpers.core import hookenv +from charmhelpers.core import host +from charmhelpers.contrib.charmsupport import nrpe + +from charms.layer import status + +from etcdctl import EtcdCtl +from etcdctl import get_connection_string +from etcd_databag import EtcdDatabag +from etcd_lib import ( + get_ingress_address, + get_ingress_addresses, + render_grafana_dashboard, +) + +from shlex import split +from subprocess import check_call +from subprocess import check_output +from subprocess import CalledProcessError +from shutil import copyfile + +import json +import os +import charms.leadership # noqa +import socket +import time +import traceback +import yaml +import shutil +import random + + +# Layer Note: the @when_not etcd.installed state checks are relating to +# a boundry that was superimposed by the etcd-24 release which added support +# for snaps. Snapped etcd is now the only supported mechanism by this charm. +# References to this state will be wiped sometime within the next 10 releases +# of the charm. + + +# Override the default nagios shortname regex to allow periods, which we +# need because our bin names contain them (e.g. 'snap.foo.daemon'). The +# default regex in charmhelpers doesn't allow periods, but nagios itself does. +nrpe.Check.shortname_re = r'[\.A-Za-z0-9-_]+$' + +GRAFANA_DASHBOARD_NAME = 'etcd' + +register_trigger(when_not="endpoint.grafana.joined", clear_flag="grafana.configured") +register_trigger(when_not="endpoint.prometheus.joined", + clear_flag="prometheus.configured") +register_trigger(when_not="endpoint.prometheus.joined", clear_flag="grafana.configured") + + +def get_target_etcd_channel(): + """ + Check whether or not etcd is already installed. i.e. we're + going through an upgrade. If so, leave the etcd version alone, + if we're a new install, we can set the default channel here. + + If the user has specified a version, then just return that. + + :return: String snap channel + """ + channel = hookenv.config('channel') + if channel == 'auto': + if snap.is_installed('etcd'): + return False + else: + return '3.4/stable' + else: + return channel + + +@when('etcd.installed') +def snap_upgrade_notice(): + status.blocked('Manual migration required. http://bit.ly/2oznAUZ') + + +@when_any('etcd.registered', 'etcd.leader.configured') +@when_not('etcd.installed') +@when_not('upgrade.series.in-progress') +def check_cluster_health(): + ''' report on the cluster health every 5 minutes''' + etcdctl = EtcdCtl() + health = etcdctl.cluster_health() + + # Determine if the unit is healthy or unhealthy + if 'unhealthy' in health['status']: + unit_health = "UnHealthy" + else: + unit_health = "Healthy" + + # Determine units peer count, and surface 0 by default + try: + peers = len(etcdctl.member_list()) + except Exception: + unit_health = "Errored" + peers = 0 + + bp = "{0} with {1} known peer{2}" + status_message = bp.format(unit_health, peers, 's' if peers != 1 else '') + + status.active(status_message) + + +@when('snap.installed.etcd') +@when_not('etcd.installed') +def set_app_version(): + ''' Surface the etcd application version on juju status ''' + # note - the snap doesn't place an etcd alias on disk. This shall infer + # the version from etcdctl, as the snap distributes both in lockstep. + application_version_set(etcd_version()) + + +@when_not('certificates.available') +def missing_relation_notice(): + status.blocked('Missing relation to certificate authority.') + + +@when('certificates.available') +def prepare_tls_certificates(tls): + common_name = hookenv.unit_public_ip() + sans = set() + sans.add(hookenv.unit_public_ip()) + sans.update(get_ingress_addresses('db')) + sans.update(get_ingress_addresses('cluster')) + sans.add(socket.gethostname()) + + # add cluster peers as alt names when present + cluster = endpoint_from_flag('cluster.joined') + if cluster: + for ip in cluster.get_db_ingress_addresses(): + sans.add(ip) + + sans = sorted(sans) + certificate_name = hookenv.local_unit().replace('/', '_') + tls.request_server_cert(common_name, sans, certificate_name) + + +@hook('upgrade-charm') +def remove_states(): + # stale state cleanup (pre rev6) + remove_state('etcd.tls.secured') + remove_state('etcd.ssl.placed') + remove_state('etcd.ssl.exported') + remove_state('etcd.nrpe.configured') + # force a config re-render in case template changed + set_state('etcd.rerender-config') + + +@hook('pre-series-upgrade') +def pre_series_upgrade(): + bag = EtcdDatabag() + host.service_pause(bag.etcd_daemon) + status.blocked('Series upgrade in progress') + + +@hook('post-series-upgrade') +def post_series_upgrade(): + bag = EtcdDatabag() + host.service_resume(bag.etcd_daemon) + + +@when('snap.installed.etcd') +@when('leadership.is_leader') +@when_any('config.changed.port', 'config.changed.management_port') +@when_not('etcd.installed') +@when_not('upgrade.series.in-progress') +def leader_config_changed(): + ''' The leader executes the runtime configuration update for the cluster, + as it is the controlling unit. Will render config, close and open ports and + restart the etcd service.''' + configuration = hookenv.config() + previous_port = configuration.previous('port') + log('Previous port: {0}'.format(previous_port)) + previous_mgmt_port = configuration.previous('management_port') + log('Previous management port: {0}'.format(previous_mgmt_port)) + + if previous_port and previous_mgmt_port: + bag = EtcdDatabag() + etcdctl = EtcdCtl() + members = etcdctl.member_list() + # Iterate over all the members in the list. + for unit_name in members: + # Grab the previous peer url and replace the management port. + peer_urls = members[unit_name]['peer_urls'] + log('Previous peer url: {0}'.format(peer_urls)) + old_port = ':{0}'.format(previous_mgmt_port) + new_port = ':{0}'.format(configuration.get('management_port')) + url = peer_urls.replace(old_port, new_port) + # Update the member's peer_urls with the new ports. + log(etcdctl.member_update(members[unit_name]['unit_id'], url)) + # Render just the leaders configuration with the new values. + render_config() + address = get_ingress_address('cluster') + leader_set({'leader_address': + get_connection_string([address], + bag.management_port)}) + host.service_restart(bag.etcd_daemon) + + +@when('snap.installed.etcd') +@when_not('leadership.is_leader') +@when_any('config.changed.port', 'config.changed.management_port') +@when_not('etcd.installed') +def follower_config_changed(): + ''' Follower units need to render the configuration file, close and open + ports, and restart the etcd service. ''' + set_state('etcd.rerender-config') + + +@when('snap.installed.etcd') +@when('config.changed.bind_to_all_interfaces') +@when_not('upgrade.series.in-progress') +def bind_to_all_interfaces_changed(): + set_state('etcd.rerender-config') + + +@when('etcd.rerender-config') +@when_not('upgrade.series.in-progress') +def rerender_config(): + ''' Config must be updated and service restarted ''' + bag = EtcdDatabag() + log('Rendering config file for {0}'.format(bag.unit_name)) + render_config() + if host.service_running(bag.etcd_daemon): + host.service_restart(bag.etcd_daemon) + set_app_version() + + +@when('cluster.joined') +def set_db_ingress_address(cluster): + ''' Send db ingress address to peers on the cluster relation ''' + address = get_ingress_address('db') + cluster.set_db_ingress_address(address) + + +@when('db.connected') +@when('etcd.ssl.placed') +@when('cluster.joined') +def send_cluster_connection_details(cluster, db): + ''' Need to set the cluster connection string and + the client key and certificate on the relation object. ''' + cert = read_tls_cert('client.crt') + key = read_tls_cert('client.key') + ca = read_tls_cert('ca.crt') + etcdctl = EtcdCtl() + + # Set the key, cert, and ca on the db relation + db.set_client_credentials(key, cert, ca) + + port = hookenv.config().get('port') + # Get all the peers participating in the cluster relation. + members = cluster.get_db_ingress_addresses() + # Append our own address to the membership list, because peers dont self + # actualize + address = get_ingress_address('db') + members.append(address) + members.sort() + # Create a connection string with all the members on the configured port. + connection_string = get_connection_string(members, port) + # Set the connection string on the db relation. + db.set_connection_string(connection_string, version=etcdctl.version()) + + +@when('db.connected') +@when('etcd.ssl.placed') +@when_not('cluster.joined') +def send_single_connection_details(db): + ''' ''' + cert = read_tls_cert('client.crt') + key = read_tls_cert('client.key') + ca = read_tls_cert('ca.crt') + + etcdctl = EtcdCtl() + + # Set the key and cert on the db relation + db.set_client_credentials(key, cert, ca) + + bag = EtcdDatabag() + # Get all the peers participating in the cluster relation. + address = get_ingress_address('db') + members = [address] + # Create a connection string with this member on the configured port. + connection_string = get_connection_string(members, bag.port) + # Set the connection string on the db relation. + db.set_connection_string(connection_string, version=etcdctl.version()) + + +@when('proxy.connected') +@when('etcd.ssl.placed') +@when_any('etcd.leader.configured', 'cluster.joined') +def send_cluster_details(proxy): + ''' Sends the peer cluster string to proxy units so they can join and act + on behalf of the cluster. ''' + cert = read_tls_cert('client.crt') + key = read_tls_cert('client.key') + ca = read_tls_cert('ca.crt') + proxy.set_client_credentials(key, cert, ca) + + # format a list of cluster participants + etcdctl = EtcdCtl() + peers = etcdctl.member_list() + cluster = [] + for peer in peers: + thispeer = peers[peer] + # Potential member doing registration. Default to skip + if 'peer_urls' not in thispeer.keys() or not thispeer['peer_urls']: + continue + peer_string = "{}={}".format(thispeer['name'], thispeer['peer_urls']) + cluster.append(peer_string) + + proxy.set_cluster_string(','.join(cluster)) + + +@when('config.changed.channel') +def channel_changed(): + ''' Ensure that the config is updated if the channel changes. ''' + set_state('etcd.rerender-config') + + +@when('config.changed.channel') +@when_not('etcd.installed') +def snap_install(): + channel = get_target_etcd_channel() + snap.install('core') + if channel: + snap.install('etcd', channel=channel, classic=False) + remove_state('etcd.ssl.exported') + + +@when('etcd.ssl.placed') +@when_not('snap.installed.etcd') +def install_etcd(): + ''' Attempt resource get on the "etcd" and "etcdctl" resources. If no + resources are provided attempt to install from the archive only on the + 16.04 (xenial) series. ''' + + if is_state('etcd.installed'): + msg = 'Manual upgrade required. run-action snap-upgrade.' + status.blocked(msg) + return + + status.maintenance('Installing etcd.') + + channel = get_target_etcd_channel() + if channel: + snap.install('etcd', channel=channel, classic=False) + + +@when('snap.installed.etcd') +@when_not('etcd.service-restart.configured') +@when_not('upgrade.series.in-progress') +def add_systemd_restart_always(): + template = 'templates/service-always-restart.systemd-latest.conf' + service = 'snap.etcd.etcd' + + try: + # Get the systemd version + cmd = ['systemd', '--version'] + output = check_output(cmd).decode('UTF-8') + line = output.splitlines()[0] + words = line.split() + assert words[0] == 'systemd' + systemd_version = int(words[1]) + + # Check for old version (for xenial support) + if systemd_version < 230: + template = 'templates/service-always-restart.systemd-229.conf' + except Exception: + traceback.print_exc() + hookenv.log('Failed to detect systemd version, using latest template', + level='ERROR') + + dest_dir = '/etc/systemd/system/{}.service.d'.format(service) + os.makedirs(dest_dir, exist_ok=True) + copyfile(template, '{}/always-restart.conf'.format(dest_dir)) + check_call(['systemctl', 'daemon-reload']) + host.service_restart('{}.service'.format(service)) + set_state('etcd.service-restart.configured') + + +@when('snap.installed.etcd') +@when('etcd.ssl.placed') +@when('cluster.joined') +@when_not('leadership.is_leader') +@when_not('etcd.registered') +@when_not('etcd.installed') +@when_not('upgrade.series.in-progress') +def register_node_with_leader(cluster): + ''' + Control flow mechanism to perform self registration with the leader. + + Before executing self registration, we must adhere to the nature of offline + static turnup rules. If we find a GUID in the member list without peering + information the unit will enter a race condition and must wait for a clean + status output before we can progress to self registration. + ''' + etcdctl = EtcdCtl() + bag = EtcdDatabag() + leader_address = leader_get('leader_address') + bag.leader_address = leader_address + + try: + # Check if we are already registered. Unregister ourselves if we are so + # we can register from scratch. + peer_url = 'https://%s:%s' % (bag.cluster_address, bag.management_port) + members = etcdctl.member_list(leader_address) + for _, member in members.items(): + if member['peer_urls'] == peer_url: + log('Found member that matches our peer URL. Unregistering...') + etcdctl.unregister(member['unit_id'], leader_address) + + # Now register. + resp = etcdctl.register(bag.__dict__) + bag.set_cluster(resp['cluster']) + except EtcdCtl.CommandFailed: + log('etcdctl.register failed, will retry') + msg = 'Waiting to retry etcd registration' + status.waiting(msg) + return + + render_config(bag) + host.service_restart(bag.etcd_daemon) + open_port(bag.port) + set_state('etcd.registered') + + +@when('etcd.ssl.placed') +@when('leadership.is_leader') +@when_not('etcd.leader.configured') +@when_not('etcd.installed') +@when_not('upgrade.series.in-progress') +def initialize_new_leader(): + ''' Create an initial cluster string to bring up a single member cluster of + etcd, and set the leadership data so the followers can join this one. ''' + bag = EtcdDatabag() + bag.token = bag.token + bag.set_cluster_state('new') + address = get_ingress_address('cluster') + cluster_connection_string = get_connection_string([address], + bag.management_port) + bag.set_cluster("{}={}".format(bag.unit_name, cluster_connection_string)) + + render_config(bag) + host.service_restart(bag.etcd_daemon) + + # sorry, some hosts need this. The charm races with systemd and wins. + time.sleep(2) + + # Check health status before we say we are good + etcdctl = EtcdCtl() + status = etcdctl.cluster_health() + if 'unhealthy' in status: + status.blocked('Cluster not healthy.') + return + # We have a healthy leader, broadcast initial data-points for followers + open_port(bag.port) + leader_connection_string = get_connection_string([address], + bag.port) + leader_set({'leader_address': leader_connection_string, + 'cluster': bag.cluster}) + + # set registered state since if we ever become a follower, we will not need + # to re-register + set_state('etcd.registered') + + # finish bootstrap delta and set configured state + set_state('etcd.leader.configured') + + +@when('snap.installed.etcd') +@when('snap.refresh.set') +@when('leadership.is_leader') +def process_snapd_timer(): + ''' Set the snapd refresh timer on the leader so all cluster members + (present and future) will refresh near the same time. ''' + # Get the current snapd refresh timer; we know layer-snap has set this + # when the 'snap.refresh.set' flag is present. + timer = snap.get(snapname='core', key='refresh.timer').decode('utf-8').strip() + if not timer: + # The core snap timer is empty. This likely means a subordinate timer + # reset ours. Try to set it back to a previously leader-set value, + # falling back to config if needed. Luckily, this should only happen + # during subordinate install, so this should remain stable afterward. + timer = leader_get('snapd_refresh') or hookenv.config('snapd_refresh') + snap.set_refresh_timer(timer) + + # Ensure we have the timer known by snapd (it may differ from config). + timer = snap.get(snapname='core', key='refresh.timer').decode('utf-8').strip() + + # The first time through, data_changed will be true. Subsequent calls + # should only update leader data if something changed. + if data_changed('etcd_snapd_refresh', timer): + log('setting snapd_refresh timer to: {}'.format(timer)) + leader_set({'snapd_refresh': timer}) + + +@when('snap.installed.etcd') +@when('snap.refresh.set') +@when('leadership.changed.snapd_refresh') +@when_not('leadership.is_leader') +def set_snapd_timer(): + ''' Set the snapd refresh.timer on non-leader cluster members. ''' + # NB: This method should only be run when 'snap.refresh.set' is present. + # Layer-snap will always set a core refresh.timer, which may not be the + # same as our leader. Gating with 'snap.refresh.set' ensures layer-snap + # has finished and we are free to set our config to the leader's timer. + timer = leader_get('snapd_refresh') or '' # None will cause error + log('setting snapd_refresh timer to: {}'.format(timer)) + snap.set_refresh_timer(timer) + + +@when('tls_client.ca.saved', 'tls_client.server.key.saved', + 'tls_client.server.certificate.saved', + 'tls_client.client.certificate.saved') +@when_not('etcd.ssl.placed') +def tls_state_control(): + ''' This state represents all the complexity of handling the TLS certs. + instead of stacking decorators, this state condenses it into a single + state we can gate on before progressing with secure setup. Also handles + ensuring users of the system can access the TLS certificates''' + + bag = EtcdDatabag() + if not os.path.isdir(bag.etcd_conf_dir): + hookenv.log('Waiting for etcd conf creation.') + return + cmd = ['chown', '-R', 'root:ubuntu', bag.etcd_conf_dir] + check_call(cmd) + set_state('etcd.ssl.placed') + + +@when('etcd.ssl.placed') +@when_any('tls_client.ca.written', + 'tls_client.server.certificate.written', + 'tls_client.client.certificate.written') +@when_not('upgrade.series.in-progress') +def tls_update(): + ''' Handle changes to the TLS data by ensuring that the service is + restarted. + ''' + # ensure config is updated with new certs and service restarted + bag = EtcdDatabag() + render_config(bag) + host.service_restart(bag.etcd_daemon) + + # ensure that certs are re-echoed to the db relations + remove_state('etcd.ssl.placed') + remove_state('tls_client.ca.written') + remove_state('tls_client.server.certificate.written') + remove_state('tls_client.client.certificate.written') + + +@when('snap.installed.etcd') +@when_not('etcd.ssl.exported') +def render_default_user_ssl_exports(): + ''' Add secure credentials to default user environment configs, + transparently adding TLS ''' + opts = layer.options('tls-client') + + ca_path = opts['ca_certificate_path'] + client_crt = opts['client_certificate_path'] + client_key = opts['client_key_path'] + + etcd_ver = etcd_version() + if etcd_ver == 'n/a': + hookenv.log('Unable to determine version format for etcd SSL config', + level=hookenv.ERROR) + return + major, minor, _ = etcd_ver.split('.') + + if int(major) >= 3 and int(minor) >= 3: + evars = [ + 'export ETCDCTL_KEY={}\n'.format(client_key), + 'export ETCDCTL_CERT={}\n'.format(client_crt), + 'export ETCDCTL_CACERT={}\n'.format(ca_path) + ] + else: + evars = [ + 'export ETCDCTL_KEY_FILE={}\n'.format(client_key), + 'export ETCDCTL_CERT_FILE={}\n'.format(client_crt), + 'export ETCDCTL_CA_FILE={}\n'.format(ca_path) + ] + + with open('/home/ubuntu/.bash_aliases', 'w') as fp: + fp.writelines(evars) + with open('/root/.bash_aliases', 'w') as fp: + fp.writelines(evars) + + set_state('etcd.ssl.exported') + + +def force_rejoin(): + """Wipe local data and rejoin new cluster formed by leader unit + + This action is required if leader unit performed snapshot restore. All + other members must remove their local data and previous cluster + identities and join newly formed, restored, cluster. + """ + log('Wiping local storage and rejoining cluster') + conf = EtcdDatabag() + host.service_stop(conf.etcd_daemon) + clear_flag('etcd.registered') + etcd_data = os.path.join(conf.storage_path(), 'member') + if os.path.exists(etcd_data): + shutil.rmtree(etcd_data) + for _ in range(11): + # We need randomized back-off timer because only one unit can be + # joining at the same time + time.sleep(random.randint(1, 10)) + register_node_with_leader(None) + if is_flag_set('etcd.registered'): + log('Successfully rejoined the cluster') + break + + +@when('leadership.changed.force_rejoin') +@when_not('leadership.is_leader') +def force_rejoin_requested(): + force_rejoin() + check_cluster_health() + + +@hook('cluster-relation-broken') +def perform_self_unregistration(cluster=None): + ''' Attempt self removal during unit teardown. ''' + etcdctl = EtcdCtl() + leader_address = leader_get('leader_address') + unit_name = os.getenv('JUJU_UNIT_NAME').replace('/', '') + members = etcdctl.member_list() + # Self Unregistration + etcdctl.unregister(members[unit_name]['unit_id'], leader_address) + + +@hook('data-storage-attached') +def format_and_mount_storage(): + ''' This allows users to request persistent volumes from the cloud provider + for the purposes of disaster recovery. ''' + set_state('data.volume.attached') + # Query juju for the information about the block storage + device_info = storage_get() + block = device_info['location'] + bag = EtcdDatabag() + bag.cluster = leader_get('cluster') + # the databag has behavior that keeps the path updated. + # Reference the default path from layer_options. + etcd_opts = layer.options('etcd') + # Split the tail of the path to mount the volume 1 level before + # the data directory. + tail = os.path.split(bag.etcd_data_dir)[0] + + if volume_is_mounted(block): + hookenv.log('Device is already attached to the system.') + hookenv.log('Refusing to take action against {}'.format(block)) + return + + # Format the device in non-interactive mode + cmd = ['mkfs.ext4', device_info['location'], '-F'] + hookenv.log('Creating filesystem on {}'.format(device_info['location'])) + hookenv.log('With command: {}'.format(' '.join(cmd))) + check_call(cmd) + + # halt etcd to perform the data-store migration + host.service_stop(bag.etcd_daemon) + + os.makedirs(tail, exist_ok=True) + mount_volume(block, tail) + # handle first run during early-attach storage, pre-config-changed hook. + os.makedirs(bag.etcd_data_dir, exist_ok=True) + + # Only attempt migration if directory exists + if os.path.isdir(etcd_opts['etcd_data_dir']): + migrate_path = "{}/".format(etcd_opts['etcd_data_dir']) + output_path = "{}/".format(bag.etcd_data_dir) + cmd = ['rsync', '-azp', migrate_path, output_path] + + hookenv.log('Detected existing data, migrating to new location.') + hookenv.log('With command: {}'.format(' '.join(cmd))) + + check_call(cmd) + + with open('/etc/fstab', 'r') as fp: + contents = fp.readlines() + + found = 0 + # scan fstab for the device + for line in contents: + if block in line: + found = found + 1 + + # if device not in fstab, append so it persists through reboots + if not found > 0: + append = "{0} {1} ext4 defaults 0 0".format(block, tail) # noqa + with open('/etc/fstab', 'a') as fp: + fp.writelines([append]) + + # Finally re-render the configuration and resume operation + render_config(bag) + host.service_restart(bag.etcd_daemon) + + +def read_tls_cert(cert): + ''' Reads the contents of the layer-configured certificate path indicated + by cert. Returns the utf-8 decoded contents of the file ''' + # Load the layer options for configured paths + opts = layer.options('tls-client') + + # Retain a dict of the certificate paths + cert_paths = {'ca.crt': opts['ca_certificate_path'], + 'server.crt': opts['server_certificate_path'], + 'server.key': opts['server_key_path'], + 'client.crt': opts['client_certificate_path'], + 'client.key': opts['client_key_path']} + + # If requesting a cert we dont know about, raise a ValueError + if cert not in cert_paths.keys(): + raise ValueError('No known certificate {}'.format(cert)) + + # Read the contents of the cert and return it in utf-8 encoded text + with open(cert_paths[cert], 'r') as fp: + data = fp.read() + return data + + +@when('nrpe-external-master.available') +@when_not('nrpe-external-master.initial-config') +def initial_nrpe_config(nagios=None): + set_state('nrpe-external-master.initial-config') + update_nrpe_config(nagios) + + +@when_any('config.changed.nagios_context', + 'config.changed.nagios_servicegroups') +def force_update_nrpe_config(): + remove_state('etcd.nrpe.configured') + + +@when('etcd.installed') +@when('nrpe-external-master.available') +@when_not('etcd.nrpe.configured') +def update_nrpe_config(unused=None): + # List of systemd services that will be checked + services = ('snap.etcd.etcd',) + + # The current nrpe-external-master interface doesn't handle a lot of logic, + # use the charm-helpers code for now. + hostname = nrpe.get_nagios_hostname() + current_unit = nrpe.get_nagios_unit_name() + nrpe_setup = nrpe.NRPE(hostname=hostname, primary=False) + # add our first check, to alert on service failure + nrpe.add_init_service_checks(nrpe_setup, services, current_unit) + + # add the cron job to populate the cache for our second check + # (we cache the output of 'etcdctl alarm list' to minimise overhead) + with open("templates/check_etcd-alarms.cron") as fp: + write_file( + path="/etc/cron.d/check_etcd-alarms", + content=fp.read().encode(), + owner="root", + perms=0o644, + ) + + # create an empty output file for the above + write_file( + path="/var/lib/nagios/etcd-alarm-list.txt", + content="", + owner="root", + perms=0o644, + ) + + # install the NRPE script for the above + with open("templates/check_etcd-alarms.py") as fp: + write_file( + path="/usr/lib/nagios/plugins/check_etcd-alarms.py", + content=fp.read().encode(), + owner="root", + perms=0o755, + ) + + # define our second check, to alert on etcd alarm status + nrpe_setup.add_check( + "etcd-alarms", + "Verify etcd has no raised alarms", + "/usr/lib/nagios/plugins/check_etcd-alarms.py", + ) + + nrpe_setup.write() + set_state('etcd.nrpe.configured') + + +@when_not('nrpe-external-master.available') +@when('nrpe-external-master.initial-config') +def remove_nrpe_config(nagios=None): + remove_state('nrpe-external-master.initial-config') + + # List of systemd services for which the checks will be removed + services = ('snap.etcd.etcd',) + + # The current nrpe-external-master interface doesn't handle a lot of logic, + # use the charm-helpers code for now. + hostname = nrpe.get_nagios_hostname() + nrpe_setup = nrpe.NRPE(hostname=hostname, primary=False) + + for service in services: + nrpe_setup.remove_check(shortname=service) + + +@when('endpoint.prometheus.joined', + 'leadership.is_leader', + 'certificates.ca.available') +def register_prometheus_jobs(): + # This function is not guarded with `when_not("prometheus.configured")` + # to account for possible changes of etcd units IP adresses and for when + # etcd units are added/removed. Repeated calls to `prometheus.register_job()` + # have no effect unless job_data changes. + log('Registering Prometheus metrics collection.') + prometheus = endpoint_from_flag('endpoint.prometheus.joined') + cluster = endpoint_from_flag('cluster.joined') + + peer_ips = cluster.get_db_ingress_addresses() if cluster else [] + peer_ips.append(get_ingress_address('db')) + targets = ["{}:{}".format(ip, config('port')) for ip in peer_ips] + log('Configuring Prometheus scrape targets: {}'.format(targets), DEBUG) + prometheus.register_job(job_name='etcd', + job_data={ + 'scheme': 'https', + 'static_configs': [ + {'targets': targets}, + ] + }) + set_flag('prometheus.configured') + + +@when( + "prometheus.configured", + "endpoint.grafana.joined", + "leadership.is_leader" +) +@when_not("grafana.configured") +def register_grafana_dashboard(): + log("Configuring grafana dashboard", level=hookenv.INFO) + grafana = endpoint_from_flag("endpoint.grafana.joined") + prometheus = endpoint_from_flag('endpoint.prometheus.joined') + + if not prometheus: + log( + "Prometheus relation not available. Skipping Grafana" + " configuration.", hookenv.WARNING) + return + + if len(prometheus.relations) > 1: + log( + "Multiple prometheus relations detected. Default Grafana dashboard" + " will configure only with one of them as datasource.", + hookenv.WARNING) + + datasource = prometheus.relations[0].application_name + dashboard = render_grafana_dashboard(datasource) + + log("Rendered Grafana dashboard:\n{}".format(json.dumps(dashboard)), + level=hookenv.DEBUG) + grafana.register_dashboard(name=GRAFANA_DASHBOARD_NAME, + dashboard=dashboard) + log('Grafana dashboard "{}" registered.'.format(GRAFANA_DASHBOARD_NAME)) + set_flag("grafana.configured") + + +def volume_is_mounted(volume): + ''' Takes a hardware path and returns true/false if it is mounted ''' + cmd = ['df', '-t', 'ext4'] + out = check_output(cmd).decode('utf-8') + return volume in out + + +def mount_volume(volume, location): + ''' Takes a device path and mounts it to location ''' + cmd = ['mount', volume, location] + hookenv.log("Mounting {0} to {1}".format(volume, location)) + check_call(cmd) + + +def unmount_path(location): + ''' Unmounts a mounted volume at path ''' + cmd = ['umount', location] + hookenv.log("Unmounting {0}".format(location)) + check_call(cmd) + + +def close_open_ports(): + ''' Close the previous port and open the port from configuration. ''' + configuration = hookenv.config() + previous_port = configuration.previous('port') + port = configuration.get('port') + if previous_port is not None and previous_port != port: + log('The port changed; closing {0} opening {1}'.format(previous_port, + port)) + close_port(previous_port) + open_port(port) + + +def install(src, tgt): + ''' This method wraps the bash "install" command ''' + return check_call(split('install {} {}'.format(src, tgt))) + + +def render_config(bag=None): + ''' Render the etcd configuration template for the given version ''' + if not bag: + bag = EtcdDatabag() + + move_etcd_data_to_standard_location() + + v2_conf_path = "{}/etcd.conf".format(bag.etcd_conf_dir) + v3_conf_path = "{}/etcd.conf.yml".format(bag.etcd_conf_dir) + + # probe for 2.x compatibility + if etcd_version().startswith('2.'): + render('etcd2.conf', v2_conf_path, bag.__dict__, owner='root', + group='root') + # default to 3.x template behavior + else: + render('etcd3.conf', v3_conf_path, bag.__dict__, owner='root', + group='root') + if os.path.exists(v2_conf_path): + # v3 will fail if the v2 config is left in place + os.remove(v2_conf_path) + # Close the previous client port and open the new one. + close_open_ports() + remove_state('etcd.rerender-config') + + +def etcd_version(): + ''' This method surfaces the version from etcdctl ''' + raw_output = None + try: + # try v3 + raw_output = check_output( + ['/snap/bin/etcd.etcdctl', 'version'], + env={'ETCDCTL_API': '3'} + ).decode('utf-8').strip() + if "No help topic for 'version'" in raw_output: + # handle v2 + raw_output = check_output( + ['/snap/bin/etcd.etcdctl', '--version'] + ).decode('utf-8').strip() + for line in raw_output.splitlines(): + if 'etcdctl version' in line: + # "etcdctl version: 3.0.17" or "etcdctl version 2.3.8" + version = line.split()[-1] + return version + hookenv.log('Unable to find etcd version: {}'.format(raw_output), + level=hookenv.ERROR) + return 'n/a' + except (ValueError, CalledProcessError): + hookenv.log('Failed to get etcd version:\n' + '{}'.format(traceback.format_exc()), level=hookenv.ERROR) + return 'n/a' + + +def move_etcd_data_to_standard_location(): + ''' Moves etcd data to the standard location if it's not already located + there. This is necessary when generating new etcd config after etcd has + been upgraded from version 2.3 to 3.x. + ''' + bag = EtcdDatabag() + conf_path = bag.etcd_conf_dir + '/etcd.conf.yml' + if not os.path.exists(conf_path): + return + with open(conf_path) as f: + conf = yaml.safe_load(f) + data_dir = conf['data-dir'] + desired_data_dir = bag.etcd_data_dir + if data_dir != desired_data_dir: + log('Moving etcd data from %s to %s' % (data_dir, desired_data_dir)) + host.service_stop('snap.etcd.etcd') + for filename in os.listdir(data_dir): + os.rename( + data_dir + '/' + filename, + desired_data_dir + '/' + filename + ) + os.rmdir(data_dir) + conf['data-dir'] = desired_data_dir + with open(conf_path, 'w') as f: + yaml.dump(conf, f) + host.service_start('snap.etcd.etcd') diff --git a/etcd/reactive/leadership.py b/etcd/reactive/leadership.py new file mode 100644 index 0000000..29c6f3a --- /dev/null +++ b/etcd/reactive/leadership.py @@ -0,0 +1,68 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Leadership Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.leadership import leader_get, leader_set + + +__all__ = ['leader_get', 'leader_set'] # Backwards compatibility + + +def initialize_leadership_state(): + '''Initialize leadership.* states from the hook environment. + + Invoked by hookenv.atstart() so states are available in + @hook decorated handlers. + ''' + is_leader = hookenv.is_leader() + if is_leader: + hookenv.log('Initializing Leadership Layer (is leader)') + else: + hookenv.log('Initializing Leadership Layer (is follower)') + + reactive.helpers.toggle_state('leadership.is_leader', is_leader) + + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + current = hookenv.leader_get() + + # Handle deletions. + for key in set(previous.keys()) - set(current.keys()): + current[key] = None + + any_changed = False + for key, value in current.items(): + reactive.helpers.toggle_state('leadership.changed.{}'.format(key), + value != previous.get(key)) + if value != previous.get(key): + any_changed = True + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + reactive.helpers.toggle_state('leadership.changed', any_changed) + + unitdata.kv().update(current, prefix='leadership.settings.') + + +# 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, '_leadership_registered'): + hookenv.atstart(initialize_leadership_state) + reactive._leadership_registered = True diff --git a/etcd/reactive/snap.py b/etcd/reactive/snap.py new file mode 100644 index 0000000..1fda7b7 --- /dev/null +++ b/etcd/reactive/snap.py @@ -0,0 +1,349 @@ +# Copyright 2016-2019 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. +""" +from collections import OrderedDict +from distutils.version import LooseVersion +import os.path +from os import uname +import shutil +import subprocess +from textwrap import dedent +import time +from urllib.request import urlretrieve + +from charmhelpers.core import hookenv, host +from charmhelpers.core.hookenv import ERROR +from charmhelpers.core.host import write_file +from charms import layer +from charms import reactive +from charms.layer import snap +from charms.reactive import register_trigger, when, when_not, toggle_flag +from charms.reactive.helpers import data_changed + + +class UnsatisfiedMinimumVersionError(Exception): + def __init__(self, desired, actual): + super().__init__() + self.desired = desired + self.actual = actual + + def __str__(self): + return "Could not install snapd >= {0.desired}, got {0.actual}".format(self) + + +class InvalidBundleError(Exception): + pass + + +def sorted_snap_opts(): + opts = layer.options("snap") + opts = sorted(opts.items(), key=lambda item: item[0] != "core") + opts = OrderedDict(opts) + return opts + + +def install(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + opts = sorted_snap_opts() + # supported-architectures is EXPERIMENTAL and undocumented. + # It probably should live in the base layer, blocking the charm + # during bootstrap if the arch is unsupported. + arch = uname().machine + for snapname, snap_opts in opts.items(): + supported_archs = snap_opts.pop("supported-architectures", None) + if supported_archs and arch not in supported_archs: + # Note that this does *not* error. The charm will need to + # cope with the snaps it requested never getting installed, + # likely by doing its own check on supported-architectures. + hookenv.log( + "Snap {} not supported on {!r} architecture" "".format(snapname, arch), + ERROR, + ) + continue + installed_flag = "snap.installed.{}".format(snapname) + if not reactive.is_flag_set(installed_flag): + snap.install(snapname, **snap_opts) + if data_changed("snap.install.opts", opts): + snap.connect_all() + + +def check_refresh_available(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + available_refreshes = snap.get_available_refreshes() + for snapname in snap.get_installed_snaps(): + toggle_flag(snap.get_refresh_available_flag(snapname), snapname in available_refreshes) + + +def refresh(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + opts = sorted_snap_opts() + # supported-architectures is EXPERIMENTAL and undocumented. + # It probably should live in the base layer, blocking the charm + # during bootstrap if the arch is unsupported. + arch = uname()[4] + check_refresh_available() + for snapname, snap_opts in opts.items(): + supported_archs = snap_opts.pop("supported-architectures", None) + if supported_archs and arch not in supported_archs: + continue + snap.refresh(snapname, **snap_opts) + snap.connect_all() + + +@reactive.hook("upgrade-charm") +def upgrade_charm(): + refresh() + + +def get_series(): + return subprocess.check_output(["lsb_release", "-sc"], universal_newlines=True).strip() + + +def snapd_supported(): + # snaps are not supported in trusty lxc containers. + if get_series() == "trusty" and host.is_container(): + return False + return True # For all other cases, assume true. + + +def kernel_supported(): + kernel_version = uname().release + + if LooseVersion(kernel_version) < LooseVersion("4.4"): + hookenv.log( + "Snaps do not work on kernel {}, a reboot " + "into a supported kernel (>4.4) is required" + "".format(kernel_version) + ) + return False + return True + + +def ensure_snapd(): + if not snapd_supported(): + hookenv.log("Snaps do not work in this environment", hookenv.ERROR) + raise Exception("Snaps do not work in this environment") + + # 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"): + os.environ["DEBIAN_FRONTEND"] = "noninteractive" + cmd = ["apt-get", "install", "-y", "snapd"] + # LP:1699986: Force install of systemd on Trusty. + if get_series() == "trusty": + cmd.append("systemd") + 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 host.is_container() and not shutil.which("squashfuse"): + os.environ["DEBIAN_FRONTEND"] = "noninteractive" + cmd = ["apt-get", "install", "-y", "squashfuse", "fuse"] + subprocess.check_call(cmd, universal_newlines=True) + + +def proxy_settings(): + proxy_vars = ("http_proxy", "https_proxy") + proxy_env = {key: value for key, value in os.environ.items() if key in proxy_vars} + + snap_proxy = hookenv.config().get("snap_proxy") + if snap_proxy: + proxy_env["http_proxy"] = snap_proxy + proxy_env["https_proxy"] = snap_proxy + return proxy_env + + +def update_snap_proxy(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + # 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 = proxy_settings() + + override_dir = "/etc/systemd/system/snapd.service.d" + path = os.path.join(override_dir, "snap_layer_proxy.conf") + if not proxy and not os.path.exists(path): + return # No proxy asked for and proxy never configured. + + # It seems we cannot rely on this directory existing, so manually + # create it. + if not os.path.exists(override_dir): + host.mkdir(override_dir, perms=0o755) + + if not data_changed("snap.proxy", proxy): + return # Short circuit avoids unnecessary restarts. + + 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] + """ + ) + for proxy_key, proxy_value in proxy.items(): + content += "Environment={}={}\n".format(proxy_key, proxy_value) + 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" + + +def _get_snapd_version(): + stdout = subprocess.check_output(["snap", "version"], stdin=subprocess.DEVNULL, universal_newlines=True) + version_info = dict(line.split(None, 1) for line in stdout.splitlines()) + return LooseVersion(version_info["snapd"]) + + +PREFERENCES = """\ +Package: * +Pin: release a={}-proposed +Pin-Priority: 400 +""" + + +def ensure_snapd_min_version(min_version): + snapd_version = _get_snapd_version() + if snapd_version < LooseVersion(min_version): + from charmhelpers.fetch import add_source, apt_update, apt_install + + # Temporary until LP:1735344 lands + add_source("distro-proposed", fail_invalid=True) + distro = get_series() + # disable proposed by default, needs to explicit + write_file( + "/etc/apt/preferences.d/proposed", + PREFERENCES.format(distro), + ) + apt_update() + # explicitly install snapd from proposed + apt_install("snapd/{}-proposed".format(distro)) + snapd_version = _get_snapd_version() + if snapd_version < LooseVersion(min_version): + hookenv.log("Failed to install snapd >= {}".format(min_version), ERROR) + raise UnsatisfiedMinimumVersionError(min_version, snapd_version) + + +def download_assertion_bundle(proxy_url): + """Download proxy assertion bundle and store id""" + assertions_url = "{}/v2/auth/store/assertions".format(proxy_url) + local_bundle, headers = urlretrieve(assertions_url) + store_id = headers["X-Assertion-Store-Id"] + return local_bundle, store_id + + +def configure_snap_store_proxy(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + if not reactive.is_flag_set("config.changed.snap_proxy_url"): + return + config = hookenv.config() + if "snap_proxy_url" not in config: + # The deprecated snap_proxy_url config items have been removed + # from config.yaml. If the charm author hasn't added them back + # explicitly, there is nothing to do. Juju is maintaining these + # settings as model configuration. + return + snap_store_proxy_url = config.get("snap_proxy_url") + if not snap_store_proxy_url and not config.previous("snap_proxy_url"): + # Proxy url is not set, and was not set previous hook. Do nothing, + # to avoid overwriting the Juju maintained setting. + return + ensure_snapd_min_version("2.30") + if snap_store_proxy_url: + bundle, store_id = download_assertion_bundle(snap_store_proxy_url) + try: + subprocess.check_output( + ["snap", "ack", bundle], + stdin=subprocess.DEVNULL, + universal_newlines=True, + ) + except subprocess.CalledProcessError as e: + raise InvalidBundleError("snapd could not ack the proxy assertion: " + e.output) + else: + store_id = "" + + try: + subprocess.check_output( + ["snap", "set", "core", "proxy.store={}".format(store_id)], + stdin=subprocess.DEVNULL, + universal_newlines=True, + ) + except subprocess.CalledProcessError as e: + raise InvalidBundleError("Proxy ID from header did not match store assertion: " + e.output) + + +register_trigger(when="config.changed.snapd_refresh", clear_flag="snap.refresh.set") + + +@when_not("snap.refresh.set") +@when("snap.installed.core") +def change_snapd_refresh(): + """Set the system refresh.timer option""" + ensure_snapd_min_version("2.31") + timer = hookenv.config()["snapd_refresh"] + was_set = reactive.is_flag_set("snap.refresh.was-set") + if timer or was_set: + snap.set_refresh_timer(timer) + reactive.toggle_flag("snap.refresh.was-set", timer) + reactive.set_flag("snap.refresh.set") + + +# Bootstrap. We don't use standard reactive handlers to ensure that +# everything is bootstrapped before any charm handlers are run. +hookenv.atstart(hookenv.log, "Initializing Snap Layer") +hookenv.atstart(ensure_snapd) +hookenv.atstart(ensure_path) +hookenv.atstart(update_snap_proxy) +hookenv.atstart(configure_snap_store_proxy) +hookenv.atstart(install) diff --git a/etcd/reactive/status.py b/etcd/reactive/status.py new file mode 100644 index 0000000..2f33f3f --- /dev/null +++ b/etcd/reactive/status.py @@ -0,0 +1,4 @@ +from charms import layer + + +layer.status._initialize() diff --git a/etcd/reactive/tls_client.py b/etcd/reactive/tls_client.py new file mode 100644 index 0000000..afa2228 --- /dev/null +++ b/etcd/reactive/tls_client.py @@ -0,0 +1,208 @@ +import os + +from pathlib import Path +from subprocess import check_call + +from charms import layer +from charms.reactive import hook +from charms.reactive import set_state, remove_state +from charms.reactive import when +from charms.reactive import set_flag, clear_flag +from charms.reactive import endpoint_from_flag +from charms.reactive.helpers import data_changed + +from charmhelpers.core import hookenv, unitdata +from charmhelpers.core.hookenv import log + + +@when('certificates.ca.available') +def store_ca(tls): + '''Read the certificate authority from the relation object and install + the ca on this system.''' + # Get the CA from the relationship object. + certificate_authority = tls.get_ca() + if certificate_authority: + layer_options = layer.options('tls-client') + ca_path = layer_options.get('ca_certificate_path') + changed = data_changed('certificate_authority', certificate_authority) + if ca_path: + if changed or not os.path.exists(ca_path): + log('Writing CA certificate to {0}'.format(ca_path)) + # ensure we have a newline at the end of the certificate. + # some things will blow up without one. + # See https://bugs.launchpad.net/charm-kubernetes-master/+bug/1828034 + if not certificate_authority.endswith('\n'): + certificate_authority += '\n' + _write_file(ca_path, certificate_authority) + set_state('tls_client.ca.written') + set_state('tls_client.ca.saved') + if changed: + # Update /etc/ssl/certs and generate ca-certificates.crt + install_ca(certificate_authority) + + +@when('certificates.server.cert.available') +def store_server(tls): + '''Read the server certificate and server key from the relation object + and save them to the certificate directory..''' + server_cert, server_key = tls.get_server_cert() + chain = tls.get_chain() + if chain: + server_cert = server_cert + '\n' + chain + if server_cert and server_key: + layer_options = layer.options('tls-client') + cert_path = layer_options.get('server_certificate_path') + key_path = layer_options.get('server_key_path') + cert_changed = data_changed('server_certificate', server_cert) + key_changed = data_changed('server_key', server_key) + if cert_path: + if cert_changed or not os.path.exists(cert_path): + log('Writing server certificate to {0}'.format(cert_path)) + _write_file(cert_path, server_cert) + set_state('tls_client.server.certificate.written') + set_state('tls_client.server.certificate.saved') + if key_path: + if key_changed or not os.path.exists(key_path): + log('Writing server key to {0}'.format(key_path)) + _write_file(key_path, server_key) + set_state('tls_client.server.key.saved') + + +@when('certificates.client.cert.available') +def store_client(tls): + '''Read the client certificate and client key from the relation object + and copy them to the certificate directory.''' + client_cert, client_key = tls.get_client_cert() + chain = tls.get_chain() + if chain: + client_cert = client_cert + '\n' + chain + if client_cert and client_key: + layer_options = layer.options('tls-client') + cert_path = layer_options.get('client_certificate_path') + key_path = layer_options.get('client_key_path') + cert_changed = data_changed('client_certificate', client_cert) + key_changed = data_changed('client_key', client_key) + if cert_path: + if cert_changed or not os.path.exists(cert_path): + log('Writing client certificate to {0}'.format(cert_path)) + _write_file(cert_path, client_cert) + set_state('tls_client.client.certificate.written') + set_state('tls_client.client.certificate.saved') + if key_path: + if key_changed or not os.path.exists(key_path): + log('Writing client key to {0}'.format(key_path)) + _write_file(key_path, client_key) + set_state('tls_client.client.key.saved') + + +@when('certificates.certs.changed') +def update_certs(): + tls = endpoint_from_flag('certificates.certs.changed') + certs_paths = unitdata.kv().get('layer.tls-client.cert-paths', {}) + all_ready = True + any_changed = False + maps = { + 'server': tls.server_certs_map, + 'client': tls.client_certs_map, + } + + if maps.get('client') == {}: + log( + 'No client certs found using maps. Checking for global \ + client certificates.', + 'WARNING' + ) + # Check for global certs, + # Backwards compatibility https://bugs.launchpad.net/charm-kubernetes-master/+bug/1825819 + cert_pair = tls.get_client_cert() + if cert_pair is not None: + for client_name in certs_paths.get('client', {}).keys(): + maps.get('client').update({ + client_name: cert_pair + }) + + chain = tls.get_chain() + for cert_type in ('server', 'client'): + for common_name, paths in certs_paths.get(cert_type, {}).items(): + cert_pair = maps[cert_type].get(common_name) + if not cert_pair: + all_ready = False + continue + if not data_changed('layer.tls-client.' + '{}.{}'.format(cert_type, common_name), cert_pair): + continue + + cert = None + key = None + if type(cert_pair) is not tuple: + if paths['crt']: + cert = cert_pair.cert + if paths['key']: + key = cert_pair.key + else: + cert, key = cert_pair + + if cert: + if chain: + cert = cert + '\n' + chain + _ensure_directory(paths['crt']) + Path(paths['crt']).write_text(cert) + + if key: + _ensure_directory(paths['key']) + Path(paths['key']).write_text(key) + + any_changed = True + # clear flags first to ensure they are re-triggered if left set + clear_flag('tls_client.{}.certs.changed'.format(cert_type)) + clear_flag('tls_client.{}.cert.{}.changed'.format(cert_type, + common_name)) + set_flag('tls_client.{}.certs.changed'.format(cert_type)) + set_flag('tls_client.{}.cert.{}.changed'.format(cert_type, + common_name)) + if all_ready: + set_flag('tls_client.certs.saved') + if any_changed: + clear_flag('tls_client.certs.changed') + set_flag('tls_client.certs.changed') + clear_flag('certificates.certs.changed') + + +def install_ca(certificate_authority): + '''Install a certificiate authority on the system by calling the + update-ca-certificates command.''' + if certificate_authority: + name = hookenv.service_name() + # Create a path to install CAs on Debian systems. + ca_path = '/usr/local/share/ca-certificates/{0}.crt'.format(name) + log('Writing CA certificate to {0}'.format(ca_path)) + _write_file(ca_path, certificate_authority) + # Update the trusted CAs on this system (a time expensive operation). + check_call(['update-ca-certificates']) + log('Generated ca-certificates.crt for {0}'.format(name)) + set_state('tls_client.ca_installed') + + +@hook('upgrade-charm') +def remove_states(): + remove_state('tls_client.ca.saved') + remove_state('tls_client.server.certificate.saved') + remove_state('tls_client.server.key.saved') + remove_state('tls_client.client.certificate.saved') + remove_state('tls_client.client.key.saved') + + +def _ensure_directory(path): + '''Ensure the parent directory exists creating directories if necessary.''' + directory = os.path.dirname(path) + if not os.path.isdir(directory): + os.makedirs(directory) + os.chmod(directory, 0o770) + + +def _write_file(path, content): + '''Write the path to a file.''' + _ensure_directory(path) + with open(path, 'w') as stream: + stream.write(content) + os.chmod(path, 0o440) diff --git a/etcd/requirements.txt b/etcd/requirements.txt new file mode 100644 index 0000000..55543d9 --- /dev/null +++ b/etcd/requirements.txt @@ -0,0 +1,3 @@ +mock +flake8 +pytest diff --git a/etcd/revision b/etcd/revision new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/etcd/revision @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/etcd/setup.py b/etcd/setup.py new file mode 100755 index 0000000..b30bff5 --- /dev/null +++ b/etcd/setup.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +import os +from setuptools import setup + +here = os.path.abspath(os.path.dirname(__file__)) + +with open(os.path.join(here, "README.md")) as f: + README = f.read() + +setup( + name="layer_snap", + version="1.0.0", + description="layer_snap", + long_description=README, + license="Apache License 2.0", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + ], + url="https://git.launchpad.net/layer-snap", + package_dir={"": "lib"}, + packages=["charms/layer"], + include_package_data=True, + zip_safe=False, + install_requires=["charmhelpers", "charms.reactive"], +) diff --git a/etcd/templates/cdk-service-kicker b/etcd/templates/cdk-service-kicker new file mode 100644 index 0000000..26d3740 --- /dev/null +++ b/etcd/templates/cdk-service-kicker @@ -0,0 +1,34 @@ +#!/bin/sh +set -eu + +# This service runs on boot to work around issues relating to LXD and snapd. + +# Workaround for https://github.com/conjure-up/conjure-up/issues/1448 +if [ -f '/proc/1/environ' ] && grep -q '^container=lxc' /proc/1/environ; then + echo "lxc detected, applying snapd apparmor profiles" + (set +e + apparmor_parser /var/lib/snapd/apparmor/profiles/* + echo "apparmor_parser: exit status $?" + ) +else + echo "lxc not detected, skipping snapd apparmor profiles" +fi + +# Workaround for https://github.com/juju-solutions/bundle-canonical-kubernetes/issues/357 +services="{{services}}" + +deadline="$(expr "$(date +%s)" + 600)" + +while [ "$(date +%s)" -lt "$deadline" ]; do + for service in $services; do + echo "$service: checking" + if ! systemctl is-active "$service"; then + echo "$service: not active, restarting" + systemctl restart "$service" || true + fi + done + + sleep 10 +done + +echo "deadline has passed, exiting gracefully" diff --git a/etcd/templates/cdk-service-kicker.service b/etcd/templates/cdk-service-kicker.service new file mode 100644 index 0000000..5c2105e --- /dev/null +++ b/etcd/templates/cdk-service-kicker.service @@ -0,0 +1,10 @@ +[Unit] +Description=cdk-service-kicker + +[Service] +ExecStart=/usr/bin/cdk-service-kicker +Restart=on-failure +Type=simple + +[Install] +WantedBy=multi-user.target diff --git a/etcd/templates/check_etcd-alarms.cron b/etcd/templates/check_etcd-alarms.cron new file mode 100644 index 0000000..5fc0365 --- /dev/null +++ b/etcd/templates/check_etcd-alarms.cron @@ -0,0 +1,2 @@ +# check_etcd_alarms +* * * * * root [ -x /snap/bin/etcdctl ] && ETCDCTL_API=3 /snap/bin/etcdctl --endpoints=127.0.0.1:4001 alarm list > /var/lib/nagios/etcd-alarm-list.txt diff --git a/etcd/templates/check_etcd-alarms.py b/etcd/templates/check_etcd-alarms.py new file mode 100644 index 0000000..1c7c7b9 --- /dev/null +++ b/etcd/templates/check_etcd-alarms.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2020 Canonical Ltd. + +import nagios_plugin3 + + +def load_alarm_list(): + """Load the cached status from disk, return it as a string""" + alarm_list_path = '/var/lib/nagios/etcd-alarm-list.txt' + + with open(alarm_list_path, 'r') as alarm_list_log: + alarm_list = alarm_list_log.read() + + return alarm_list.strip() + + +def check_alarms(): + """Raise an error if the cached status contains any non-blank lines""" + alarms = [] + alarm_list = load_alarm_list() + for line in alarm_list.splitlines(): + line = line.strip() + if line: + alarms.append(line) + if alarms: + raise nagios_plugin3.CriticalError(' '.join(alarms)) + + +def main(): + nagios_plugin3.try_check(check_alarms) + print("OK - no active alarms") + + +if __name__ == "__main__": + main() diff --git a/etcd/templates/etcd2.conf b/etcd/templates/etcd2.conf new file mode 100644 index 0000000..0e97a28 --- /dev/null +++ b/etcd/templates/etcd2.conf @@ -0,0 +1,23 @@ +# This file is rendered by Juju, manual edits will not be persisted +ETCD_DATA_DIR={{ etcd_data_dir }}/{{ unit_name }}.etcd +ETCD_NAME={{ unit_name }} +ETCD_ADVERTISE_CLIENT_URLS="https://{{ db_address }}:{{ port }}" +ETCD_LISTEN_CLIENT_URLS="http://127.0.0.1:4001,https://{{ db_bind_address }}:{{ port }}" +ETCD_LISTEN_PEER_URLS="https://{{ cluster_bind_address }}:{{ management_port }}" +ETCD_INITIAL_ADVERTISE_PEER_URLS="https://{{ cluster_address }}:{{ management_port }}" +{% if cluster %} +ETCD_INITIAL_CLUSTER="{{ cluster }}" +ETCD_INITIAL_CLUSTER_STATE={{ cluster_state }} +ETCD_INITIAL_CLUSTER_TOKEN={{ token }} +{% endif %} +# SSL CONFIGURATION +ETCD_CERT_FILE={{ server_certificate }} +ETCD_KEY_FILE={{ server_key }} +ETCD_TRUSTED_CA_FILE={{ ca_certificate }} +ETCD_PEER_CERT_FILE={{ server_certificate }} +ETCD_PEER_KEY_FILE={{ server_key }} +ETCD_PEER_TRUSTED_CA_FILE={{ ca_certificate }} +# SSL Strict Mode +ETCD_PEER_CLIENT_CERT_AUTH=true +ETCD_CLIENT_CERT_AUTH=true +ETCD_STRICT_RECONFIG_CHECK=true diff --git a/etcd/templates/etcd3.conf b/etcd/templates/etcd3.conf new file mode 100644 index 0000000..754c917 --- /dev/null +++ b/etcd/templates/etcd3.conf @@ -0,0 +1,141 @@ +# This is the configuration file for the etcd server. + +# Human-readable name for this member. +name: {{ unit_name }} + +# Enable API v2 support for flannel and +# certain charm executions. +enable-v2: true + +# Path to the data directory. +data-dir: {{ etcd_data_dir }} + +{% if wal_path %} +# Path to the dedicated wal directory. +wal-dir: {{ etcd_data_dir }} +{% endif %} +# Number of committed transactions to trigger a snapshot to disk. +snapshot-count: 10000 + +# Time (in milliseconds) of a heartbeat interval. +heartbeat-interval: 100 + +# Time (in milliseconds) for an election to timeout. +election-timeout: 1000 + +# Raise alarms when backend size exceeds the given quota. 0 means use the +# default quota. +quota-backend-bytes: 0 + +# List of comma separated URLs to listen on for peer traffic. +listen-peer-urls: https://{{ cluster_bind_address }}:{{ management_port}} +# List of comma separated URLs to listen on for client traffic. +listen-client-urls: http://127.0.0.1:4001,https://{{ db_bind_address }}:{{ port }} + +# Maximum number of snapshot files to retain (0 is unlimited). +max-snapshots: 5 + +# Maximum number of wal files to retain (0 is unlimited). +max-wals: 5 + +# Comma-separated white list of origins for CORS (cross-origin resource sharing). +cors: + +# List of this member's peer URLs to advertise to the rest of the cluster. +# The URLs needed to be a comma-separated list. +initial-advertise-peer-urls: https://{{ cluster_address }}:{{ management_port }} + +# List of this member's client URLs to advertise to the public. +# The URLs needed to be a comma-separated list. +advertise-client-urls: https://{{ db_address }}:{{ port }} + +# Discovery URL used to bootstrap the cluster. +discovery: + +# Valid values include 'exit', 'proxy' +discovery-fallback: 'proxy' + +# HTTP proxy to use for traffic to discovery service. +discovery-proxy: + +# DNS domain used to bootstrap initial cluster. +discovery-srv: + +# Initial cluster configuration for bootstrapping. +initial-cluster: {{ cluster }} + +# Initial cluster token for the etcd cluster during bootstrap. +initial-cluster-token: '{{ token }}' + +# Initial cluster state ('new' or 'existing'). +initial-cluster-state: {{ cluster_state }} + +# Reject reconfiguration requests that would cause quorum loss. +strict-reconfig-check: true + +# Valid values include 'on', 'readonly', 'off' +proxy: 'off' + +# Time (in milliseconds) an endpoint will be held in a failed state. +proxy-failure-wait: 5000 + +# Time (in milliseconds) of the endpoints refresh interval. +proxy-refresh-interval: 30000 + +# Time (in milliseconds) for a dial to timeout. +proxy-dial-timeout: 1000 + +# Time (in milliseconds) for a write to timeout. +proxy-write-timeout: 5000 + +# Time (in milliseconds) for a read to timeout. +proxy-read-timeout: 0 + +client-transport-security: + # DEPRECATED: Path to the client server TLS CA file. + # ca-file: {{ ca_certificate }} + + # Path to the client server TLS cert file. + cert-file: {{ server_certificate }} + + # Path to the client server TLS key file. + key-file: {{ server_key }} + + # Enable client cert authentication. + client-cert-auth: true + + # Path to the client server TLS trusted CA key file. + trusted-ca-file: {{ ca_certificate }} + + # Client TLS using generated certificates + auto-tls: false + +peer-transport-security: + # DEPRECATED: Path to the peer server TLS CA file. + # ca-file: {{ ca_certificate }} + + # Path to the peer server TLS cert file. + cert-file: {{ server_certificate }} + + # Path to the peer server TLS key file. + key-file: {{ server_key }} + + # Enable peer client cert authentication. + client-cert-auth: true + + # Path to the peer server TLS trusted CA key file. + trusted-ca-file: {{ ca_certificate }} + + # Peer TLS using generated certificates. + auto-tls: false + +# Enable debug-level logging for etcd. +debug: false + +{% if loglevel %} +# Specify a particular log level for each etcd package (eg: 'etcdmain=CRITICAL,etcdserver=DEBUG'. +log-package-levels: +{% endif %} + +# Force to create a new one member cluster. +force-new-cluster: false diff --git a/etcd/templates/grafana_dashboard.json.j2 b/etcd/templates/grafana_dashboard.json.j2 new file mode 100644 index 0000000..e6f1046 --- /dev/null +++ b/etcd/templates/grafana_dashboard.json.j2 @@ -0,0 +1,1124 @@ +{ + "__inputs": [], + "annotations": { + "list": [] + }, + "description": "etcd sample Grafana dashboard with Prometheus", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "hideControls": false, + "id": null, + "links": [], + "refresh": false, + "rows": [ + { + "collapse": false, + "height": "250px", + "panels": [ + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "<< datasource >>", + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "id": 28, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(etcd_server_has_leader)", + "format": "time_series", + "instant": true, + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 20 + } + ], + "thresholds": "", + "title": "Up", + "type": "singlestat", + "valueFontSize": "200%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "editable": true, + "error": false, + "fill": 0, + "id": 23, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 5, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(grpc_server_started_total{grpc_type=\"unary\"}[5m]))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "RPC Rate", + "metric": "grpc_server_started_total", + "refId": "A", + "step": 4 + }, + { + "expr": "sum(rate(grpc_server_handled_total{grpc_type=\"unary\",grpc_code!=\"OK\"}[5m]))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "RPC Failed Rate", + "metric": "grpc_server_handled_total", + "refId": "B", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "RPC Rate", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "editable": true, + "error": false, + "fill": 0, + "id": 41, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 4, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "sum(grpc_server_started_total{grpc_service=\"etcdserverpb.Watch\",grpc_type=\"bidi_stream\"}) - sum(grpc_server_handled_total{grpc_service=\"etcdserverpb.Watch\",grpc_type=\"bidi_stream\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Watch Streams", + "metric": "grpc_server_handled_total", + "refId": "A", + "step": 4 + }, + { + "expr": "sum(grpc_server_started_total{grpc_service=\"etcdserverpb.Lease\",grpc_type=\"bidi_stream\"}) - sum(grpc_server_handled_total{grpc_service=\"etcdserverpb.Lease\",grpc_type=\"bidi_stream\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Lease Streams", + "metric": "grpc_server_handled_total", + "refId": "B", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Active Streams", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": false, + "title": "Row", + "titleSize": "h6" + }, + { + "collapse": false, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "decimals": null, + "editable": true, + "error": false, + "fill": 0, + "grid": {}, + "id": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "etcd_debugging_mvcc_db_total_size_in_bytes", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{instance}} DB Size", + "metric": "", + "refId": "A", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "DB Size", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "editable": true, + "error": false, + "fill": 0, + "grid": {}, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 1, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": true, + "targets": [ + { + "expr": "histogram_quantile(0.99, sum(rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])) by (instance, le))", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "{{instance}} WAL fsync", + "metric": "etcd_disk_wal_fsync_duration_seconds_bucket", + "refId": "A", + "step": 4 + }, + { + "expr": "histogram_quantile(0.99, sum(rate(etcd_disk_backend_commit_duration_seconds_bucket[5m])) by (instance, le))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}} DB fsync", + "metric": "etcd_disk_backend_commit_duration_seconds_bucket", + "refId": "B", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Disk Sync Duration", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "editable": true, + "error": false, + "fill": 0, + "id": 29, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "process_resident_memory_bytes", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}} Resident Memory", + "metric": "process_resident_memory_bytes", + "refId": "A", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Memory", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": false, + "title": "New row", + "titleSize": "h6" + }, + { + "collapse": false, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "editable": true, + "error": false, + "fill": 5, + "id": 22, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 3, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "rate(etcd_network_client_grpc_received_bytes_total[5m])", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}} Client Traffic In", + "metric": "etcd_network_client_grpc_received_bytes_total", + "refId": "A", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Client Traffic In", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "editable": true, + "error": false, + "fill": 5, + "id": 21, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 3, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "rate(etcd_network_client_grpc_sent_bytes_total[5m])", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}} Client Traffic Out", + "metric": "etcd_network_client_grpc_sent_bytes_total", + "refId": "A", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Client Traffic Out", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "editable": true, + "error": false, + "fill": 0, + "id": 20, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 3, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(etcd_network_peer_received_bytes_total[5m])) by (instance)", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}} Peer Traffic In", + "metric": "etcd_network_peer_received_bytes_total", + "refId": "A", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Peer Traffic In", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "decimals": null, + "editable": true, + "error": false, + "fill": 0, + "grid": {}, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 3, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(etcd_network_peer_sent_bytes_total[5m])) by (instance)", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{instance}} Peer Traffic Out", + "metric": "etcd_network_peer_sent_bytes_total", + "refId": "A", + "step": 4 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Peer Traffic Out", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "Bps", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": false, + "title": "New row", + "titleSize": "h6" + }, + { + "collapse": false, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "editable": true, + "error": false, + "fill": 0, + "id": 40, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(etcd_server_proposals_failed_total[5m]))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Proposal Failure Rate", + "metric": "etcd_server_proposals_failed_total", + "refId": "A", + "step": 2 + }, + { + "expr": "sum(etcd_server_proposals_pending)", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Proposal Pending Total", + "metric": "etcd_server_proposals_pending", + "refId": "B", + "step": 2 + }, + { + "expr": "sum(rate(etcd_server_proposals_committed_total[5m]))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Proposal Commit Rate", + "metric": "etcd_server_proposals_committed_total", + "refId": "C", + "step": 2 + }, + { + "expr": "sum(rate(etcd_server_proposals_applied_total[5m]))", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "Proposal Apply Rate", + "refId": "D", + "step": 2 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Raft Proposals", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "<< datasource >>", + "decimals": 0, + "editable": true, + "error": false, + "fill": 0, + "id": 19, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": false, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "changes(etcd_server_leader_changes_seen_total[1d])", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{instance}} Total Leader Elections Per Day", + "metric": "etcd_server_leader_changes_seen_total", + "refId": "A", + "step": 2 + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Total Leader Elections Per Day", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": false, + "title": "New row", + "titleSize": "h6" + } + ], + "schemaVersion": 14, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": { + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "etcd", + "version": 2 +} diff --git a/etcd/templates/service-always-restart.systemd-229.conf b/etcd/templates/service-always-restart.systemd-229.conf new file mode 100644 index 0000000..d5cf4b1 --- /dev/null +++ b/etcd/templates/service-always-restart.systemd-229.conf @@ -0,0 +1,5 @@ +[Unit] +StartLimitInterval=0 + +[Service] +RestartSec=10 diff --git a/etcd/templates/service-always-restart.systemd-latest.conf b/etcd/templates/service-always-restart.systemd-latest.conf new file mode 100644 index 0000000..3dd37ab --- /dev/null +++ b/etcd/templates/service-always-restart.systemd-latest.conf @@ -0,0 +1,5 @@ +[Unit] +StartLimitIntervalSec=0 + +[Service] +RestartSec=10 diff --git a/etcd/tests/10-deploy.py b/etcd/tests/10-deploy.py new file mode 100755 index 0000000..4c336d8 --- /dev/null +++ b/etcd/tests/10-deploy.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +import amulet +import unittest +import re + + +class TestDeployment(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.d = amulet.Deployment(series='xenial') + cls.d.add('etcd') + cls.d.add('easyrsa', 'cs:~containers/easyrsa') + cls.d.configure('etcd', {'channel': '3.0/stable'}) + cls.d.relate('easyrsa:client', 'etcd:certificates') + cls.d.setup(timeout=1200) + cls.d.sentry.wait_for_messages({'etcd': + re.compile('Healthy*|Unhealthy*')}) + # cls.d.sentry.wait() + cls.etcd = cls.d.sentry['etcd'] + # find the leader + for unit in cls.etcd: + leader_result = unit.run('is-leader') + if leader_result[0] == 'True': + cls.leader = unit + + def test_leader_status(self): + ''' Verify our leader is running the etcd daemon ''' + status = self.leader.run('systemctl is-active snap.etcd.etcd') + self.assertFalse("inactive" in status[0]) + self.assertTrue("active" in status[0]) + + def test_config_snapd_refresh(self): + ''' Verify initial snap refresh config is set and can be changed ''' + # default timer should be some day of the week followed by a number + timer = self.leader.run('snap get core refresh.timer') + self.assertTrue(len(timer[0]) == len('dayX')) + + # verify a new timer value + self.d.configure('etcd', {'snapd_refresh': 'fri5'}) + self.d.sentry.wait() + timer = self.leader.run('snap get core refresh.timer') + self.assertTrue(timer[0] == 'fri5') + + def test_node_scale(self): + ''' Scale beyond 1 node because etcd supports peering as a standalone + application.''' + # Ensure we aren't testing a single node + if not len(self.etcd) > 1: + self.d.add_unit('etcd', timeout=1200) + self.d.sentry.wait() + + for unit in self.etcd: + status = unit.run('systemctl is-active snap.etcd.etcd') + self.assertFalse(status[1] == 1) + self.assertFalse("inactive" in status[0]) + self.assertTrue("active" in status[0]) + + def test_cluster_health(self): + ''' Iterate all the units and verify we have a clean bill of health + from etcd ''' + + certs = "ETCDCTL_KEY_FILE=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT_FILE=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CA_FILE=/var/snap/etcd/common/ca.crt " \ + "ETCDCTL_KEY=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CACERT=/var/snap/etcd/common/ca.crt" + + for unit in self.etcd: + cmd = '{} /snap/bin/etcdctl cluster-health'.format(certs) + health = unit.run(cmd) + self.assertTrue('unhealthy' not in health) + self.assertTrue('unavailable' not in health) + + def test_leader_knows_all_members(self): + ''' Test we have the same number of units deployed and reporting in + the etcd cluster as participating''' + + # The spacing here is semi-important as its a string of ENV exports + # also, this is hard coding for the defaults. if the defaults in + # layer.yaml change, this will need to change. + certs = "ETCDCTL_KEY_FILE=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT_FILE=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CA_FILE=/var/snap/etcd/common/ca.crt " \ + "ETCDCTL_KEY=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CACERT=/var/snap/etcd/common/ca.crt" + + # format the command, and execute on the leader + cmd = '{} etcdctl member list'.format(certs) + out = self.leader.run(cmd)[0] + # turn the output into a list so we can iterate + members = out.split('\n') + for item in members: + # this is responded when TLS is enabled and we don't have proper + # Keys. This is kind of a "ssl works test" but of the worst + # variety... assuming the full stack completed. + self.assertTrue('etcd cluster is unavailable' not in members) + self.assertTrue(len(members) == len(self.etcd)) + + def test_node_scale_down_members(self): + ''' Scale the cluster down and ensure the cluster state is still + healthy ''' + # Remove the leader + self.d.remove_unit(self.leader.info['unit_name']) + self.d.sentry.wait() + # re-use the cluster-health test to validate we are still healthy. + self.test_cluster_health() + + +if __name__ == '__main__': + unittest.main() diff --git a/etcd/tests/20-actions.py b/etcd/tests/20-actions.py new file mode 100755 index 0000000..28c651e --- /dev/null +++ b/etcd/tests/20-actions.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 + +import os +import re +import unittest +import subprocess + +import amulet + + +class TestActions(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.d = amulet.Deployment(series='xenial') + cls.d.add('etcd') + cls.d.add('easyrsa', 'cs:~containers/easyrsa') + cls.d.configure('etcd', {'channel': '3.0/stable'}) + cls.d.relate('easyrsa:client', 'etcd:certificates') + cls.d.setup(timeout=1200) + cls.d.sentry.wait_for_messages({'etcd': + re.compile('Healthy*|Unhealthy*')}) + # cls.d.sentry.wait() + cls.etcd = cls.d.sentry['etcd'] + + def test_health_check(self): + """ + Trigger health action + """ + action_id = self.etcd[0].run_action('health') + outcome = self.d.action_fetch(action_id, + timeout=7200, + raise_on_timeout=True, + full_output=True) + self.assertEqual(outcome['status'], 'completed') + self.assertTrue("cluster is healthy" in outcome['results']['result-map']['message']) + + def test_snapshot_restore(self): + """ + Trigger snapshot and restore actions + """ + # Load dummy data + self.load_data() + self.assertTrue(self.is_data_present('v2')) + self.assertTrue(self.is_data_present('v3')) + + filenames = {} + for dataset in ['v2', 'v3']: + # Take snapshot of data + action_id = self.etcd[0].run_action('snapshot', {'keys-version': dataset}) + outcome = self.d.action_fetch(action_id, + timeout=7200, + raise_on_timeout=True, + full_output=True) + self.assertEqual(outcome['status'], 'completed') + cpcmd = outcome['results']['copy']['cmd'] + subprocess.check_call(cpcmd.split()) + filenames[dataset] = os.path.basename(outcome['results']['snapshot']['path']) + + self.delete_data() + self.assertFalse(self.is_data_present('v2')) + self.assertFalse(self.is_data_present('v3')) + + # Restore v2 data + cmd = 'juju attach etcd snapshot=%s' % filenames['v2'] + subprocess.check_call(cmd.split()) + action_id = self.etcd[0].run_action('restore') + outcome = self.d.action_fetch(action_id, + timeout=7200, + raise_on_timeout=True, + full_output=True) + self.assertEqual(outcome['status'], 'completed') + self.assertTrue(self.is_data_present('v2')) + self.assertFalse(self.is_data_present('v3')) + + # Restore v3 data + cmd = 'juju attach etcd snapshot=%s' % filenames['v3'] + subprocess.check_call(cmd.split()) + action_id = self.etcd[0].run_action('restore') + outcome = self.d.action_fetch(action_id, + timeout=7200, + raise_on_timeout=True, + full_output=True) + self.assertEqual(outcome['status'], 'completed') + self.assertFalse(self.is_data_present('v2')) + self.assertTrue(self.is_data_present('v3')) + + def load_data(self): + """ + Load dummy data + + """ + certs = "ETCDCTL_KEY_FILE=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT_FILE=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CA_FILE=/var/snap/etcd/common/ca.crt " \ + "ETCDCTL_KEY=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CACERT=/var/snap/etcd/common/ca.crt" + + cmd = '{} ETCDCTL_API=2 /snap/bin/etcdctl set /etcd2key etcd2value'.format(certs) + self.etcd[0].run(cmd) + cmd = '{} ETCDCTL_API=3 /snap/bin/etcdctl --endpoints=http://localhost:4001 ' \ + 'put etcd3key etcd3value'.format(certs) + self.etcd[0].run(cmd) + + def is_data_present(self, version): + ''' + Check if we have the data present on the datastore of the version + Args: + version: v2 or v3 etcd datastore + + Returns: True if the data is present + + ''' + certs = "ETCDCTL_KEY_FILE=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT_FILE=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CA_FILE=/var/snap/etcd/common/ca.crt " \ + "ETCDCTL_KEY=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CACERT=/var/snap/etcd/common/ca.crt" + + if version == 'v2': + cmd = '{} ETCDCTL_API=2 /snap/bin/etcdctl ls'.format(certs) + data = self.etcd[0].run(cmd) + return 'etcd2key' in data[0] + elif version == 'v3': + cmd = '{} ETCDCTL_API=3 /snap/bin/etcdctl --endpoints=http://localhost:4001 ' \ + 'get "" --prefix --keys-only'.format(certs) + data = self.etcd[0].run(cmd) + return 'etcd3key' in data[0] + else: + return False + + def delete_data(self): + ''' + Delete all dummy data on etcd + ''' + certs = "ETCDCTL_KEY_FILE=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT_FILE=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CA_FILE=/var/snap/etcd/common/ca.crt " \ + "ETCDCTL_KEY=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CACERT=/var/snap/etcd/common/ca.crt" + + cmd = '{} ETCDCTL_API=2 /snap/bin/etcdctl rm /etcd2key'.format(certs) + self.etcd[0].run(cmd) + cmd = '{} ETCDCTL_API=3 /snap/bin/etcdctl --endpoints=http://localhost:4001 ' \ + 'del etcd3key'.format(certs) + self.etcd[0].run(cmd) + + +if __name__ == '__main__': + unittest.main() diff --git a/etcd/tests/30-deb-bundle.yml b/etcd/tests/30-deb-bundle.yml new file mode 100644 index 0000000..7a9c082 --- /dev/null +++ b/etcd/tests/30-deb-bundle.yml @@ -0,0 +1,11 @@ +series: xenial +applications: + easyrsa: + charm: cs:~containers/easyrsa + num_units: 1 + etcd: + charm: cs:etcd-24 + num_units: 1 +relations: + - - "etcd:certificates" + - "easyrsa:client" diff --git a/etcd/tests/30-deb-snap-migrate.py b/etcd/tests/30-deb-snap-migrate.py new file mode 100755 index 0000000..007e701 --- /dev/null +++ b/etcd/tests/30-deb-snap-migrate.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +import amulet +import os +import re +import unittest +import yaml + +from amulet.helpers import juju + +TEST_TIMEOUT = 600 + + +class TestDeployment(unittest.TestCase): + bundle_file = os.path.join(os.path.dirname(__file__), '30-deb-bundle.yml') + + @classmethod + def setUpClass(cls): + cls.d = amulet.Deployment(series='xenial') + + # Deploy the scenario from the bundle + with open(cls.bundle_file) as f: + bun = f.read() + bundle = yaml.safe_load(bun) + cls.d.load(bundle) + cls.d.setup(timeout=TEST_TIMEOUT) + + cls.etcd = cls.d.sentry['etcd'] + # This is a hacky work-around to amulet not supporting charm upgrades. + juju(['upgrade-charm', 'etcd', '--path', os.getcwd()]) + # This is kind of a litmus test. + cls.d.sentry.wait_for_messages({'etcd': + re.compile('snap-upgrade')}) + + # this is the legacy location of these TLS certs. As of rev-25 this is + # no longer the case, and this is safe to leave as is for the remainder + # of this tests lifecycle. + certs = "ETCDCTL_KEY_FILE=/etc/ssl/etcd/client.key " \ + "ETCDCTL_CERT_FILE=/etc/ssl/etcd/client.crt " \ + "ETCDCTL_CA_FILE=/etc/ssl/etcd/ca.crt" + + # preseed the deployment with some data keys before releasing execution + cls.etcd[0].run('{} etcdctl set juju rocks'.format(certs)) + cls.etcd[0].run('{} etcdctl set nested/data works'.format(certs)) + + def test_snap_action(self): + ''' When the charm is upgraded, a message should appear requesting the + user to run a manual upgrade.''' + + action_id = self.etcd[0].run_action('snap-upgrade') + # This by default waits 600 seconds, incrase in slower clouds. + out = self.d.get_action_output(action_id, full_output=True) + # This will be failed if the upgrade didnt work + assert 'completed' in out['status'] + # This will be missing if the operation bailed early + assert 'results' in out.keys() + self.validate_running_snap_daemon() + self.validate_etcd_fixture_data() + + def test_snap_upgrade_to_three_oh(self): + ''' Default configured channel is 2.3/stable. Ensure we can jump to + 3.0 ''' + self.d.configure('etcd', {'channel': '3.0/stable'}) + self.d.sentry.wait() + self.validate_running_snap_daemon() + self.validate_etcd_fixture_data() + + def validate_etcd_fixture_data(self): + ''' Recall data set by set_etcd_fixture_data to ensure it persisted + through the upgrade ''' + + # The spacing here is semi-important as its a string of ENV exports + # also, this is hard coding for the defaults. if the defaults in + # layer.yaml change, this will need to change. + certs = "ETCDCTL_KEY_FILE=/var/snap/etcd/common/client.key " \ + "ETCDCTL_CERT_FILE=/var/snap/etcd/common/client.crt " \ + "ETCDCTL_CA_FILE=/var/snap/etcd/common/ca.crt" + + jcmd = "{} /snap/bin/etcd.etcdctl get juju".format(certs) + juju_key = self.etcd[0].run(jcmd) + nscmd = "{} /snap/bin/etcd.etcdctl get nested/data".format(certs) + nested_key = self.etcd[0].run(nscmd) + + assert 'rocks' in juju_key[0] + assert 'works' in nested_key[0] + + def validate_running_snap_daemon(self): + ''' Validate the snap based etcd daemon is running after an op ''' + daemon_status = self.etcd[0].run('systemctl is-active snap.etcd.etcd') + assert 'active' in daemon_status[0] + + +if __name__ == '__main__': + unittest.main() diff --git a/etcd/tests/conftest.py b/etcd/tests/conftest.py new file mode 100644 index 0000000..97d7668 --- /dev/null +++ b/etcd/tests/conftest.py @@ -0,0 +1,5 @@ +import charms.unit_test + + +charms.unit_test.patch_reactive() +charms.unit_test.patch_module('charms.leadership') diff --git a/etcd/tests/snap-upgrade.yaml b/etcd/tests/snap-upgrade.yaml new file mode 100644 index 0000000..6fd8a3d --- /dev/null +++ b/etcd/tests/snap-upgrade.yaml @@ -0,0 +1,3 @@ +tests: "30-*" +reset: true +makefile: [] diff --git a/etcd/tests/tests.yaml b/etcd/tests/tests.yaml new file mode 100644 index 0000000..c8e5257 --- /dev/null +++ b/etcd/tests/tests.yaml @@ -0,0 +1,7 @@ +tests: "[1-2]0-*" +reset: false +makefile: + - lint +packages: + - amulet + - tox diff --git a/etcd/tox.ini b/etcd/tox.ini new file mode 100644 index 0000000..ea08877 --- /dev/null +++ b/etcd/tox.ini @@ -0,0 +1,28 @@ +[flake8] +max-line-length = 120 + +[tox] +skipsdist = True +envlist = lint,py3 + +[testenv] +basepython = python3 +setenv = + PYTHONPATH={toxinidir}:{toxinidir}/lib +deps = + jinja2 + netaddr<=0.7.19 + pyyaml + pytest + pytest-mock + pytest-cov + flake8 + charms.unit_test +commands = + pytest --cov-report term-missing \ + --cov lib --cov-fail-under 33 \ + --tb native -s {posargs} + +[testenv:lint] +envdir = {toxworkdir}/py3 +commands = flake8 {toxinidir}/reactive {toxinidir}/lib {toxinidir}/tests {toxinidir}/unit_tests diff --git a/etcd/unit_tests/lib/test_etcd_lib.py b/etcd/unit_tests/lib/test_etcd_lib.py new file mode 100644 index 0000000..174fdad --- /dev/null +++ b/etcd/unit_tests/lib/test_etcd_lib.py @@ -0,0 +1,19 @@ +from charmhelpers.contrib.templating import jinja + +from etcd_lib import render_grafana_dashboard + + +def test_render_grafana_dashboard(): + """Test loading of Grafana dashboard.""" + datasource = 'prometheus' + raw_template = ('{{"panels": [{{"datasource": "{} - ' + 'Juju generated source"}}]}}'.format(datasource)) + expected_dashboard = { + 'panels': [ + {'datasource': '{} - Juju generated source'.format(datasource)} + ]} + + jinja.render.return_value = raw_template + rendered_dashboard = render_grafana_dashboard(datasource) + + assert rendered_dashboard == expected_dashboard diff --git a/etcd/unit_tests/test_etcdctl.py b/etcd/unit_tests/test_etcdctl.py new file mode 100644 index 0000000..b766fc7 --- /dev/null +++ b/etcd/unit_tests/test_etcdctl.py @@ -0,0 +1,196 @@ +import pytest +from unittest.mock import patch, MagicMock + +import reactive.etcd + +from etcdctl import ( + EtcdCtl, + etcdctl_command, + get_connection_string, +) # noqa + +from etcd_databag import EtcdDatabag + +from reactive.etcd import ( + clear_flag, + endpoint_from_flag, + force_rejoin_requested, + force_rejoin, + GRAFANA_DASHBOARD_NAME, + host, + pre_series_upgrade, + post_series_upgrade, + register_grafana_dashboard, + register_prometheus_jobs, + status, +) + + +class TestEtcdCtl: + + @pytest.fixture + def etcdctl(self): + return EtcdCtl() + + def test_register(self, etcdctl): + with patch('etcdctl.EtcdCtl.run') as spcm: + etcdctl.register({'cluster_address': '127.0.0.1', + 'unit_name': 'etcd0', + 'management_port': '1313', + 'leader_address': 'http://127.1.1.1:1212'}) + spcm.assert_called_with('member add etcd0 https://127.0.0.1:1313', api=2, endpoints='http://127.1.1.1:1212') + + def test_unregister(self, etcdctl): + with patch('etcdctl.EtcdCtl.run') as spcm: + etcdctl.unregister('br1212121212') + + spcm.assert_called_with(['member', 'remove', 'br1212121212'], api=2, endpoints=None) + + def test_member_list(self, etcdctl): + with patch('etcdctl.EtcdCtl.run') as comock: + comock.return_value = '7dc8404daa2b8ca0: name=etcd22 peerURLs=https://10.113.96.220:2380 clientURLs=https://10.113.96.220:2379\n' # noqa + members = etcdctl.member_list() + assert(members['etcd22']['unit_id'] == '7dc8404daa2b8ca0') + assert(members['etcd22']['peer_urls'] == 'https://10.113.96.220:2380') + assert(members['etcd22']['client_urls'] == 'https://10.113.96.220:2379') + + def test_member_list_with_unstarted_member(self, etcdctl): + ''' Validate we receive information only about members we can parse + from the current status string ''' + # 57fa5c39949c138e[unstarted]: peerURLs=http://10.113.96.80:2380 + # bb0f83ebb26386f7: name=etcd9 peerURLs=https://10.113.96.178:2380 clientURLs=https://10.113.96.178:2379 + with patch('etcdctl.EtcdCtl.run') as comock: + comock.return_value = '57fa5c39949c138e[unstarted]: peerURLs=http://10.113.96.80:2380]\nbb0f83ebb26386f7: name=etcd9 peerURLs=https://10.113.96.178:2380 clientURLs=https://10.113.96.178:2379\n' # noqa + members = etcdctl.member_list() + assert(members['etcd9']['unit_id'] == 'bb0f83ebb26386f7') + assert(members['etcd9']['peer_urls'] == 'https://10.113.96.178:2380') + assert(members['etcd9']['client_urls'] == 'https://10.113.96.178:2379') + assert('unstarted' in members.keys()) + assert(members['unstarted']['unit_id'] == '57fa5c39949c138e') + assert("10.113.96.80:2380" in members['unstarted']['peer_urls']) + + def test_etcd_v2_version(self, etcdctl): + ''' Validate that etcdctl can parse versions for both etcd v2 and + etcd v3 ''' + # Define fixtures of what we expect for the version output + etcdctl_2_version = b"etcdctl version 2.3.8\n" + with patch('etcdctl.check_output') as comock: + comock.return_value = etcdctl_2_version + ver = etcdctl.version() + assert(ver == '2.3.8') + + def test_etcd_v3_version(self, etcdctl): + ''' Validate that etcdctl can parse version for etcdctl v3 ''' + etcdctl_3_version = b"etcdctl version: 3.0.17\nAPI version: 2\n" + with patch('etcdctl.check_output') as comock: + comock.return_value = etcdctl_3_version + ver = etcdctl.version() + assert(ver == '3.0.17') + + def test_etcdctl_command(self): + ''' Validate sane results from etcdctl_command ''' + assert(isinstance(etcdctl_command(), str)) + + def test_etcdctl_environment_with_version_2(self, etcdctl): + ''' Validate that environment gets set correctly + spoiler alert; it shouldn't be set when passing --version ''' + with patch('etcdctl.check_output') as comock: + etcdctl.run('member list', api=2) + api_version = comock.call_args[1].get('env').get('ETCDCTL_API') + assert(api_version == '2') + + def test_etcdctl_environment_with_version_3(self, etcdctl): + ''' Validate that environment gets set correctly + spoiler alert; it shouldn't be set when passing --version ''' + with patch('etcdctl.check_output') as comock: + etcdctl.run('member list', api=3) + api_version = comock.call_args[1].get('env').get('ETCDCTL_API') + assert(api_version == '3') + + def test_get_connection_string(self): + ''' Validate the get_connection_string function + gives a sane return. + ''' + assert( + get_connection_string(['1.1.1.1'], '1111') == + 'https://1.1.1.1:1111' + ) + + @patch('reactive.etcd.render_grafana_dashboard') + def test_register_grafana_dashboard(self, mock_dashboard_render): + """Register grafana dashboard.""" + dashboard_json = {'foo': 'bar'} + mock_dashboard_render.return_value = dashboard_json + grafana = MagicMock() + endpoint_from_flag.return_value = grafana + + register_grafana_dashboard() + + mock_dashboard_render.assert_called_once() + grafana.register_dashboard.assert_called_with( + name=GRAFANA_DASHBOARD_NAME, dashboard=dashboard_json) + reactive.etcd.set_flag.assert_called_with('grafana.configured') + + def test_register_prometheus_job(self, mocker): + """Test successful registration of prometheus job.""" + ingress_address = '10.0.0.1' + port = '2379' + targets = ['{}:{}'.format(ingress_address, port)] + prometheus_mock = MagicMock() + etcd_cluster_mock = MagicMock() + job_data = {'scheme': 'https', + 'static_configs': [{'targets': targets}] + } + + etcd_cluster_mock.get_db_ingress_addresses.return_value = [] + endpoint_from_flag.side_effect = [prometheus_mock, etcd_cluster_mock] + mocker.patch.object(reactive.etcd, 'get_ingress_address', + return_value=ingress_address) + reactive.etcd.config.return_value = port + + register_prometheus_jobs() + + prometheus_mock.register_job.assert_called_with(job_name='etcd', + job_data=job_data) + reactive.etcd.set_flag.assert_called_with('prometheus.configured') + + def test_series_upgrade(self): + assert host.service_pause.call_count == 0 + assert host.service_resume.call_count == 0 + assert status.blocked.call_count == 0 + pre_series_upgrade() + assert host.service_pause.call_count == 1 + assert host.service_resume.call_count == 0 + assert status.blocked.call_count == 1 + post_series_upgrade() + assert host.service_pause.call_count == 1 + assert host.service_resume.call_count == 1 + assert status.blocked.call_count == 1 + + @patch('reactive.etcd.force_rejoin') + @patch('reactive.etcd.check_cluster_health') + def test_rejoin_trigger(self, cluster_health_mock, + rejoin_mock): + """Test that unit will trigger force_rejoin on new request""" + force_rejoin_requested() + + rejoin_mock.assert_called_once() + cluster_health_mock.assert_called_once() + + @patch('reactive.etcd.register_node_with_leader') + @patch('os.path.exists') + @patch('shutil.rmtree') + @patch('os.path.join') + @patch('time.sleep') + def test_force_rejoin(self, sleep, path_join, rmtree, path_exists, + register_node): + """Test that force_rejoin performs required steps.""" + data_dir = '/foo/bar' + path_exists.return_value = True + path_join.return_value = data_dir + force_rejoin() + + host.service_stop.assert_called_with(EtcdDatabag().etcd_daemon) + clear_flag.assert_called_with('etcd.registered') + rmtree.assert_called_with(data_dir) + register_node.assert_called() diff --git a/etcd/version b/etcd/version new file mode 100644 index 0000000..20817dd --- /dev/null +++ b/etcd/version @@ -0,0 +1 @@ +ccfa68be \ No newline at end of file diff --git a/etcd/wheelhouse.txt b/etcd/wheelhouse.txt new file mode 100644 index 0000000..055e9cd --- /dev/null +++ b/etcd/wheelhouse.txt @@ -0,0 +1,30 @@ +# layer:basic +# pip is pinned to <19.0 to avoid https://github.com/pypa/pip/issues/6164 +# even with installing setuptools before upgrading pip ends up with pip seeing +# the older setuptools at the system level if include_system_packages is true +pip>=18.1,<19.0 +# pin Jinja2, PyYAML and MarkupSafe to the last versions supporting python 3.5 +# for trusty +Jinja2<=2.10.1 +PyYAML<=5.2 +MarkupSafe<2.0.0 +setuptools<42 +setuptools-scm<=1.17.0 +charmhelpers>=0.4.0,<1.0.0 +charms.reactive>=0.1.0,<2.0.0 +wheel<0.34 +# pin netaddr to avoid pulling importlib-resources +# netaddr<=0.7.19 # overridden by etcd + +# layer:snap +# Newer versions of tenacity rely on `typing` which is in stdlib in +# python3.5 but not python3.4. We want to continue to support +# python3.4 (Trusty) +tenacity<5.0.4 + +# etcd +charms.templating.jinja2>=1.0.0,<2.0.0 + +# pin netaddr to avoid pulling importlib-resources from above lib +netaddr<=0.7.19 + diff --git a/etcd/wheelhouse/Jinja2-2.10.1.tar.gz b/etcd/wheelhouse/Jinja2-2.10.1.tar.gz new file mode 100644 index 0000000..ffd1054 Binary files /dev/null and b/etcd/wheelhouse/Jinja2-2.10.1.tar.gz differ diff --git a/etcd/wheelhouse/MarkupSafe-1.1.1.tar.gz b/etcd/wheelhouse/MarkupSafe-1.1.1.tar.gz new file mode 100644 index 0000000..a6dad8e Binary files /dev/null and b/etcd/wheelhouse/MarkupSafe-1.1.1.tar.gz differ diff --git a/etcd/wheelhouse/PyYAML-5.2.tar.gz b/etcd/wheelhouse/PyYAML-5.2.tar.gz new file mode 100644 index 0000000..666d12a Binary files /dev/null and b/etcd/wheelhouse/PyYAML-5.2.tar.gz differ diff --git a/etcd/wheelhouse/Tempita-0.5.2.tar.gz b/etcd/wheelhouse/Tempita-0.5.2.tar.gz new file mode 100644 index 0000000..755befc Binary files /dev/null and b/etcd/wheelhouse/Tempita-0.5.2.tar.gz differ diff --git a/etcd/wheelhouse/charmhelpers-0.20.23.tar.gz b/etcd/wheelhouse/charmhelpers-0.20.23.tar.gz new file mode 100644 index 0000000..8fbc8ec Binary files /dev/null and b/etcd/wheelhouse/charmhelpers-0.20.23.tar.gz differ diff --git a/etcd/wheelhouse/charms.reactive-1.4.1.tar.gz b/etcd/wheelhouse/charms.reactive-1.4.1.tar.gz new file mode 100644 index 0000000..03bc1fe Binary files /dev/null and b/etcd/wheelhouse/charms.reactive-1.4.1.tar.gz differ diff --git a/etcd/wheelhouse/charms.templating.jinja2-1.0.2.tar.gz b/etcd/wheelhouse/charms.templating.jinja2-1.0.2.tar.gz new file mode 100644 index 0000000..5c03a81 Binary files /dev/null and b/etcd/wheelhouse/charms.templating.jinja2-1.0.2.tar.gz differ diff --git a/etcd/wheelhouse/netaddr-0.7.19.tar.gz b/etcd/wheelhouse/netaddr-0.7.19.tar.gz new file mode 100644 index 0000000..cc31d9d Binary files /dev/null and b/etcd/wheelhouse/netaddr-0.7.19.tar.gz differ diff --git a/etcd/wheelhouse/pbr-5.6.0.tar.gz b/etcd/wheelhouse/pbr-5.6.0.tar.gz new file mode 100644 index 0000000..0d5c965 Binary files /dev/null and b/etcd/wheelhouse/pbr-5.6.0.tar.gz differ diff --git a/etcd/wheelhouse/pip-18.1.tar.gz b/etcd/wheelhouse/pip-18.1.tar.gz new file mode 100644 index 0000000..a18192d Binary files /dev/null and b/etcd/wheelhouse/pip-18.1.tar.gz differ diff --git a/etcd/wheelhouse/pyaml-21.10.1.tar.gz b/etcd/wheelhouse/pyaml-21.10.1.tar.gz new file mode 100644 index 0000000..b19aad3 Binary files /dev/null and b/etcd/wheelhouse/pyaml-21.10.1.tar.gz differ diff --git a/etcd/wheelhouse/setuptools-41.6.0.zip b/etcd/wheelhouse/setuptools-41.6.0.zip new file mode 100644 index 0000000..3345759 Binary files /dev/null and b/etcd/wheelhouse/setuptools-41.6.0.zip differ diff --git a/etcd/wheelhouse/setuptools_scm-1.17.0.tar.gz b/etcd/wheelhouse/setuptools_scm-1.17.0.tar.gz new file mode 100644 index 0000000..43b16c7 Binary files /dev/null and b/etcd/wheelhouse/setuptools_scm-1.17.0.tar.gz differ diff --git a/etcd/wheelhouse/six-1.16.0.tar.gz b/etcd/wheelhouse/six-1.16.0.tar.gz new file mode 100644 index 0000000..5bf3a27 Binary files /dev/null and b/etcd/wheelhouse/six-1.16.0.tar.gz differ diff --git a/etcd/wheelhouse/tenacity-5.0.3.tar.gz b/etcd/wheelhouse/tenacity-5.0.3.tar.gz new file mode 100644 index 0000000..c7d05ba Binary files /dev/null and b/etcd/wheelhouse/tenacity-5.0.3.tar.gz differ diff --git a/etcd/wheelhouse/wheel-0.33.6.tar.gz b/etcd/wheelhouse/wheel-0.33.6.tar.gz new file mode 100644 index 0000000..c922c4e Binary files /dev/null and b/etcd/wheelhouse/wheel-0.33.6.tar.gz differ diff --git a/kata/.build.manifest b/kata/.build.manifest new file mode 100644 index 0000000..5589d6b --- /dev/null +++ b/kata/.build.manifest @@ -0,0 +1,531 @@ +{ + "layers": [ + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "fcdcea4e5de3e1556c24e6704607862d0ba00a56", + "url": "layer:options" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "a3ff62c32c993d80417f6e093e3ef95e42f62083", + "url": "layer:basic" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "527dd64fc4b9a6b0f8d80a3c2c0b865155050275", + "url": "layer:debug" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "a7d7b6423db37a47611310039e6ed1929c0a2eab", + "url": "layer:status" + }, + { + "branch": "refs/heads/stable", + "rev": "b2fa345285b14fe339084fd35865973ca05eefbf", + "url": "kata" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "6f927f10b97f45c566481cf57a29d433f17373e1", + "url": "interface:container-runtime" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "b59ce0c44bc52c789175750ce18b42f76c9a4578", + "url": "interface:untrusted-container-runtime" + } + ], + "signatures": { + ".build.manifest": [ + "build", + "dynamic", + "unchecked" + ], + ".github/workflows/main.yml": [ + "layer:basic", + "static", + "96a48a981ceb2a96f427a6b5226d2da6d7191981793804055d70a88ca1987473" + ], + ".gitignore": [ + "kata", + "static", + "589384c900fb8e573ae6939a9efa0813087ea526761ba661d96aa2526a494eef" + ], + ".travis.yml": [ + "kata", + "static", + "714ed5453bd5a053676efb64370194a7c130f426ec11acba7d1509d558dc979c" + ], + "CONTRIBUTING.md": [ + "kata", + "static", + "c44755a6800e330bd939b7a27a4bb75adaef3a1ccdc15df62cb5533a3ea6252f" + ], + "LICENSE": [ + "kata", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "Makefile": [ + "layer:basic", + "static", + "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301" + ], + "README.md": [ + "kata", + "static", + "ac3b4f06b6e4a23f80a12f898645c4d4c2daedf961e72a2d851cf9c4b37d538a" + ], + "actions.yaml": [ + "layer:debug", + "dynamic", + "cea290e28bc78458ea4a56dcad39b9a880c67e4ba53b774ac46bd8778618c7b9" + ], + "actions/debug": [ + "layer:debug", + "static", + "db0a42dae4c5045b2c06385bf22209dfe0e2ded55822ef847d84b01d9ff2b046" + ], + "bin/charm-env": [ + "layer:basic", + "static", + "fb6a20fac4102a6a4b6ffe903fcf666998f9a95a3647e6f9af7a1eeb44e58fd5" + ], + "bin/layer_option": [ + "layer:options", + "static", + "e959bf29da4c5edff28b2602c24113c4df9e25cdc9f2aa3b5d46c8577b2a40cc" + ], + "copyright": [ + "layer:status", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "copyright.layer-basic": [ + "layer:basic", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-options": [ + "layer:options", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "debug-scripts/charm-unitdata": [ + "layer:debug", + "static", + "c952b9d31f3942e4e722cb3e70f5119707b69b8e76cc44e2e906bc6d9aef49b7" + ], + "debug-scripts/filesystem": [ + "layer:debug", + "static", + "d29cc8687f4422d024001c91b1ac756ee6bf8a2a125bc98db1199ba775eb8fd7" + ], + "debug-scripts/juju-logs": [ + "layer:debug", + "static", + "d260b35753a917368cb8c64c1312546a0a40ef49cba84c75bc6369549807c55e" + ], + "debug-scripts/juju-network-get": [ + "layer:debug", + "static", + "6d849a1f8e6569bd0d5ea38299f7937cb8b36a5f505e3532f6c756eabeb8b6c5" + ], + "debug-scripts/network": [ + "layer:debug", + "static", + "714afae5dcb45554ff1f05285501e3b7fcc656c8de51217e263b93dab25a9d2e" + ], + "debug-scripts/packages": [ + "layer:debug", + "static", + "e8177102dc2ca853cb9272c1257cf2cfd5253d2a074e602d07c8bc4ea8e27c75" + ], + "debug-scripts/sysctl": [ + "layer:debug", + "static", + "990035b320e09cc2228e1f2f880e795d51118b2959339eacddff9cbb74349c6a" + ], + "debug-scripts/systemd": [ + "layer:debug", + "static", + "23ddf533198bf5b1ce723acde31ada806aab8539292b514c721d8ec08af74106" + ], + "docs/status.md": [ + "layer:status", + "static", + "975dec9f8c938196e102e954a80226bda293407c4e5ae857c118bf692154702a" + ], + "hooks/config-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/containerd-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/containerd-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/containerd-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/containerd-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/containerd-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/hook.template": [ + "layer:basic", + "static", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/install": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-elected": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-settings-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/post-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/pre-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/relations/container-runtime/.gitignore": [ + "interface:container-runtime", + "static", + "a2ebfecdb6c1b58267fbe97e6e2ac02c2b963df7673fc1047270f0f0cff16732" + ], + "hooks/relations/container-runtime/LICENSE": [ + "interface:container-runtime", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "hooks/relations/container-runtime/README.md": [ + "interface:container-runtime", + "static", + "44273265818229d2c858c3af0e0eee3a7df05aaa9ab20d28c3872190d4b48611" + ], + "hooks/relations/container-runtime/__init__.py": [ + "interface:container-runtime", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/container-runtime/interface.yaml": [ + "interface:container-runtime", + "static", + "e5343dcb11a6817a6050df4ea1c463eeaa0dd4777098566d4e27b056775426c6" + ], + "hooks/relations/container-runtime/provides.py": [ + "interface:container-runtime", + "static", + "4e818da222f507604179a828629787a1250083c847277f6b5b8e028cfbbb6d06" + ], + "hooks/relations/container-runtime/requires.py": [ + "interface:container-runtime", + "static", + "95285168b02f1f70be15c03098833a85e60fa1658ed72a46acd42e8e85ded761" + ], + "hooks/relations/untrusted-container-runtime/.gitignore": [ + "interface:untrusted-container-runtime", + "static", + "a2ebfecdb6c1b58267fbe97e6e2ac02c2b963df7673fc1047270f0f0cff16732" + ], + "hooks/relations/untrusted-container-runtime/LICENSE": [ + "interface:untrusted-container-runtime", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "hooks/relations/untrusted-container-runtime/README.md": [ + "interface:untrusted-container-runtime", + "static", + "e3dc7db9ee98b716cb9a3a281fad88ca313bc11888a0da2f4b63c4306d91b64f" + ], + "hooks/relations/untrusted-container-runtime/__init__.py": [ + "interface:untrusted-container-runtime", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/untrusted-container-runtime/interface.yaml": [ + "interface:untrusted-container-runtime", + "static", + "1fcb0305295206dc2b9926bf1870cae2c6cd8eee6eef72b6060c85e4f2109a45" + ], + "hooks/relations/untrusted-container-runtime/provides.py": [ + "interface:untrusted-container-runtime", + "static", + "05a52be7ad18df5cac9fb5dcc27c2ab24fe12e65fa809e0ea4d395dbcb36e6f2" + ], + "hooks/relations/untrusted-container-runtime/requires.py": [ + "interface:untrusted-container-runtime", + "static", + "958e03e254ee27bee761a6af3e032a273204b356dc51438489cde726b1a6e060" + ], + "hooks/start": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/stop": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/untrusted-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/untrusted-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/untrusted-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/untrusted-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/untrusted-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/update-status": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/upgrade-charm": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "icon.svg": [ + "kata", + "static", + "d20624e9389af6506a8d8a69ac9bba4d41709601b624c0875fd7d6717b395088" + ], + "layer.yaml": [ + "kata", + "dynamic", + "599574e1d3dda3bf1d63047ac0b152caffcf22058e2f61370a37c8bb89317e4c" + ], + "lib/charms/layer/__init__.py": [ + "layer:basic", + "static", + "dfe0d26c6bf409767de6e2546bc648f150e1b396243619bad3aa0553ab7e0e6f" + ], + "lib/charms/layer/basic.py": [ + "layer:basic", + "static", + "98b47134770ed6e4c0b2d4aad73cd5bc200bec84aa9c1c4e075fd70c3222a0c9" + ], + "lib/charms/layer/execd.py": [ + "layer:basic", + "static", + "fda8bd491032db1db8ddaf4e99e7cc878c6fb5432efe1f91cadb5b34765d076d" + ], + "lib/charms/layer/options.py": [ + "layer:options", + "static", + "8ae7a07d22542fc964f2d2bee8219d1c78a68dace70a1b38d36d4aea47b1c3b2" + ], + "lib/charms/layer/status.py": [ + "layer:status", + "static", + "d560a5e07b2e5f2b0f25f30e1f0278b06f3f90c01e4dbad5c83d71efc79018c6" + ], + "lib/debug_script.py": [ + "layer:debug", + "static", + "a4d56f2d3e712b1b5cadb657c7195c6268d0aac6d228991049fd769e0ddaf453" + ], + "make_docs": [ + "layer:status", + "static", + "c990f55c8e879793a62ed8464ee3d7e0d7d2225fdecaf17af24b0df0e2daa8c1" + ], + "metadata.yaml": [ + "kata", + "dynamic", + "883f95d6180166d507365b3374b733fde27e0eb988d9532e88bb66e002c3fd68" + ], + "pydocmd.yml": [ + "layer:status", + "static", + "11d9293901f32f75f4256ae4ac2073b92ce1d7ef7b6c892ba9fbb98690a0b330" + ], + "reactive/__init__.py": [ + "layer:basic", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "reactive/kata.py": [ + "kata", + "static", + "7863484c83034271ea1f7a645c9f904405047db1be0fd7857f80008f47e073bf" + ], + "reactive/status.py": [ + "layer:status", + "static", + "30207fc206f24e91def5252f1c7f7c8e23c0aed0e93076babf5e03c05296d207" + ], + "requirements.txt": [ + "layer:basic", + "static", + "a00f75d80849e5b4fc5ad2e7536f947c25b1a4044b341caa8ee87a92d3a4c804" + ], + "tests/conftest.py": [ + "kata", + "static", + "fd53e0c38b4dda0c18096167889cd0d85b98b0a13225f9f8853261241e94078c" + ], + "tests/test_kata_reactive.py": [ + "kata", + "static", + "24d714d03b6f2c2faa67ecdbd7d102f700087973eb5c98d7b9c8e5542d61541c" + ], + "tox.ini": [ + "kata", + "static", + "b04898a3c4de3bf48ca4363751048ec83ed185bc27af7d956ae799d88d3827ab" + ], + "version": [ + "kata", + "dynamic", + "f6c325fd13ee5c726bc2e631996963198f2cfbaa50599b4962b151630fa86cf4" + ], + "wheelhouse.txt": [ + "kata", + "dynamic", + "425000e4406bf00f663cf41789c409e7980e4bd4a1b557b0470770502f71ed09" + ], + "wheelhouse/Jinja2-2.10.1.tar.gz": [ + "layer:basic", + "dynamic", + "065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013" + ], + "wheelhouse/MarkupSafe-1.1.1.tar.gz": [ + "layer:basic", + "dynamic", + "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b" + ], + "wheelhouse/PyYAML-5.2.tar.gz": [ + "layer:basic", + "dynamic", + "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c" + ], + "wheelhouse/Tempita-0.5.2.tar.gz": [ + "__pip__", + "dynamic", + "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c" + ], + "wheelhouse/certifi-2021.10.8.tar.gz": [ + "__pip__", + "dynamic", + "78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872" + ], + "wheelhouse/charmhelpers-0.20.23.tar.gz": [ + "layer:basic", + "dynamic", + "59a9776594e91cd3e3e000043f8668b4d7b279422dbb17e320f01dc16385b80e" + ], + "wheelhouse/charms.reactive-1.4.1.tar.gz": [ + "layer:basic", + "dynamic", + "bba21b4fd40b26c240c9ef2aa10c6fdf73592031c68591da4e7ccc46ca9cb616" + ], + "wheelhouse/charset-normalizer-2.0.7.tar.gz": [ + "__pip__", + "dynamic", + "e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0" + ], + "wheelhouse/idna-3.3.tar.gz": [ + "__pip__", + "dynamic", + "9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "wheelhouse/netaddr-0.7.19.tar.gz": [ + "layer:basic", + "dynamic", + "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd" + ], + "wheelhouse/pbr-5.6.0.tar.gz": [ + "__pip__", + "dynamic", + "42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd" + ], + "wheelhouse/pip-18.1.tar.gz": [ + "layer:basic", + "dynamic", + "c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1" + ], + "wheelhouse/pyaml-21.10.1.tar.gz": [ + "__pip__", + "dynamic", + "c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383" + ], + "wheelhouse/requests-2.26.0.tar.gz": [ + "kata", + "dynamic", + "b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + ], + "wheelhouse/setuptools-41.6.0.zip": [ + "layer:basic", + "dynamic", + "6afa61b391dcd16cb8890ec9f66cc4015a8a31a6e1c2b4e0c464514be1a3d722" + ], + "wheelhouse/setuptools_scm-1.17.0.tar.gz": [ + "layer:basic", + "dynamic", + "70a4cf5584e966ae92f54a764e6437af992ba42ac4bca7eb37cc5d02b98ec40a" + ], + "wheelhouse/six-1.16.0.tar.gz": [ + "__pip__", + "dynamic", + "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926" + ], + "wheelhouse/urllib3-1.26.7.tar.gz": [ + "__pip__", + "dynamic", + "4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece" + ], + "wheelhouse/wheel-0.33.6.tar.gz": [ + "layer:basic", + "dynamic", + "10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646" + ] + } +} \ No newline at end of file diff --git a/kata/.github/workflows/main.yml b/kata/.github/workflows/main.yml new file mode 100644 index 0000000..565bfaf --- /dev/null +++ b/kata/.github/workflows/main.yml @@ -0,0 +1,50 @@ +name: Test Suite +on: [pull_request] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.5, 3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + - name: Run lint + run: tox -e flake8 + functional-test: + name: Functional test with LXD + runs-on: ubuntu-latest + timeout-minutes: 360 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install Dependencies + run: | + pip install tox + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@master + - name: Run test + run: tox -e func + - name: Show Status + if: ${{ always() }} + run: | + model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/); + juju status -m "$model" + - name: Show Error Logs + if: ${{ always() }} + run: | + model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/); + juju debug-log -m "$model" --replay --no-tail --level ERROR diff --git a/kata/.gitignore b/kata/.gitignore new file mode 100644 index 0000000..8003a31 --- /dev/null +++ b/kata/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +.coverage +.tox/ +.venv/ diff --git a/kata/.travis.yml b/kata/.travis.yml new file mode 100644 index 0000000..694ddcb --- /dev/null +++ b/kata/.travis.yml @@ -0,0 +1,11 @@ +dist: bionic +language: python +python: + - "3.5" + - "3.6" + - "3.7" + - "3.8" +install: + - pip install tox-travis +script: + - tox diff --git a/kata/CONTRIBUTING.md b/kata/CONTRIBUTING.md new file mode 100644 index 0000000..7a8f252 --- /dev/null +++ b/kata/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributor Guide + +This Juju charm is open source ([Apache License 2.0](./LICENSE)) and we actively seek any community contibutions +for code, suggestions and documentation. +This page details a few notes, workflows and suggestions for how to make contributions most effective and help us +all build a better charm - please give them a read before working on any contributions. + +## Licensing + +This charm has been created under the [Apache License 2.0](./LICENSE), which will cover any contributions you may +make to this project. Please familiarise yourself with the terms of the license. + +Additionally, this charm uses the Harmony CLA agreement. It’s the easiest way for you to give us permission to +use your contributions. +In effect, you’re giving us a license, but you still own the copyright — so you retain the right to modify your +code and use it in other projects. Please [sign the CLA here](https://ubuntu.com/legal/contributors/agreement) before +making any contributions. + +## Code of conduct + +We have adopted the Ubuntu code of Conduct. You can read this in full [here](https://ubuntu.com/community/code-of-conduct). + +## Contributing code + +To contribute code to this project, please use the following workflow: + +1. [Submit a bug][bug] to explain the need for and track the change. +2. Create a branch on your fork of the repo with your changes, including a unit test covering the new or modified code. +3. Submit a PR. The PR description should include a link to the bug on Launchpad. +4. Update the Launchpad bug to include a link to the PR and the `review-needed` tag. +5. Once reviewed and merged, the change will become available on the edge channel and assigned to an appropriate milestone + for further release according to priority. + +## Documentation + +Documentation for this charm is currently maintained as part of the Charmed Kubernetes docs. +See [this page][docs] + + +[bug]: https://bugs.launchpad.net/charm-kata/+filebug +[docs]: https://github.com/charmed-kubernetes/kubernetes-docs/blob/master/pages/k8s/charm-kata.md \ No newline at end of file diff --git a/kata/LICENSE b/kata/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/kata/LICENSE @@ -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/kata/Makefile b/kata/Makefile new file mode 100644 index 0000000..a1ad3a5 --- /dev/null +++ b/kata/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/kata/README.md b/kata/README.md new file mode 100644 index 0000000..39cb53b --- /dev/null +++ b/kata/README.md @@ -0,0 +1,7 @@ +# Charm for Kata Containers + +This subordinate charm deploys the [Kata](https://katacontainers.io/) +untrusted container runtime within a running Juju charm model. + +This charm is maintained along with the components of Charmed Kubernetes. +For full information, please visit the official [Charmed Kubernetes docs](https://ubuntu.com/kubernetes/docs/charm-kata). diff --git a/kata/actions.yaml b/kata/actions.yaml new file mode 100644 index 0000000..8712b6b --- /dev/null +++ b/kata/actions.yaml @@ -0,0 +1,2 @@ +"debug": + "description": "Collect debug data" diff --git a/kata/actions/debug b/kata/actions/debug new file mode 100755 index 0000000..8ba160e --- /dev/null +++ b/kata/actions/debug @@ -0,0 +1,102 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import tarfile +import tempfile +import traceback +from contextlib import contextmanager +from datetime import datetime +from charmhelpers.core.hookenv import action_set, local_unit + +archive_dir = None +log_file = None + + +@contextmanager +def archive_context(): + """ Open a context with a new temporary directory. + + When the context closes, the directory is archived, and the archive + location is added to Juju action output. """ + global archive_dir + global log_file + with tempfile.TemporaryDirectory() as temp_dir: + name = "debug-" + datetime.now().strftime("%Y%m%d%H%M%S") + archive_dir = os.path.join(temp_dir, name) + os.makedirs(archive_dir) + with open("%s/debug.log" % archive_dir, "w") as log_file: + yield + os.chdir(temp_dir) + tar_path = "/home/ubuntu/%s.tar.gz" % name + with tarfile.open(tar_path, "w:gz") as f: + f.add(name) + action_set({ + "path": tar_path, + "command": "juju scp %s:%s ." % (local_unit(), tar_path), + "message": " ".join([ + "Archive has been created on unit %s." % local_unit(), + "Use the juju scp command to copy it to your local machine." + ]) + }) + + +def log(msg): + """ Log a message that will be included in the debug archive. + + Must be run within archive_context """ + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for line in str(msg).splitlines(): + log_file.write(timestamp + " | " + line.rstrip() + "\n") + + +def run_script(script): + """ Run a single script. Must be run within archive_context """ + log("Running script: " + script) + script_dir = os.path.join(archive_dir, script) + os.makedirs(script_dir) + env = os.environ.copy() + env["PYTHONPATH"] = "lib" # allow same imports as reactive code + env["DEBUG_SCRIPT_DIR"] = script_dir + with open(script_dir + "/stdout", "w") as stdout: + with open(script_dir + "/stderr", "w") as stderr: + process = subprocess.Popen( + "debug-scripts/" + script, + stdout=stdout, stderr=stderr, env=env + ) + try: + exit_code = process.wait(timeout=300) + except subprocess.TimeoutExpired: + log("ERROR: still running, terminating") + process.terminate() + try: + exit_code = process.wait(timeout=10) + except subprocess.TimeoutExpired: + log("ERROR: still running, killing") + process.kill() + exit_code = process.wait(timeout=10) + if exit_code != 0: + log("ERROR: %s failed with exit code %d" % (script, exit_code)) + + +def run_all_scripts(): + """ Run all scripts. For the sake of robustness, log and ignore any + exceptions that occur. + + Must be run within archive_context """ + scripts = os.listdir("debug-scripts") + for script in scripts: + try: + run_script(script) + except: + log(traceback.format_exc()) + + +def main(): + """ Open an archive context and run all scripts. """ + with archive_context(): + run_all_scripts() + + +if __name__ == "__main__": + main() diff --git a/kata/bin/charm-env b/kata/bin/charm-env new file mode 100755 index 0000000..d211ce9 --- /dev/null +++ b/kata/bin/charm-env @@ -0,0 +1,107 @@ +#!/bin/bash + +VERSION="1.0.0" + + +find_charm_dirs() { + # Hopefully, $JUJU_CHARM_DIR is set so which venv to use in unambiguous. + if [[ -n "$JUJU_CHARM_DIR" || -n "$CHARM_DIR" ]]; then + if [[ -z "$JUJU_CHARM_DIR" ]]; then + # accept $CHARM_DIR to be more forgiving + export JUJU_CHARM_DIR="$CHARM_DIR" + fi + if [[ -z "$CHARM_DIR" ]]; then + # set CHARM_DIR as well to help with backwards compatibility + export CHARM_DIR="$JUJU_CHARM_DIR" + fi + return + fi + # Try to guess the value for JUJU_CHARM_DIR by looking for a non-subordinate + # (because there's got to be at least one principle) charm directory; + # if there are several, pick the first by alpha order. + agents_dir="/var/lib/juju/agents" + if [[ -d "$agents_dir" ]]; then + desired_charm="$1" + found_charm_dir="" + if [[ -n "$desired_charm" ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + charm_name="$(grep -o '^['\''"]\?name['\''"]\?:.*' $charm_dir/metadata.yaml 2> /dev/null | sed -e 's/.*: *//' -e 's/['\''"]//g')" + if [[ "$charm_name" == "$desired_charm" ]]; then + if [[ -n "$found_charm_dir" ]]; then + >&2 echo "Ambiguous possibilities for JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + found_charm_dir="$charm_dir" + fi + done + if [[ -z "$found_charm_dir" ]]; then + >&2 echo "Unable to determine JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + export JUJU_CHARM_DIR="$found_charm_dir" + export CHARM_DIR="$found_charm_dir" + return + fi + # shellcheck disable=SC2126 + non_subordinates="$(grep -L 'subordinate"\?:.*true' "$agents_dir"/unit-*/charm/metadata.yaml | wc -l)" + if [[ "$non_subordinates" -gt 1 ]]; then + >&2 echo 'Ambiguous possibilities for JUJU_CHARM_DIR; please use --charm or run within a Juju hook context' + exit 1 + elif [[ "$non_subordinates" -eq 1 ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + if grep -q 'subordinate"\?:.*true' "$charm_dir/metadata.yaml"; then + continue + fi + export JUJU_CHARM_DIR="$charm_dir" + export CHARM_DIR="$charm_dir" + return + done + fi + fi + >&2 echo 'Unable to determine JUJU_CHARM_DIR; please run within a Juju hook context' + exit 1 +} + +try_activate_venv() { + if [[ -d "$JUJU_CHARM_DIR/../.venv" ]]; then + . "$JUJU_CHARM_DIR/../.venv/bin/activate" + fi +} + +find_wrapped() { + PATH="${PATH/\/usr\/local\/sbin:}" which "$(basename "$0")" +} + + +if [[ "$1" == "--version" || "$1" == "-v" ]]; then + echo "$VERSION" + exit 0 +fi + + +# allow --charm option to hint which JUJU_CHARM_DIR to choose when ambiguous +# NB: --charm option must come first +# NB: option must be processed outside find_charm_dirs to modify $@ +charm_name="" +if [[ "$1" == "--charm" ]]; then + charm_name="$2" + shift; shift +fi + +find_charm_dirs "$charm_name" +try_activate_venv +export PYTHONPATH="$JUJU_CHARM_DIR/lib:$PYTHONPATH" + +if [[ "$(basename "$0")" == "charm-env" ]]; then + # being used as a shebang + exec "$@" +elif [[ "$0" == "$BASH_SOURCE" ]]; then + # being invoked as a symlink wrapping something to find in the venv + exec "$(find_wrapped)" "$@" +elif [[ "$(basename "$BASH_SOURCE")" == "charm-env" ]]; then + # being sourced directly; do nothing + /bin/true +else + # being sourced for wrapped bash helpers + . "$(find_wrapped)" +fi diff --git a/kata/bin/layer_option b/kata/bin/layer_option new file mode 100755 index 0000000..3253ef8 --- /dev/null +++ b/kata/bin/layer_option @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from charms import layer + + +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 = layer.options.get(args.section, 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/kata/copyright b/kata/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kata/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kata/copyright.layer-basic b/kata/copyright.layer-basic new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/kata/copyright.layer-basic @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kata/copyright.layer-options b/kata/copyright.layer-options new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/kata/copyright.layer-options @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kata/debug-scripts/charm-unitdata b/kata/debug-scripts/charm-unitdata new file mode 100755 index 0000000..d2aac60 --- /dev/null +++ b/kata/debug-scripts/charm-unitdata @@ -0,0 +1,12 @@ +#!/usr/local/sbin/charm-env python3 + +import debug_script +import json +from charmhelpers.core import unitdata + +kv = unitdata.kv() +data = kv.getrange("") + +with debug_script.open_file("unitdata.json", "w") as f: + json.dump(data, f, indent=2) + f.write("\n") diff --git a/kata/debug-scripts/filesystem b/kata/debug-scripts/filesystem new file mode 100755 index 0000000..c5ec6d8 --- /dev/null +++ b/kata/debug-scripts/filesystem @@ -0,0 +1,17 @@ +#!/bin/sh +set -ux + +# report file system disk space usage +df -hT > $DEBUG_SCRIPT_DIR/df-hT +# estimate file space usage +du -h / 2>&1 > $DEBUG_SCRIPT_DIR/du-h +# list the mounted filesystems +mount > $DEBUG_SCRIPT_DIR/mount +# list the mounted systems with ascii trees +findmnt -A > $DEBUG_SCRIPT_DIR/findmnt +# list block devices +lsblk > $DEBUG_SCRIPT_DIR/lsblk +# list open files +lsof 2>&1 > $DEBUG_SCRIPT_DIR/lsof +# list local system locks +lslocks > $DEBUG_SCRIPT_DIR/lslocks diff --git a/kata/debug-scripts/juju-logs b/kata/debug-scripts/juju-logs new file mode 100755 index 0000000..d27c458 --- /dev/null +++ b/kata/debug-scripts/juju-logs @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +cp -v /var/log/juju/* $DEBUG_SCRIPT_DIR diff --git a/kata/debug-scripts/juju-network-get b/kata/debug-scripts/juju-network-get new file mode 100755 index 0000000..983c8c4 --- /dev/null +++ b/kata/debug-scripts/juju-network-get @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import yaml +import debug_script + +with open('metadata.yaml') as f: + metadata = yaml.load(f) + +relations = [] +for key in ['requires', 'provides', 'peers']: + relations += list(metadata.get(key, {}).keys()) + +os.mkdir(os.path.join(debug_script.dir, 'relations')) + +for relation in relations: + path = 'relations/' + relation + with debug_script.open_file(path, 'w') as f: + cmd = ['network-get', relation] + subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) diff --git a/kata/debug-scripts/network b/kata/debug-scripts/network new file mode 100755 index 0000000..944a355 --- /dev/null +++ b/kata/debug-scripts/network @@ -0,0 +1,11 @@ +#!/bin/sh +set -ux + +ifconfig -a > $DEBUG_SCRIPT_DIR/ifconfig +cp -v /etc/resolv.conf $DEBUG_SCRIPT_DIR/resolv.conf +cp -v /etc/network/interfaces $DEBUG_SCRIPT_DIR/interfaces +netstat -planut > $DEBUG_SCRIPT_DIR/netstat +route -n > $DEBUG_SCRIPT_DIR/route +iptables-save > $DEBUG_SCRIPT_DIR/iptables-save +dig google.com > $DEBUG_SCRIPT_DIR/dig-google +ping -w 2 -i 0.1 google.com > $DEBUG_SCRIPT_DIR/ping-google diff --git a/kata/debug-scripts/packages b/kata/debug-scripts/packages new file mode 100755 index 0000000..b60a9cf --- /dev/null +++ b/kata/debug-scripts/packages @@ -0,0 +1,7 @@ +#!/bin/sh +set -ux + +dpkg --list > $DEBUG_SCRIPT_DIR/dpkg-list +snap list > $DEBUG_SCRIPT_DIR/snap-list +pip2 list > $DEBUG_SCRIPT_DIR/pip2-list +pip3 list > $DEBUG_SCRIPT_DIR/pip3-list diff --git a/kata/debug-scripts/sysctl b/kata/debug-scripts/sysctl new file mode 100755 index 0000000..a86a6c8 --- /dev/null +++ b/kata/debug-scripts/sysctl @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +sysctl -a > $DEBUG_SCRIPT_DIR/sysctl diff --git a/kata/debug-scripts/systemd b/kata/debug-scripts/systemd new file mode 100755 index 0000000..8bb9b6f --- /dev/null +++ b/kata/debug-scripts/systemd @@ -0,0 +1,9 @@ +#!/bin/sh +set -ux + +systemctl --all > $DEBUG_SCRIPT_DIR/systemctl +journalctl > $DEBUG_SCRIPT_DIR/journalctl +systemd-analyze time > $DEBUG_SCRIPT_DIR/systemd-analyze-time +systemd-analyze blame > $DEBUG_SCRIPT_DIR/systemd-analyze-blame +systemd-analyze critical-chain > $DEBUG_SCRIPT_DIR/systemd-analyze-critical-chain +systemd-analyze dump > $DEBUG_SCRIPT_DIR/systemd-analyze-dump diff --git a/kata/docs/status.md b/kata/docs/status.md new file mode 100644 index 0000000..c6cceab --- /dev/null +++ b/kata/docs/status.md @@ -0,0 +1,91 @@ +

WorkloadState

+ +```python +WorkloadState(self, /, *args, **kwargs) +``` + +Enum of the valid workload states. + +Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + +

maintenance

+ +```python +maintenance(message) +``` + +Set the status to the `MAINTENANCE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

maint

+ +```python +maint(message) +``` + +Shorthand alias for +[maintenance](status.md#charms.layer.status.maintenance). + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

blocked

+ +```python +blocked(message) +``` + +Set the status to the `BLOCKED` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

waiting

+ +```python +waiting(message) +``` + +Set the status to the `WAITING` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

active

+ +```python +active(message) +``` + +Set the status to the `ACTIVE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

status_set

+ +```python +status_set(workload_state, message) +``` + +Set the status to the given workload state with a message. + +__Parameters__ + +- __`workload_state` (WorkloadState or str)__: State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. +- __`message` (str)__: Message to convey to the operator. + diff --git a/kata/hooks/config-changed b/kata/hooks/config-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/config-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/containerd-relation-broken b/kata/hooks/containerd-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/containerd-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/containerd-relation-changed b/kata/hooks/containerd-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/containerd-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/containerd-relation-created b/kata/hooks/containerd-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/containerd-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/containerd-relation-departed b/kata/hooks/containerd-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/containerd-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/containerd-relation-joined b/kata/hooks/containerd-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/containerd-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/hook.template b/kata/hooks/hook.template new file mode 100644 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/hook.template @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/install b/kata/hooks/install new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/install @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/leader-elected b/kata/hooks/leader-elected new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/leader-elected @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/leader-settings-changed b/kata/hooks/leader-settings-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/leader-settings-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/post-series-upgrade b/kata/hooks/post-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/post-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/pre-series-upgrade b/kata/hooks/pre-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/pre-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/relations/container-runtime/.gitignore b/kata/hooks/relations/container-runtime/.gitignore new file mode 100644 index 0000000..894a44c --- /dev/null +++ b/kata/hooks/relations/container-runtime/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/kata/hooks/relations/container-runtime/LICENSE b/kata/hooks/relations/container-runtime/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/kata/hooks/relations/container-runtime/LICENSE @@ -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/kata/hooks/relations/container-runtime/README.md b/kata/hooks/relations/container-runtime/README.md new file mode 100644 index 0000000..4620013 --- /dev/null +++ b/kata/hooks/relations/container-runtime/README.md @@ -0,0 +1,45 @@ +# interface-container-runtime + +## Overview + +This interface handles communication between subordinate charms, that provide a container runtime and charms requiring a container runtime. + +## Usage + +### Provides + +The providing side of the container interface provides a place for a container runtime to connect to. + +Your charm should respond to the `endpoint.{endpoint_name}.available` state, +which indicates that there is a container runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def update_kubelet_config(containerd): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + config = endpoint.get_config() + kubelet.config['container-runtime'] = \ + config['runtime'] +``` + +### Requires + +The requiring side of the container interface requires a place for a container runtime to connect to. + +Your charm should set `{endpoint_name}.available` state, +which indicates that the container is runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def pubish_config(): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + endpoint.set_config( + socket='unix:///var/run/containerd/containerd.sock', + runtime='remote', + nvidia_enabled=False + ) +``` diff --git a/kata/hooks/relations/container-runtime/__init__.py b/kata/hooks/relations/container-runtime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kata/hooks/relations/container-runtime/interface.yaml b/kata/hooks/relations/container-runtime/interface.yaml new file mode 100644 index 0000000..294be1e --- /dev/null +++ b/kata/hooks/relations/container-runtime/interface.yaml @@ -0,0 +1,4 @@ +name: container-runtime +summary: Interface for relating to container runtimes +version: 1 +maintainer: "Joe Borg " diff --git a/kata/hooks/relations/container-runtime/provides.py b/kata/hooks/relations/container-runtime/provides.py new file mode 100644 index 0000000..a9768a8 --- /dev/null +++ b/kata/hooks/relations/container-runtime/provides.py @@ -0,0 +1,55 @@ +from charms.reactive import ( + Endpoint, + toggle_flag +) + + +class ContainerRuntimeProvides(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined) + + def _get_config(self, key): + """ + Get the published configuration for a given key. + + :param key: String dict key + :return: String value for given key + """ + return self.all_joined_units.received.get(key) + + def get_nvidia_enabled(self): + """ + Get the published nvidia config. + + :return: String + """ + return self._get_config(key='nvidia_enabled') + + def get_runtime(self): + """ + Get the published runtime config. + + :return: String + """ + return self._get_config(key='runtime') + + def get_socket(self): + """ + Get the published socket config. + + :return: String + """ + return self._get_config(key='socket') + + def set_config(self, sandbox_image=None): + """ + Set the configuration to be published. + + :param sandbox_image: String to optionally override the sandbox image + :return: None + """ + for relation in self.relations: + relation.to_publish.update({ + 'sandbox_image': sandbox_image + }) diff --git a/kata/hooks/relations/container-runtime/requires.py b/kata/hooks/relations/container-runtime/requires.py new file mode 100644 index 0000000..c461b68 --- /dev/null +++ b/kata/hooks/relations/container-runtime/requires.py @@ -0,0 +1,61 @@ +from charms.reactive import ( + Endpoint, + clear_flag, + data_changed, + is_data_changed, + toggle_flag +) + + +class ContainerRuntimeRequires(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined) + toggle_flag(self.expand_name('endpoint.{endpoint_name}.reconfigure'), + self.is_joined and self._config_changed()) + + def _config_changed(self): + """ + Determine if our received data has changed. + + :return: Boolean + """ + # NB: this call should match whatever we're tracking in handle_remote_config + return is_data_changed('containerd.remote_config', + [self.get_sandbox_image()]) + + def handle_remote_config(self): + """ + Keep track of received data so we can know if it changes. + + :return: None + """ + clear_flag(self.expand_name('endpoint.{endpoint_name}.reconfigure')) + # Presently, we only care about one piece of remote config. Expand + # the list as needed. + data_changed('containerd.remote_config', + [self.get_sandbox_image()]) + + def get_sandbox_image(self): + """ + Get the sandbox image URI if a remote has published one. + + :return: String: remotely configured sandbox image + """ + return self.all_joined_units.received.get('sandbox_image') + + def set_config(self, socket, runtime, nvidia_enabled): + """ + Set the configuration to be published. + + :param socket: String uri to runtime socket + :param runtime: String runtime executable + :param nvidia_enabled: Boolean nvidia runtime enabled + :return: None + """ + for relation in self.relations: + relation.to_publish.update({ + 'socket': socket, + 'runtime': runtime, + 'nvidia_enabled': nvidia_enabled + }) diff --git a/kata/hooks/relations/untrusted-container-runtime/.gitignore b/kata/hooks/relations/untrusted-container-runtime/.gitignore new file mode 100644 index 0000000..894a44c --- /dev/null +++ b/kata/hooks/relations/untrusted-container-runtime/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/kata/hooks/relations/untrusted-container-runtime/LICENSE b/kata/hooks/relations/untrusted-container-runtime/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/kata/hooks/relations/untrusted-container-runtime/LICENSE @@ -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/kata/hooks/relations/untrusted-container-runtime/README.md b/kata/hooks/relations/untrusted-container-runtime/README.md new file mode 100644 index 0000000..135dca5 --- /dev/null +++ b/kata/hooks/relations/untrusted-container-runtime/README.md @@ -0,0 +1,54 @@ +# interface-untrusted-container-runtime + +## Overview + +This interface handles communication between subordinate container runtimes +and this subordinate untrusted container runtime, such as `containerd` and +`kata-containers`. + +## Usage + +### Provides + +The providing side of the container interface provides a place for an +untrusted container runtime to connect to. + +Your charm should respond to the `endpoint.{endpoint_name}.available` state, +which indicates that there is an untrusted container runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def update_kubelet_config(containerd): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + config = endpoint.get_config() + + render( + 'config.toml', + { + 'runtime_name': config['name'], + 'runtime_binary': config['binary_path'] + } + ) +``` + +### Requires + +The requiring side of the untrusted container interface requires a place for +an untrusted container runtime to connect to. + +Your charm should set `{endpoint_name}.available` state, +which indicates that the container is runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def pubish_config(): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + endpoint.set_config( + 'name': 'kata', + 'binary_path': '/usr/bin/kata-runtime' + ) +``` diff --git a/kata/hooks/relations/untrusted-container-runtime/__init__.py b/kata/hooks/relations/untrusted-container-runtime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kata/hooks/relations/untrusted-container-runtime/interface.yaml b/kata/hooks/relations/untrusted-container-runtime/interface.yaml new file mode 100644 index 0000000..d0d7dbc --- /dev/null +++ b/kata/hooks/relations/untrusted-container-runtime/interface.yaml @@ -0,0 +1,4 @@ +name: untrusted-container-runtime +summary: Interface for relating to untrusted container runtimes +version: 1 +maintainer: "Joe Borg " diff --git a/kata/hooks/relations/untrusted-container-runtime/provides.py b/kata/hooks/relations/untrusted-container-runtime/provides.py new file mode 100644 index 0000000..09deb26 --- /dev/null +++ b/kata/hooks/relations/untrusted-container-runtime/provides.py @@ -0,0 +1,28 @@ +from charms.reactive import ( + Endpoint, + set_flag, + clear_flag +) + +from charms.reactive import ( + when, + when_not +) + + +class ContainerRuntimeProvides(Endpoint): + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('endpoint.{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('endpoint.{endpoint_name}.available')) + + def get_config(self): + """ + Get the configuration published. + + :return: Dictionary configuration + """ + return self.all_joined_units.received diff --git a/kata/hooks/relations/untrusted-container-runtime/requires.py b/kata/hooks/relations/untrusted-container-runtime/requires.py new file mode 100644 index 0000000..f717ba6 --- /dev/null +++ b/kata/hooks/relations/untrusted-container-runtime/requires.py @@ -0,0 +1,34 @@ +from charms.reactive import ( + Endpoint, + set_flag, + clear_flag +) + +from charms.reactive import ( + when, + when_not +) + + +class ContainerRuntimeRequires(Endpoint): + @when('endpoint.{endpoint_name}.changed') + def changed(self): + set_flag(self.expand_name('endpoint.{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('endpoint.{endpoint_name}.available')) + + def set_config(self, name, binary_path): + """ + Set the configuration to be published. + + :param name: String name of runtime + :param binary_path: String runtime executable + :return: None + """ + for relation in self.relations: + relation.to_publish.update({ + 'name': name, + 'binary_path': binary_path + }) diff --git a/kata/hooks/start b/kata/hooks/start new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/start @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/stop b/kata/hooks/stop new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/stop @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/untrusted-relation-broken b/kata/hooks/untrusted-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/untrusted-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/untrusted-relation-changed b/kata/hooks/untrusted-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/untrusted-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/untrusted-relation-created b/kata/hooks/untrusted-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/untrusted-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/untrusted-relation-departed b/kata/hooks/untrusted-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/untrusted-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/untrusted-relation-joined b/kata/hooks/untrusted-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/untrusted-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/update-status b/kata/hooks/update-status new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/update-status @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/hooks/upgrade-charm b/kata/hooks/upgrade-charm new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kata/hooks/upgrade-charm @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kata/icon.svg b/kata/icon.svg new file mode 100644 index 0000000..96a5d0c --- /dev/null +++ b/kata/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/kata/layer.yaml b/kata/layer.yaml new file mode 100644 index 0000000..7dfb411 --- /dev/null +++ b/kata/layer.yaml @@ -0,0 +1,20 @@ +"includes": +- "layer:options" +- "layer:basic" +- "layer:debug" +- "interface:container-runtime" +- "interface:untrusted-container-runtime" +- "layer:status" +"exclude": [".travis.yml", "tests", "tox.ini", "test-requirements.txt", "unit_tests"] +"options": + "basic": + "packages": [] + "python_packages": [] + "use_venv": !!bool "true" + "include_system_packages": !!bool "false" + "debug": {} + "status": + "patch-hookenv": !!bool "true" + "kata": {} +"repo": "https://github.com/charmed-kubernetes/charm-kata" +"is": "kata" diff --git a/kata/lib/charms/layer/__init__.py b/kata/lib/charms/layer/__init__.py new file mode 100644 index 0000000..a8e0c64 --- /dev/null +++ b/kata/lib/charms/layer/__init__.py @@ -0,0 +1,60 @@ +import sys +from importlib import import_module +from pathlib import Path + + +def import_layer_libs(): + """ + Ensure that all layer libraries are imported. + + This makes it possible to do the following: + + from charms import layer + + layer.foo.do_foo_thing() + + Note: This function must be called after bootstrap. + """ + for module_file in Path('lib/charms/layer').glob('*'): + module_name = module_file.stem + if module_name in ('__init__', 'basic', 'execd') or not ( + module_file.suffix == '.py' or module_file.is_dir() + ): + continue + import_module('charms.layer.{}'.format(module_name)) + + +# Terrible hack to support the old terrible interface. +# Try to get people to call layer.options.get() instead so +# that we can remove this garbage. +# Cribbed from https://stackoverfLow.com/a/48100440/4941864 +class OptionsBackwardsCompatibilityHack(sys.modules[__name__].__class__): + def __call__(self, section=None, layer_file=None): + if layer_file is None: + return self.get(section=section) + else: + return self.get(section=section, + layer_file=Path(layer_file)) + + +def patch_options_interface(): + from charms.layer import options + if sys.version_info.minor >= 5: + options.__class__ = OptionsBackwardsCompatibilityHack + else: + # Py 3.4 doesn't support changing the __class__, so we have to do it + # another way. The last line is needed because we already have a + # reference that doesn't get updated with sys.modules. + name = options.__name__ + hack = OptionsBackwardsCompatibilityHack(name) + hack.get = options.get + sys.modules[name] = hack + sys.modules[__name__].options = hack + + +try: + patch_options_interface() +except ImportError: + # This may fail if pyyaml hasn't been installed yet. But in that + # case, the bootstrap logic will try it again once it has. + pass diff --git a/kata/lib/charms/layer/basic.py b/kata/lib/charms/layer/basic.py new file mode 100644 index 0000000..bbdd074 --- /dev/null +++ b/kata/lib/charms/layer/basic.py @@ -0,0 +1,501 @@ +import os +import sys +import re +import shutil +from distutils.version import LooseVersion +from pkg_resources import Requirement +from glob import glob +from subprocess import check_call, check_output, CalledProcessError +from time import sleep + +from charms import layer +from charms.layer.execd import execd_preinstall + + +def _get_subprocess_env(): + env = os.environ.copy() + env['LANG'] = env.get('LANG', 'C.UTF-8') + return env + + +def get_series(): + """ + Return series for a few known OS:es. + Tested as of 2019 november: + * centos6, centos7, rhel6. + * bionic + """ + series = "" + + # Looking for content in /etc/os-release + # works for ubuntu + some centos + if os.path.isfile('/etc/os-release'): + d = {} + with open('/etc/os-release', 'r') as rel: + for l in rel: + if not re.match(r'^\s*$', l): + k, v = l.split('=') + d[k.strip()] = v.strip().replace('"', '') + series = "{ID}{VERSION_ID}".format(**d) + + # Looking for content in /etc/redhat-release + # works for redhat enterprise systems + elif os.path.isfile('/etc/redhat-release'): + with open('/etc/redhat-release', 'r') as redhatlsb: + # CentOS Linux release 7.7.1908 (Core) + line = redhatlsb.readline() + release = int(line.split("release")[1].split()[0][0]) + series = "centos" + str(release) + + # Looking for content in /etc/lsb-release + # works for ubuntu + elif os.path.isfile('/etc/lsb-release'): + d = {} + with open('/etc/lsb-release', 'r') as lsb: + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + series = d['DISTRIB_CODENAME'] + + # This is what happens if we cant figure out the OS. + else: + series = "unknown" + return series + + +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 + + series = get_series() + + # OMG?! is build-essentials needed? + ubuntu_packages = ['python3-pip', + 'python3-setuptools', + 'python3-yaml', + 'python3-dev', + 'python3-wheel', + 'build-essential'] + + # I'm not going to "yum group info "Development Tools" + # omitting above madness + centos_packages = ['python3-pip', + 'python3-setuptools', + 'python3-devel', + 'python3-wheel'] + + packages_needed = [] + if 'centos' in series: + packages_needed = centos_packages + else: + packages_needed = ubuntu_packages + + 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') + hook_name = os.path.basename(sys.argv[0]) + is_bootstrapped = os.path.exists('wheelhouse/.bootstrapped') + is_charm_upgrade = hook_name == 'upgrade-charm' + is_series_upgrade = hook_name == 'post-series-upgrade' + is_post_upgrade = os.path.exists('wheelhouse/.upgraded') + is_upgrade = (not is_post_upgrade and + (is_charm_upgrade or is_series_upgrade)) + if is_bootstrapped and not is_upgrade: + # older subordinates might have downgraded charm-env, so we should + # restore it if necessary + install_or_update_charm_env() + activate_venv() + # the .upgrade file prevents us from getting stuck in a loop + # when re-execing to activate the venv; at this point, we've + # activated the venv, so it's safe to clear it + if is_post_upgrade: + os.unlink('wheelhouse/.upgraded') + return + if os.path.exists(venv): + try: + # focal installs or upgrades prior to PR 160 could leave the venv + # in a broken state which would prevent subsequent charm upgrades + _load_installed_versions(vpip) + except CalledProcessError: + is_broken_venv = True + else: + is_broken_venv = False + if is_upgrade or is_broken_venv: + # All upgrades should do a full clear of the venv, rather than + # just updating it, to bring in updates to Python itself + shutil.rmtree(venv) + if is_upgrade: + if os.path.exists('wheelhouse/.bootstrapped'): + os.unlink('wheelhouse/.bootstrapped') + # bootstrap wheelhouse + if os.path.exists('wheelhouse'): + pre_eoan = series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty', + 'ubuntu16.04', 'xenial', + 'ubuntu18.04', 'bionic') + pydistutils_lines = [ + "[easy_install]\n", + "find_links = file://{}/wheelhouse/\n".format(charm_dir), + "no_index=True\n", + "index_url=\n", # deliberately nothing here; disables it. + ] + if pre_eoan: + pydistutils_lines.append("allow_hosts = ''\n") + 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(pydistutils_lines) + if 'centos' in series: + yum_install(packages_needed) + else: + apt_install(packages_needed) + from charms.layer import options + cfg = options.get('basic') + # include packages defined in layer.yaml + if 'centos' in series: + yum_install(cfg.get('packages', [])) + else: + 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 = get_series() + if series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty'): + apt_install(['python-virtualenv']) + elif 'centos' in series: + yum_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, env=_get_subprocess_env()) + 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') + pre_install_pkgs = ['pip', 'setuptools', 'setuptools-scm'] + # we bundle these packages to work around bugs in older versions (such + # as https://github.com/pypa/pip/issues/56), but if the system already + # provided a newer version, downgrading it can cause other problems + _update_if_newer(pip, pre_install_pkgs) + # install the rest of the wheelhouse deps (extract the pkg names into + # a set so that we can ignore the pre-install packages and let pip + # choose the best version in case there are multiple from layer + # conflicts) + _versions = _load_wheelhouse_versions() + _pkgs = _versions.keys() - set(pre_install_pkgs) + # add back the versions such that each package in pkgs is + # ==. + # This ensures that pip 20.3.4+ will install the packages from the + # wheelhouse without (erroneously) flagging an error. + pkgs = _add_back_versions(_pkgs, _versions) + reinstall_flag = '--force-reinstall' + if not cfg.get('use_venv', True) and pre_eoan: + reinstall_flag = '--ignore-installed' + check_call([pip, 'install', '-U', reinstall_flag, '--no-index', + '--no-cache-dir', '-f', 'wheelhouse'] + list(pkgs), + env=_get_subprocess_env()) + # re-enable installation from pypi + os.remove('/root/.pydistutils.cfg') + + # install pyyaml for centos7, since, unlike the ubuntu image, the + # default image for centos doesn't include pyyaml; see the discussion: + # https://discourse.jujucharms.com/t/charms-for-centos-lets-begin + if 'centos' in series: + check_call([pip, 'install', '-U', 'pyyaml'], + env=_get_subprocess_env()) + + # install python packages from layer options + if cfg.get('python_packages'): + check_call([pip, 'install', '-U'] + cfg.get('python_packages'), + env=_get_subprocess_env()) + 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') + # setup wrappers to ensure envs are used for scripts + install_or_update_charm_env() + for wrapper in ('charms.reactive', 'charms.reactive.sh', + 'chlp', 'layer_option'): + src = os.path.join('/usr/local/sbin', 'charm-env') + dst = os.path.join('/usr/local/sbin', wrapper) + if not os.path.exists(dst): + os.symlink(src, dst) + if cfg.get('use_venv'): + shutil.copy2('bin/layer_option', vbin) + else: + shutil.copy2('bin/layer_option', '/usr/local/bin/') + # re-link the charm copy to the wrapper in case charms + # call bin/layer_option directly (as was the old pattern) + os.remove('bin/layer_option') + os.symlink('/usr/local/sbin/layer_option', 'bin/layer_option') + # flag us as having already bootstrapped so we don't do it again + open('wheelhouse/.bootstrapped', 'w').close() + if is_upgrade: + # flag us as having already upgraded so we don't do it again + open('wheelhouse/.upgraded', '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 _load_installed_versions(pip): + pip_freeze = check_output([pip, 'freeze']).decode('utf8') + versions = {} + for pkg_ver in pip_freeze.splitlines(): + try: + req = Requirement.parse(pkg_ver) + except ValueError: + continue + versions.update({ + req.project_name: LooseVersion(ver) + for op, ver in req.specs if op == '==' + }) + return versions + + +def _load_wheelhouse_versions(): + versions = {} + for wheel in glob('wheelhouse/*'): + pkg, ver = os.path.basename(wheel).rsplit('-', 1) + # nb: LooseVersion ignores the file extension + versions[pkg.replace('_', '-')] = LooseVersion(ver) + return versions + + +def _add_back_versions(pkgs, versions): + """Add back the version strings to each of the packages. + + The versions are LooseVersion() from _load_wheelhouse_versions(). This + function strips the ".zip" or ".tar.gz" from the end of the version string + and adds it back to the package in the form of == + + If a package name is not a key in the versions dictionary, then it is + returned in the list unchanged. + + :param pkgs: A list of package names + :type pkgs: List[str] + :param versions: A map of package to LooseVersion + :type versions: Dict[str, LooseVersion] + :returns: A list of (maybe) versioned packages + :rtype: List[str] + """ + def _strip_ext(s): + """Strip an extension (if it exists) from the string + + :param s: the string to strip an extension off if it exists + :type s: str + :returns: string without an extension of .zip or .tar.gz + :rtype: str + """ + for ending in [".zip", ".tar.gz"]: + if s.endswith(ending): + return s[:-len(ending)] + return s + + def _maybe_add_version(pkg): + """Maybe add back the version number to a package if it exists. + + Adds the version number, if the package exists in the lexically + captured `versions` dictionary, in the form ==. Strips + the extension if it exists. + + :param pkg: the package name to (maybe) add the version number to. + :type pkg: str + """ + try: + return "{}=={}".format(pkg, _strip_ext(str(versions[pkg]))) + except KeyError: + pass + return pkg + + return [_maybe_add_version(pkg) for pkg in pkgs] + + +def _update_if_newer(pip, pkgs): + installed = _load_installed_versions(pip) + wheelhouse = _load_wheelhouse_versions() + for pkg in pkgs: + if pkg not in installed or wheelhouse[pkg] > installed[pkg]: + check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', + pkg], env=_get_subprocess_env()) + + +def install_or_update_charm_env(): + # On Trusty python3-pkg-resources is not installed + try: + from pkg_resources import parse_version + except ImportError: + apt_install(['python3-pkg-resources']) + from pkg_resources import parse_version + + try: + installed_version = parse_version( + check_output(['/usr/local/sbin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + installed_version = parse_version('0.0.0') + try: + bundled_version = parse_version( + check_output(['bin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + bundled_version = parse_version('0.0.0') + if installed_version < bundled_version: + shutil.copy2('bin/charm-env', '/usr/local/sbin/') + + +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. + """ + from charms.layer import options + venv = os.path.abspath('../.venv') + vbin = os.path.join(venv, 'bin') + vpy = os.path.join(vbin, 'python') + use_venv = options.get('basic', 'use_venv') + if use_venv and '.venv' not in sys.executable: + # activate the venv + os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) + reload_interpreter(vpy) + layer.patch_options_interface() + layer.import_layer_libs() + + +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 = _get_subprocess_env() + + 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 + try: + # sometimes apt-get update needs to be run + check_call(['apt-get', 'update'], env=env) + except CalledProcessError: + # sometimes it's a dpkg lock issue + pass + sleep(5) + else: + break + + +def yum_install(packages): + """ Installs packages with yum. + This function largely mimics the apt_install function for consistency. + """ + if packages: + env = os.environ.copy() + cmd = ['yum', '-y', 'install'] + for attempt in range(3): + try: + check_call(cmd + packages, env=env) + except CalledProcessError: + if attempt == 2: + raise + try: + check_call(['yum', 'update'], env=env) + except CalledProcessError: + pass + sleep(5) + else: + break + else: + pass + + +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]) + + +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/kata/lib/charms/layer/execd.py b/kata/lib/charms/layer/execd.py new file mode 100644 index 0000000..438d9a1 --- /dev/null +++ b/kata/lib/charms/layer/execd.py @@ -0,0 +1,114 @@ +# 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 . + +# This module may only import from the Python standard library. +import os +import sys +import subprocess +import time + +''' +execd/preinstall + +Read the layer-basic docs for more info on how to use this feature. +https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#exec-d-support +''' + + +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<" +"description": | + Kata Containers is an open source community working to build a secure + container runtime with lightweight virtual machines that feel and perform + like containers, but provide stronger workload isolation using hardware + virtualization technology as a second layer of defense. +"tags": +- "misc" +- "containers" +"series": +- "focal" +- "bionic" +"requires": + "containerd": + "interface": "container-runtime" + "scope": "container" + "untrusted": + "interface": "untrusted-container-runtime" + "scope": "container" +"resources": + "kata-archive": + "type": "file" + "filename": "kata-archive.tar.gz" + "description": "Offline archive of kata" + +"subordinate": !!bool "true" diff --git a/kata/pydocmd.yml b/kata/pydocmd.yml new file mode 100644 index 0000000..ab3b2ef --- /dev/null +++ b/kata/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'Status Management Layer' + +generate: + - status.md: + - charms.layer.status.WorkloadState + - charms.layer.status.maintenance + - charms.layer.status.maint + - charms.layer.status.blocked + - charms.layer.status.waiting + - charms.layer.status.active + - charms.layer.status.status_set + +pages: + - Status Management Layer: status.md + +gens_dir: docs diff --git a/kata/reactive/__init__.py b/kata/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kata/reactive/kata.py b/kata/reactive/kata.py new file mode 100644 index 0000000..e11c45f --- /dev/null +++ b/kata/reactive/kata.py @@ -0,0 +1,136 @@ +import os +import requests + +from subprocess import ( + check_call, + check_output +) + +from charmhelpers.core import host + +from charms.reactive import ( + when, + when_not, + set_state, + remove_state, + endpoint_from_flag, + hook, +) + +from charmhelpers.fetch import ( + apt_install, + apt_update, + apt_purge, + apt_autoremove, + import_key +) + +from charmhelpers.core.hookenv import ( + resource_get +) + +from charms.layer import status + + +KATA_PACKAGES = [ + 'kata-runtime', + 'kata-proxy', + 'kata-shim' +] + + +@when_not('kata.installed') +@when_not('endpoint.untrusted.departed') +def install_kata(): + """ + Install the Kata container runtime. + + :returns: None + """ + dist = host.lsb_release() + release = '{}_{}'.format( + dist['DISTRIB_ID'], + dist['DISTRIB_RELEASE'] + ) + + arch = check_output(['arch']).decode().strip() + + archive = resource_get('kata-archive') + + if not archive or os.path.getsize(archive) == 0: + status.maintenance('Installing Kata via apt') + gpg_key = requests.get( + 'http://download.opensuse.org/repositories/home:/katacontainers:/' + 'releases:/{}:/master/x{}/Release.key'.format(arch, release)).text + import_key(gpg_key) + + with open('/etc/apt/sources.list.d/kata-containers.list', 'w') as f: + f.write( + 'deb http://download.opensuse.org/repositories/home:/' + 'katacontainers:/releases:/{}:/master/x{}/ /' + .format(arch, release) + ) + + apt_update() + apt_install(KATA_PACKAGES) + + else: + status.maintenance('Installing Kata via resource') + unpack = '/tmp/kata-debs' + + if not os.path.isdir(unpack): + os.makedirs(unpack, exist_ok=True) + + check_call(['tar', '-xvf', archive, '-C', unpack]) + check_call('apt-get install -y {}/*.deb'.format(unpack), shell=True) + + status.active('Kata runtime available') + set_state('kata.installed') + + +@when('endpoint.untrusted.departed') +def purge_kata(): + """ + Purge Kata containers. + + :return: None + """ + status.maintenance('Purging Kata') + + apt_purge(KATA_PACKAGES, fatal=False) + + source = '/etc/apt/sources.list.d/kata-containers.list' + if os.path.isfile(source): + os.remove(source) + + apt_autoremove() + + remove_state('kata.installed') + + +@when('kata.installed') +@when('endpoint.untrusted.joined') +@when_not('endpoint.untrusted.departed') +def publish_config(): + """ + Pass configuration over the interface. + + :return: None + """ + endpoint = endpoint_from_flag('endpoint.untrusted.joined') + endpoint.set_config( + name='kata', + binary_path='/usr/bin/kata-runtime' + ) + + +@hook('pre-series-upgrade') +def pre_series_upgrade(): + """Set status during series upgrade.""" + status.blocked('Series upgrade in progress') + + +@hook('post-series-upgrade') +def post_series_upgrade(): + """Reset status to active after series upgrade.""" + status.active('Kata runtime available') diff --git a/kata/reactive/status.py b/kata/reactive/status.py new file mode 100644 index 0000000..2f33f3f --- /dev/null +++ b/kata/reactive/status.py @@ -0,0 +1,4 @@ +from charms import layer + + +layer.status._initialize() diff --git a/kata/requirements.txt b/kata/requirements.txt new file mode 100644 index 0000000..55543d9 --- /dev/null +++ b/kata/requirements.txt @@ -0,0 +1,3 @@ +mock +flake8 +pytest diff --git a/kata/revision b/kata/revision new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/kata/revision @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/kata/tests/conftest.py b/kata/tests/conftest.py new file mode 100644 index 0000000..a92e249 --- /dev/null +++ b/kata/tests/conftest.py @@ -0,0 +1,4 @@ +import charms.unit_test + + +charms.unit_test.patch_reactive() diff --git a/kata/tests/test_kata_reactive.py b/kata/tests/test_kata_reactive.py new file mode 100644 index 0000000..421b0dd --- /dev/null +++ b/kata/tests/test_kata_reactive.py @@ -0,0 +1,35 @@ +from reactive import kata + + +def test_packages_list(): + """Assert KATA_PACKAGES is a list of strings.""" + assert isinstance(kata.KATA_PACKAGES, list) + for item in kata.KATA_PACKAGES: + assert isinstance(item, str) + + +def test_install_kata(): + """Assert install_kata is a method.""" + assert callable(kata.install_kata) + + +def test_purge_kata(): + """Assert purge_kata is a method.""" + assert callable(kata.purge_kata) + + +def test_publist_config(): + """Assert publish_config is a method.""" + assert callable(kata.publish_config) + + +def test_series_upgrade(): + """Assert status is set during series upgrade.""" + assert kata.status.blocked.call_count == 0 + assert kata.status.active.call_count == 0 + kata.pre_series_upgrade() + assert kata.status.blocked.call_count == 1 + assert kata.status.active.call_count == 0 + kata.post_series_upgrade() + assert kata.status.blocked.call_count == 1 + assert kata.status.active.call_count == 1 diff --git a/kata/tox.ini b/kata/tox.ini new file mode 100644 index 0000000..0c9eaec --- /dev/null +++ b/kata/tox.ini @@ -0,0 +1,34 @@ +[flake8] +max-line-length = 120 +ignore = D100 + +[tox] +skipsdist = True +envlist = lint,py3 + +[tox:travis] +3.5: lint,py3 +3.6: lint, py3 +3.7: lint, py3 +3.8: lint, py3 + +[testenv] +basepython = python3 +setenv = + PYTHONPATH={toxinidir} +deps = + pyyaml + pytest + pytest-cov + flake8 + flake8-docstrings + requests + git+https://github.com/juju-solutions/charms.unit_test/#egg=charms.unit_test +commands = + pytest --cov-report term-missing \ + --cov reactive.kata --cov-fail-under 30 \ + --tb native -s {posargs} + +[testenv:lint] +envdir = {toxworkdir}/py3 +commands = flake8 {toxinidir}/reactive {toxinidir}/tests diff --git a/kata/version b/kata/version new file mode 100644 index 0000000..20817dd --- /dev/null +++ b/kata/version @@ -0,0 +1 @@ +ccfa68be \ No newline at end of file diff --git a/kata/wheelhouse.txt b/kata/wheelhouse.txt new file mode 100644 index 0000000..4413da7 --- /dev/null +++ b/kata/wheelhouse.txt @@ -0,0 +1,21 @@ +# layer:basic +# pip is pinned to <19.0 to avoid https://github.com/pypa/pip/issues/6164 +# even with installing setuptools before upgrading pip ends up with pip seeing +# the older setuptools at the system level if include_system_packages is true +pip>=18.1,<19.0 +# pin Jinja2, PyYAML and MarkupSafe to the last versions supporting python 3.5 +# for trusty +Jinja2<=2.10.1 +PyYAML<=5.2 +MarkupSafe<2.0.0 +setuptools<42 +setuptools-scm<=1.17.0 +charmhelpers>=0.4.0,<1.0.0 +charms.reactive>=0.1.0,<2.0.0 +wheel<0.34 +# pin netaddr to avoid pulling importlib-resources +netaddr<=0.7.19 + +# kata +requests + diff --git a/kata/wheelhouse/Jinja2-2.10.1.tar.gz b/kata/wheelhouse/Jinja2-2.10.1.tar.gz new file mode 100644 index 0000000..ffd1054 Binary files /dev/null and b/kata/wheelhouse/Jinja2-2.10.1.tar.gz differ diff --git a/kata/wheelhouse/MarkupSafe-1.1.1.tar.gz b/kata/wheelhouse/MarkupSafe-1.1.1.tar.gz new file mode 100644 index 0000000..a6dad8e Binary files /dev/null and b/kata/wheelhouse/MarkupSafe-1.1.1.tar.gz differ diff --git a/kata/wheelhouse/PyYAML-5.2.tar.gz b/kata/wheelhouse/PyYAML-5.2.tar.gz new file mode 100644 index 0000000..666d12a Binary files /dev/null and b/kata/wheelhouse/PyYAML-5.2.tar.gz differ diff --git a/kata/wheelhouse/Tempita-0.5.2.tar.gz b/kata/wheelhouse/Tempita-0.5.2.tar.gz new file mode 100644 index 0000000..755befc Binary files /dev/null and b/kata/wheelhouse/Tempita-0.5.2.tar.gz differ diff --git a/kata/wheelhouse/certifi-2021.10.8.tar.gz b/kata/wheelhouse/certifi-2021.10.8.tar.gz new file mode 100644 index 0000000..9e1581b Binary files /dev/null and b/kata/wheelhouse/certifi-2021.10.8.tar.gz differ diff --git a/kata/wheelhouse/charmhelpers-0.20.23.tar.gz b/kata/wheelhouse/charmhelpers-0.20.23.tar.gz new file mode 100644 index 0000000..8fbc8ec Binary files /dev/null and b/kata/wheelhouse/charmhelpers-0.20.23.tar.gz differ diff --git a/kata/wheelhouse/charms.reactive-1.4.1.tar.gz b/kata/wheelhouse/charms.reactive-1.4.1.tar.gz new file mode 100644 index 0000000..03bc1fe Binary files /dev/null and b/kata/wheelhouse/charms.reactive-1.4.1.tar.gz differ diff --git a/kata/wheelhouse/charset-normalizer-2.0.7.tar.gz b/kata/wheelhouse/charset-normalizer-2.0.7.tar.gz new file mode 100644 index 0000000..61df022 Binary files /dev/null and b/kata/wheelhouse/charset-normalizer-2.0.7.tar.gz differ diff --git a/kata/wheelhouse/idna-3.3.tar.gz b/kata/wheelhouse/idna-3.3.tar.gz new file mode 100644 index 0000000..ff2bcbf Binary files /dev/null and b/kata/wheelhouse/idna-3.3.tar.gz differ diff --git a/kata/wheelhouse/netaddr-0.7.19.tar.gz b/kata/wheelhouse/netaddr-0.7.19.tar.gz new file mode 100644 index 0000000..cc31d9d Binary files /dev/null and b/kata/wheelhouse/netaddr-0.7.19.tar.gz differ diff --git a/kata/wheelhouse/pbr-5.6.0.tar.gz b/kata/wheelhouse/pbr-5.6.0.tar.gz new file mode 100644 index 0000000..0d5c965 Binary files /dev/null and b/kata/wheelhouse/pbr-5.6.0.tar.gz differ diff --git a/kata/wheelhouse/pip-18.1.tar.gz b/kata/wheelhouse/pip-18.1.tar.gz new file mode 100644 index 0000000..a18192d Binary files /dev/null and b/kata/wheelhouse/pip-18.1.tar.gz differ diff --git a/kata/wheelhouse/pyaml-21.10.1.tar.gz b/kata/wheelhouse/pyaml-21.10.1.tar.gz new file mode 100644 index 0000000..b19aad3 Binary files /dev/null and b/kata/wheelhouse/pyaml-21.10.1.tar.gz differ diff --git a/kata/wheelhouse/requests-2.26.0.tar.gz b/kata/wheelhouse/requests-2.26.0.tar.gz new file mode 100644 index 0000000..101dc79 Binary files /dev/null and b/kata/wheelhouse/requests-2.26.0.tar.gz differ diff --git a/kata/wheelhouse/setuptools-41.6.0.zip b/kata/wheelhouse/setuptools-41.6.0.zip new file mode 100644 index 0000000..3345759 Binary files /dev/null and b/kata/wheelhouse/setuptools-41.6.0.zip differ diff --git a/kata/wheelhouse/setuptools_scm-1.17.0.tar.gz b/kata/wheelhouse/setuptools_scm-1.17.0.tar.gz new file mode 100644 index 0000000..43b16c7 Binary files /dev/null and b/kata/wheelhouse/setuptools_scm-1.17.0.tar.gz differ diff --git a/kata/wheelhouse/six-1.16.0.tar.gz b/kata/wheelhouse/six-1.16.0.tar.gz new file mode 100644 index 0000000..5bf3a27 Binary files /dev/null and b/kata/wheelhouse/six-1.16.0.tar.gz differ diff --git a/kata/wheelhouse/urllib3-1.26.7.tar.gz b/kata/wheelhouse/urllib3-1.26.7.tar.gz new file mode 100644 index 0000000..990abe6 Binary files /dev/null and b/kata/wheelhouse/urllib3-1.26.7.tar.gz differ diff --git a/kata/wheelhouse/wheel-0.33.6.tar.gz b/kata/wheelhouse/wheel-0.33.6.tar.gz new file mode 100644 index 0000000..c922c4e Binary files /dev/null and b/kata/wheelhouse/wheel-0.33.6.tar.gz differ diff --git a/keepalived/.build.manifest b/keepalived/.build.manifest new file mode 100644 index 0000000..cf1dfc6 --- /dev/null +++ b/keepalived/.build.manifest @@ -0,0 +1,546 @@ +{ + "layers": [ + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "fcdcea4e5de3e1556c24e6704607862d0ba00a56", + "url": "layer:options" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "a3ff62c32c993d80417f6e093e3ef95e42f62083", + "url": "layer:basic" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "a7d7b6423db37a47611310039e6ed1929c0a2eab", + "url": "layer:status" + }, + { + "branch": "refs/heads/stable", + "rev": "53a31f0c00b5625162024e47b0788d2f640b817c", + "url": "keepalived" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "4e2f90052b3c02031d09f10900c9e9cb22565dee", + "url": "interface:juju-info" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "5021f8a23f6e6e4cc449d2d02f2d8cb99763ec27", + "url": "interface:public-address" + }, + { + "branch": "refs/heads/master\nrefs/heads/stable", + "rev": "632131b1f122daf6fb601fd4c9f1e4dbb1a92e09", + "url": "interface:http" + } + ], + "signatures": { + ".build.manifest": [ + "build", + "dynamic", + "unchecked" + ], + ".github/workflows/main.yml": [ + "layer:basic", + "static", + "96a48a981ceb2a96f427a6b5226d2da6d7191981793804055d70a88ca1987473" + ], + ".gitignore": [ + "layer:status", + "static", + "315971ad9cc5d6ada2391f0940e1800149b211a18be3c7a8f396735d7978702b" + ], + "LICENSE": [ + "layer:status", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "Makefile": [ + "layer:basic", + "static", + "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301" + ], + "README.md": [ + "keepalived", + "static", + "c65fa846ba9a75aab5a5d9f1a38818ed806df4246d58fc1034c7f21cb168f470" + ], + "bin/charm-env": [ + "layer:basic", + "static", + "fb6a20fac4102a6a4b6ffe903fcf666998f9a95a3647e6f9af7a1eeb44e58fd5" + ], + "bin/layer_option": [ + "layer:options", + "static", + "e959bf29da4c5edff28b2602c24113c4df9e25cdc9f2aa3b5d46c8577b2a40cc" + ], + "config.yaml": [ + "keepalived", + "dynamic", + "d04d990bb872982a66430cf14b74a864df38eb0e96dd54b80d7f28c83b599249" + ], + "copyright": [ + "layer:status", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "copyright.layer-basic": [ + "layer:basic", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-options": [ + "layer:options", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "docs/status.md": [ + "layer:status", + "static", + "975dec9f8c938196e102e954a80226bda293407c4e5ae857c118bf692154702a" + ], + "hooks/config-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/hook.template": [ + "layer:basic", + "static", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/install": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/juju-info-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/juju-info-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/juju-info-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/juju-info-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/juju-info-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/lb-sink-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/lb-sink-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/lb-sink-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/lb-sink-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/lb-sink-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-elected": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-settings-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/post-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/pre-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/relations/http/.gitignore": [ + "interface:http", + "static", + "83b4ca18cc39800b1d260b5633cd0252e21501b21e7c33e718db44f1a68a09b8" + ], + "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", + "8c72cd8a5a6ea24f53b6dba11f4353c75265bfa7d3ecc2dd096c8963eab8c877" + ], + "hooks/relations/http/requires.py": [ + "interface:http", + "static", + "76cc886368eaf9c2403a6dc46b40531c3f4eaf67b08829f890c57cb645430abd" + ], + "hooks/relations/juju-info/.gitignore": [ + "interface:juju-info", + "static", + "315971ad9cc5d6ada2391f0940e1800149b211a18be3c7a8f396735d7978702b" + ], + "hooks/relations/juju-info/README.md": [ + "interface:juju-info", + "static", + "745aade1bda4e3cb7b07109c8c7560b2f56e076fcd6e8298465c7f66a970cfa0" + ], + "hooks/relations/juju-info/__init__.py": [ + "interface:juju-info", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/juju-info/docs/peers.md": [ + "interface:juju-info", + "static", + "710be74f2673dd71a8a41d51417b109bfe68782cf527f100138b628fce531c87" + ], + "hooks/relations/juju-info/docs/provides.md": [ + "interface:juju-info", + "static", + "f986015a7c6790057f694d91ccda2c281cf07634a2dfbe6607e67768a9d9f7b0" + ], + "hooks/relations/juju-info/docs/requires.md": [ + "interface:juju-info", + "static", + "bbe0d5b69bfee4992adcfcbf974703e351027f23062dda20845616cb49878010" + ], + "hooks/relations/juju-info/interface.yaml": [ + "interface:juju-info", + "static", + "0d0057ed0bda75157e8314a1259e1a8bb883d2f738ad1b731177e821b5b1b542" + ], + "hooks/relations/juju-info/make_docs": [ + "interface:juju-info", + "static", + "136818e53eb2ee7c5a178c5793d1a06811bebfbeb8875b9fc8e425d93ad9b433" + ], + "hooks/relations/juju-info/peers.py": [ + "interface:juju-info", + "static", + "3a778e2d89f736caf4e1e743ebc96d9ce53af7f3e0bef4b706dd74e5eb373b6e" + ], + "hooks/relations/juju-info/provides.py": [ + "interface:juju-info", + "static", + "3a778e2d89f736caf4e1e743ebc96d9ce53af7f3e0bef4b706dd74e5eb373b6e" + ], + "hooks/relations/juju-info/pydocmd.yml": [ + "interface:juju-info", + "static", + "78f3a1376cc4ef1c297a239104203f70701da29d089fefceb5323503422ee848" + ], + "hooks/relations/juju-info/requires.py": [ + "interface:juju-info", + "static", + "3a778e2d89f736caf4e1e743ebc96d9ce53af7f3e0bef4b706dd74e5eb373b6e" + ], + "hooks/relations/juju-info/tox.ini": [ + "interface:juju-info", + "static", + "caeace713d04686331d19b2d466e066e4123bd9197152ab99deb1a29a1501502" + ], + "hooks/relations/public-address/README.md": [ + "interface:public-address", + "static", + "7225effe61bfd8571447b8b685a2ecb52be17431b3066a5306330954c4cb064d" + ], + "hooks/relations/public-address/__init__.py": [ + "interface:public-address", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/public-address/interface.yaml": [ + "interface:public-address", + "static", + "49d6777a54aa84c7d3be8d531be237564e90f2e4cb2be05ef5617a372a382340" + ], + "hooks/relations/public-address/provides.py": [ + "interface:public-address", + "static", + "7c99b0fe987d38773ed3e67c0378fdb78748c04d6895489cd4bca40aaeb051b2" + ], + "hooks/relations/public-address/requires.py": [ + "interface:public-address", + "static", + "d6a7c6c0762d29a5db19afb4cf82af50812988d5e19a3a48fcbe8b0f6fec12a5" + ], + "hooks/start": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/stop": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/update-status": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/upgrade-charm": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/website-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/website-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/website-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/website-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/website-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "icon.svg": [ + "keepalived", + "static", + "7c51f3a3274f85f40de905124d32227711d3eea8e81bc9b14d6962471026f6af" + ], + "layer.yaml": [ + "keepalived", + "dynamic", + "900ed82afe183b924d8ebe49ad3d886b1967b35094e2e6431359619c19e6b9cc" + ], + "lib/charms/layer/__init__.py": [ + "layer:basic", + "static", + "dfe0d26c6bf409767de6e2546bc648f150e1b396243619bad3aa0553ab7e0e6f" + ], + "lib/charms/layer/basic.py": [ + "layer:basic", + "static", + "98b47134770ed6e4c0b2d4aad73cd5bc200bec84aa9c1c4e075fd70c3222a0c9" + ], + "lib/charms/layer/execd.py": [ + "layer:basic", + "static", + "fda8bd491032db1db8ddaf4e99e7cc878c6fb5432efe1f91cadb5b34765d076d" + ], + "lib/charms/layer/options.py": [ + "layer:options", + "static", + "8ae7a07d22542fc964f2d2bee8219d1c78a68dace70a1b38d36d4aea47b1c3b2" + ], + "lib/charms/layer/status.py": [ + "layer:status", + "static", + "d560a5e07b2e5f2b0f25f30e1f0278b06f3f90c01e4dbad5c83d71efc79018c6" + ], + "make_docs": [ + "layer:status", + "static", + "c990f55c8e879793a62ed8464ee3d7e0d7d2225fdecaf17af24b0df0e2daa8c1" + ], + "metadata.yaml": [ + "keepalived", + "dynamic", + "dac11c4ab89f07c6202bde9cd8a88843df4b4f130c74a24cebb2e1806c733094" + ], + "pydocmd.yml": [ + "layer:status", + "static", + "11d9293901f32f75f4256ae4ac2073b92ce1d7ef7b6c892ba9fbb98690a0b330" + ], + "reactive/__init__.py": [ + "layer:basic", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "reactive/keepalived.py": [ + "keepalived", + "static", + "496473d95ce5f261a7119431236a68675a93ab2f35db9c355591f77d598e8caf" + ], + "reactive/status.py": [ + "layer:status", + "static", + "30207fc206f24e91def5252f1c7f7c8e23c0aed0e93076babf5e03c05296d207" + ], + "requirements.txt": [ + "layer:basic", + "static", + "a00f75d80849e5b4fc5ad2e7536f947c25b1a4044b341caa8ee87a92d3a4c804" + ], + "templates/50-keepalived.conf": [ + "keepalived", + "static", + "0feef827d3edbbdb047f4efa9fcb81c8b88600c07856f00eedeebf6d43010e96" + ], + "templates/keepalived.conf": [ + "keepalived", + "static", + "a8a28fb2ad0195f78dece2f6594e5c2e826340e33d01bd71c1b9ededd6bb0955" + ], + "tox.ini": [ + "layer:status", + "static", + "2669a78e8e51c1606874e1cc97ca99e660ff547a79592572a38a268d99b25b67" + ], + "version": [ + "keepalived", + "dynamic", + "84c2c1461c9a1c34c65d7e620e19234566d6e8bd5eb279ee556d0750321b2046" + ], + "wheelhouse.txt": [ + "layer:basic", + "dynamic", + "44b8a3ab6ccaf3a81c8a96526a285462e01964e6090fd40104f3a087bab43c0c" + ], + "wheelhouse/Jinja2-2.10.1.tar.gz": [ + "layer:basic", + "dynamic", + "065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013" + ], + "wheelhouse/MarkupSafe-1.1.1.tar.gz": [ + "layer:basic", + "dynamic", + "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b" + ], + "wheelhouse/PyYAML-5.2.tar.gz": [ + "layer:basic", + "dynamic", + "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c" + ], + "wheelhouse/Tempita-0.5.2.tar.gz": [ + "__pip__", + "dynamic", + "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c" + ], + "wheelhouse/charmhelpers-0.20.23.tar.gz": [ + "layer:basic", + "dynamic", + "59a9776594e91cd3e3e000043f8668b4d7b279422dbb17e320f01dc16385b80e" + ], + "wheelhouse/charms.reactive-1.4.1.tar.gz": [ + "layer:basic", + "dynamic", + "bba21b4fd40b26c240c9ef2aa10c6fdf73592031c68591da4e7ccc46ca9cb616" + ], + "wheelhouse/netaddr-0.7.19.tar.gz": [ + "layer:basic", + "dynamic", + "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd" + ], + "wheelhouse/pbr-5.6.0.tar.gz": [ + "__pip__", + "dynamic", + "42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd" + ], + "wheelhouse/pip-18.1.tar.gz": [ + "layer:basic", + "dynamic", + "c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1" + ], + "wheelhouse/pyaml-21.10.1.tar.gz": [ + "__pip__", + "dynamic", + "c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383" + ], + "wheelhouse/setuptools-41.6.0.zip": [ + "layer:basic", + "dynamic", + "6afa61b391dcd16cb8890ec9f66cc4015a8a31a6e1c2b4e0c464514be1a3d722" + ], + "wheelhouse/setuptools_scm-1.17.0.tar.gz": [ + "layer:basic", + "dynamic", + "70a4cf5584e966ae92f54a764e6437af992ba42ac4bca7eb37cc5d02b98ec40a" + ], + "wheelhouse/six-1.16.0.tar.gz": [ + "__pip__", + "dynamic", + "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926" + ], + "wheelhouse/wheel-0.33.6.tar.gz": [ + "layer:basic", + "dynamic", + "10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646" + ] + } +} \ No newline at end of file diff --git a/keepalived/.github/workflows/main.yml b/keepalived/.github/workflows/main.yml new file mode 100644 index 0000000..565bfaf --- /dev/null +++ b/keepalived/.github/workflows/main.yml @@ -0,0 +1,50 @@ +name: Test Suite +on: [pull_request] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.5, 3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + - name: Run lint + run: tox -e flake8 + functional-test: + name: Functional test with LXD + runs-on: ubuntu-latest + timeout-minutes: 360 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install Dependencies + run: | + pip install tox + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@master + - name: Run test + run: tox -e func + - name: Show Status + if: ${{ always() }} + run: | + model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/); + juju status -m "$model" + - name: Show Error Logs + if: ${{ always() }} + run: | + model=$(juju models --format yaml|grep "^- name:.*zaza"|cut -f2 -d/); + juju debug-log -m "$model" --replay --no-tail --level ERROR diff --git a/keepalived/.gitignore b/keepalived/.gitignore new file mode 100644 index 0000000..ba1431e --- /dev/null +++ b/keepalived/.gitignore @@ -0,0 +1,2 @@ +.tox +__pycache__ diff --git a/keepalived/LICENSE b/keepalived/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/keepalived/LICENSE @@ -0,0 +1,202 @@ + + 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/keepalived/Makefile b/keepalived/Makefile new file mode 100644 index 0000000..a1ad3a5 --- /dev/null +++ b/keepalived/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/keepalived/README.md b/keepalived/README.md new file mode 100644 index 0000000..e332ef4 --- /dev/null +++ b/keepalived/README.md @@ -0,0 +1,8 @@ +# Keepalived Charm + +[Keepalived](http://www.keepalived.org/) is software which provides high +availability by assigning two or more nodes a virtual IP and monitoring +those nodes, failing over when one goes down. + +This charm is maintained along with the components of Charmed Kubernetes. For full information, +please visit the [official Charmed Kubernetes docs](https://www.ubuntu.com/kubernetes/docs/charm-keepalived). diff --git a/keepalived/bin/charm-env b/keepalived/bin/charm-env new file mode 100755 index 0000000..d211ce9 --- /dev/null +++ b/keepalived/bin/charm-env @@ -0,0 +1,107 @@ +#!/bin/bash + +VERSION="1.0.0" + + +find_charm_dirs() { + # Hopefully, $JUJU_CHARM_DIR is set so which venv to use in unambiguous. + if [[ -n "$JUJU_CHARM_DIR" || -n "$CHARM_DIR" ]]; then + if [[ -z "$JUJU_CHARM_DIR" ]]; then + # accept $CHARM_DIR to be more forgiving + export JUJU_CHARM_DIR="$CHARM_DIR" + fi + if [[ -z "$CHARM_DIR" ]]; then + # set CHARM_DIR as well to help with backwards compatibility + export CHARM_DIR="$JUJU_CHARM_DIR" + fi + return + fi + # Try to guess the value for JUJU_CHARM_DIR by looking for a non-subordinate + # (because there's got to be at least one principle) charm directory; + # if there are several, pick the first by alpha order. + agents_dir="/var/lib/juju/agents" + if [[ -d "$agents_dir" ]]; then + desired_charm="$1" + found_charm_dir="" + if [[ -n "$desired_charm" ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + charm_name="$(grep -o '^['\''"]\?name['\''"]\?:.*' $charm_dir/metadata.yaml 2> /dev/null | sed -e 's/.*: *//' -e 's/['\''"]//g')" + if [[ "$charm_name" == "$desired_charm" ]]; then + if [[ -n "$found_charm_dir" ]]; then + >&2 echo "Ambiguous possibilities for JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + found_charm_dir="$charm_dir" + fi + done + if [[ -z "$found_charm_dir" ]]; then + >&2 echo "Unable to determine JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + export JUJU_CHARM_DIR="$found_charm_dir" + export CHARM_DIR="$found_charm_dir" + return + fi + # shellcheck disable=SC2126 + non_subordinates="$(grep -L 'subordinate"\?:.*true' "$agents_dir"/unit-*/charm/metadata.yaml | wc -l)" + if [[ "$non_subordinates" -gt 1 ]]; then + >&2 echo 'Ambiguous possibilities for JUJU_CHARM_DIR; please use --charm or run within a Juju hook context' + exit 1 + elif [[ "$non_subordinates" -eq 1 ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + if grep -q 'subordinate"\?:.*true' "$charm_dir/metadata.yaml"; then + continue + fi + export JUJU_CHARM_DIR="$charm_dir" + export CHARM_DIR="$charm_dir" + return + done + fi + fi + >&2 echo 'Unable to determine JUJU_CHARM_DIR; please run within a Juju hook context' + exit 1 +} + +try_activate_venv() { + if [[ -d "$JUJU_CHARM_DIR/../.venv" ]]; then + . "$JUJU_CHARM_DIR/../.venv/bin/activate" + fi +} + +find_wrapped() { + PATH="${PATH/\/usr\/local\/sbin:}" which "$(basename "$0")" +} + + +if [[ "$1" == "--version" || "$1" == "-v" ]]; then + echo "$VERSION" + exit 0 +fi + + +# allow --charm option to hint which JUJU_CHARM_DIR to choose when ambiguous +# NB: --charm option must come first +# NB: option must be processed outside find_charm_dirs to modify $@ +charm_name="" +if [[ "$1" == "--charm" ]]; then + charm_name="$2" + shift; shift +fi + +find_charm_dirs "$charm_name" +try_activate_venv +export PYTHONPATH="$JUJU_CHARM_DIR/lib:$PYTHONPATH" + +if [[ "$(basename "$0")" == "charm-env" ]]; then + # being used as a shebang + exec "$@" +elif [[ "$0" == "$BASH_SOURCE" ]]; then + # being invoked as a symlink wrapping something to find in the venv + exec "$(find_wrapped)" "$@" +elif [[ "$(basename "$BASH_SOURCE")" == "charm-env" ]]; then + # being sourced directly; do nothing + /bin/true +else + # being sourced for wrapped bash helpers + . "$(find_wrapped)" +fi diff --git a/keepalived/bin/layer_option b/keepalived/bin/layer_option new file mode 100755 index 0000000..3253ef8 --- /dev/null +++ b/keepalived/bin/layer_option @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from charms import layer + + +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 = layer.options.get(args.section, 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/keepalived/config.yaml b/keepalived/config.yaml new file mode 100644 index 0000000..9e32946 --- /dev/null +++ b/keepalived/config.yaml @@ -0,0 +1,35 @@ +"options": + "virtual_ip": + "type": "string" + "default": "" + "description": | + Virtual IP/netmask that will be moved between instances, + e.g.: 10.1.2.3/16 + "vip_hostname": + "type": "string" + "default": "" + "description": | + A VIP hostname to pass to clients. + "port": + "type": "int" + "default": !!int "443" + "description": | + A port to pass to clients. + "router_id": + "type": "int" + "default": !!int "23" + "description": | + Virtual router identifier - a number between 1 and 255 + that's unique within the network segment + "network_interface": + "type": "string" + "default": "" + "description": | + Network interface name for the VIP. The default value is + the result of running the following command: + `route | grep default | head -n 1 | awk {'print $8'}`. + "healthcheck_interval": + "type": "int" + "default": !!int "2" + "description": | + vrrp_script-based health-check interval, in seconds diff --git a/keepalived/copyright b/keepalived/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/keepalived/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/keepalived/copyright.layer-basic b/keepalived/copyright.layer-basic new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/keepalived/copyright.layer-basic @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/keepalived/copyright.layer-options b/keepalived/copyright.layer-options new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/keepalived/copyright.layer-options @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/keepalived/docs/status.md b/keepalived/docs/status.md new file mode 100644 index 0000000..c6cceab --- /dev/null +++ b/keepalived/docs/status.md @@ -0,0 +1,91 @@ +

WorkloadState

+ +```python +WorkloadState(self, /, *args, **kwargs) +``` + +Enum of the valid workload states. + +Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + +

maintenance

+ +```python +maintenance(message) +``` + +Set the status to the `MAINTENANCE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

maint

+ +```python +maint(message) +``` + +Shorthand alias for +[maintenance](status.md#charms.layer.status.maintenance). + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

blocked

+ +```python +blocked(message) +``` + +Set the status to the `BLOCKED` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

waiting

+ +```python +waiting(message) +``` + +Set the status to the `WAITING` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

active

+ +```python +active(message) +``` + +Set the status to the `ACTIVE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

status_set

+ +```python +status_set(workload_state, message) +``` + +Set the status to the given workload state with a message. + +__Parameters__ + +- __`workload_state` (WorkloadState or str)__: State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. +- __`message` (str)__: Message to convey to the operator. + diff --git a/keepalived/hooks/config-changed b/keepalived/hooks/config-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/config-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/hook.template b/keepalived/hooks/hook.template new file mode 100644 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/hook.template @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/install b/keepalived/hooks/install new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/install @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/juju-info-relation-broken b/keepalived/hooks/juju-info-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/juju-info-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/juju-info-relation-changed b/keepalived/hooks/juju-info-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/juju-info-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/juju-info-relation-created b/keepalived/hooks/juju-info-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/juju-info-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/juju-info-relation-departed b/keepalived/hooks/juju-info-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/juju-info-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/juju-info-relation-joined b/keepalived/hooks/juju-info-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/juju-info-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/lb-sink-relation-broken b/keepalived/hooks/lb-sink-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/lb-sink-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/lb-sink-relation-changed b/keepalived/hooks/lb-sink-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/lb-sink-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/lb-sink-relation-created b/keepalived/hooks/lb-sink-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/lb-sink-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/lb-sink-relation-departed b/keepalived/hooks/lb-sink-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/lb-sink-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/lb-sink-relation-joined b/keepalived/hooks/lb-sink-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/lb-sink-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/leader-elected b/keepalived/hooks/leader-elected new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/leader-elected @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/leader-settings-changed b/keepalived/hooks/leader-settings-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/leader-settings-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/loadbalancer-relation-broken b/keepalived/hooks/loadbalancer-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/loadbalancer-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/loadbalancer-relation-changed b/keepalived/hooks/loadbalancer-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/loadbalancer-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/loadbalancer-relation-created b/keepalived/hooks/loadbalancer-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/loadbalancer-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/loadbalancer-relation-departed b/keepalived/hooks/loadbalancer-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/loadbalancer-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/loadbalancer-relation-joined b/keepalived/hooks/loadbalancer-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/loadbalancer-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/post-series-upgrade b/keepalived/hooks/post-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/post-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/pre-series-upgrade b/keepalived/hooks/pre-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/pre-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/relations/http/.gitignore b/keepalived/hooks/relations/http/.gitignore new file mode 100644 index 0000000..3374ec2 --- /dev/null +++ b/keepalived/hooks/relations/http/.gitignore @@ -0,0 +1,5 @@ +# Emacs save files +*~ +\#*\# +.\#* + diff --git a/keepalived/hooks/relations/http/README.md b/keepalived/hooks/relations/http/README.md new file mode 100644 index 0000000..3d7822a --- /dev/null +++ b/keepalived/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/keepalived/hooks/relations/http/__init__.py b/keepalived/hooks/relations/http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/keepalived/hooks/relations/http/interface.yaml b/keepalived/hooks/relations/http/interface.yaml new file mode 100644 index 0000000..54e7748 --- /dev/null +++ b/keepalived/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/keepalived/hooks/relations/http/provides.py b/keepalived/hooks/relations/http/provides.py new file mode 100644 index 0000000..86fa9b3 --- /dev/null +++ b/keepalived/hooks/relations/http/provides.py @@ -0,0 +1,67 @@ +import json + +from charmhelpers.core import hookenv +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag +from charms.reactive import Endpoint + + +class HttpProvides(Endpoint): + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + + def get_ingress_address(self, rel_id=None): + # If no rel_id is provided, we fallback to the first one + if rel_id is None: + rel_id = self.relations[0].relation_id + return hookenv.ingress_address(rel_id, hookenv.local_unit()) + + def configure(self, port, private_address=None, hostname=None): + ''' configure the address(es). private_address and hostname can + be None, a single string address/hostname, or a list of addresses + and hostnames. Note that if a list is passed, it is assumed both + private_address and hostname are either lists or None ''' + for relation in self.relations: + ingress_address = self.get_ingress_address(relation.relation_id) + if type(private_address) is list or type(hostname) is list: + # build 3 lists to zip together that are the same length + length = max(len(private_address), len(hostname)) + p = [port] * length + a = private_address + [ingress_address] *\ + (length - len(private_address)) + h = hostname + [ingress_address] * (length - len(hostname)) + zipped_list = zip(p, a, h) + # now build an array of dictionaries from that in the desired + # format for the interface + data_list = [{'hostname': h, 'port': p, 'private-address': a} + for p, a, h in zipped_list] + # for backwards compatibility, we just send a single entry + # and have an array of dictionaries in a field of that + # entry for the other entries. + data = data_list.pop(0) + data['extended_data'] = json.dumps(data_list) + + relation.to_publish_raw.update(data) + else: + relation.to_publish_raw.update({ + 'hostname': hostname or ingress_address, + 'private-address': private_address or ingress_address, + 'port': port, + }) + + def set_remote(self, **kwargs): + # NB: This method provides backwards compatibility for charms that + # called RelationBase.set_remote. Most commonly, this was done by + # charms that needed to pass reverse proxy stanzas to http proxies. + # This type of interaction with base relation classes is discouraged, + # and should be handled with logic encapsulated in appropriate + # interfaces. Eventually, this method will be deprecated in favor of + # that behavior. + for relation in self.relations: + relation.to_publish_raw.update(kwargs) diff --git a/keepalived/hooks/relations/http/requires.py b/keepalived/hooks/relations/http/requires.py new file mode 100644 index 0000000..17ea6b7 --- /dev/null +++ b/keepalived/hooks/relations/http/requires.py @@ -0,0 +1,76 @@ +import json + +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag +from charms.reactive import Endpoint + + +class HttpRequires(Endpoint): + + @when('endpoint.{endpoint_name}.changed') + def changed(self): + if any(unit.received_raw['port'] for unit in self.all_joined_units): + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_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, + 'private-address': private_address_of_host, + 'port': port_for_host, + }, + # ... + ], + }, + # ... + ] + """ + def build_service_host(data): + private_address = data['private-address'] + host = data['hostname'] or private_address + if host and data['port']: + return (host, private_address, data['port']) + else: + return None + + services = {} + for relation in self.relations: + service_name = relation.application_name + service = services.setdefault(service_name, { + 'service_name': service_name, + 'hosts': [], + }) + host_set = set() + for unit in relation.joined_units: + data = unit.received_raw + host = build_service_host(data) + if host: + host_set.add(host) + + # if we have extended data, add it + if 'extended_data' in data: + for ed in json.loads(data['extended_data']): + host = build_service_host(ed) + if host: + host_set.add(host) + + service['hosts'] = [ + {'hostname': h, 'private-address': pa, 'port': p} + for h, pa, p in sorted(host_set) + ] + + ret = [s for s in services.values() if s['hosts']] + return ret diff --git a/keepalived/hooks/relations/juju-info/.gitignore b/keepalived/hooks/relations/juju-info/.gitignore new file mode 100644 index 0000000..ba1431e --- /dev/null +++ b/keepalived/hooks/relations/juju-info/.gitignore @@ -0,0 +1,2 @@ +.tox +__pycache__ diff --git a/keepalived/hooks/relations/juju-info/README.md b/keepalived/hooks/relations/juju-info/README.md new file mode 100644 index 0000000..d19e435 --- /dev/null +++ b/keepalived/hooks/relations/juju-info/README.md @@ -0,0 +1,39 @@ +# Juju-Info Interface + +The juju info interface is a special and implicit relationship that works with +any charm. It is mainly useful for subordinate charms that can add +functionality to any exisiting machine without the host charm being aware of +it. + + +### Flags + +`{{endpoint_name}}.connected` + +Note: This flag keys off of what the charm author names the relationship +endpoint, which should *not* be the name of the interface: + +An example of a properly implemented relationship would resemble the following: + + +```yaml +requires: + host-system: + interface: juju-info +``` + +This might then be used in your charm would like: + +```python +@when_any('host-system.connected') +def handle_host(): + host = endpoint_from_flag('host-system.connected') + for address in host.addresses: + hookenv.log('Connected to: {}'.format(address)) +``` + +## Reference + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) +* [Peers API documentation](docs/peers.md) diff --git a/keepalived/hooks/relations/juju-info/__init__.py b/keepalived/hooks/relations/juju-info/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/keepalived/hooks/relations/juju-info/docs/peers.md b/keepalived/hooks/relations/juju-info/docs/peers.md new file mode 100644 index 0000000..39c9601 --- /dev/null +++ b/keepalived/hooks/relations/juju-info/docs/peers.md @@ -0,0 +1,52 @@ +

peers

+ + +

JujuInfoClient

+ +```python +JujuInfoClient(endpoint_name, relation_ids=None) +``` + +

addresses

+ + +A flat list of all addresses received from related apps / units. + +This list is de-duplicated and sorted by address, so it will be stable +for change comparison. If you need to know which app / unit an address +comes from, see `received_addresses_map`. + +Note: This uses ingress-address, so it will work with cross-model +relations. + +

addresses_map

+ + +A nested dictionary of all addresses received from related apps / units +by app name then unit name. + +For example:: + + { + 'app1': { + 'app1/0': '10.0.0.1', + 'app1/1': '10.0.0.2', + } + } + +Note: This uses ingress-address, so it will work with cross-model +relations. + +

unit_count

+ + +Number of joined units. + +

get_private_address

+ +```python +JujuInfoClient.get_private_address() +``` + +Deprecated. + diff --git a/keepalived/hooks/relations/juju-info/docs/provides.md b/keepalived/hooks/relations/juju-info/docs/provides.md new file mode 100644 index 0000000..f045ae7 --- /dev/null +++ b/keepalived/hooks/relations/juju-info/docs/provides.md @@ -0,0 +1,52 @@ +

provides

+ + +

JujuInfoClient

+ +```python +JujuInfoClient(endpoint_name, relation_ids=None) +``` + +

addresses

+ + +A flat list of all addresses received from related apps / units. + +This list is de-duplicated and sorted by address, so it will be stable +for change comparison. If you need to know which app / unit an address +comes from, see `received_addresses_map`. + +Note: This uses ingress-address, so it will work with cross-model +relations. + +

addresses_map

+ + +A nested dictionary of all addresses received from related apps / units +by app name then unit name. + +For example:: + + { + 'app1': { + 'app1/0': '10.0.0.1', + 'app1/1': '10.0.0.2', + } + } + +Note: This uses ingress-address, so it will work with cross-model +relations. + +

unit_count

+ + +Number of joined units. + +

get_private_address

+ +```python +JujuInfoClient.get_private_address() +``` + +Deprecated. + diff --git a/keepalived/hooks/relations/juju-info/docs/requires.md b/keepalived/hooks/relations/juju-info/docs/requires.md new file mode 100644 index 0000000..63fa076 --- /dev/null +++ b/keepalived/hooks/relations/juju-info/docs/requires.md @@ -0,0 +1,52 @@ +

requires

+ + +

JujuInfoClient

+ +```python +JujuInfoClient(endpoint_name, relation_ids=None) +``` + +

addresses

+ + +A flat list of all addresses received from related apps / units. + +This list is de-duplicated and sorted by address, so it will be stable +for change comparison. If you need to know which app / unit an address +comes from, see `received_addresses_map`. + +Note: This uses ingress-address, so it will work with cross-model +relations. + +

addresses_map

+ + +A nested dictionary of all addresses received from related apps / units +by app name then unit name. + +For example:: + + { + 'app1': { + 'app1/0': '10.0.0.1', + 'app1/1': '10.0.0.2', + } + } + +Note: This uses ingress-address, so it will work with cross-model +relations. + +

unit_count

+ + +Number of joined units. + +

get_private_address

+ +```python +JujuInfoClient.get_private_address() +``` + +Deprecated. + diff --git a/keepalived/hooks/relations/juju-info/interface.yaml b/keepalived/hooks/relations/juju-info/interface.yaml new file mode 100644 index 0000000..b98f1fc --- /dev/null +++ b/keepalived/hooks/relations/juju-info/interface.yaml @@ -0,0 +1,4 @@ +name: juju-info +summary: Used in subordinate charms (rarely) +version: 1 +maintainer: "Charles Butler " diff --git a/keepalived/hooks/relations/juju-info/make_docs b/keepalived/hooks/relations/juju-info/make_docs new file mode 100644 index 0000000..7f382d9 --- /dev/null +++ b/keepalived/hooks/relations/juju-info/make_docs @@ -0,0 +1,21 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'juju-info': {'interface': 'juju-info'}}, + 'provides': {'juju-info': {'interface': 'juju-info'}}, + 'peers': {'juju-info': {'interface': 'juju-info'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/keepalived/hooks/relations/juju-info/peers.py b/keepalived/hooks/relations/juju-info/peers.py new file mode 100644 index 0000000..9c637f5 --- /dev/null +++ b/keepalived/hooks/relations/juju-info/peers.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import Endpoint +from charms.reactive import when, when_not, set_flag, clear_flag + + +class JujuInfoClient(Endpoint): + @when('endpoint.{endpoint_name}.joined') + def changed(self): + set_flag(self.expand_name('{endpoint_name}.connected')) + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + clear_flag(self.expand_name('{endpoint_name}.connected')) + + def get_private_address(self): + """ + Deprecated. + """ + return self.all_joined_units[0]['private-address'] + + @property + def unit_count(self): + """ + Number of joined units. + """ + return len(self.all_joined_units) + + @property + def addresses(self): + """ + A flat list of all addresses received from related apps / units. + + This list is de-duplicated and sorted by address, so it will be stable + for change comparison. If you need to know which app / unit an address + comes from, see `received_addresses_map`. + + Note: This uses ingress-address, so it will work with cross-model + relations. + """ + addrs = {u.received_raw['ingress-address'] + for u in self.all_joined_units} + return list(sorted(addrs)) + + @property + def addresses_map(self): + """ + A nested dictionary of all addresses received from related apps / units + by app name then unit name. + + For example:: + + { + 'app1': { + 'app1/0': '10.0.0.1', + 'app1/1': '10.0.0.2', + } + } + + Note: This uses ingress-address, so it will work with cross-model + relations. + """ + return { + r.application_name: { + u.unit_name: u.received_raw['ingress-address'] + for u in r.joined_units + } for r in self.relations + } diff --git a/keepalived/hooks/relations/juju-info/provides.py b/keepalived/hooks/relations/juju-info/provides.py new file mode 100644 index 0000000..9c637f5 --- /dev/null +++ b/keepalived/hooks/relations/juju-info/provides.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import Endpoint +from charms.reactive import when, when_not, set_flag, clear_flag + + +class JujuInfoClient(Endpoint): + @when('endpoint.{endpoint_name}.joined') + def changed(self): + set_flag(self.expand_name('{endpoint_name}.connected')) + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + clear_flag(self.expand_name('{endpoint_name}.connected')) + + def get_private_address(self): + """ + Deprecated. + """ + return self.all_joined_units[0]['private-address'] + + @property + def unit_count(self): + """ + Number of joined units. + """ + return len(self.all_joined_units) + + @property + def addresses(self): + """ + A flat list of all addresses received from related apps / units. + + This list is de-duplicated and sorted by address, so it will be stable + for change comparison. If you need to know which app / unit an address + comes from, see `received_addresses_map`. + + Note: This uses ingress-address, so it will work with cross-model + relations. + """ + addrs = {u.received_raw['ingress-address'] + for u in self.all_joined_units} + return list(sorted(addrs)) + + @property + def addresses_map(self): + """ + A nested dictionary of all addresses received from related apps / units + by app name then unit name. + + For example:: + + { + 'app1': { + 'app1/0': '10.0.0.1', + 'app1/1': '10.0.0.2', + } + } + + Note: This uses ingress-address, so it will work with cross-model + relations. + """ + return { + r.application_name: { + u.unit_name: u.received_raw['ingress-address'] + for u in r.joined_units + } for r in self.relations + } diff --git a/keepalived/hooks/relations/juju-info/pydocmd.yml b/keepalived/hooks/relations/juju-info/pydocmd.yml new file mode 100644 index 0000000..7d95883 --- /dev/null +++ b/keepalived/hooks/relations/juju-info/pydocmd.yml @@ -0,0 +1,19 @@ +site_name: 'Juju Info Interface' + +generate: + - requires.md: + - requires + - requires.JujuInfoClient+ + - provides.md: + - provides + - provides.JujuInfoClient+ + - peers.md: + - peers + - peers.JujuInfoClient+ + +pages: + - Requires: requires.md + - Provides: provides.md + - Peers: peers.md + +gens_dir: docs diff --git a/keepalived/hooks/relations/juju-info/requires.py b/keepalived/hooks/relations/juju-info/requires.py new file mode 100644 index 0000000..9c637f5 --- /dev/null +++ b/keepalived/hooks/relations/juju-info/requires.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import Endpoint +from charms.reactive import when, when_not, set_flag, clear_flag + + +class JujuInfoClient(Endpoint): + @when('endpoint.{endpoint_name}.joined') + def changed(self): + set_flag(self.expand_name('{endpoint_name}.connected')) + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + clear_flag(self.expand_name('{endpoint_name}.connected')) + + def get_private_address(self): + """ + Deprecated. + """ + return self.all_joined_units[0]['private-address'] + + @property + def unit_count(self): + """ + Number of joined units. + """ + return len(self.all_joined_units) + + @property + def addresses(self): + """ + A flat list of all addresses received from related apps / units. + + This list is de-duplicated and sorted by address, so it will be stable + for change comparison. If you need to know which app / unit an address + comes from, see `received_addresses_map`. + + Note: This uses ingress-address, so it will work with cross-model + relations. + """ + addrs = {u.received_raw['ingress-address'] + for u in self.all_joined_units} + return list(sorted(addrs)) + + @property + def addresses_map(self): + """ + A nested dictionary of all addresses received from related apps / units + by app name then unit name. + + For example:: + + { + 'app1': { + 'app1/0': '10.0.0.1', + 'app1/1': '10.0.0.2', + } + } + + Note: This uses ingress-address, so it will work with cross-model + relations. + """ + return { + r.application_name: { + u.unit_name: u.received_raw['ingress-address'] + for u in r.joined_units + } for r in self.relations + } diff --git a/keepalived/hooks/relations/juju-info/tox.ini b/keepalived/hooks/relations/juju-info/tox.ini new file mode 100644 index 0000000..fcec9c1 --- /dev/null +++ b/keepalived/hooks/relations/juju-info/tox.ini @@ -0,0 +1,14 @@ +[tox] +envlist = py3 +skipsdist = true + +[testenv] +basepython=python3 +envdir={toxworkdir}/py3 +deps= + pytest + charms.reactive + git+https://github.com/NiklasRosenstein/pydoc-markdown#egg=pydoc-markdown + +[testenv:docs] +commands=python make_docs diff --git a/keepalived/hooks/relations/public-address/README.md b/keepalived/hooks/relations/public-address/README.md new file mode 100644 index 0000000..06be3ae --- /dev/null +++ b/keepalived/hooks/relations/public-address/README.md @@ -0,0 +1,59 @@ +# Overview + +This interface layer implements a public address protocol useful for load +balancers and their subordinates. The load balancers (providers) set their +own public address and port, which is then available to the subordinates +(requirers). + +# Usage + +## Provides + +By providing the `public-address` interface, your charm is providing an HTTP +server that can load-balance for another HTTP based service. + +Your charm need only provide the address and port on which it is serving its +content, as soon as the `{relation_name}.available` state is set: + +```python +from charmhelpers.core import hookenv +@when('website.available') +def configure_website(website): + website.set_address_port(hookenv.unit_get('public-address'), hookenv.config('port')) +``` + +## Requires + +By requiring the `public-address` interface, your charm is consuming one or +more HTTP servers, 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 `get_addresses_ports()` method returns a list of available addresses and +ports. + +The return value is a list of dicts of the following form: + +```python +[ + { + 'public-address': address_of_host, + 'port': port_for_host, + }, + # ... +] +``` + +A trivial example of handling this interface would be: + +```python +from charmhelpers.core import hookenv +@when('loadbalancer.available') +def update_reverse_proxy_config(loadbalancer): + hosts = loadbalancer.get_addresses_ports() + for host in hosts: + hookenv.log('The loadbalancer for this unit is {}:{}'.format( + host['public-address'], + host['port'])) +``` diff --git a/keepalived/hooks/relations/public-address/__init__.py b/keepalived/hooks/relations/public-address/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/keepalived/hooks/relations/public-address/interface.yaml b/keepalived/hooks/relations/public-address/interface.yaml new file mode 100644 index 0000000..c9849e4 --- /dev/null +++ b/keepalived/hooks/relations/public-address/interface.yaml @@ -0,0 +1,4 @@ +name: public-address +summary: A basic interface to provide the public address for load balancers. +version: 1 +repo: https://githb.com/juju-solutions/interface-public-address.git diff --git a/keepalived/hooks/relations/public-address/provides.py b/keepalived/hooks/relations/public-address/provides.py new file mode 100644 index 0000000..09b9915 --- /dev/null +++ b/keepalived/hooks/relations/public-address/provides.py @@ -0,0 +1,60 @@ +import json + +from charms.reactive import toggle_flag +from charms.reactive import Endpoint + + +class PublicAdddressProvides(Endpoint): + + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.available'), + self.is_joined) + + def set_address_port(self, address, port, relation=None): + if relation is None: + # no relation specified, so send the same data to everyone + relations = self.relations + else: + # specific relation given, so only send the data to that one + relations = [relation] + if type(address) is list: + # build 2 lists to zip together that are the same length + length = len(address) + p = [port] * length + combined = zip(address, p) + clients = [{'public-address': a, 'port': p} + for a, p in combined] + # for backwards compatibility, we just send a single entry + # and have an array of dictionaries in a field of that + # entry for the other entries. + first = clients.pop(0) + first['extended_data'] = json.dumps(clients) + for relation in relations: + relation.to_publish_raw.update(first) + else: + for relation in relations: + relation.to_publish_raw.update({'public-address': address, + 'port': port}) + + @property + def requests(self): + return [Request(rel) for rel in self.relations] + + +class Request: + def __init__(self, rel): + self.rel = rel + + @property + def application_name(self): + return self.rel.application_name + + @property + def members(self): + return [(u.received_raw.get('ingress-address', + u.received_raw['private-address']), + u.received_raw.get('port', '6443')) + for u in self.rel.joined_units] + + def set_address_port(self, address, port): + self.rel.endpoint.set_address_port(address, port, self.rel) diff --git a/keepalived/hooks/relations/public-address/requires.py b/keepalived/hooks/relations/public-address/requires.py new file mode 100644 index 0000000..467d129 --- /dev/null +++ b/keepalived/hooks/relations/public-address/requires.py @@ -0,0 +1,44 @@ +import json + +from charms.reactive import toggle_flag, Endpoint + + +class PublicAddressRequires(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.available'), + len(self.get_addresses_ports()) > 0) + + def set_backend_port(self, port): + """ + Set the port that the backend service is listening on. + + Defaults to 6443 if not set. + """ + for rel in self.relations: + rel.to_publish_raw['port'] = str(port) + + def get_addresses_ports(self): + '''Returns a list of available HTTP providers and their associated + public addresses and ports. + + The return value is a list of dicts of the following form:: + [ + { + 'public-address': address_for_frontend, + 'port': port_for_frontend, + }, + # ... + ] + ''' + hosts = set() + for relation in self.relations: + for unit in relation.joined_units: + data = unit.received_raw + hosts.add((data['public-address'], data['port'])) + if 'extended_data' in data: + for ed in json.loads(data['extended_data']): + hosts.add((ed['public-address'], ed['port'])) + + return [{'public-address': pa, 'port': p} + for pa, p in sorted(host for host in hosts + if None not in host)] diff --git a/keepalived/hooks/start b/keepalived/hooks/start new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/start @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/stop b/keepalived/hooks/stop new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/stop @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/update-status b/keepalived/hooks/update-status new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/update-status @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/upgrade-charm b/keepalived/hooks/upgrade-charm new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/upgrade-charm @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/website-relation-broken b/keepalived/hooks/website-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/website-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/website-relation-changed b/keepalived/hooks/website-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/website-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/website-relation-created b/keepalived/hooks/website-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/website-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/website-relation-departed b/keepalived/hooks/website-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/website-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/hooks/website-relation-joined b/keepalived/hooks/website-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/keepalived/hooks/website-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/keepalived/icon.svg b/keepalived/icon.svg new file mode 100644 index 0000000..7b82b7a --- /dev/null +++ b/keepalived/icon.svg @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ka + + + + diff --git a/keepalived/layer.yaml b/keepalived/layer.yaml new file mode 100644 index 0000000..5a202c9 --- /dev/null +++ b/keepalived/layer.yaml @@ -0,0 +1,19 @@ +"includes": +- "layer:options" +- "layer:basic" +- "interface:juju-info" +- "interface:public-address" +- "interface:http" +- "layer:status" +"exclude": [".travis.yml", "tests", "tox.ini", "test-requirements.txt", "unit_tests"] +"options": + "basic": + "use_venv": !!bool "true" + "packages": [] + "python_packages": [] + "include_system_packages": !!bool "false" + "status": + "patch-hookenv": !!bool "true" + "keepalived": {} +"repo": "https://github.com/juju-solutions/charm-keepalived.git" +"is": "keepalived" diff --git a/keepalived/lib/charms/layer/__init__.py b/keepalived/lib/charms/layer/__init__.py new file mode 100644 index 0000000..a8e0c64 --- /dev/null +++ b/keepalived/lib/charms/layer/__init__.py @@ -0,0 +1,60 @@ +import sys +from importlib import import_module +from pathlib import Path + + +def import_layer_libs(): + """ + Ensure that all layer libraries are imported. + + This makes it possible to do the following: + + from charms import layer + + layer.foo.do_foo_thing() + + Note: This function must be called after bootstrap. + """ + for module_file in Path('lib/charms/layer').glob('*'): + module_name = module_file.stem + if module_name in ('__init__', 'basic', 'execd') or not ( + module_file.suffix == '.py' or module_file.is_dir() + ): + continue + import_module('charms.layer.{}'.format(module_name)) + + +# Terrible hack to support the old terrible interface. +# Try to get people to call layer.options.get() instead so +# that we can remove this garbage. +# Cribbed from https://stackoverfLow.com/a/48100440/4941864 +class OptionsBackwardsCompatibilityHack(sys.modules[__name__].__class__): + def __call__(self, section=None, layer_file=None): + if layer_file is None: + return self.get(section=section) + else: + return self.get(section=section, + layer_file=Path(layer_file)) + + +def patch_options_interface(): + from charms.layer import options + if sys.version_info.minor >= 5: + options.__class__ = OptionsBackwardsCompatibilityHack + else: + # Py 3.4 doesn't support changing the __class__, so we have to do it + # another way. The last line is needed because we already have a + # reference that doesn't get updated with sys.modules. + name = options.__name__ + hack = OptionsBackwardsCompatibilityHack(name) + hack.get = options.get + sys.modules[name] = hack + sys.modules[__name__].options = hack + + +try: + patch_options_interface() +except ImportError: + # This may fail if pyyaml hasn't been installed yet. But in that + # case, the bootstrap logic will try it again once it has. + pass diff --git a/keepalived/lib/charms/layer/basic.py b/keepalived/lib/charms/layer/basic.py new file mode 100644 index 0000000..bbdd074 --- /dev/null +++ b/keepalived/lib/charms/layer/basic.py @@ -0,0 +1,501 @@ +import os +import sys +import re +import shutil +from distutils.version import LooseVersion +from pkg_resources import Requirement +from glob import glob +from subprocess import check_call, check_output, CalledProcessError +from time import sleep + +from charms import layer +from charms.layer.execd import execd_preinstall + + +def _get_subprocess_env(): + env = os.environ.copy() + env['LANG'] = env.get('LANG', 'C.UTF-8') + return env + + +def get_series(): + """ + Return series for a few known OS:es. + Tested as of 2019 november: + * centos6, centos7, rhel6. + * bionic + """ + series = "" + + # Looking for content in /etc/os-release + # works for ubuntu + some centos + if os.path.isfile('/etc/os-release'): + d = {} + with open('/etc/os-release', 'r') as rel: + for l in rel: + if not re.match(r'^\s*$', l): + k, v = l.split('=') + d[k.strip()] = v.strip().replace('"', '') + series = "{ID}{VERSION_ID}".format(**d) + + # Looking for content in /etc/redhat-release + # works for redhat enterprise systems + elif os.path.isfile('/etc/redhat-release'): + with open('/etc/redhat-release', 'r') as redhatlsb: + # CentOS Linux release 7.7.1908 (Core) + line = redhatlsb.readline() + release = int(line.split("release")[1].split()[0][0]) + series = "centos" + str(release) + + # Looking for content in /etc/lsb-release + # works for ubuntu + elif os.path.isfile('/etc/lsb-release'): + d = {} + with open('/etc/lsb-release', 'r') as lsb: + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + series = d['DISTRIB_CODENAME'] + + # This is what happens if we cant figure out the OS. + else: + series = "unknown" + return series + + +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 + + series = get_series() + + # OMG?! is build-essentials needed? + ubuntu_packages = ['python3-pip', + 'python3-setuptools', + 'python3-yaml', + 'python3-dev', + 'python3-wheel', + 'build-essential'] + + # I'm not going to "yum group info "Development Tools" + # omitting above madness + centos_packages = ['python3-pip', + 'python3-setuptools', + 'python3-devel', + 'python3-wheel'] + + packages_needed = [] + if 'centos' in series: + packages_needed = centos_packages + else: + packages_needed = ubuntu_packages + + 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') + hook_name = os.path.basename(sys.argv[0]) + is_bootstrapped = os.path.exists('wheelhouse/.bootstrapped') + is_charm_upgrade = hook_name == 'upgrade-charm' + is_series_upgrade = hook_name == 'post-series-upgrade' + is_post_upgrade = os.path.exists('wheelhouse/.upgraded') + is_upgrade = (not is_post_upgrade and + (is_charm_upgrade or is_series_upgrade)) + if is_bootstrapped and not is_upgrade: + # older subordinates might have downgraded charm-env, so we should + # restore it if necessary + install_or_update_charm_env() + activate_venv() + # the .upgrade file prevents us from getting stuck in a loop + # when re-execing to activate the venv; at this point, we've + # activated the venv, so it's safe to clear it + if is_post_upgrade: + os.unlink('wheelhouse/.upgraded') + return + if os.path.exists(venv): + try: + # focal installs or upgrades prior to PR 160 could leave the venv + # in a broken state which would prevent subsequent charm upgrades + _load_installed_versions(vpip) + except CalledProcessError: + is_broken_venv = True + else: + is_broken_venv = False + if is_upgrade or is_broken_venv: + # All upgrades should do a full clear of the venv, rather than + # just updating it, to bring in updates to Python itself + shutil.rmtree(venv) + if is_upgrade: + if os.path.exists('wheelhouse/.bootstrapped'): + os.unlink('wheelhouse/.bootstrapped') + # bootstrap wheelhouse + if os.path.exists('wheelhouse'): + pre_eoan = series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty', + 'ubuntu16.04', 'xenial', + 'ubuntu18.04', 'bionic') + pydistutils_lines = [ + "[easy_install]\n", + "find_links = file://{}/wheelhouse/\n".format(charm_dir), + "no_index=True\n", + "index_url=\n", # deliberately nothing here; disables it. + ] + if pre_eoan: + pydistutils_lines.append("allow_hosts = ''\n") + 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(pydistutils_lines) + if 'centos' in series: + yum_install(packages_needed) + else: + apt_install(packages_needed) + from charms.layer import options + cfg = options.get('basic') + # include packages defined in layer.yaml + if 'centos' in series: + yum_install(cfg.get('packages', [])) + else: + 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 = get_series() + if series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty'): + apt_install(['python-virtualenv']) + elif 'centos' in series: + yum_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, env=_get_subprocess_env()) + 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') + pre_install_pkgs = ['pip', 'setuptools', 'setuptools-scm'] + # we bundle these packages to work around bugs in older versions (such + # as https://github.com/pypa/pip/issues/56), but if the system already + # provided a newer version, downgrading it can cause other problems + _update_if_newer(pip, pre_install_pkgs) + # install the rest of the wheelhouse deps (extract the pkg names into + # a set so that we can ignore the pre-install packages and let pip + # choose the best version in case there are multiple from layer + # conflicts) + _versions = _load_wheelhouse_versions() + _pkgs = _versions.keys() - set(pre_install_pkgs) + # add back the versions such that each package in pkgs is + # ==. + # This ensures that pip 20.3.4+ will install the packages from the + # wheelhouse without (erroneously) flagging an error. + pkgs = _add_back_versions(_pkgs, _versions) + reinstall_flag = '--force-reinstall' + if not cfg.get('use_venv', True) and pre_eoan: + reinstall_flag = '--ignore-installed' + check_call([pip, 'install', '-U', reinstall_flag, '--no-index', + '--no-cache-dir', '-f', 'wheelhouse'] + list(pkgs), + env=_get_subprocess_env()) + # re-enable installation from pypi + os.remove('/root/.pydistutils.cfg') + + # install pyyaml for centos7, since, unlike the ubuntu image, the + # default image for centos doesn't include pyyaml; see the discussion: + # https://discourse.jujucharms.com/t/charms-for-centos-lets-begin + if 'centos' in series: + check_call([pip, 'install', '-U', 'pyyaml'], + env=_get_subprocess_env()) + + # install python packages from layer options + if cfg.get('python_packages'): + check_call([pip, 'install', '-U'] + cfg.get('python_packages'), + env=_get_subprocess_env()) + 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') + # setup wrappers to ensure envs are used for scripts + install_or_update_charm_env() + for wrapper in ('charms.reactive', 'charms.reactive.sh', + 'chlp', 'layer_option'): + src = os.path.join('/usr/local/sbin', 'charm-env') + dst = os.path.join('/usr/local/sbin', wrapper) + if not os.path.exists(dst): + os.symlink(src, dst) + if cfg.get('use_venv'): + shutil.copy2('bin/layer_option', vbin) + else: + shutil.copy2('bin/layer_option', '/usr/local/bin/') + # re-link the charm copy to the wrapper in case charms + # call bin/layer_option directly (as was the old pattern) + os.remove('bin/layer_option') + os.symlink('/usr/local/sbin/layer_option', 'bin/layer_option') + # flag us as having already bootstrapped so we don't do it again + open('wheelhouse/.bootstrapped', 'w').close() + if is_upgrade: + # flag us as having already upgraded so we don't do it again + open('wheelhouse/.upgraded', '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 _load_installed_versions(pip): + pip_freeze = check_output([pip, 'freeze']).decode('utf8') + versions = {} + for pkg_ver in pip_freeze.splitlines(): + try: + req = Requirement.parse(pkg_ver) + except ValueError: + continue + versions.update({ + req.project_name: LooseVersion(ver) + for op, ver in req.specs if op == '==' + }) + return versions + + +def _load_wheelhouse_versions(): + versions = {} + for wheel in glob('wheelhouse/*'): + pkg, ver = os.path.basename(wheel).rsplit('-', 1) + # nb: LooseVersion ignores the file extension + versions[pkg.replace('_', '-')] = LooseVersion(ver) + return versions + + +def _add_back_versions(pkgs, versions): + """Add back the version strings to each of the packages. + + The versions are LooseVersion() from _load_wheelhouse_versions(). This + function strips the ".zip" or ".tar.gz" from the end of the version string + and adds it back to the package in the form of == + + If a package name is not a key in the versions dictionary, then it is + returned in the list unchanged. + + :param pkgs: A list of package names + :type pkgs: List[str] + :param versions: A map of package to LooseVersion + :type versions: Dict[str, LooseVersion] + :returns: A list of (maybe) versioned packages + :rtype: List[str] + """ + def _strip_ext(s): + """Strip an extension (if it exists) from the string + + :param s: the string to strip an extension off if it exists + :type s: str + :returns: string without an extension of .zip or .tar.gz + :rtype: str + """ + for ending in [".zip", ".tar.gz"]: + if s.endswith(ending): + return s[:-len(ending)] + return s + + def _maybe_add_version(pkg): + """Maybe add back the version number to a package if it exists. + + Adds the version number, if the package exists in the lexically + captured `versions` dictionary, in the form ==. Strips + the extension if it exists. + + :param pkg: the package name to (maybe) add the version number to. + :type pkg: str + """ + try: + return "{}=={}".format(pkg, _strip_ext(str(versions[pkg]))) + except KeyError: + pass + return pkg + + return [_maybe_add_version(pkg) for pkg in pkgs] + + +def _update_if_newer(pip, pkgs): + installed = _load_installed_versions(pip) + wheelhouse = _load_wheelhouse_versions() + for pkg in pkgs: + if pkg not in installed or wheelhouse[pkg] > installed[pkg]: + check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', + pkg], env=_get_subprocess_env()) + + +def install_or_update_charm_env(): + # On Trusty python3-pkg-resources is not installed + try: + from pkg_resources import parse_version + except ImportError: + apt_install(['python3-pkg-resources']) + from pkg_resources import parse_version + + try: + installed_version = parse_version( + check_output(['/usr/local/sbin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + installed_version = parse_version('0.0.0') + try: + bundled_version = parse_version( + check_output(['bin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + bundled_version = parse_version('0.0.0') + if installed_version < bundled_version: + shutil.copy2('bin/charm-env', '/usr/local/sbin/') + + +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. + """ + from charms.layer import options + venv = os.path.abspath('../.venv') + vbin = os.path.join(venv, 'bin') + vpy = os.path.join(vbin, 'python') + use_venv = options.get('basic', 'use_venv') + if use_venv and '.venv' not in sys.executable: + # activate the venv + os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) + reload_interpreter(vpy) + layer.patch_options_interface() + layer.import_layer_libs() + + +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 = _get_subprocess_env() + + 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 + try: + # sometimes apt-get update needs to be run + check_call(['apt-get', 'update'], env=env) + except CalledProcessError: + # sometimes it's a dpkg lock issue + pass + sleep(5) + else: + break + + +def yum_install(packages): + """ Installs packages with yum. + This function largely mimics the apt_install function for consistency. + """ + if packages: + env = os.environ.copy() + cmd = ['yum', '-y', 'install'] + for attempt in range(3): + try: + check_call(cmd + packages, env=env) + except CalledProcessError: + if attempt == 2: + raise + try: + check_call(['yum', 'update'], env=env) + except CalledProcessError: + pass + sleep(5) + else: + break + else: + pass + + +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]) + + +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/keepalived/lib/charms/layer/execd.py b/keepalived/lib/charms/layer/execd.py new file mode 100644 index 0000000..438d9a1 --- /dev/null +++ b/keepalived/lib/charms/layer/execd.py @@ -0,0 +1,114 @@ +# 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 . + +# This module may only import from the Python standard library. +import os +import sys +import subprocess +import time + +''' +execd/preinstall + +Read the layer-basic docs for more info on how to use this feature. +https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#exec-d-support +''' + + +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<" +- "Valentin Boucher " +"description": | + keepalived is used for monitoring real servers within a Linux Virtual + Server (LVS) cluster. +"tags": +- "networking" +"series": +- "focal" +- "bionic" +- "xenial" +"requires": + "juju-info": + "scope": "container" + "interface": "juju-info" + "lb-sink": + "interface": "http" +"provides": + "loadbalancer": + "interface": "public-address" + "website": + "interface": "http" +"subordinate": !!bool "true" diff --git a/keepalived/pydocmd.yml b/keepalived/pydocmd.yml new file mode 100644 index 0000000..ab3b2ef --- /dev/null +++ b/keepalived/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'Status Management Layer' + +generate: + - status.md: + - charms.layer.status.WorkloadState + - charms.layer.status.maintenance + - charms.layer.status.maint + - charms.layer.status.blocked + - charms.layer.status.waiting + - charms.layer.status.active + - charms.layer.status.status_set + +pages: + - Status Management Layer: status.md + +gens_dir: docs diff --git a/keepalived/reactive/__init__.py b/keepalived/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/keepalived/reactive/keepalived.py b/keepalived/reactive/keepalived.py new file mode 100644 index 0000000..fcc6c64 --- /dev/null +++ b/keepalived/reactive/keepalived.py @@ -0,0 +1,124 @@ +import os +import re +from subprocess import check_output + +from charms.reactive import set_flag, when, when_not, hook +from charms.reactive.flags import clear_flag + + +from charmhelpers.core.templating import render +from charmhelpers.fetch import apt_update, apt_install +from charmhelpers.core.hookenv import config, is_leader +from charmhelpers.core.host import service_restart +from charmhelpers.core.host import service_pause, service_resume + +from charms.layer import status + + +SYSCTL_FILE = os.path.join(os.sep, 'etc', 'sysctl.d', '50-keepalived.conf') +KEEPALIVED_CONFIG_FILE = os.path.join(os.sep, 'etc', 'keepalived', + 'keepalived.conf') + + +@when_not('keepalived.package.installed') +def install_keepalived_package(): + ''' Install keepalived package ''' + status.maintenance('Installing keepalived') + + apt_update(fatal=True) + apt_install(['keepalived', 'net-tools'], fatal=True) + + set_flag('keepalived.package.installed') + + +def default_route_interface(): + ''' Returns the network interface of the system's default route ''' + default_interface = None + cmd = ['route'] + output = check_output(cmd).decode('utf8') + for line in output.split('\n'): + if 'default' in line: + default_interface = line.split(' ')[-1] + return default_interface + + +@when('keepalived.package.installed') +@when_not('keepalived.started') +@when_not('upgrade.series.in-progress') +def configure_keepalived_service(): + ''' Set up the keepalived service ''' + + virtual_ip = config().get('virtual_ip') + if virtual_ip == "": + status.blocked('Please configure virtual ips') + return + + network_interface = config().get('network_interface') + if network_interface == "": + network_interface = default_route_interface() + + context = {'is_leader': is_leader(), + 'virtual_ip': virtual_ip, + 'network_interface': network_interface, + 'router_id': config().get('router_id'), + 'service_port': config().get('port'), + 'healthcheck_interval': config().get('healthcheck_interval'), + } + render(source='keepalived.conf', + target=KEEPALIVED_CONFIG_FILE, + context=context, + perms=0o644) + service_restart('keepalived') + + render(source='50-keepalived.conf', + target=SYSCTL_FILE, + context={'sysctl': {'net.ipv4.ip_nonlocal_bind': 1}}, + perms=0o644) + service_restart('procps') + + status.active('VIP ready') + set_flag('keepalived.started') + + +@when('config.changed') +def reconfigure(): + clear_flag('keepalived.started') + + +@when('website.available', 'keepalived.started') +def website_available(website): + ipaddr = re.split('/', config()['virtual_ip'])[0] + vip_hostname = config()['vip_hostname'] + hostname = vip_hostname if vip_hostname else ipaddr + # a port to export over a relation + # TODO: this could be more tightly coupled with the actual + # service via a relation + port = config()['port'] + website.configure(port=port, private_address=ipaddr, hostname=hostname) + + +@when('loadbalancer.available', 'keepalived.started') +def loadbalancer_available(loadbalancer): + ''' Send the virtual IP ''' + ipaddr = re.split('/', config()['virtual_ip'])[0] + port = config()['port'] + loadbalancer.set_address_port(ipaddr, port) + + +@hook('upgrade-charm') +def upgrade_charm(): + clear_flag('keepalived.started') + + +@hook('pre-series-upgrade') +def pre_series_upgrade(): + service_pause('keepalived') + service_pause('procps') + status.blocked('Series upgrade in progress') + + +@hook('post-series-upgrade') +def post_series_upgrade(): + service_resume('keepalived') + service_resume('procps') + clear_flag('keepalived.started') diff --git a/keepalived/reactive/status.py b/keepalived/reactive/status.py new file mode 100644 index 0000000..2f33f3f --- /dev/null +++ b/keepalived/reactive/status.py @@ -0,0 +1,4 @@ +from charms import layer + + +layer.status._initialize() diff --git a/keepalived/requirements.txt b/keepalived/requirements.txt new file mode 100644 index 0000000..55543d9 --- /dev/null +++ b/keepalived/requirements.txt @@ -0,0 +1,3 @@ +mock +flake8 +pytest diff --git a/keepalived/revision b/keepalived/revision new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/keepalived/revision @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/keepalived/templates/50-keepalived.conf b/keepalived/templates/50-keepalived.conf new file mode 100644 index 0000000..6b023e1 --- /dev/null +++ b/keepalived/templates/50-keepalived.conf @@ -0,0 +1,3 @@ +{% for key in sysctl %} +{{ key }}={{ sysctl[key] }} +{% endfor %} \ No newline at end of file diff --git a/keepalived/templates/keepalived.conf b/keepalived/templates/keepalived.conf new file mode 100644 index 0000000..db80787 --- /dev/null +++ b/keepalived/templates/keepalived.conf @@ -0,0 +1,22 @@ +vrrp_script chk_svc_port { + # returns 1 if connection is refused + script "/bin/bash -c '=18.1,<19.0 +# pin Jinja2, PyYAML and MarkupSafe to the last versions supporting python 3.5 +# for trusty +Jinja2<=2.10.1 +PyYAML<=5.2 +MarkupSafe<2.0.0 +setuptools<42 +setuptools-scm<=1.17.0 +charmhelpers>=0.4.0,<1.0.0 +charms.reactive>=0.1.0,<2.0.0 +wheel<0.34 +# pin netaddr to avoid pulling importlib-resources +netaddr<=0.7.19 + diff --git a/keepalived/wheelhouse/Jinja2-2.10.1.tar.gz b/keepalived/wheelhouse/Jinja2-2.10.1.tar.gz new file mode 100644 index 0000000..ffd1054 Binary files /dev/null and b/keepalived/wheelhouse/Jinja2-2.10.1.tar.gz differ diff --git a/keepalived/wheelhouse/MarkupSafe-1.1.1.tar.gz b/keepalived/wheelhouse/MarkupSafe-1.1.1.tar.gz new file mode 100644 index 0000000..a6dad8e Binary files /dev/null and b/keepalived/wheelhouse/MarkupSafe-1.1.1.tar.gz differ diff --git a/keepalived/wheelhouse/PyYAML-5.2.tar.gz b/keepalived/wheelhouse/PyYAML-5.2.tar.gz new file mode 100644 index 0000000..666d12a Binary files /dev/null and b/keepalived/wheelhouse/PyYAML-5.2.tar.gz differ diff --git a/keepalived/wheelhouse/Tempita-0.5.2.tar.gz b/keepalived/wheelhouse/Tempita-0.5.2.tar.gz new file mode 100644 index 0000000..755befc Binary files /dev/null and b/keepalived/wheelhouse/Tempita-0.5.2.tar.gz differ diff --git a/keepalived/wheelhouse/charmhelpers-0.20.23.tar.gz b/keepalived/wheelhouse/charmhelpers-0.20.23.tar.gz new file mode 100644 index 0000000..8fbc8ec Binary files /dev/null and b/keepalived/wheelhouse/charmhelpers-0.20.23.tar.gz differ diff --git a/keepalived/wheelhouse/charms.reactive-1.4.1.tar.gz b/keepalived/wheelhouse/charms.reactive-1.4.1.tar.gz new file mode 100644 index 0000000..03bc1fe Binary files /dev/null and b/keepalived/wheelhouse/charms.reactive-1.4.1.tar.gz differ diff --git a/keepalived/wheelhouse/netaddr-0.7.19.tar.gz b/keepalived/wheelhouse/netaddr-0.7.19.tar.gz new file mode 100644 index 0000000..cc31d9d Binary files /dev/null and b/keepalived/wheelhouse/netaddr-0.7.19.tar.gz differ diff --git a/keepalived/wheelhouse/pbr-5.6.0.tar.gz b/keepalived/wheelhouse/pbr-5.6.0.tar.gz new file mode 100644 index 0000000..0d5c965 Binary files /dev/null and b/keepalived/wheelhouse/pbr-5.6.0.tar.gz differ diff --git a/keepalived/wheelhouse/pip-18.1.tar.gz b/keepalived/wheelhouse/pip-18.1.tar.gz new file mode 100644 index 0000000..a18192d Binary files /dev/null and b/keepalived/wheelhouse/pip-18.1.tar.gz differ diff --git a/keepalived/wheelhouse/pyaml-21.10.1.tar.gz b/keepalived/wheelhouse/pyaml-21.10.1.tar.gz new file mode 100644 index 0000000..b19aad3 Binary files /dev/null and b/keepalived/wheelhouse/pyaml-21.10.1.tar.gz differ diff --git a/keepalived/wheelhouse/setuptools-41.6.0.zip b/keepalived/wheelhouse/setuptools-41.6.0.zip new file mode 100644 index 0000000..3345759 Binary files /dev/null and b/keepalived/wheelhouse/setuptools-41.6.0.zip differ diff --git a/keepalived/wheelhouse/setuptools_scm-1.17.0.tar.gz b/keepalived/wheelhouse/setuptools_scm-1.17.0.tar.gz new file mode 100644 index 0000000..43b16c7 Binary files /dev/null and b/keepalived/wheelhouse/setuptools_scm-1.17.0.tar.gz differ diff --git a/keepalived/wheelhouse/six-1.16.0.tar.gz b/keepalived/wheelhouse/six-1.16.0.tar.gz new file mode 100644 index 0000000..5bf3a27 Binary files /dev/null and b/keepalived/wheelhouse/six-1.16.0.tar.gz differ diff --git a/keepalived/wheelhouse/wheel-0.33.6.tar.gz b/keepalived/wheelhouse/wheel-0.33.6.tar.gz new file mode 100644 index 0000000..c922c4e Binary files /dev/null and b/keepalived/wheelhouse/wheel-0.33.6.tar.gz differ diff --git a/kubeapi-load-balancer/.build.manifest b/kubeapi-load-balancer/.build.manifest new file mode 100644 index 0000000..ccd1c4b --- /dev/null +++ b/kubeapi-load-balancer/.build.manifest @@ -0,0 +1,1016 @@ +{ + "layers": [ + { + "branch": "refs/heads/master", + "rev": "f491ebe32b503c9712d2f8cd602dcce18f4aab46", + "url": "layer:metrics" + }, + { + "branch": "refs/heads/master", + "rev": "fcdcea4e5de3e1556c24e6704607862d0ba00a56", + "url": "layer:options" + }, + { + "branch": "refs/heads/master", + "rev": "a3ff62c32c993d80417f6e093e3ef95e42f62083", + "url": "layer:basic" + }, + { + "branch": "refs/heads/master", + "rev": "47dfcd4920ef6317850a4837ef0057ab0092a18e", + "url": "layer:nagios" + }, + { + "branch": "refs/heads/master", + "rev": "a7d7b6423db37a47611310039e6ed1929c0a2eab", + "url": "layer:status" + }, + { + "branch": "refs/heads/master", + "rev": "76bddfb640ab8767fc7e4a4b73a4a4e781948f34", + "url": "layer:apt" + }, + { + "branch": "refs/heads/master", + "rev": "672d27695b512e50f51777b1eb63c5ff157b3d9e", + "url": "layer:nginx" + }, + { + "branch": "refs/heads/master", + "rev": "527dd64fc4b9a6b0f8d80a3c2c0b865155050275", + "url": "layer:debug" + }, + { + "branch": "refs/heads/master", + "rev": "fb46dec78d390571753d21876bbba689bbbca9e4", + "url": "layer:tls-client" + }, + { + "branch": "refs/heads/master", + "rev": "bbeabfee52c4442cdaf3a34e5e35530a3bd71156", + "url": "layer:kubernetes-common" + }, + { + "branch": "refs/heads/master", + "rev": "a8f88f16bb7771807a0f7fdb17ee16b0e310fc2b", + "url": "layer:hacluster" + }, + { + "branch": "refs/heads/master", + "rev": "cc5bd3f49b2fa5e6c3ab2336763c313ec8bf083f", + "url": "layer:leadership" + }, + { + "branch": "refs/heads/master", + "rev": "64ca102a51286f1b4c0e8e5820912c7affc5806f", + "url": "kubeapi-load-balancer" + }, + { + "branch": "refs/heads/master", + "rev": "2e0e1fdea6d83b55078200aacb537d60013ec5bc", + "url": "interface:nrpe-external-master" + }, + { + "branch": "refs/heads/master", + "rev": "632131b1f122daf6fb601fd4c9f1e4dbb1a92e09", + "url": "interface:http" + }, + { + "branch": "refs/heads/master", + "rev": "d9850016d930a6d507b9fd45e2598d327922b140", + "url": "interface:tls-certificates" + }, + { + "branch": "refs/heads/master", + "rev": "8125a7baecccf9b0869e515b92300dde3a86f31b", + "url": "interface:hacluster" + }, + { + "branch": "refs/heads/master", + "rev": "5021f8a23f6e6e4cc449d2d02f2d8cb99763ec27", + "url": "interface:public-address" + } + ], + "signatures": { + ".build.manifest": [ + "build", + "dynamic", + "unchecked" + ], + ".github/workflows/main.yml": [ + "kubeapi-load-balancer", + "static", + "1ab5c85a6d4c418917573011f6da399d86be327eaec650a8c9e3d1e3159c44e1" + ], + ".gitignore": [ + "kubeapi-load-balancer", + "static", + "3437c2cd90de443f44766939172b82e750e19fd474df499ffe003bb807e8cef4" + ], + "AUTHORS": [ + "layer:nginx", + "static", + "5e460cc5d7fe5ce6dc5c4e8eefc13159ee58874667baf9af3b5fa9b597a10fa2" + ], + "CONTRIBUTING.md": [ + "kubeapi-load-balancer", + "static", + "7155516596ae597b0b7065f0463ff69031d689c0fc565998b51c06d999129d5a" + ], + "LICENSE": [ + "kubeapi-load-balancer", + "static", + "58d1e17ffe5109a7ae296caafcadfdbe6a7d176f0bc4ab01e12a689b0499d8bd" + ], + "Makefile": [ + "layer:basic", + "static", + "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301" + ], + "README.md": [ + "kubeapi-load-balancer", + "static", + "55d7f0325996fff89a6b4277acd58e0e04c62a977ab83bd45c288789cd29dd9d" + ], + "actions.yaml": [ + "layer:debug", + "dynamic", + "cea290e28bc78458ea4a56dcad39b9a880c67e4ba53b774ac46bd8778618c7b9" + ], + "actions/debug": [ + "layer:debug", + "static", + "db0a42dae4c5045b2c06385bf22209dfe0e2ded55822ef847d84b01d9ff2b046" + ], + "bin/charm-env": [ + "layer:basic", + "static", + "fb6a20fac4102a6a4b6ffe903fcf666998f9a95a3647e6f9af7a1eeb44e58fd5" + ], + "bin/layer_option": [ + "layer:options", + "static", + "e959bf29da4c5edff28b2602c24113c4df9e25cdc9f2aa3b5d46c8577b2a40cc" + ], + "config.yaml": [ + "kubeapi-load-balancer", + "dynamic", + "586a155cd5fb93090f379e3c1ec9d350b89d73c58ebad447b03e36a886010ba7" + ], + "copyright": [ + "kubeapi-load-balancer", + "static", + "badd4492d214890abd07b615f9e1a7a5ff3339b6c44655a826c746a9263ff00d" + ], + "copyright.layer-apt": [ + "layer:apt", + "static", + "5123b2d0220fefb4424a463216fb41a6dd7cfad49c9799ba7037f1e74a2fd6bc" + ], + "copyright.layer-basic": [ + "layer:basic", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-leadership": [ + "layer:leadership", + "static", + "8ce407829378fc0f72ce44c7f624e4951c7ccb3db1cfb949bee026b701728cc9" + ], + "copyright.layer-metrics": [ + "layer:metrics", + "static", + "08509dcbade4c20761ba4382ef23c831744dbab1d4a8dd94a1c2b4d4e913334c" + ], + "copyright.layer-nagios": [ + "layer:nagios", + "static", + "47b2363574909e748bcc471d9004780ac084b301c154905654b5b6f088474749" + ], + "copyright.layer-nginx": [ + "layer:nginx", + "static", + "66b7d69f452f9203cbf702c57c58b16b359be9970781deb0e21893620dd52516" + ], + "copyright.layer-options": [ + "layer:options", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-status": [ + "layer:status", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "debug-scripts/charm-unitdata": [ + "layer:debug", + "static", + "c952b9d31f3942e4e722cb3e70f5119707b69b8e76cc44e2e906bc6d9aef49b7" + ], + "debug-scripts/filesystem": [ + "layer:debug", + "static", + "d29cc8687f4422d024001c91b1ac756ee6bf8a2a125bc98db1199ba775eb8fd7" + ], + "debug-scripts/juju-logs": [ + "layer:debug", + "static", + "d260b35753a917368cb8c64c1312546a0a40ef49cba84c75bc6369549807c55e" + ], + "debug-scripts/juju-network-get": [ + "layer:debug", + "static", + "6d849a1f8e6569bd0d5ea38299f7937cb8b36a5f505e3532f6c756eabeb8b6c5" + ], + "debug-scripts/network": [ + "layer:debug", + "static", + "714afae5dcb45554ff1f05285501e3b7fcc656c8de51217e263b93dab25a9d2e" + ], + "debug-scripts/packages": [ + "layer:debug", + "static", + "e8177102dc2ca853cb9272c1257cf2cfd5253d2a074e602d07c8bc4ea8e27c75" + ], + "debug-scripts/sysctl": [ + "layer:debug", + "static", + "990035b320e09cc2228e1f2f880e795d51118b2959339eacddff9cbb74349c6a" + ], + "debug-scripts/systemd": [ + "layer:debug", + "static", + "23ddf533198bf5b1ce723acde31ada806aab8539292b514c721d8ec08af74106" + ], + "debug-scripts/tls-certs": [ + "layer:tls-client", + "static", + "ebf7f23ef6e39fb8e664bac2e9429e32aaeb673b4a51751724b835c007e85d3b" + ], + "docs/status.md": [ + "layer:status", + "static", + "975dec9f8c938196e102e954a80226bda293407c4e5ae857c118bf692154702a" + ], + "hooks/apiserver-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/apiserver-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/apiserver-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/apiserver-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/apiserver-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/collect-metrics": [ + "layer:metrics", + "static", + "139fe18ce4cf2bed2155d3d0fce1c3b4cf1bc2598242cda42b3d772ec9bf8558" + ], + "hooks/config-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ha-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ha-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ha-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ha-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ha-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/hook.template": [ + "layer:basic", + "static", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/install": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/lb-consumers-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/lb-consumers-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/lb-consumers-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/lb-consumers-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/lb-consumers-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-elected": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-settings-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/post-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/pre-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/relations/hacluster/.stestr.conf": [ + "interface:hacluster", + "static", + "46965969e6df6ac729b7dac68d57bc4e677e9f4d79d445be77f54ca3b9e58774" + ], + "hooks/relations/hacluster/README.md": [ + "interface:hacluster", + "static", + "7fad91e409c6e559cdb76d11c89c325531adc25679049a629a28c4f890755f1f" + ], + "hooks/relations/hacluster/__init__.py": [ + "interface:hacluster", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/hacluster/copyright": [ + "interface:hacluster", + "static", + "7a296596102da98cecee289a195e00d6af44241911321699b3d4d4af93f11893" + ], + "hooks/relations/hacluster/interface.yaml": [ + "interface:hacluster", + "static", + "5f4e6c8d7b2884bdceeee422821f4db7163dbfa7994d86cb405ffef2c3dea43c" + ], + "hooks/relations/hacluster/interface_hacluster/__init__.py": [ + "interface:hacluster", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/hacluster/interface_hacluster/common.py": [ + "interface:hacluster", + "static", + "abcc0d2940d142976ccfa3fa7518227549ee13041292af7ad61101a7d0c02f7e" + ], + "hooks/relations/hacluster/requires.py": [ + "interface:hacluster", + "static", + "68cf3ed22af30e42f34fc70ca484e8e4eeaedac6410bd3f228677cc791e6f46c" + ], + "hooks/relations/hacluster/test-requirements.txt": [ + "interface:hacluster", + "static", + "63756e4b1c67bc161cee0d30d460dbb83911b2c064dc1c55454a30c1ab877616" + ], + "hooks/relations/http/.gitignore": [ + "interface:http", + "static", + "83b4ca18cc39800b1d260b5633cd0252e21501b21e7c33e718db44f1a68a09b8" + ], + "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", + "8c72cd8a5a6ea24f53b6dba11f4353c75265bfa7d3ecc2dd096c8963eab8c877" + ], + "hooks/relations/http/requires.py": [ + "interface:http", + "static", + "76cc886368eaf9c2403a6dc46b40531c3f4eaf67b08829f890c57cb645430abd" + ], + "hooks/relations/nrpe-external-master/README.md": [ + "interface:nrpe-external-master", + "static", + "d8ed3bc7334f6581b12b6091923f58e6f5ef62075a095a4e78fb8f434a948636" + ], + "hooks/relations/nrpe-external-master/__init__.py": [ + "interface:nrpe-external-master", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/nrpe-external-master/interface.yaml": [ + "interface:nrpe-external-master", + "static", + "894f24ba56148044dae5b7febf874b427d199239bcbe1f2f55c3db06bb77b5f0" + ], + "hooks/relations/nrpe-external-master/provides.py": [ + "interface:nrpe-external-master", + "static", + "e6ba708d05b227b139a86be59c83ed95a2bad030bc81e5819167ba5e1e67ecd4" + ], + "hooks/relations/nrpe-external-master/requires.py": [ + "interface:nrpe-external-master", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/public-address/README.md": [ + "interface:public-address", + "static", + "7225effe61bfd8571447b8b685a2ecb52be17431b3066a5306330954c4cb064d" + ], + "hooks/relations/public-address/__init__.py": [ + "interface:public-address", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/public-address/interface.yaml": [ + "interface:public-address", + "static", + "49d6777a54aa84c7d3be8d531be237564e90f2e4cb2be05ef5617a372a382340" + ], + "hooks/relations/public-address/provides.py": [ + "interface:public-address", + "static", + "7c99b0fe987d38773ed3e67c0378fdb78748c04d6895489cd4bca40aaeb051b2" + ], + "hooks/relations/public-address/requires.py": [ + "interface:public-address", + "static", + "d6a7c6c0762d29a5db19afb4cf82af50812988d5e19a3a48fcbe8b0f6fec12a5" + ], + "hooks/relations/tls-certificates/.gitignore": [ + "interface:tls-certificates", + "static", + "b485e74def213c534676224e655e9276b62d401ebc643508ddc545dd335cb6dc" + ], + "hooks/relations/tls-certificates/README.md": [ + "interface:tls-certificates", + "static", + "6851227de8fcca7edfd504159dbe3e3af31080af64df46f3d3b345da7630827a" + ], + "hooks/relations/tls-certificates/__init__.py": [ + "interface:tls-certificates", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/tls-certificates/docs/common.md": [ + "interface:tls-certificates", + "static", + "5e91d6637fc0ccc50af2776de9e59a0f8098244b627816b2e18fabb266e980ff" + ], + "hooks/relations/tls-certificates/docs/provides.md": [ + "interface:tls-certificates", + "static", + "5c12dfca99b5c15ba10b4e7f7cff4cb4c9b621b198deba5f2397d3c837d035fe" + ], + "hooks/relations/tls-certificates/docs/requires.md": [ + "interface:tls-certificates", + "static", + "148dd1de163d75253f0a9d3c35e108dcaacbc9bdf97e47186743e6c82a67b62e" + ], + "hooks/relations/tls-certificates/interface.yaml": [ + "interface:tls-certificates", + "static", + "e412e54b1d327bad15a882f7f0bf996212090db576b863cc9cff7a68afc0e4fa" + ], + "hooks/relations/tls-certificates/make_docs": [ + "interface:tls-certificates", + "static", + "3671543bddc9d277171263310e404df3f11660429582cb27b39b7e7ec8757a37" + ], + "hooks/relations/tls-certificates/provides.py": [ + "interface:tls-certificates", + "static", + "be2a4b9a411c770989c529fd887070ad91649481a13f5239cfd8751f234b637c" + ], + "hooks/relations/tls-certificates/pydocmd.yml": [ + "interface:tls-certificates", + "static", + "48a233f60a89f87d56e9bc715e05766f5d39bbea2bc8741ed31f67b30c8cfcb8" + ], + "hooks/relations/tls-certificates/requires.py": [ + "interface:tls-certificates", + "static", + "442d773112079bc674d3e6be75b00323fcad7efd2f03613a1972b575dd438dba" + ], + "hooks/relations/tls-certificates/tls_certificates_common.py": [ + "interface:tls-certificates", + "static", + "068bd32ba69bfa514e1da386919d18b348ee678b40c372f275c9110f2cc4677c" + ], + "hooks/relations/tls-certificates/tox.ini": [ + "interface:tls-certificates", + "static", + "7ab8ab53e5ed98cfa7fb5c1d5009f84077a4bb76640ba64f561ef7ea3a702eab" + ], + "hooks/start": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/stop": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/update-status": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/upgrade-charm": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/website-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/website-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/website-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/website-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/website-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "icon.svg": [ + "kubeapi-load-balancer", + "static", + "92271bf7063cc3a85a6d0fe2841250cf9bf8cd72697f3655f03ada39f8aee029" + ], + "layer.yaml": [ + "kubeapi-load-balancer", + "dynamic", + "d7bac049bb8874aaab83bbe0339f1c1a4e726f27e548fa9705a0c890db70d5b2" + ], + "lib/.gitkeep": [ + "layer:nginx", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "lib/charms/apt.py": [ + "layer:apt", + "static", + "c7613992eb33ac94d83fbf02f467b614ea5112eaf561c4715def90989cefa531" + ], + "lib/charms/layer/__init__.py": [ + "layer:basic", + "static", + "dfe0d26c6bf409767de6e2546bc648f150e1b396243619bad3aa0553ab7e0e6f" + ], + "lib/charms/layer/basic.py": [ + "layer:basic", + "static", + "98b47134770ed6e4c0b2d4aad73cd5bc200bec84aa9c1c4e075fd70c3222a0c9" + ], + "lib/charms/layer/execd.py": [ + "layer:basic", + "static", + "fda8bd491032db1db8ddaf4e99e7cc878c6fb5432efe1f91cadb5b34765d076d" + ], + "lib/charms/layer/hacluster.py": [ + "layer:hacluster", + "static", + "f58e0c1503187247f858ff3c9a1166d59107afd1557ba89e4878ec2e79304f8a" + ], + "lib/charms/layer/kubernetes_common.py": [ + "layer:kubernetes-common", + "static", + "29cedffd490e6295273d195a7c9bace2fcdf149826e7427f2af9698f7f75055b" + ], + "lib/charms/layer/nagios.py": [ + "layer:nagios", + "static", + "0246710bdbea844356007a64409907d93e6e94a289d83266e8b7c5d921fb3a6c" + ], + "lib/charms/layer/nginx.py": [ + "layer:nginx", + "static", + "5fea9e756b8e9ad09d0256d9f2a1e8e2169a97741af256653ca85b4412e40174" + ], + "lib/charms/layer/options.py": [ + "layer:options", + "static", + "8ae7a07d22542fc964f2d2bee8219d1c78a68dace70a1b38d36d4aea47b1c3b2" + ], + "lib/charms/layer/status.py": [ + "layer:status", + "static", + "d560a5e07b2e5f2b0f25f30e1f0278b06f3f90c01e4dbad5c83d71efc79018c6" + ], + "lib/charms/layer/tls_client.py": [ + "layer:tls-client", + "static", + "34531c3980777b661b913d77c432fc371ed10425473c2eb365b1dd5540c2ec6e" + ], + "lib/charms/leadership.py": [ + "layer:leadership", + "static", + "20ffcbbc08147506759726ad51567420659ffb8a2e0121079240b8706658e332" + ], + "lib/debug_script.py": [ + "layer:debug", + "static", + "a4d56f2d3e712b1b5cadb657c7195c6268d0aac6d228991049fd769e0ddaf453" + ], + "lib/nginxlib.py": [ + "layer:nginx", + "static", + "bae474acba0fbf9da21f1372dcda1dba848757c5e7cebb6fb22c29f04a67c0aa" + ], + "make_docs": [ + "layer:status", + "static", + "c990f55c8e879793a62ed8464ee3d7e0d7d2225fdecaf17af24b0df0e2daa8c1" + ], + "metadata.yaml": [ + "kubeapi-load-balancer", + "dynamic", + "d7ed0ef2446de4644bc2840f2290ba72faa5136474d36817d4e4633495964c33" + ], + "metrics.yaml": [ + "kubeapi-load-balancer", + "static", + "94a5eb0b0966f8ba434d91ff1e9b99b1b4c3b3044657b236d4e742d3e0d57c47" + ], + "pydocmd.yml": [ + "layer:status", + "static", + "11d9293901f32f75f4256ae4ac2073b92ce1d7ef7b6c892ba9fbb98690a0b330" + ], + "pyproject.toml": [ + "layer:apt", + "static", + "19689509a5fb9bfc90ed1e873122ac0a90f22533b7f40055c38fdd587fe297de" + ], + "reactive/__init__.py": [ + "layer:leadership", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "reactive/apt.py": [ + "layer:apt", + "static", + "6fe40f18eb84a910a71a4acb7ec74856128de846de6029b4fc297a875692c837" + ], + "reactive/hacluster.py": [ + "layer:hacluster", + "static", + "0b34980232eec9866c85b55070db7e72a04689f92b338207c5839531abd0eadc" + ], + "reactive/leadership.py": [ + "layer:leadership", + "static", + "e2b233cf861adc3b2d9e9c062134ce2f104953f03283cdddd88f49efee652e8f" + ], + "reactive/load_balancer.py": [ + "kubeapi-load-balancer", + "static", + "a461c5478bd62fbe35c1cff1b867d5c876bf4d87a7d10dabe3ab4298c1dbb120" + ], + "reactive/nginx.py": [ + "layer:nginx", + "static", + "046769111b72a5a5aa7bfd6362db988361719586bee4e9b40a472f33c0cf09a8" + ], + "reactive/status.py": [ + "layer:status", + "static", + "30207fc206f24e91def5252f1c7f7c8e23c0aed0e93076babf5e03c05296d207" + ], + "reactive/tls_client.py": [ + "layer:tls-client", + "static", + "08e850e401d2004523dca6b5e6bc47c33d558bf575dd55969491e11cd3ed98c8" + ], + "requirements.txt": [ + "layer:basic", + "static", + "a00f75d80849e5b4fc5ad2e7536f947c25b1a4044b341caa8ee87a92d3a4c804" + ], + "templates/.gitkeep": [ + "layer:nginx", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "templates/apilb.conf": [ + "kubeapi-load-balancer", + "static", + "bce0d30720ebe4e2173047e699e2ddc75f2a4e1e3e53966cdfddc9723de80d75" + ], + "templates/cdk.auth-webhook-secret.yaml": [ + "layer:kubernetes-common", + "static", + "efaf34c12a5c961fa7843199070945ba05717b3656a0f3acc3327f45334bcaec" + ], + "templates/vhost.conf.ex": [ + "layer:nginx", + "static", + "f68c366c35a8487acb78da6f1086eeee33a3eccdbe5a524509039c0c41ad5d5a" + ], + "tests/data/bundle.yaml": [ + "kubeapi-load-balancer", + "static", + "48c2cfe2d03a11f33b63e789465db7f5509bb85a44194d523d3db0bd1c51add3" + ], + "tests/functional/conftest.py": [ + "layer:kubernetes-common", + "static", + "fd53e0c38b4dda0c18096167889cd0d85b98b0a13225f9f8853261241e94078c" + ], + "tests/functional/test_k8s_common.py": [ + "layer:kubernetes-common", + "static", + "680a53724154771dd78422bbaf24b151788d86dd07960712c5d9e0d758499b50" + ], + "tests/integration/test_kubeapi-load-balancer_integration.py": [ + "kubeapi-load-balancer", + "static", + "3188d793533cbc95314eceaf613168160413da84828fbe11e09dd50afef1209e" + ], + "tests/unit/conftest.py": [ + "kubeapi-load-balancer", + "static", + "6b67fae874cf23514acce521237850807e1b45f5ddaac1777237392e66b8ad53" + ], + "tests/unit/test_k8s_common.py": [ + "layer:kubernetes-common", + "static", + "da9bcea8e75160311a4055c1cbf577b497ddd45dc00223c5f1667598f94d9be4" + ], + "tests/unit/test_kubeapi_load_balancer.py": [ + "kubeapi-load-balancer", + "static", + "8c31c2541800259eab3461d0295ed0c76d763596b2a99a5ecdd683d65402517f" + ], + "tests/validate-wheelhouse.sh": [ + "kubeapi-load-balancer", + "static", + "1c74bea041866cf4bd75763190d3c512e1d63a19b04e35178a64b8c517bb3231" + ], + "tox.ini": [ + "kubeapi-load-balancer", + "static", + "21bc06f83720144cb194ddd8725cf356e0beebcaf87c8a0b7c2a269ffb82ae35" + ], + "version": [ + "kubeapi-load-balancer", + "dynamic", + "41dbfa1b715c748c7de3b265c51d85f0ada5d4f48184f02c963c1d39e36fd8c9" + ], + "wheelhouse.txt": [ + "kubeapi-load-balancer", + "dynamic", + "3c1119359719500a5c3fabab7215289529e4dc8ad65826278846ef11ef78ed19" + ], + "wheelhouse/Jinja2-2.10.1.tar.gz": [ + "layer:basic", + "dynamic", + "065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013" + ], + "wheelhouse/MarkupSafe-1.1.1.tar.gz": [ + "layer:basic", + "dynamic", + "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b" + ], + "wheelhouse/PyYAML-5.2.tar.gz": [ + "layer:basic", + "dynamic", + "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c" + ], + "wheelhouse/Tempita-0.5.2.tar.gz": [ + "__pip__", + "dynamic", + "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c" + ], + "wheelhouse/cached-property-1.5.2.tar.gz": [ + "__pip__", + "dynamic", + "9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130" + ], + "wheelhouse/charmhelpers-0.20.23.tar.gz": [ + "layer:basic", + "dynamic", + "59a9776594e91cd3e3e000043f8668b4d7b279422dbb17e320f01dc16385b80e" + ], + "wheelhouse/charms.reactive-1.4.1.tar.gz": [ + "layer:basic", + "dynamic", + "bba21b4fd40b26c240c9ef2aa10c6fdf73592031c68591da4e7ccc46ca9cb616" + ], + "wheelhouse/loadbalancer_interface-1.1.1.tar.gz": [ + "kubeapi-load-balancer", + "dynamic", + "c71d50bb66286d6e15a5f2975c0a316a3cd43c2042428258c96d1b4b95e5706b" + ], + "wheelhouse/marshmallow-3.14.0.tar.gz": [ + "__pip__", + "dynamic", + "bba1a940985c052c5cc7849f97da196ebc81f3b85ec10c56ef1f3228aa9cbe74" + ], + "wheelhouse/marshmallow-enum-1.5.1.tar.gz": [ + "__pip__", + "dynamic", + "38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58" + ], + "wheelhouse/netaddr-0.7.19.tar.gz": [ + "layer:basic", + "dynamic", + "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd" + ], + "wheelhouse/ops-1.2.0.tar.gz": [ + "__pip__", + "dynamic", + "3deb00ad7952b203502290a79bf8c8ce9b70e4f34fec3307fd45133c97a45824" + ], + "wheelhouse/ops_reactive_interface-1.0.1.tar.gz": [ + "__pip__", + "dynamic", + "9ed351c42fc187299c23125975aa3dfee9f6aaae0c9d49bce8904ac079255dba" + ], + "wheelhouse/pbr-5.6.0.tar.gz": [ + "__pip__", + "dynamic", + "42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd" + ], + "wheelhouse/pip-18.1.tar.gz": [ + "layer:basic", + "dynamic", + "c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1" + ], + "wheelhouse/pyaml-21.10.1.tar.gz": [ + "__pip__", + "dynamic", + "c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383" + ], + "wheelhouse/setuptools-41.6.0.zip": [ + "layer:basic", + "dynamic", + "6afa61b391dcd16cb8890ec9f66cc4015a8a31a6e1c2b4e0c464514be1a3d722" + ], + "wheelhouse/setuptools_scm-1.17.0.tar.gz": [ + "layer:basic", + "dynamic", + "70a4cf5584e966ae92f54a764e6437af992ba42ac4bca7eb37cc5d02b98ec40a" + ], + "wheelhouse/six-1.16.0.tar.gz": [ + "__pip__", + "dynamic", + "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926" + ], + "wheelhouse/toml-0.10.2.tar.gz": [ + "layer:nginx", + "dynamic", + "b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "wheelhouse/wheel-0.33.6.tar.gz": [ + "layer:basic", + "dynamic", + "10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646" + ] + } +} \ No newline at end of file diff --git a/kubeapi-load-balancer/.github/workflows/main.yml b/kubeapi-load-balancer/.github/workflows/main.yml new file mode 100644 index 0000000..fb217c9 --- /dev/null +++ b/kubeapi-load-balancer/.github/workflows/main.yml @@ -0,0 +1,47 @@ +name: Test Suite +on: [pull_request] + +jobs: + lint-unit-wheelhouse: + name: Lint, Unit, Wheelhouse + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + sudo snap install charm --classic + - name: Lint + run: tox -vve lint + - name: Unit Tests + run: tox -vve unit + - name: Validate Wheelhouse + run: tox -vve validate-wheelhouse + integration-test: + name: Integration test with VMWare + runs-on: self-hosted + timeout-minutes: 360 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@master + with: + provider: vsphere + credentials-yaml: ${{ secrets.CREDENTIALS_YAML }} + clouds-yaml: ${{ secrets.CLOUDS_YAML }} + bootstrap-options: "--model-default datastore=vsanDatastore --model-default primary-network=VLAN_2764" + - name: Run test + run: tox -e integration diff --git a/kubeapi-load-balancer/.gitignore b/kubeapi-load-balancer/.gitignore new file mode 100644 index 0000000..cc02691 --- /dev/null +++ b/kubeapi-load-balancer/.gitignore @@ -0,0 +1,3 @@ +.tox/ +__pycache__/ +*.pyc diff --git a/kubeapi-load-balancer/AUTHORS b/kubeapi-load-balancer/AUTHORS new file mode 100644 index 0000000..60e3e7d --- /dev/null +++ b/kubeapi-load-balancer/AUTHORS @@ -0,0 +1,2 @@ +Adam Stokes +Marco Ceppi diff --git a/kubeapi-load-balancer/CONTRIBUTING.md b/kubeapi-load-balancer/CONTRIBUTING.md new file mode 100644 index 0000000..f198d7c --- /dev/null +++ b/kubeapi-load-balancer/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributor Guide + +This Juju charm is open source ([Apache License 2.0](./LICENSE)) and we actively seek any community contibutions +for code, suggestions and documentation. +This page details a few notes, workflows and suggestions for how to make contributions most effective and help us +all build a better charm - please give them a read before working on any contributions. + +## Licensing + +This charm has been created under the [Apache License 2.0](./LICENSE), which will cover any contributions you may +make to this project. Please familiarise yourself with the terms of the license. + +Additionally, this charm uses the Harmony CLA agreement. It’s the easiest way for you to give us permission to +use your contributions. +In effect, you’re giving us a license, but you still own the copyright — so you retain the right to modify your +code and use it in other projects. Please [sign the CLA here](https://ubuntu.com/legal/contributors/agreement) before +making any contributions. + +## Code of conduct + +We have adopted the Ubuntu code of Conduct. You can read this in full [here](https://ubuntu.com/community/code-of-conduct). + +## Contributing code + +To contribute code to this project, please use the following workflow: + +1. [Submit a bug](https://bugs.launchpad.net/charm-kubeapi-load-balancer/+filebug) to explain the need for and track the change. +2. Create a branch on your fork of the repo with your changes, including a unit test covering the new or modified code. +3. Submit a PR. The PR description should include a link to the bug on Launchpad. +4. Update the Launchpad bug to include a link to the PR and the `review-needed` tag. +5. Once reviewed and merged, the change will become available on the edge channel and assigned to an appropriate milestone + for further release according to priority. + +## Documentation + +Documentation for this charm is currently maintained as part of the Charmed Kubernetes docs. +See [this page](https://github.com/charmed-kubernetes/kubernetes-docs/blob/master/pages/k8s/charm-kubeapi-load-balancer.md) diff --git a/kubeapi-load-balancer/LICENSE b/kubeapi-load-balancer/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/kubeapi-load-balancer/LICENSE @@ -0,0 +1,202 @@ + + 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. \ No newline at end of file diff --git a/kubeapi-load-balancer/Makefile b/kubeapi-load-balancer/Makefile new file mode 100644 index 0000000..a1ad3a5 --- /dev/null +++ b/kubeapi-load-balancer/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/kubeapi-load-balancer/README.md b/kubeapi-load-balancer/README.md new file mode 100644 index 0000000..2809e47 --- /dev/null +++ b/kubeapi-load-balancer/README.md @@ -0,0 +1,15 @@ +# kubeapi-load-balancer + +Simple NGINX reverse proxy to lend a hand in HA kubernetes-master deployments. + + +This charm is a component of Charmed Kubernetes. For full information, +please visit the [official Charmed Kubernetes docs](https://www.ubuntu.com/kubernetes/docs/charm-kubeapi-load-balancer). + +# Developers + +## Building the charm + +``` +make charm +``` diff --git a/kubeapi-load-balancer/actions.yaml b/kubeapi-load-balancer/actions.yaml new file mode 100644 index 0000000..8712b6b --- /dev/null +++ b/kubeapi-load-balancer/actions.yaml @@ -0,0 +1,2 @@ +"debug": + "description": "Collect debug data" diff --git a/kubeapi-load-balancer/actions/debug b/kubeapi-load-balancer/actions/debug new file mode 100755 index 0000000..8ba160e --- /dev/null +++ b/kubeapi-load-balancer/actions/debug @@ -0,0 +1,102 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import tarfile +import tempfile +import traceback +from contextlib import contextmanager +from datetime import datetime +from charmhelpers.core.hookenv import action_set, local_unit + +archive_dir = None +log_file = None + + +@contextmanager +def archive_context(): + """ Open a context with a new temporary directory. + + When the context closes, the directory is archived, and the archive + location is added to Juju action output. """ + global archive_dir + global log_file + with tempfile.TemporaryDirectory() as temp_dir: + name = "debug-" + datetime.now().strftime("%Y%m%d%H%M%S") + archive_dir = os.path.join(temp_dir, name) + os.makedirs(archive_dir) + with open("%s/debug.log" % archive_dir, "w") as log_file: + yield + os.chdir(temp_dir) + tar_path = "/home/ubuntu/%s.tar.gz" % name + with tarfile.open(tar_path, "w:gz") as f: + f.add(name) + action_set({ + "path": tar_path, + "command": "juju scp %s:%s ." % (local_unit(), tar_path), + "message": " ".join([ + "Archive has been created on unit %s." % local_unit(), + "Use the juju scp command to copy it to your local machine." + ]) + }) + + +def log(msg): + """ Log a message that will be included in the debug archive. + + Must be run within archive_context """ + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for line in str(msg).splitlines(): + log_file.write(timestamp + " | " + line.rstrip() + "\n") + + +def run_script(script): + """ Run a single script. Must be run within archive_context """ + log("Running script: " + script) + script_dir = os.path.join(archive_dir, script) + os.makedirs(script_dir) + env = os.environ.copy() + env["PYTHONPATH"] = "lib" # allow same imports as reactive code + env["DEBUG_SCRIPT_DIR"] = script_dir + with open(script_dir + "/stdout", "w") as stdout: + with open(script_dir + "/stderr", "w") as stderr: + process = subprocess.Popen( + "debug-scripts/" + script, + stdout=stdout, stderr=stderr, env=env + ) + try: + exit_code = process.wait(timeout=300) + except subprocess.TimeoutExpired: + log("ERROR: still running, terminating") + process.terminate() + try: + exit_code = process.wait(timeout=10) + except subprocess.TimeoutExpired: + log("ERROR: still running, killing") + process.kill() + exit_code = process.wait(timeout=10) + if exit_code != 0: + log("ERROR: %s failed with exit code %d" % (script, exit_code)) + + +def run_all_scripts(): + """ Run all scripts. For the sake of robustness, log and ignore any + exceptions that occur. + + Must be run within archive_context """ + scripts = os.listdir("debug-scripts") + for script in scripts: + try: + run_script(script) + except: + log(traceback.format_exc()) + + +def main(): + """ Open an archive context and run all scripts. """ + with archive_context(): + run_all_scripts() + + +if __name__ == "__main__": + main() diff --git a/kubeapi-load-balancer/bin/charm-env b/kubeapi-load-balancer/bin/charm-env new file mode 100755 index 0000000..d211ce9 --- /dev/null +++ b/kubeapi-load-balancer/bin/charm-env @@ -0,0 +1,107 @@ +#!/bin/bash + +VERSION="1.0.0" + + +find_charm_dirs() { + # Hopefully, $JUJU_CHARM_DIR is set so which venv to use in unambiguous. + if [[ -n "$JUJU_CHARM_DIR" || -n "$CHARM_DIR" ]]; then + if [[ -z "$JUJU_CHARM_DIR" ]]; then + # accept $CHARM_DIR to be more forgiving + export JUJU_CHARM_DIR="$CHARM_DIR" + fi + if [[ -z "$CHARM_DIR" ]]; then + # set CHARM_DIR as well to help with backwards compatibility + export CHARM_DIR="$JUJU_CHARM_DIR" + fi + return + fi + # Try to guess the value for JUJU_CHARM_DIR by looking for a non-subordinate + # (because there's got to be at least one principle) charm directory; + # if there are several, pick the first by alpha order. + agents_dir="/var/lib/juju/agents" + if [[ -d "$agents_dir" ]]; then + desired_charm="$1" + found_charm_dir="" + if [[ -n "$desired_charm" ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + charm_name="$(grep -o '^['\''"]\?name['\''"]\?:.*' $charm_dir/metadata.yaml 2> /dev/null | sed -e 's/.*: *//' -e 's/['\''"]//g')" + if [[ "$charm_name" == "$desired_charm" ]]; then + if [[ -n "$found_charm_dir" ]]; then + >&2 echo "Ambiguous possibilities for JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + found_charm_dir="$charm_dir" + fi + done + if [[ -z "$found_charm_dir" ]]; then + >&2 echo "Unable to determine JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + export JUJU_CHARM_DIR="$found_charm_dir" + export CHARM_DIR="$found_charm_dir" + return + fi + # shellcheck disable=SC2126 + non_subordinates="$(grep -L 'subordinate"\?:.*true' "$agents_dir"/unit-*/charm/metadata.yaml | wc -l)" + if [[ "$non_subordinates" -gt 1 ]]; then + >&2 echo 'Ambiguous possibilities for JUJU_CHARM_DIR; please use --charm or run within a Juju hook context' + exit 1 + elif [[ "$non_subordinates" -eq 1 ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + if grep -q 'subordinate"\?:.*true' "$charm_dir/metadata.yaml"; then + continue + fi + export JUJU_CHARM_DIR="$charm_dir" + export CHARM_DIR="$charm_dir" + return + done + fi + fi + >&2 echo 'Unable to determine JUJU_CHARM_DIR; please run within a Juju hook context' + exit 1 +} + +try_activate_venv() { + if [[ -d "$JUJU_CHARM_DIR/../.venv" ]]; then + . "$JUJU_CHARM_DIR/../.venv/bin/activate" + fi +} + +find_wrapped() { + PATH="${PATH/\/usr\/local\/sbin:}" which "$(basename "$0")" +} + + +if [[ "$1" == "--version" || "$1" == "-v" ]]; then + echo "$VERSION" + exit 0 +fi + + +# allow --charm option to hint which JUJU_CHARM_DIR to choose when ambiguous +# NB: --charm option must come first +# NB: option must be processed outside find_charm_dirs to modify $@ +charm_name="" +if [[ "$1" == "--charm" ]]; then + charm_name="$2" + shift; shift +fi + +find_charm_dirs "$charm_name" +try_activate_venv +export PYTHONPATH="$JUJU_CHARM_DIR/lib:$PYTHONPATH" + +if [[ "$(basename "$0")" == "charm-env" ]]; then + # being used as a shebang + exec "$@" +elif [[ "$0" == "$BASH_SOURCE" ]]; then + # being invoked as a symlink wrapping something to find in the venv + exec "$(find_wrapped)" "$@" +elif [[ "$(basename "$BASH_SOURCE")" == "charm-env" ]]; then + # being sourced directly; do nothing + /bin/true +else + # being sourced for wrapped bash helpers + . "$(find_wrapped)" +fi diff --git a/kubeapi-load-balancer/bin/layer_option b/kubeapi-load-balancer/bin/layer_option new file mode 100755 index 0000000..3253ef8 --- /dev/null +++ b/kubeapi-load-balancer/bin/layer_option @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from charms import layer + + +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 = layer.options.get(args.section, 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/kubeapi-load-balancer/config.yaml b/kubeapi-load-balancer/config.yaml new file mode 100644 index 0000000..e6a860d --- /dev/null +++ b/kubeapi-load-balancer/config.yaml @@ -0,0 +1,91 @@ +"options": + "nagios_context": + "default": "juju" + "type": "string" + "description": | + Used by the nrpe subordinate charms. + A string that will be prepended to instance name to set the host name + in nagios. So for instance the hostname would be something like: + juju-myservice-0 + If you're running multiple environments with the same services in them + this allows you to differentiate between them. + "nagios_servicegroups": + "default": "" + "type": "string" + "description": | + A comma-separated list of nagios servicegroups. + If left empty, the nagios_context will be used as the servicegroup + "extra_packages": + "description": > + Space separated list of extra deb packages to install. + "type": "string" + "default": "" + "package_status": + "default": "install" + "type": "string" + "description": > + The status of service-affecting packages will be set to this + value in the dpkg database. Valid values are "install" and "hold". + "install_sources": + "description": > + List of extra apt sources, per charm-helpers standard + format (a yaml list of strings encoded as a string). Each source + may be either a line that can be added directly to + sources.list(5), or in the form ppa:/ for adding + Personal Package Archives, or a distribution component to enable. + "type": "string" + "default": "" + "install_keys": + "description": > + List of signing keys for install_sources package sources, per + charmhelpers standard format (a yaml list of strings encoded as + a string). The keys should be the full ASCII armoured GPG public + keys. While GPG key ids are also supported and looked up on a + keyserver, operators should be aware that this mechanism is + insecure. null can be used if a standard package signing key is + used that will already be installed on the machine, and for PPA + sources where the package signing key is securely retrieved from + Launchpad. + "type": "string" + "default": "" + "port": + "type": "int" + "default": !!int "443" + "description": |- + The port to run the loadbalancer + "host": + "type": "string" + "default": "127.0.0.1" + "description": "listen address" + "ha-cluster-vip": + "type": "string" + "description": | + Virtual IP for the charm to use with the HA Cluster subordinate charm + Mutually exclusive with ha-cluster-dns. Multiple virtual IPs are + separated by spaces. + "default": "" + "ha-cluster-dns": + "type": "string" + "description": | + DNS entry to use with the HA Cluster subordinate charm. + Mutually exclusive with ha-cluster-vip. + "default": "" + "extra_sans": + "type": "string" + "default": "" + "description": | + Space-separated list of extra SAN entries to add to the x509 certificate + created for the load balancers. + "proxy_read_timeout": + "type": "int" + "default": !!int "600" + "description": "Timeout in seconds for reading a response from proxy server." + "loadbalancer-ips": + "type": "string" + "description": | + Space seperated list of IP addresses of loadbalancers in front of control plane. + A common case for this is virtual IP addresses that are floated in front of the + kubeapi-load-balancer charm. The workers will alternate IP addresses from this + list to distribute load. If you have 2 IPs and 4 workers, each IP will be used + by 2 workers. + "default": "" diff --git a/kubeapi-load-balancer/copyright b/kubeapi-load-balancer/copyright new file mode 100644 index 0000000..ac5e525 --- /dev/null +++ b/kubeapi-load-balancer/copyright @@ -0,0 +1,13 @@ +Copyright 2016 The Kubernetes Authors. + + 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/kubeapi-load-balancer/copyright.layer-apt b/kubeapi-load-balancer/copyright.layer-apt new file mode 100644 index 0000000..0814dc1 --- /dev/null +++ b/kubeapi-load-balancer/copyright.layer-apt @@ -0,0 +1,15 @@ +Copyright 2015-2016 Canonical Ltd. + +This file is part of the Apt layer for Juju. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/kubeapi-load-balancer/copyright.layer-basic b/kubeapi-load-balancer/copyright.layer-basic new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/kubeapi-load-balancer/copyright.layer-basic @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubeapi-load-balancer/copyright.layer-leadership b/kubeapi-load-balancer/copyright.layer-leadership new file mode 100644 index 0000000..08b983f --- /dev/null +++ b/kubeapi-load-balancer/copyright.layer-leadership @@ -0,0 +1,15 @@ +Copyright 2015-2016 Canonical Ltd. + +This file is part of the Leadership Layer for Juju. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/kubeapi-load-balancer/copyright.layer-metrics b/kubeapi-load-balancer/copyright.layer-metrics new file mode 100644 index 0000000..2df15bd --- /dev/null +++ b/kubeapi-load-balancer/copyright.layer-metrics @@ -0,0 +1,13 @@ +Copyright 2016 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. diff --git a/kubeapi-load-balancer/copyright.layer-nagios b/kubeapi-load-balancer/copyright.layer-nagios new file mode 100644 index 0000000..c80db95 --- /dev/null +++ b/kubeapi-load-balancer/copyright.layer-nagios @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2016, Canonical Ltd. +License: GPL-3 + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranties of + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . diff --git a/kubeapi-load-balancer/copyright.layer-nginx b/kubeapi-load-balancer/copyright.layer-nginx new file mode 100644 index 0000000..953f220 --- /dev/null +++ b/kubeapi-load-balancer/copyright.layer-nginx @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Adam Stokes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/kubeapi-load-balancer/copyright.layer-options b/kubeapi-load-balancer/copyright.layer-options new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/kubeapi-load-balancer/copyright.layer-options @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubeapi-load-balancer/copyright.layer-status b/kubeapi-load-balancer/copyright.layer-status new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubeapi-load-balancer/copyright.layer-status @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubeapi-load-balancer/debug-scripts/charm-unitdata b/kubeapi-load-balancer/debug-scripts/charm-unitdata new file mode 100755 index 0000000..d2aac60 --- /dev/null +++ b/kubeapi-load-balancer/debug-scripts/charm-unitdata @@ -0,0 +1,12 @@ +#!/usr/local/sbin/charm-env python3 + +import debug_script +import json +from charmhelpers.core import unitdata + +kv = unitdata.kv() +data = kv.getrange("") + +with debug_script.open_file("unitdata.json", "w") as f: + json.dump(data, f, indent=2) + f.write("\n") diff --git a/kubeapi-load-balancer/debug-scripts/filesystem b/kubeapi-load-balancer/debug-scripts/filesystem new file mode 100755 index 0000000..c5ec6d8 --- /dev/null +++ b/kubeapi-load-balancer/debug-scripts/filesystem @@ -0,0 +1,17 @@ +#!/bin/sh +set -ux + +# report file system disk space usage +df -hT > $DEBUG_SCRIPT_DIR/df-hT +# estimate file space usage +du -h / 2>&1 > $DEBUG_SCRIPT_DIR/du-h +# list the mounted filesystems +mount > $DEBUG_SCRIPT_DIR/mount +# list the mounted systems with ascii trees +findmnt -A > $DEBUG_SCRIPT_DIR/findmnt +# list block devices +lsblk > $DEBUG_SCRIPT_DIR/lsblk +# list open files +lsof 2>&1 > $DEBUG_SCRIPT_DIR/lsof +# list local system locks +lslocks > $DEBUG_SCRIPT_DIR/lslocks diff --git a/kubeapi-load-balancer/debug-scripts/juju-logs b/kubeapi-load-balancer/debug-scripts/juju-logs new file mode 100755 index 0000000..d27c458 --- /dev/null +++ b/kubeapi-load-balancer/debug-scripts/juju-logs @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +cp -v /var/log/juju/* $DEBUG_SCRIPT_DIR diff --git a/kubeapi-load-balancer/debug-scripts/juju-network-get b/kubeapi-load-balancer/debug-scripts/juju-network-get new file mode 100755 index 0000000..983c8c4 --- /dev/null +++ b/kubeapi-load-balancer/debug-scripts/juju-network-get @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import yaml +import debug_script + +with open('metadata.yaml') as f: + metadata = yaml.load(f) + +relations = [] +for key in ['requires', 'provides', 'peers']: + relations += list(metadata.get(key, {}).keys()) + +os.mkdir(os.path.join(debug_script.dir, 'relations')) + +for relation in relations: + path = 'relations/' + relation + with debug_script.open_file(path, 'w') as f: + cmd = ['network-get', relation] + subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) diff --git a/kubeapi-load-balancer/debug-scripts/network b/kubeapi-load-balancer/debug-scripts/network new file mode 100755 index 0000000..944a355 --- /dev/null +++ b/kubeapi-load-balancer/debug-scripts/network @@ -0,0 +1,11 @@ +#!/bin/sh +set -ux + +ifconfig -a > $DEBUG_SCRIPT_DIR/ifconfig +cp -v /etc/resolv.conf $DEBUG_SCRIPT_DIR/resolv.conf +cp -v /etc/network/interfaces $DEBUG_SCRIPT_DIR/interfaces +netstat -planut > $DEBUG_SCRIPT_DIR/netstat +route -n > $DEBUG_SCRIPT_DIR/route +iptables-save > $DEBUG_SCRIPT_DIR/iptables-save +dig google.com > $DEBUG_SCRIPT_DIR/dig-google +ping -w 2 -i 0.1 google.com > $DEBUG_SCRIPT_DIR/ping-google diff --git a/kubeapi-load-balancer/debug-scripts/packages b/kubeapi-load-balancer/debug-scripts/packages new file mode 100755 index 0000000..b60a9cf --- /dev/null +++ b/kubeapi-load-balancer/debug-scripts/packages @@ -0,0 +1,7 @@ +#!/bin/sh +set -ux + +dpkg --list > $DEBUG_SCRIPT_DIR/dpkg-list +snap list > $DEBUG_SCRIPT_DIR/snap-list +pip2 list > $DEBUG_SCRIPT_DIR/pip2-list +pip3 list > $DEBUG_SCRIPT_DIR/pip3-list diff --git a/kubeapi-load-balancer/debug-scripts/sysctl b/kubeapi-load-balancer/debug-scripts/sysctl new file mode 100755 index 0000000..a86a6c8 --- /dev/null +++ b/kubeapi-load-balancer/debug-scripts/sysctl @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +sysctl -a > $DEBUG_SCRIPT_DIR/sysctl diff --git a/kubeapi-load-balancer/debug-scripts/systemd b/kubeapi-load-balancer/debug-scripts/systemd new file mode 100755 index 0000000..8bb9b6f --- /dev/null +++ b/kubeapi-load-balancer/debug-scripts/systemd @@ -0,0 +1,9 @@ +#!/bin/sh +set -ux + +systemctl --all > $DEBUG_SCRIPT_DIR/systemctl +journalctl > $DEBUG_SCRIPT_DIR/journalctl +systemd-analyze time > $DEBUG_SCRIPT_DIR/systemd-analyze-time +systemd-analyze blame > $DEBUG_SCRIPT_DIR/systemd-analyze-blame +systemd-analyze critical-chain > $DEBUG_SCRIPT_DIR/systemd-analyze-critical-chain +systemd-analyze dump > $DEBUG_SCRIPT_DIR/systemd-analyze-dump diff --git a/kubeapi-load-balancer/debug-scripts/tls-certs b/kubeapi-load-balancer/debug-scripts/tls-certs new file mode 100755 index 0000000..2692e51 --- /dev/null +++ b/kubeapi-load-balancer/debug-scripts/tls-certs @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import shutil +import traceback +import debug_script +from charms import layer + +options = layer.options.get('tls-client') + +def copy_cert(source_key, name): + try: + source = options[source_key] + dest = os.path.join(debug_script.dir, name) + shutil.copy(source, dest) + except Exception: + traceback.print_exc() + +copy_cert('client_certificate_path', 'client.crt') +copy_cert('server_certificate_path', 'server.crt') +copy_cert('ca_certificate_path', 'ca.crt') diff --git a/kubeapi-load-balancer/docs/status.md b/kubeapi-load-balancer/docs/status.md new file mode 100644 index 0000000..c6cceab --- /dev/null +++ b/kubeapi-load-balancer/docs/status.md @@ -0,0 +1,91 @@ +

WorkloadState

+ +```python +WorkloadState(self, /, *args, **kwargs) +``` + +Enum of the valid workload states. + +Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + +

maintenance

+ +```python +maintenance(message) +``` + +Set the status to the `MAINTENANCE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

maint

+ +```python +maint(message) +``` + +Shorthand alias for +[maintenance](status.md#charms.layer.status.maintenance). + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

blocked

+ +```python +blocked(message) +``` + +Set the status to the `BLOCKED` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

waiting

+ +```python +waiting(message) +``` + +Set the status to the `WAITING` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

active

+ +```python +active(message) +``` + +Set the status to the `ACTIVE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

status_set

+ +```python +status_set(workload_state, message) +``` + +Set the status to the given workload state with a message. + +__Parameters__ + +- __`workload_state` (WorkloadState or str)__: State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. +- __`message` (str)__: Message to convey to the operator. + diff --git a/kubeapi-load-balancer/hooks/apiserver-relation-broken b/kubeapi-load-balancer/hooks/apiserver-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/apiserver-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/apiserver-relation-changed b/kubeapi-load-balancer/hooks/apiserver-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/apiserver-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/apiserver-relation-created b/kubeapi-load-balancer/hooks/apiserver-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/apiserver-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/apiserver-relation-departed b/kubeapi-load-balancer/hooks/apiserver-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/apiserver-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/apiserver-relation-joined b/kubeapi-load-balancer/hooks/apiserver-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/apiserver-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/certificates-relation-broken b/kubeapi-load-balancer/hooks/certificates-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/certificates-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/certificates-relation-changed b/kubeapi-load-balancer/hooks/certificates-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/certificates-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/certificates-relation-created b/kubeapi-load-balancer/hooks/certificates-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/certificates-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/certificates-relation-departed b/kubeapi-load-balancer/hooks/certificates-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/certificates-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/certificates-relation-joined b/kubeapi-load-balancer/hooks/certificates-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/certificates-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/collect-metrics b/kubeapi-load-balancer/hooks/collect-metrics new file mode 100755 index 0000000..8a27863 --- /dev/null +++ b/kubeapi-load-balancer/hooks/collect-metrics @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +import yaml +import os +from subprocess import check_output, check_call, CalledProcessError + + +def build_command(doc): + values = {} + metrics = doc.get("metrics", {}) + for metric, mdoc in metrics.items(): + if not mdoc: + continue + cmd = mdoc.get("command") + if cmd: + try: + value = check_output(cmd, shell=True, universal_newlines=True) + except CalledProcessError as e: + check_call(['juju-log', '-lERROR', + 'Error collecting metric {}:\n{}'.format( + metric, e.output)]) + continue + value = value.strip() + if value: + values[metric] = value + + if not values: + return None + command = ["add-metric"] + for metric, value in values.items(): + command.append("%s=%s" % (metric, value)) + return command + + +if __name__ == '__main__': + charm_dir = os.path.dirname(os.path.abspath(os.path.join(__file__, ".."))) + metrics_yaml = os.path.join(charm_dir, "metrics.yaml") + with open(metrics_yaml) as f: + doc = yaml.load(f) + command = build_command(doc) + if command: + check_call(command) diff --git a/kubeapi-load-balancer/hooks/config-changed b/kubeapi-load-balancer/hooks/config-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/config-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/ha-relation-broken b/kubeapi-load-balancer/hooks/ha-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/ha-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/ha-relation-changed b/kubeapi-load-balancer/hooks/ha-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/ha-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/ha-relation-created b/kubeapi-load-balancer/hooks/ha-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/ha-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/ha-relation-departed b/kubeapi-load-balancer/hooks/ha-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/ha-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/ha-relation-joined b/kubeapi-load-balancer/hooks/ha-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/ha-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/hook.template b/kubeapi-load-balancer/hooks/hook.template new file mode 100644 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/hook.template @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/install b/kubeapi-load-balancer/hooks/install new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/install @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/lb-consumers-relation-broken b/kubeapi-load-balancer/hooks/lb-consumers-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/lb-consumers-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/lb-consumers-relation-changed b/kubeapi-load-balancer/hooks/lb-consumers-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/lb-consumers-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/lb-consumers-relation-created b/kubeapi-load-balancer/hooks/lb-consumers-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/lb-consumers-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/lb-consumers-relation-departed b/kubeapi-load-balancer/hooks/lb-consumers-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/lb-consumers-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/lb-consumers-relation-joined b/kubeapi-load-balancer/hooks/lb-consumers-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/lb-consumers-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/leader-elected b/kubeapi-load-balancer/hooks/leader-elected new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/leader-elected @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/leader-settings-changed b/kubeapi-load-balancer/hooks/leader-settings-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/leader-settings-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/loadbalancer-relation-broken b/kubeapi-load-balancer/hooks/loadbalancer-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/loadbalancer-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/loadbalancer-relation-changed b/kubeapi-load-balancer/hooks/loadbalancer-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/loadbalancer-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/loadbalancer-relation-created b/kubeapi-load-balancer/hooks/loadbalancer-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/loadbalancer-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/loadbalancer-relation-departed b/kubeapi-load-balancer/hooks/loadbalancer-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/loadbalancer-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/loadbalancer-relation-joined b/kubeapi-load-balancer/hooks/loadbalancer-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/loadbalancer-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/nrpe-external-master-relation-broken b/kubeapi-load-balancer/hooks/nrpe-external-master-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/nrpe-external-master-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/nrpe-external-master-relation-changed b/kubeapi-load-balancer/hooks/nrpe-external-master-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/nrpe-external-master-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/nrpe-external-master-relation-created b/kubeapi-load-balancer/hooks/nrpe-external-master-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/nrpe-external-master-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/nrpe-external-master-relation-departed b/kubeapi-load-balancer/hooks/nrpe-external-master-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/nrpe-external-master-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/nrpe-external-master-relation-joined b/kubeapi-load-balancer/hooks/nrpe-external-master-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/nrpe-external-master-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/post-series-upgrade b/kubeapi-load-balancer/hooks/post-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/post-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/pre-series-upgrade b/kubeapi-load-balancer/hooks/pre-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/pre-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/relations/hacluster/.stestr.conf b/kubeapi-load-balancer/hooks/relations/hacluster/.stestr.conf new file mode 100644 index 0000000..5fcccac --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/hacluster/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./unit_tests +top_dir=./ diff --git a/kubeapi-load-balancer/hooks/relations/hacluster/README.md b/kubeapi-load-balancer/hooks/relations/hacluster/README.md new file mode 100644 index 0000000..e8147ac --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/hacluster/README.md @@ -0,0 +1,90 @@ +# Overview + +This interface handles the communication with the hacluster subordinate +charm using the `ha` interface protocol. + +# Usage + +## Requires + +The interface layer will set the following reactive states, as appropriate: + + * `{relation_name}.connected` The relation is established and ready for + the local charm to configure the hacluster subordinate charm. The + configuration of the resources to manage for the hacluster charm + can be managed via one of the following methods: + + * `manage_resources` method + * `bind_on` method + + Configuration of the managed resources within the hacluster can be + managed by passing `common.CRM` object definitions to the + `manage_resources` method. + + * `{relation_name}.available` The hacluster is up and ready. + +For example: +```python +from charms.reactive import when, when_not +from charms.reactive import set_state, remove_state + +from relations.hacluster.common import CRM + + +@when('ha.connected') +def cluster_connected(hacluster): + + resources = CRM() + resources.primitive('res_vip', 'ocf:IPAddr2', + params='ip=10.0.3.100 nic=eth0', + op='monitor interval="10s"') + resources.clone('cl_res_vip', 'res_vip') + + hacluster.bind_on(iface='eth0', mcastport=4430) + hacluster.manage_resources(resources) +``` + +Additionally, for more code clarity a custom object implements the interface +defined in common.ResourceDescriptor can be used to simplify the code for +reuse. + +For example: +```python +import ipaddress + +from relation.hacluster.common import CRM +from relation.hacluster.common import ResourceDescriptor + +class VirtualIP(ResourceDescriptor): + def __init__(self, vip, nic='eth0'): + self.vip = vip + self.nic = 'eth0' + + def configure_resource(self, crm): + ipaddr = ipaddress.ip_address(self.vip) + if isinstance(ipaddr, ipaddress.IPv4Address): + res_type = 'ocf:heartbeat:IPAddr2' + res_parms = 'ip={ip} nic={nic}'.format(ip=self.vip, + nic=self.nic) + else: + res_type = 'ocf:heartbeat:IPv6addr' + res_params = 'ipv6addr={ip} nic={nic}'.format(ip=self.vip, + nic=self.nic) + + crm.primitive('res_vip', res_type, params=res_params, + op='monitor interval="10s"') + crm.clone('cl_res_vip', 'res_vip') +``` + +Once the VirtualIP class above has been defined in charm code, it can make +the code a bit cleaner. The example above can thusly be written as: + +```python +@when('ha.connected') +def cluster_connected(hacluster): + resources = CRM() + resources.add(VirtualIP('10.0.3.100')) + + hacluster.bind_on(iface='eth0', mcastport=4430) + hacluster.manage_resources(resources) +``` diff --git a/kubeapi-load-balancer/hooks/relations/hacluster/__init__.py b/kubeapi-load-balancer/hooks/relations/hacluster/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubeapi-load-balancer/hooks/relations/hacluster/copyright b/kubeapi-load-balancer/hooks/relations/hacluster/copyright new file mode 100644 index 0000000..5a49dcb --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/hacluster/copyright @@ -0,0 +1,21 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0 + +Files: * +Copyright: 2015, Canonical Ltd. +License: Apache-2.0 + +License: Apache-2.0 + 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. + . + On Debian-based systems the full text of the Apache version 2.0 license + can be found in `/usr/share/common-licenses/Apache-2.0'. diff --git a/kubeapi-load-balancer/hooks/relations/hacluster/interface.yaml b/kubeapi-load-balancer/hooks/relations/hacluster/interface.yaml new file mode 100644 index 0000000..f03f3d7 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/hacluster/interface.yaml @@ -0,0 +1,16 @@ +name: hacluster +summary: | + Provides the hacluster interface used for configuring Corosync + and Pacemaker services. +maintainer: OpenStack Charmers +ignore: + - '.gitignore' + - '.gitreview' + - '.testr.conf' + - 'test-requirements' + - 'tox.ini' + - 'unit_tests' + - '.zuul.yaml' + - 'setup.cfg' + - 'setup.py' + - '**/ops_ha_interface.py' diff --git a/kubeapi-load-balancer/hooks/relations/hacluster/interface_hacluster/__init__.py b/kubeapi-load-balancer/hooks/relations/hacluster/interface_hacluster/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubeapi-load-balancer/hooks/relations/hacluster/interface_hacluster/common.py b/kubeapi-load-balancer/hooks/relations/hacluster/interface_hacluster/common.py new file mode 100644 index 0000000..e4b13ff --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/hacluster/interface_hacluster/common.py @@ -0,0 +1,1009 @@ +#!/usr/bin/python +# 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 hashlib +import ipaddress +import json +from six import string_types + + +class ResourceManagement(): + + def data_changed(self, data_id, data, hash_type='md5'): + raise NotImplementedError + + def get_local(self, key, default=None, scope=None): + raise NotImplementedError + + def set_local(self, key=None, value=None, data=None, scope=None, **kwdata): + raise NotImplementedError + + def set_remote(self, key=None, value=None, data=None, scope=None, + **kwdata): + raise NotImplementedError + + def is_clustered(self): + """Has the hacluster charm set clustered? + + The hacluster charm sets cluster=True when it determines it is ready. + Check the relation data for clustered and force a boolean return. + + :returns: boolean + """ + clustered_values = self.get_remote_all('clustered') + if clustered_values: + # There is only ever one subordinate hacluster unit + clustered = clustered_values[0] + # Future versions of hacluster will return a bool + # Current versions return a string + if type(clustered) is bool: + return clustered + elif (clustered is not None and + (clustered.lower() == 'true' or + clustered.lower() == 'yes')): + return True + return False + + def bind_on(self, iface=None, mcastport=None): + relation_data = {} + if iface: + relation_data['corosync_bindiface'] = iface + if mcastport: + relation_data['corosync_mcastport'] = mcastport + + if relation_data and self.data_changed('hacluster-bind_on', + relation_data): + self.set_local(**relation_data) + self.set_remote(**relation_data) + + def manage_resources(self, crm): + """ + Request for the hacluster to manage the resources defined in the + crm object. + + res = CRM() + res.primitive('res_neutron_haproxy', 'lsb:haproxy', + op='monitor interval="5s"') + res.init_services('haproxy') + res.clone('cl_nova_haproxy', 'res_neutron_haproxy') + + hacluster.manage_resources(crm) + + :param crm: CRM() instance - Config object for Pacemaker resources + :returns: None + """ + relation_data = { + 'json_{}'.format(k): json.dumps(v, sort_keys=True) + for k, v in crm.items() if v + } + if self.data_changed('hacluster-manage_resources', relation_data): + self.set_local(**relation_data) + self.set_remote(**relation_data) + + def bind_resources(self, iface=None, mcastport=None): + """Inform the ha subordinate about each service it should manage. The + child class specifies the services via self.ha_resources + + :param iface: string - Network interface to bind to + :param mcastport: int - Multicast port corosync should use for cluster + management traffic + """ + if mcastport is None: + mcastport = 4440 + resources_dict = self.get_local('resources') + self.bind_on(iface=iface, mcastport=mcastport) + if resources_dict: + resources = CRM(**resources_dict) + self.manage_resources(resources) + + def delete_resource(self, resource_name): + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.add_delete_resource(resource_name) + self.set_local(resources=resources) + + def add_vip(self, name, vip, iface=None, netmask=None): + """Add a VirtualIP object for each user specified vip to self.resources + + :param name: string - Name of service + :param vip: string - Virtual IP to be managed + :param iface: string - Network interface to bind vip to + :param netmask: string - Netmask for vip + :returns: None + """ + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.add( + VirtualIP( + name, + vip, + nic=iface, + cidr=netmask,)) + + # Vip Group + group = 'grp_{}_vips'.format(name) + vip_res_group_members = [] + if resource_dict: + vip_resources = resource_dict.get('resources') + if vip_resources: + for vip_res in vip_resources: + if 'vip' in vip_res: + vip_res_group_members.append(vip_res) + resources.group(group, + *sorted(vip_res_group_members)) + + self.set_local(resources=resources) + + def remove_vip(self, name, vip, iface=None): + """Remove a virtual IP + + :param name: string - Name of service + :param vip: string - Virtual IP + :param iface: string - Network interface vip bound to + """ + if iface: + nic_name = iface + else: + nic_name = hashlib.sha1(vip.encode('UTF-8')).hexdigest()[:7] + self.delete_resource('res_{}_{}_vip'.format(name, nic_name)) + + def add_init_service(self, name, service, clone=True): + """Add a InitService object for haproxy to self.resources + + :param name: string - Name of service + :param service: string - Name service uses in init system + :returns: None + """ + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.add( + InitService(name, service, clone)) + self.set_local(resources=resources) + + def remove_init_service(self, name, service): + """Remove an init service + + :param name: string - Name of service + :param service: string - Name of service used in init system + """ + res_key = 'res_{}_{}'.format( + name.replace('-', '_'), + service.replace('-', '_')) + self.delete_resource(res_key) + + def add_systemd_service(self, name, service, clone=True): + """Add a SystemdService object to self.resources + + :param name: string - Name of service + :param service: string - Name service uses in systemd + :returns: None + """ + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.add( + SystemdService(name, service, clone)) + self.set_local(resources=resources) + + def remove_systemd_service(self, name, service): + """Remove a systemd service + + :param name: string - Name of service + :param service: string - Name of service used in systemd + """ + res_key = 'res_{}_{}'.format( + name.replace('-', '_'), + service.replace('-', '_')) + self.delete_resource(res_key) + + def add_dnsha(self, name, ip, fqdn, endpoint_type): + """Add a DNS entry to self.resources + + :param name: string - Name of service + :param ip: string - IP address dns entry should resolve to + :param fqdn: string - The DNS entry name + :param endpoint_type: string - Public, private, internal etc + :returns: None + """ + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.add( + DNSEntry(name, ip, fqdn, endpoint_type)) + + # DNS Group + group = 'grp_{}_hostnames'.format(name) + dns_res_group_members = [] + if resource_dict: + dns_resources = resource_dict.get('resources') + if dns_resources: + for dns_res in dns_resources: + if 'hostname' in dns_res: + dns_res_group_members.append(dns_res) + resources.group(group, + *sorted(dns_res_group_members)) + + self.set_local(resources=resources) + + def remove_dnsha(self, name, endpoint_type): + """Remove a DNS entry + + :param name: string - Name of service + :param endpoint_type: string - Public, private, internal etc + :returns: None + """ + res_key = 'res_{}_{}_hostname'.format( + self.service_name.replace('-', '_'), + self.endpoint_type) + self.delete_resource(res_key) + + def add_colocation(self, name, score, colo_resources, node_attribute=None): + """Add a colocation directive + + :param name: string - Name of colocation directive + :param score: string - ALWAYS, INFINITY, NEVER, NEGATIVE_INFINITY}. See + CRM.colocation for more details + :param colo_resources: List[string] - List of resource names to + colocate + :param node_attribute: Colocate resources on a set of nodes with this + attribute and not necessarily on the same node. + """ + node_config = {} + if node_attribute: + node_config = { + 'node_attribute': node_attribute} + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.colocation( + name, + score, + *colo_resources, + **node_config) + self.set_local(resources=resources) + + def remove_colocation(self, name): + """Remove a colocation directive + + :param name: string - Name of colocation directive + """ + self.delete_resource(name) + + def get_remote_all(self, key, default=None): + """Return a list of all values presented by remote units for key""" + raise NotImplementedError + + +class CRM(dict): + """ + Configuration object for Pacemaker resources for the HACluster + interface. This class provides access to the supported resources + available in the 'crm configure' within the HACluster. + + See Also + -------- + More documentation is available regarding the definitions of + primitives, clones, and other pacemaker resources at the crmsh + site at http://crmsh.github.io/man + """ + + # Constants provided for ordering constraints (e.g. the kind value) + MANDATORY = "Mandatory" + OPTIONAL = "Optional" + SERIALIZE = "Serialize" + + # Constants defining weights of constraints + INFINITY = "inf" + NEG_INFINITY = "-inf" + + # Constaints aliased to their interpretations for constraints + ALWAYS = INFINITY + NEVER = NEG_INFINITY + + def __init__(self, *args, **kwargs): + self['resources'] = {} + self['delete_resources'] = [] + self['resource_params'] = {} + self['groups'] = {} + self['ms'] = {} + self['orders'] = {} + self['colocations'] = {} + self['clones'] = {} + self['locations'] = {} + self['init_services'] = [] + self['systemd_services'] = [] + super(CRM, self).__init__(*args, **kwargs) + + def primitive(self, name, agent, description=None, **kwargs): + """Configures a primitive resource within Pacemaker. + + A primitive is used to describe a resource which should be managed + by the cluster. Primitives consist of a name, the agent type, and + various configuration options to the primitive. For example: + + crm.primitive('www8', 'apache', + params='configfile=/etc/apache/www8.conf', + operations='$id-ref=apache_ops') + + will create the an apache primitive (resource) for the www8 service + hosted by the Apache HTTP server. The parameters specified can either + be provided individually (e.g. a string) or as an iterable. + + The following example shows how to specify multiple ops for a drbd + volume in a Master/Slave configuration:: + + ops = ['monitor role=Master interval=60s', + 'monitor role=Slave interval=300s'] + + crm.primitive('r0', 'ocf:linbit:drbd', + params='drbd_resource=r0', + op=ops) + + Additional arguments may be passed in as kwargs in which the key of + the kwarg is prepended to the value. + + Parameters + ---------- + name: str + the name of the primitive. + agent: str + the type of agent to use to monitor the primitive resource + (e.g. ocf:linbit:drbd). + description: str, optional, kwarg + a description about the resource + params: str or iterable, optional, kwarg + parameters which are provided to the resource agent + meta: str or iterable, optional, kwarg + metadata information for the primitive resource + utilization: str or iterable, optional, kwarg + utilization information for the primitive resource + operations: str or iterable, optional, kwarg + operations information for the primitive resource in id_spec + format (e.g. $id= or $id-ref=) + op: str or iterable, optional, kwarg + op information regarding the primitive resource. This takes the + form of ' [= = ...]' + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_primitive + """ + resources = self['resources'] + resources[name] = agent + + specs = '' + if description: + specs = specs + 'description="%s"' % description + + # Use the ordering specified in the crm manual + for key in 'params', 'meta', 'utilization', 'operations', 'op': + if key not in kwargs: + continue + specs = specs + (' %s' % self._parse(key, kwargs[key])) + + if specs: + self['resource_params'][name] = specs + + def _parse(self, prefix, data): + results = '' + if isinstance(data, string_types): + data = [data] + + first = True + for d in data: + if first: + results = results + ' ' + first = False + results = results + ('%s %s ' % (prefix, d)) + results = results.rstrip() + return results + + def clone(self, name, resource, description=None, **kwargs): + """Creates a resource which should run on all nodes. + + Parameters + ---------- + name: str + the name of the clone + resource: str + the name or id of the resource to clone + description: str, optional + text containing a description for the clone + meta: str or list of str, optional, kwarg + metadata attributes to assign to the clone + params: str or list of str, optional, kwarg + parameters to assign to the clone + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_clone + """ + clone_specs = resource + if description: + clone_specs = clone_specs + (' description="%s"' % description) + + for key in 'meta', 'params': + if key not in kwargs: + continue + value = kwargs[key] + if not value: + continue + clone_specs = clone_specs + (' %s' % self._parse(key, value)) + + self['clones'][name] = clone_specs + + def colocation(self, name, score=ALWAYS, *resources, **kwargs): + """Configures the colocation constraints of resources. + + Provides placement constraints regarding resources defined within + the cluster. Using the colocate function, resource affinity or + anti-affinity can be defined. + + For example, the following code ensures that the nova-console service + always runs where the cluster vip is running: + + crm.colocation('console_with_vip', ALWAYS, + 'nova-console', 'vip') + + The affinity or anti-affinity of resources relationships is be + expressed in the `score` parameter. A positive score indicates that + the resources should run on the same node.A score of INFINITY (or + ALWAYS) will ensure the resources are always run on the same node(s) + and a score of NEG_INFINITY (or NEVER) ensures that the resources are + never run on the same node(s). + + crm.colocation('never_apache_with_dummy', NEVER, + 'apache', 'dummy') + + Any *resources values which are provided are treated as resources which + the colocation constraint applies to. At least two resources must be + defined as part of the ordering constraint. + + The resources take the form of [:role]. If the + colocation constraint applies specifically to a role, this information + should be included int he resource supplied. + + Parameters + ---------- + id: str + id or name of the colocation constraint + score: str {ALWAYS, INFINITY, NEVER, NEGATIVE_INFINITY} or int + the score or weight of the colocation constraint. A positive value + will indicate that the resources should run on the same node. A + negative value indicates that the resources should run on separate + nodes. + resources: str or list + the list of resources which the colocation constraint applies to. + node_attribute: str, optional, kwarg + can be used to run the resources on a set of nodes, not just a + single node. + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_colocation + """ + specs = '%s: %s' % (score, ' '.join(resources)) + if 'node_attribute' in kwargs: + specs = specs + (' node-attribute=%s' % kwargs['node_attribute']) + self['colocations'][name] = specs + + def group(self, name, *resources, **kwargs): + """Creates a group of resources within Pacemaker. + + The created group includes the list of resources provided in the list + of resources supplied. For example:: + + crm.group('grp_mysql', 'res_mysql_rbd', 'res_mysql_fs', + 'res_mysql_vip', 'res_mysqld') + + will create the 'grp_mysql' resource group consisting of the + res_mysql_rbd, res_mysql_fs, res_mysql_vip, and res_mysqld resources. + + Parameters + ---------- + name: str + the name of the group resource + resources: list of str + the names or ids of resources to include within the group. + description: str, optional, kwarg + text to describe the resource + meta: str or list of str, optional, kwarg + metadata attributes to assign to the group + params: str or list of str, optional, kwarg + parameters to assign to the group + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_group + """ + specs = ' '.join(resources) + if 'description' in kwargs: + specs = specs + (' description=%s"' % kwargs['description']) + + for key in 'meta', 'params': + if key not in kwargs: + continue + value = kwargs[key] + specs = specs + (' %s' % self._parse(key, value)) + + self['groups'][name] = specs + + def remove_deleted_resources(self): + """Work through the existing resources and remove any mention of ones + which have been marked for deletion.""" + for res in self['delete_resources']: + for key in self.keys(): + if key == 'delete_resources': + continue + if isinstance(self[key], dict) and res in self[key].keys(): + del self[key][res] + elif isinstance(self[key], list) and res in self[key]: + self[key].remove(res) + elif isinstance(self[key], tuple) and res in self[key]: + self[key] = tuple(x for x in self[key] if x != res) + + def delete_resource(self, *resources): + """Specify objects/resources to be deleted from within Pacemaker. This + is not additive, the list of resources is set to exaclty what was + passed in. + + Parameters + ---------- + resources: str or list + the name or id of the specific resource to delete. + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_delete + """ + self['delete_resources'] = resources + self.remove_deleted_resources() + + def add_delete_resource(self, resource): + """Specify an object/resource to delete from within Pacemaker. It can + be called multiple times to add additional resources to the deletion + list. + + Parameters + ---------- + resources: str + the name or id of the specific resource to delete. + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_delete + """ + if resource not in self['delete_resources']: + # NOTE(fnordahl): this unpleasant piece of code is regrettably + # necessary for Python3.4 (and trusty) compability see LP: #1814218 + # and LP: #1813982 + self['delete_resources'] = tuple( + self['delete_resources'] or ()) + (resource,) + self.remove_deleted_resources() + + def init_services(self, *resources): + """Specifies that the service(s) is an init or upstart service. + + Services (resources) which are noted as upstart services are + disabled, stopped, and left to pacemaker to manage the resource. + + Parameters + ---------- + resources: str or list of str, varargs + The resources which should be noted as init services. + + Returns + ------- + None + """ + self['init_services'] = resources + + def systemd_services(self, *resources): + """Specifies that the service(s) is a systemd service. + + Services (resources) which are noted as systemd services are + disabled, stopped, and left to pacemaker to manage the resource. + + Parameters + ---------- + resources: str or list of str, varargs + The resources which should be noted as systemd services. + + Returns + ------- + None + """ + self['systemd_services'] = resources + + def ms(self, name, resource, description=None, **kwargs): + """Create a master/slave resource type. + + The following code provides an example of creating a master/slave + resource on drbd disk1:: + + crm.ms('disk1', 'drbd1', meta='notify=true globally-unique=false') + + Parameters + ---------- + name: str + the name or id of the master resource + resource: str + the name or id of the resource which now ha a master/slave + assocation tied to it. + description: str, optional + a textual description of the master resource + meta: str or list of strs, optional, kwargs + strings defining the metadata for the master/slave resource type + params: str or list of strs, optional, kwargs + parameter strings which should be passed to the master/slave + resource creation + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_ms + """ + specs = resource + if description: + specs = specs + (' description="%s"' % description) + + for key in 'meta', 'params': + if key not in kwargs: + continue + value = kwargs[key] + specs = specs + (' %s' % self._parse(key, value)) + + self['ms'][name] = specs + + def location(self, name, resource, **kwargs): + """Defines the preference of nodes for the given resource. + + The location constraitns consist of one or more rules which specify + a score to be awarded if the rules match. + + Parameters + ---------- + name: str + the name or id of the location constraint + resource: str + the name, id, resource, set, tag, or resoruce pattern defining the + set of resources which match the location placement constraint. + attributes: str or list str, optional, kwarg + attributes which should be assigned to the location constraint + rule: str or list of str, optional, kwarg + the rule(s) which define the location constraint rules when + selecting a location to run the resource. + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_location + """ + specs = resource + + # Check if there are attributes assigned to the location and if so, + # format the spec string with the attributes + if 'attributes' in kwargs: + attrs = kwargs['attributes'] + if isinstance(attrs, string_types): + attrs = [attrs] + specs = specs + (' %s' % ' '.join(attrs)) + + if 'rule' in kwargs: + rules = kwargs['rule'] + specs = specs + (' %s' % self._parse('rule', rules)) + + self['locations'][name] = specs + + def order(self, name, score=None, *resources, **kwargs): + """Configures the ordering constraints of resources. + + Provides ordering constraints to resources defined in a Pacemaker + cluster which affect the way that resources are started, stopped, + promoted, etc. Basic ordering is provided by simply specifying the + ordering name and an ordered list of the resources which the ordering + constraint applies to. + + For example, the following code ensures that the apache resource is + started after the ClusterIP is started:: + + hacluster.order('apache-after-ip', 'ClusterIP', 'apache') + + By default, the ordering constraint will specify that the ordering + constraint is mandatory. The constraint behavior can be specified + using the 'score' keyword argument, e.g.:: + + hacluster.order('apache-after-ip', score=hacluster.OPTIONAL, + 'ClusterIP', 'apache') + + Any *resources values which are provided are treated as resources which + the ordering constraint applies to. At least two resources must be + defined as part of the ordering constraint. + + The resources take the form of [:]. If the + ordering constraint applies to a specific action for the resource, + this information should be included in the resource supplied. + + Parameters + ---------- + name: str + the id or name of the order constraint + resoures: str or list of strs in varargs format + the resources the ordering constraint applies to. The ordering + of the list of resources is used to provide the ordering. + score: {MANDATORY, OPTIONAL, SERIALIZED}, optional + the score of the ordering constraint. + symmetrical: boolean, optional, kwarg + when True, then the services for the resources will be stopped in + the reverse order. The default value for this is True. + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_order + """ + specs = '' + if score: + specs = '%s:' % score + + specs = specs + (' %s' % ' '.join(resources)) + if 'symmetrical' in kwargs: + specs = specs + (' symmetrical=' % kwargs['symmetrical']) + + self['orders'][name] = specs + + def add(self, resource_desc): + """Adds a resource descriptor object to the CRM configuration. + + Adds a `ResourceDescriptor` object to the CRM configuration which + understands how to configure the resource itself. The + `ResourceDescriptor` object needs to know how to interact with this + CRM class in order to properly configure the pacemaker resources. + + The minimum viable resource descriptor object will implement a method + which takes a reference parameter to this CRM in order to configure + itself. + + Parameters + ---------- + resource_desC: ResourceDescriptor + an object which provides an abstraction of a monitored resource + within pacemaker. + + Returns + ------- + None + """ + method = getattr(resource_desc, 'configure_resource', None) + if not callable(method): + raise ValueError('Invalid resource_desc. The "configure_resource"' + ' function has not been defined.') + + method(self) + + +class ResourceDescriptor(object): + """ + A ResourceDescriptor provides a logical resource or concept and knows + how to configure pacemaker. + """ + + def configure_resource(self, crm): + """Configures the logical resource(s) within the CRM. + + This is the callback method which is invoked by the CRM in order + to allow this ResourceDescriptor to fully configure the logical + resource. + + For example, a Virtual IP may provide a standard abstraction and + configure the specific details under the covers. + """ + pass + + +class InitService(ResourceDescriptor): + def __init__(self, service_name, init_service_name, clone=True): + """Class for managing init resource + + :param service_name: string - Name of service + :param init_service_name: string - Name service uses in init system + :param clone: bool - clone service across all units + :returns: None + """ + self.service_name = service_name + self.init_service_name = init_service_name + self.clone = clone + + def configure_resource(self, crm): + """"Configure new init system service resource in crm + + :param crm: CRM() instance - Config object for Pacemaker resources + :returns: None + """ + res_key = 'res_{}_{}'.format( + self.service_name.replace('-', '_'), + self.init_service_name.replace('-', '_')) + res_type = 'lsb:{}'.format(self.init_service_name) + _meta = 'migration-threshold="INFINITY" failure-timeout="5s"' + crm.primitive( + res_key, res_type, op='monitor interval="5s"', meta=_meta) + crm.init_services(self.init_service_name) + if self.clone: + clone_key = 'cl_{}'.format(res_key) + crm.clone(clone_key, res_key) + + +class VirtualIP(ResourceDescriptor): + def __init__(self, service_name, vip, nic=None, cidr=None): + """Class for managing VIP resource + + :param service_name: string - Name of service + :param vip: string - Virtual IP to be managed + :param nic: string - Network interface to bind vip to + :param cidr: string - Netmask for vip + :returns: None + """ + self.service_name = service_name + self.vip = vip + self.nic = nic + self.cidr = cidr + + def configure_resource(self, crm): + """Configure new vip resource in crm + + :param crm: CRM() instance - Config object for Pacemaker resources + :returns: None + """ + if self.nic: + vip_key = 'res_{}_{}_vip'.format(self.service_name, self.nic) + else: + vip_key = 'res_{}_{}_vip'.format( + self.service_name, + hashlib.sha1(self.vip.encode('UTF-8')).hexdigest()[:7]) + ipaddr = ipaddress.ip_address(self.vip) + if isinstance(ipaddr, ipaddress.IPv4Address): + res_type = 'ocf:heartbeat:IPaddr2' + res_params = 'ip="{}"'.format(self.vip) + else: + res_type = 'ocf:heartbeat:IPv6addr' + res_params = 'ipv6addr="{}"'.format(self.vip) + vip_params = 'ipv6addr' + vip_key = 'res_{}_{}_{}_vip'.format(self.service_name, self.nic, + vip_params) + + if self.nic: + res_params = '{} nic="{}"'.format(res_params, self.nic) + if self.cidr: + res_params = '{} cidr_netmask="{}"'.format(res_params, self.cidr) + # Monitor the VIP + _op_monitor = 'monitor timeout="20s" interval="10s" depth="0"' + _meta = 'migration-threshold="INFINITY" failure-timeout="5s"' + crm.primitive( + vip_key, res_type, params=res_params, op=_op_monitor, meta=_meta) + + +class DNSEntry(ResourceDescriptor): + + def __init__(self, service_name, ip, fqdn, endpoint_type): + """Class for managing DNS entries + + :param service_name: string - Name of service + :param ip: string - IP to point DNS entry at + :param fqdn: string - DNS Entry + :param endpoint_type: string - The type of the endpoint represented by + the DNS record eg public, admin etc + :returns: None + """ + self.service_name = service_name + self.ip = ip + self.fqdn = fqdn + self.endpoint_type = endpoint_type + + def configure_resource(self, crm, res_type='ocf:maas:dns'): + """Configure new DNS resource in crm + + :param crm: CRM() instance - Config object for Pacemaker resources + :param res_type: string - Corosync Open Cluster Framework resource + agent to use for DNS HA + :returns: None + """ + res_key = 'res_{}_{}_hostname'.format( + self.service_name.replace('-', '_'), + self.endpoint_type) + res_params = '' + if self.fqdn: + res_params = '{} fqdn="{}"'.format(res_params, self.fqdn) + if self.ip: + res_params = '{} ip_address="{}"'.format(res_params, self.ip) + crm.primitive(res_key, res_type, params=res_params) + + +class SystemdService(ResourceDescriptor): + def __init__(self, service_name, systemd_service_name, clone=True): + """Class for managing systemd resource + + :param service_name: string - Name of service + :param systemd_service_name: string - Name service uses in + systemd system + :param clone: bool - clone service across all units + :returns: None + """ + self.service_name = service_name + self.systemd_service_name = systemd_service_name + self.clone = clone + + def configure_resource(self, crm): + """"Configure new systemd system service resource in crm + + :param crm: CRM() instance - Config object for Pacemaker resources + :returns: None + """ + res_key = 'res_{}_{}'.format( + self.service_name.replace('-', '_'), + self.systemd_service_name.replace('-', '_')) + res_type = 'systemd:{}'.format(self.systemd_service_name) + _meta = 'migration-threshold="INFINITY" failure-timeout="5s"' + crm.primitive( + res_key, res_type, op='monitor interval="5s"', meta=_meta) + crm.systemd_services(self.systemd_service_name) + if self.clone: + clone_key = 'cl_{}'.format(res_key) + crm.clone(clone_key, res_key) diff --git a/kubeapi-load-balancer/hooks/relations/hacluster/requires.py b/kubeapi-load-balancer/hooks/relations/hacluster/requires.py new file mode 100644 index 0000000..395a658 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/hacluster/requires.py @@ -0,0 +1,58 @@ +#!/usr/bin/python +# 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 relations.hacluster.interface_hacluster.common as common +from charms.reactive import hook +from charms.reactive import RelationBase +from charms.reactive import scopes +from charms.reactive.helpers import data_changed as rh_data_changed +from charmhelpers.core import hookenv + + +class HAClusterRequires(RelationBase, common.ResourceManagement): + # The hacluster charm is a subordinate charm and really only works + # for a single service to the HA Cluster relation, therefore set the + # expected scope to be GLOBAL. + scope = scopes.GLOBAL + + @hook('{requires:hacluster}-relation-joined') + def joined(self): + self.set_state('{relation_name}.connected') + + @hook('{requires:hacluster}-relation-changed') + def changed(self): + if self.is_clustered(): + self.set_state('{relation_name}.available') + else: + self.remove_state('{relation_name}.available') + + @hook('{requires:hacluster}-relation-{broken,departed}') + def departed(self): + self.remove_state('{relation_name}.available') + self.remove_state('{relation_name}.connected') + + def data_changed(self, data_id, data, hash_type='md5'): + return rh_data_changed(data_id, data, hash_type) + + def get_remote_all(self, key, default=None): + """Return a list of all values presented by remote units for key""" + values = [] + for conversation in self.conversations(): + for relation_id in conversation.relation_ids: + for unit in hookenv.related_units(relation_id): + value = hookenv.relation_get(key, + unit, + relation_id) or default + if value: + values.append(value) + return list(set(values)) diff --git a/kubeapi-load-balancer/hooks/relations/hacluster/test-requirements.txt b/kubeapi-load-balancer/hooks/relations/hacluster/test-requirements.txt new file mode 100644 index 0000000..12452e5 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/hacluster/test-requirements.txt @@ -0,0 +1,7 @@ +# Lint and unit test requirements +flake8 +stestr>=2.2.0 +charms.reactive +coverage>=3.6 +netifaces +git+https://github.com/canonical/operator.git#egg=ops diff --git a/kubeapi-load-balancer/hooks/relations/http/.gitignore b/kubeapi-load-balancer/hooks/relations/http/.gitignore new file mode 100644 index 0000000..3374ec2 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/http/.gitignore @@ -0,0 +1,5 @@ +# Emacs save files +*~ +\#*\# +.\#* + diff --git a/kubeapi-load-balancer/hooks/relations/http/README.md b/kubeapi-load-balancer/hooks/relations/http/README.md new file mode 100644 index 0000000..3d7822a --- /dev/null +++ b/kubeapi-load-balancer/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/kubeapi-load-balancer/hooks/relations/http/__init__.py b/kubeapi-load-balancer/hooks/relations/http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubeapi-load-balancer/hooks/relations/http/interface.yaml b/kubeapi-load-balancer/hooks/relations/http/interface.yaml new file mode 100644 index 0000000..54e7748 --- /dev/null +++ b/kubeapi-load-balancer/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/kubeapi-load-balancer/hooks/relations/http/provides.py b/kubeapi-load-balancer/hooks/relations/http/provides.py new file mode 100644 index 0000000..86fa9b3 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/http/provides.py @@ -0,0 +1,67 @@ +import json + +from charmhelpers.core import hookenv +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag +from charms.reactive import Endpoint + + +class HttpProvides(Endpoint): + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + + def get_ingress_address(self, rel_id=None): + # If no rel_id is provided, we fallback to the first one + if rel_id is None: + rel_id = self.relations[0].relation_id + return hookenv.ingress_address(rel_id, hookenv.local_unit()) + + def configure(self, port, private_address=None, hostname=None): + ''' configure the address(es). private_address and hostname can + be None, a single string address/hostname, or a list of addresses + and hostnames. Note that if a list is passed, it is assumed both + private_address and hostname are either lists or None ''' + for relation in self.relations: + ingress_address = self.get_ingress_address(relation.relation_id) + if type(private_address) is list or type(hostname) is list: + # build 3 lists to zip together that are the same length + length = max(len(private_address), len(hostname)) + p = [port] * length + a = private_address + [ingress_address] *\ + (length - len(private_address)) + h = hostname + [ingress_address] * (length - len(hostname)) + zipped_list = zip(p, a, h) + # now build an array of dictionaries from that in the desired + # format for the interface + data_list = [{'hostname': h, 'port': p, 'private-address': a} + for p, a, h in zipped_list] + # for backwards compatibility, we just send a single entry + # and have an array of dictionaries in a field of that + # entry for the other entries. + data = data_list.pop(0) + data['extended_data'] = json.dumps(data_list) + + relation.to_publish_raw.update(data) + else: + relation.to_publish_raw.update({ + 'hostname': hostname or ingress_address, + 'private-address': private_address or ingress_address, + 'port': port, + }) + + def set_remote(self, **kwargs): + # NB: This method provides backwards compatibility for charms that + # called RelationBase.set_remote. Most commonly, this was done by + # charms that needed to pass reverse proxy stanzas to http proxies. + # This type of interaction with base relation classes is discouraged, + # and should be handled with logic encapsulated in appropriate + # interfaces. Eventually, this method will be deprecated in favor of + # that behavior. + for relation in self.relations: + relation.to_publish_raw.update(kwargs) diff --git a/kubeapi-load-balancer/hooks/relations/http/requires.py b/kubeapi-load-balancer/hooks/relations/http/requires.py new file mode 100644 index 0000000..17ea6b7 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/http/requires.py @@ -0,0 +1,76 @@ +import json + +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag +from charms.reactive import Endpoint + + +class HttpRequires(Endpoint): + + @when('endpoint.{endpoint_name}.changed') + def changed(self): + if any(unit.received_raw['port'] for unit in self.all_joined_units): + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_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, + 'private-address': private_address_of_host, + 'port': port_for_host, + }, + # ... + ], + }, + # ... + ] + """ + def build_service_host(data): + private_address = data['private-address'] + host = data['hostname'] or private_address + if host and data['port']: + return (host, private_address, data['port']) + else: + return None + + services = {} + for relation in self.relations: + service_name = relation.application_name + service = services.setdefault(service_name, { + 'service_name': service_name, + 'hosts': [], + }) + host_set = set() + for unit in relation.joined_units: + data = unit.received_raw + host = build_service_host(data) + if host: + host_set.add(host) + + # if we have extended data, add it + if 'extended_data' in data: + for ed in json.loads(data['extended_data']): + host = build_service_host(ed) + if host: + host_set.add(host) + + service['hosts'] = [ + {'hostname': h, 'private-address': pa, 'port': p} + for h, pa, p in sorted(host_set) + ] + + ret = [s for s in services.values() if s['hosts']] + return ret diff --git a/kubeapi-load-balancer/hooks/relations/nrpe-external-master/README.md b/kubeapi-load-balancer/hooks/relations/nrpe-external-master/README.md new file mode 100644 index 0000000..e33deb8 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/nrpe-external-master/README.md @@ -0,0 +1,66 @@ +# nrpe-external-master interface + +Use this interface to register nagios checks in your charm layers. + +## Purpose + +This interface is designed to interoperate with the +[nrpe-external-master](https://jujucharms.com/nrpe-external-master) subordinate charm. + +## How to use in your layers + +The event handler for `nrpe-external-master.available` is called with an object +through which you can register your own custom nagios checks, when a relation +is established with `nrpe-external-master:nrpe-external-master`. + +This object provides a method, + +_add_check_(args, name=_check_name_, description=_description_, context=_context_, unit=_unit_) + +which is called to register a nagios plugin check for your service. + +All arguments are required. + +*args* is a list of nagios plugin command line arguments, starting with the path to the plugin executable. + +*name* is the name of the check registered in nagios + +*description* is some text that describes what the check is for and what it does + +*context* is the nagios context name, something that identifies your application + +*unit* is `hookenv.local_unit()` + +The nrpe subordinate installs `check_http`, so you can use it like this: + +``` +@when('nrpe-external-master.available') +def setup_nagios(nagios): + config = hookenv.config() + unit_name = hookenv.local_unit() + nagios.add_check(['/usr/lib/nagios/plugins/check_http', + '-I', '127.0.0.1', '-p', str(config['port']), + '-e', " 200 OK", '-u', '/publickey'], + name="check_http", + description="Verify my awesome service is responding", + context=config["nagios_context"], + unit=unit_name, + ) +``` +If your `nagios.add_check` defines a custom plugin, you will also need to restart the `nagios-nrpe-server` service. + +Consult the nagios documentation for more information on [how to write your own +plugins](https://assets.nagios.com/downloads/nagioscore/docs/nagioscore/4/en/pluginapi.html) +or [find one](https://www.nagios.org/projects/nagios-plugins/) that does what you need. + +## Example deployment + +``` +$ juju deploy your-awesome-charm +$ juju deploy nrpe-external-master --config site-nagios.yaml +$ juju add-relation your-awesome-charm nrpe-external-master +``` + +where `site-nagios.yaml` has the necessary configuration settings for the +subordinate to connect to nagios. + diff --git a/kubeapi-load-balancer/hooks/relations/nrpe-external-master/__init__.py b/kubeapi-load-balancer/hooks/relations/nrpe-external-master/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubeapi-load-balancer/hooks/relations/nrpe-external-master/interface.yaml b/kubeapi-load-balancer/hooks/relations/nrpe-external-master/interface.yaml new file mode 100644 index 0000000..859a423 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/nrpe-external-master/interface.yaml @@ -0,0 +1,3 @@ +name: nrpe-external-master +summary: Nagios interface +version: 1 diff --git a/kubeapi-load-balancer/hooks/relations/nrpe-external-master/provides.py b/kubeapi-load-balancer/hooks/relations/nrpe-external-master/provides.py new file mode 100644 index 0000000..b6c7f0d --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/nrpe-external-master/provides.py @@ -0,0 +1,91 @@ +import datetime +import os + +from charmhelpers.core import hookenv + +from charms.reactive import hook +from charms.reactive import RelationBase +from charms.reactive import scopes + + +class NrpeExternalMasterProvides(RelationBase): + scope = scopes.GLOBAL + + @hook('{provides:nrpe-external-master}-relation-{joined,changed}') + def changed_nrpe(self): + self.set_state('{relation_name}.available') + + @hook('{provides:nrpe-external-master}-relation-{broken,departed}') + def broken_nrpe(self): + self.remove_state('{relation_name}.available') + + def add_check(self, args, name=None, description=None, context=None, + servicegroups=None, unit=None): + nagios_files = self.get_local('nagios.check.files', []) + + if not unit: + unit = hookenv.local_unit() + unit = unit.replace('/', '-') + context = self.get_remote('nagios_host_context', context) + host_name = self.get_remote('nagios_hostname', + '%s-%s' % (context, unit)) + + check_tmpl = """ +#--------------------------------------------------- +# This file is Juju managed +#--------------------------------------------------- +command[%(check_name)s]=%(check_args)s +""" + service_tmpl = """ +#--------------------------------------------------- +# This file is Juju managed +#--------------------------------------------------- +define service { + use active-service + host_name %(host_name)s + service_description %(description)s + check_command check_nrpe!%(check_name)s + servicegroups %(servicegroups)s +} +""" + check_filename = "/etc/nagios/nrpe.d/check_%s.cfg" % (name) + with open(check_filename, "w") as fh: + fh.write(check_tmpl % { + 'check_args': ' '.join(args), + 'check_name': name, + }) + nagios_files.append(check_filename) + + service_filename = "/var/lib/nagios/export/service__%s_%s.cfg" % ( + unit, name) + with open(service_filename, "w") as fh: + fh.write(service_tmpl % { + 'servicegroups': servicegroups or context, + 'context': context, + 'description': description, + 'check_name': name, + 'host_name': host_name, + 'unit_name': unit, + }) + nagios_files.append(service_filename) + + self.set_local('nagios.check.files', nagios_files) + + def removed(self): + files = self.get_local('nagios.check.files', []) + for f in files: + try: + os.unlink(f) + except Exception as e: + hookenv.log("failed to remove %s: %s" % (f, e)) + self.set_local('nagios.check.files', []) + self.remove_state('{relation_name}.removed') + + def added(self): + self.updated() + + def updated(self): + relation_info = { + 'timestamp': datetime.datetime.now().isoformat(), + } + self.set_remote(**relation_info) diff --git a/kubeapi-load-balancer/hooks/relations/nrpe-external-master/requires.py b/kubeapi-load-balancer/hooks/relations/nrpe-external-master/requires.py new file mode 100644 index 0000000..e69de29 diff --git a/kubeapi-load-balancer/hooks/relations/public-address/README.md b/kubeapi-load-balancer/hooks/relations/public-address/README.md new file mode 100644 index 0000000..06be3ae --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/public-address/README.md @@ -0,0 +1,59 @@ +# Overview + +This interface layer implements a public address protocol useful for load +balancers and their subordinates. The load balancers (providers) set their +own public address and port, which is then available to the subordinates +(requirers). + +# Usage + +## Provides + +By providing the `public-address` interface, your charm is providing an HTTP +server that can load-balance for another HTTP based service. + +Your charm need only provide the address and port on which it is serving its +content, as soon as the `{relation_name}.available` state is set: + +```python +from charmhelpers.core import hookenv +@when('website.available') +def configure_website(website): + website.set_address_port(hookenv.unit_get('public-address'), hookenv.config('port')) +``` + +## Requires + +By requiring the `public-address` interface, your charm is consuming one or +more HTTP servers, 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 `get_addresses_ports()` method returns a list of available addresses and +ports. + +The return value is a list of dicts of the following form: + +```python +[ + { + 'public-address': address_of_host, + 'port': port_for_host, + }, + # ... +] +``` + +A trivial example of handling this interface would be: + +```python +from charmhelpers.core import hookenv +@when('loadbalancer.available') +def update_reverse_proxy_config(loadbalancer): + hosts = loadbalancer.get_addresses_ports() + for host in hosts: + hookenv.log('The loadbalancer for this unit is {}:{}'.format( + host['public-address'], + host['port'])) +``` diff --git a/kubeapi-load-balancer/hooks/relations/public-address/__init__.py b/kubeapi-load-balancer/hooks/relations/public-address/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubeapi-load-balancer/hooks/relations/public-address/interface.yaml b/kubeapi-load-balancer/hooks/relations/public-address/interface.yaml new file mode 100644 index 0000000..c9849e4 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/public-address/interface.yaml @@ -0,0 +1,4 @@ +name: public-address +summary: A basic interface to provide the public address for load balancers. +version: 1 +repo: https://githb.com/juju-solutions/interface-public-address.git diff --git a/kubeapi-load-balancer/hooks/relations/public-address/provides.py b/kubeapi-load-balancer/hooks/relations/public-address/provides.py new file mode 100644 index 0000000..09b9915 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/public-address/provides.py @@ -0,0 +1,60 @@ +import json + +from charms.reactive import toggle_flag +from charms.reactive import Endpoint + + +class PublicAdddressProvides(Endpoint): + + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.available'), + self.is_joined) + + def set_address_port(self, address, port, relation=None): + if relation is None: + # no relation specified, so send the same data to everyone + relations = self.relations + else: + # specific relation given, so only send the data to that one + relations = [relation] + if type(address) is list: + # build 2 lists to zip together that are the same length + length = len(address) + p = [port] * length + combined = zip(address, p) + clients = [{'public-address': a, 'port': p} + for a, p in combined] + # for backwards compatibility, we just send a single entry + # and have an array of dictionaries in a field of that + # entry for the other entries. + first = clients.pop(0) + first['extended_data'] = json.dumps(clients) + for relation in relations: + relation.to_publish_raw.update(first) + else: + for relation in relations: + relation.to_publish_raw.update({'public-address': address, + 'port': port}) + + @property + def requests(self): + return [Request(rel) for rel in self.relations] + + +class Request: + def __init__(self, rel): + self.rel = rel + + @property + def application_name(self): + return self.rel.application_name + + @property + def members(self): + return [(u.received_raw.get('ingress-address', + u.received_raw['private-address']), + u.received_raw.get('port', '6443')) + for u in self.rel.joined_units] + + def set_address_port(self, address, port): + self.rel.endpoint.set_address_port(address, port, self.rel) diff --git a/kubeapi-load-balancer/hooks/relations/public-address/requires.py b/kubeapi-load-balancer/hooks/relations/public-address/requires.py new file mode 100644 index 0000000..467d129 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/public-address/requires.py @@ -0,0 +1,44 @@ +import json + +from charms.reactive import toggle_flag, Endpoint + + +class PublicAddressRequires(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.available'), + len(self.get_addresses_ports()) > 0) + + def set_backend_port(self, port): + """ + Set the port that the backend service is listening on. + + Defaults to 6443 if not set. + """ + for rel in self.relations: + rel.to_publish_raw['port'] = str(port) + + def get_addresses_ports(self): + '''Returns a list of available HTTP providers and their associated + public addresses and ports. + + The return value is a list of dicts of the following form:: + [ + { + 'public-address': address_for_frontend, + 'port': port_for_frontend, + }, + # ... + ] + ''' + hosts = set() + for relation in self.relations: + for unit in relation.joined_units: + data = unit.received_raw + hosts.add((data['public-address'], data['port'])) + if 'extended_data' in data: + for ed in json.loads(data['extended_data']): + hosts.add((ed['public-address'], ed['port'])) + + return [{'public-address': pa, 'port': p} + for pa, p in sorted(host for host in hosts + if None not in host)] diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/.gitignore b/kubeapi-load-balancer/hooks/relations/tls-certificates/.gitignore new file mode 100644 index 0000000..93813bc --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/.gitignore @@ -0,0 +1,4 @@ +.tox +__pycache__ +*.pyc +_build diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/README.md b/kubeapi-load-balancer/hooks/relations/tls-certificates/README.md new file mode 100644 index 0000000..733da6d --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/README.md @@ -0,0 +1,90 @@ +# Interface tls-certificates + +This is a [Juju][] interface layer that enables a charm which requires TLS +certificates to relate to a charm which can provide them, such as [Vault][] or +[EasyRSA][] + +To get started please read the [Introduction to PKI][] which defines some PKI +terms, concepts and processes used in this document. + +# Example Usage + +Let's say you have a charm which needs a server certificate for a service it +provides to other charms and a client certificate for a database it consumes +from another charm. The charm provides its own service on the `clients` +relation endpoint, and it consumes the database on the `db` relation endpoint. + +First, you must define the relation endpoint in your charm's `metadata.yaml`: + +```yaml +requires: + cert-provider: + interface: tls-certificates +``` + +Next, you must ensure the interface layer is included in your `layer.yaml`: + +```yaml +includes: + - interface:tls-certificates +``` + +Then, in your reactive code, add the following, changing `update_certs` to +handle the certificates however your charm needs: + +```python +from charmhelpers.core import hookenv, host +from charms.reactive import endpoint_from_flag + + +@when('cert-provider.ca.changed') +def install_root_ca_cert(): + cert_provider = endpoint_from_flag('cert-provider.ca.available') + host.install_ca_cert(cert_provider.root_ca_cert) + clear_flag('cert-provider.ca.changed') + + +@when('cert-provider.available') +def request_certificates(): + cert_provider = endpoint_from_flag('cert-provider.available') + + # get ingress info + ingress_for_clients = hookenv.network_get('clients')['ingress-addresses'] + ingress_for_db = hookenv.network_get('db')['ingress-addresses'] + + # use first ingress address as primary and any additional as SANs + server_cn, server_sans = ingress_for_clients[0], ingress_for_clients[:1] + client_cn, client_sans = ingress_for_db[0], ingress_for_db[:1] + + # request a single server and single client cert; note that multiple certs + # of either type can be requested as long as they have unique common names + cert_provider.request_server_cert(server_cn, server_sans) + cert_provider.request_client_cert(client_cn, client_sans) + + +@when('cert-provider.certs.changed') +def update_certs(): + cert_provider = endpoint_from_flag('cert-provider.available') + server_cert = cert_provider.server_certs[0] # only requested one + myserver.update_server_cert(server_cert.cert, server_cert.key) + + client_cert = cert_provider.client_certs[0] # only requested one + myclient.update_client_cert(client_cert.cert, client_cert.key) + clear_flag('cert-provider.certs.changed') +``` + + +# Reference + + * [Requires](docs/requires.md) + * [Provides](docs/provides.md) + +# Contact Information + +Maintainer: Cory Johns <Cory.Johns@canonical.com> + + +[Juju]: https://jujucharms.com +[Vault]: https://jujucharms.com/u/openstack-charmers/vault +[EasyRSA]: https://jujucharms.com/u/containers/easyrsa +[Introduction to PKI]: https://github.com/OpenVPN/easy-rsa/blob/master/doc/Intro-To-PKI.md diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/__init__.py b/kubeapi-load-balancer/hooks/relations/tls-certificates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/docs/common.md b/kubeapi-load-balancer/hooks/relations/tls-certificates/docs/common.md new file mode 100644 index 0000000..25d0e08 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/docs/common.md @@ -0,0 +1,51 @@ +

CertificateRequest

+ +```python +CertificateRequest(self, unit, cert_type, cert_name, common_name, sans) +``` + +

application_name

+ +Name of the application which the request came from. + +:returns: Name of application +:rtype: str + +

cert

+ + +The cert published for this request, if any. + +

cert_type

+ + +Type of certificate, 'server' or 'client', being requested. + +

resolve_unit_name

+ +```python +CertificateRequest.resolve_unit_name(unit) +``` +Return name of unit associated with this request. + +unit_name should be provided in the relation data to ensure +compatability with cross-model relations. If the unit name +is absent then fall back to unit_name attribute of the +unit associated with this request. + +:param unit: Unit to extract name from +:type unit: charms.reactive.endpoints.RelatedUnit +:returns: Name of unit +:rtype: str + +

Certificate

+ +```python +Certificate(self, cert_type, common_name, cert, key) +``` + +Represents a created certificate and key. + +The ``cert_type``, ``common_name``, ``cert``, and ``key`` values can +be accessed either as properties or as the contents of the dict. + diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/docs/provides.md b/kubeapi-load-balancer/hooks/relations/tls-certificates/docs/provides.md new file mode 100644 index 0000000..c213546 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/docs/provides.md @@ -0,0 +1,212 @@ +

provides

+ + +

TlsProvides

+ +```python +TlsProvides(self, endpoint_name, relation_ids=None) +``` + +The provider's side of the interface protocol. + +The following flags may be set: + + * `{endpoint_name}.available` + Whenever any clients are joined. + + * `{endpoint_name}.certs.requested` + When there are new certificate requests of any kind to be processed. + The requests can be accessed via [new_requests][]. + + * `{endpoint_name}.server.certs.requested` + When there are new server certificate requests to be processed. + The requests can be accessed via [new_server_requests][]. + + * `{endpoint_name}.client.certs.requested` + When there are new client certificate requests to be processed. + The requests can be accessed via [new_client_requests][]. + +[Certificate]: common.md#tls_certificates_common.Certificate +[CertificateRequest]: common.md#tls_certificates_common.CertificateRequest +[all_requests]: provides.md#provides.TlsProvides.all_requests +[new_requests]: provides.md#provides.TlsProvides.new_requests +[new_server_requests]: provides.md#provides.TlsProvides.new_server_requests +[new_client_requests]: provides.md#provides.TlsProvides.new_client_requests + +

all_published_certs

+ + +List of all [Certificate][] instances that this provider has published +for all related applications. + +

all_requests

+ + +List of all requests that have been made. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('certs.regen', + 'tls.certs.available') +def regen_all_certs(): + tls = endpoint_from_flag('tls.certs.available') + for request in tls.all_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_application_requests

+ + +Filtered view of [new_requests][] that only includes application cert +requests. + +Each will be an instance of [ApplicationCertificateRequest][]. + +Example usage: + +```python +@when('tls.application.certs.requested') +def gen_application_certs(): + tls = endpoint_from_flag('tls.application.certs.requested') + for request in tls.new_application_requests: + cert, key = generate_application_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_client_requests

+ + +Filtered view of [new_requests][] that only includes client cert +requests. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('tls.client.certs.requested') +def gen_client_certs(): + tls = endpoint_from_flag('tls.client.certs.requested') + for request in tls.new_client_requests: + cert, key = generate_client_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_requests

+ + +Filtered view of [all_requests][] that only includes requests that +haven't been handled. + +Each will be an instance of [CertificateRequest][]. + +This collection can also be further filtered by request type using +[new_server_requests][] or [new_client_requests][]. + +Example usage: + +```python +@when('tls.certs.requested') +def gen_certs(): + tls = endpoint_from_flag('tls.certs.requested') + for request in tls.new_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_server_requests

+ + +Filtered view of [new_requests][] that only includes server cert +requests. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('tls.server.certs.requested') +def gen_server_certs(): + tls = endpoint_from_flag('tls.server.certs.requested') + for request in tls.new_server_requests: + cert, key = generate_server_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

set_ca

+ +```python +TlsProvides.set_ca(certificate_authority) +``` + +Publish the CA to all related applications. + +

set_chain

+ +```python +TlsProvides.set_chain(chain) +``` + +Publish the chain of trust to all related applications. + +

set_client_cert

+ +```python +TlsProvides.set_client_cert(cert, key) +``` + +Deprecated. This is only for backwards compatibility. + +Publish a globally shared client cert and key. + +

set_server_cert

+ +```python +TlsProvides.set_server_cert(scope, cert, key) +``` + +Deprecated. Use one of the [new_requests][] collections and +`request.set_cert()` instead. + +Set the server cert and key for the request identified by `scope`. + +

set_server_multicerts

+ +```python +TlsProvides.set_server_multicerts(scope) +``` + +Deprecated. Done automatically. + +

add_server_cert

+ +```python +TlsProvides.add_server_cert(scope, cn, cert, key) +``` + +Deprecated. Use `request.set_cert()` instead. + +

get_server_requests

+ +```python +TlsProvides.get_server_requests() +``` + +Deprecated. Use the [new_requests][] or [server_requests][] +collections instead. + +One provider can have many requests to generate server certificates. +Return a map of all server request objects indexed by a unique +identifier. + diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/docs/requires.md b/kubeapi-load-balancer/hooks/relations/tls-certificates/docs/requires.md new file mode 100644 index 0000000..fdec902 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/docs/requires.md @@ -0,0 +1,207 @@ +

requires

+ + +

TlsRequires

+ +```python +TlsRequires(self, endpoint_name, relation_ids=None) +``` + +The client's side of the interface protocol. + +The following flags may be set: + + * `{endpoint_name}.available` + Whenever the relation is joined. + + * `{endpoint_name}.ca.available` + When the root CA information is available via the [root_ca_cert][] and + [root_ca_chain][] properties. + + * `{endpoint_name}.ca.changed` + When the root CA information has changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.certs.available` + When the requested server or client certs are available. + + * `{endpoint_name}.certs.changed` + When the requested server or client certs have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.server.certs.available` + When the server certificates requested by [request_server_cert][] are + available via the [server_certs][] collection. + + * `{endpoint_name}.server.certs.changed` + When the requested server certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.client.certs.available` + When the client certificates requested by [request_client_cert][] are + available via the [client_certs][] collection. + + * `{endpoint_name}.client.certs.changed` + When the requested client certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + +The following flags have been deprecated: + + * `{endpoint_name}.server.cert.available` + * `{endpoint_name}.client.cert.available` + * `{endpoint_name}.batch.cert.available` + +[Certificate]: common.md#tls_certificates_common.Certificate +[CertificateRequest]: common.md#tls_certificates_common.CertificateRequest +[root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert +[root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain +[request_server_cert]: requires.md#requires.TlsRequires.request_server_cert +[request_client_cert]: requires.md#requires.TlsRequires.request_client_cert +[server_certs]: requires.md#requires.TlsRequires.server_certs +[server_certs_map]: requires.md#requires.TlsRequires.server_certs_map +[client_certs]: requires.md#requires.TlsRequires.server_certs + +

application_certs

+ + +List of [Certificate][] instances for all available application certs. + +

client_certs

+ + +List of [Certificate][] instances for all available client certs. + +

client_certs_map

+ + +Mapping of client [Certificate][] instances by their `common_name`. + +

root_ca_cert

+ + +Root CA certificate. + +

root_ca_chain

+ + +The chain of trust for the root CA. + +

server_certs

+ + +List of [Certificate][] instances for all available server certs. + +

server_certs_map

+ + +Mapping of server [Certificate][] instances by their `common_name`. + +

get_ca

+ +```python +TlsRequires.get_ca() +``` + +Return the root CA certificate. + +Same as [root_ca_cert][]. + +

get_chain

+ +```python +TlsRequires.get_chain() +``` + +Return the chain of trust for the root CA. + +Same as [root_ca_chain][]. + +

get_client_cert

+ +```python +TlsRequires.get_client_cert() +``` + +Deprecated. Use [request_client_cert][] and the [client_certs][] +collection instead. + +Return a globally shared client certificate and key. + +

get_server_cert

+ +```python +TlsRequires.get_server_cert() +``` + +Deprecated. Use the [server_certs][] collection instead. + +Return the cert and key of the first server certificate requested. + +

get_batch_requests

+ +```python +TlsRequires.get_batch_requests() +``` + +Deprecated. Use [server_certs_map][] instead. + +Mapping of server [Certificate][] instances by their `common_name`. + +

request_server_cert

+ +```python +TlsRequires.request_server_cert(cn, sans=None, cert_name=None) +``` + +Request a server certificate and key be generated for the given +common name (`cn`) and optional list of alternative names (`sans`). + +The `cert_name` is deprecated and not needed. + +This can be called multiple times to request more than one server +certificate, although the common names must be unique. If called +again with the same common name, it will be ignored. + +

add_request_server_cert

+ +```python +TlsRequires.add_request_server_cert(cn, sans) +``` + +Deprecated. Use [request_server_cert][] instead. + +

request_server_certs

+ +```python +TlsRequires.request_server_certs() +``` + +Deprecated. Just use [request_server_cert][]; this does nothing. + +

request_client_cert

+ +```python +TlsRequires.request_client_cert(cn, sans) +``` + +Request a client certificate and key be generated for the given +common name (`cn`) and list of alternative names (`sans`). + +This can be called multiple times to request more than one client +certificate, although the common names must be unique. If called +again with the same common name, it will be ignored. + +

request_application_cert

+ +```python +TlsRequires.request_application_cert(cn, sans) +``` + +Request an application certificate and key be generated for the given +common name (`cn`) and list of alternative names (`sans` ) of this +unit and all peer units. All units will share a single certificates. + diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/interface.yaml b/kubeapi-load-balancer/hooks/relations/tls-certificates/interface.yaml new file mode 100644 index 0000000..beec53b --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/interface.yaml @@ -0,0 +1,6 @@ +name: tls-certificates +summary: | + A Transport Layer Security (TLS) charm layer that uses requires and provides + to exchange certifcates. +version: 1 +repo: https://github.com/juju-solutions/interface-tls-certificates diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/make_docs b/kubeapi-load-balancer/hooks/relations/tls-certificates/make_docs new file mode 100644 index 0000000..2f2274a --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/make_docs @@ -0,0 +1,23 @@ +#!.tox/py3/bin/python + +import sys +import importlib +from pathlib import Path +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'cert': {'interface': 'tls-certificates'}}, + 'provides': {'cert': {'interface': 'tls-certificates'}}, + } + sys.path.append('..') + sys.modules[''] = importlib.import_module(Path.cwd().name) + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/provides.py b/kubeapi-load-balancer/hooks/relations/tls-certificates/provides.py new file mode 100644 index 0000000..0262baa --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/provides.py @@ -0,0 +1,301 @@ +if not __package__: + # fix relative imports when building docs + import sys + __package__ = sys.modules[''].__name__ + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag + +from .tls_certificates_common import ( + ApplicationCertificateRequest, + CertificateRequest +) + + +class TlsProvides(Endpoint): + """ + The provider's side of the interface protocol. + + The following flags may be set: + + * `{endpoint_name}.available` + Whenever any clients are joined. + + * `{endpoint_name}.certs.requested` + When there are new certificate requests of any kind to be processed. + The requests can be accessed via [new_requests][]. + + * `{endpoint_name}.server.certs.requested` + When there are new server certificate requests to be processed. + The requests can be accessed via [new_server_requests][]. + + * `{endpoint_name}.client.certs.requested` + When there are new client certificate requests to be processed. + The requests can be accessed via [new_client_requests][]. + + [Certificate]: common.md#tls_certificates_common.Certificate + [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest + [all_requests]: provides.md#provides.TlsProvides.all_requests + [new_requests]: provides.md#provides.TlsProvides.new_requests + [new_server_requests]: provides.md#provides.TlsProvides.new_server_requests + [new_client_requests]: provides.md#provides.TlsProvides.new_client_requests + """ + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('{endpoint_name}.available')) + toggle_flag(self.expand_name('{endpoint_name}.certs.requested'), + self.new_requests) + toggle_flag(self.expand_name('{endpoint_name}.server.certs.requested'), + self.new_server_requests) + toggle_flag(self.expand_name('{endpoint_name}.client.certs.requested'), + self.new_client_requests) + toggle_flag( + self.expand_name('{endpoint_name}.application.certs.requested'), + self.new_application_requests) + # For backwards compatibility, set the old "cert" flags as well + toggle_flag(self.expand_name('{endpoint_name}.server.cert.requested'), + self.new_server_requests) + toggle_flag(self.expand_name('{endpoint_name}.client.cert.requested'), + self.new_client_requests) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + clear_flag(self.expand_name('{endpoint_name}.certs.requested')) + clear_flag(self.expand_name('{endpoint_name}.server.certs.requested')) + clear_flag(self.expand_name('{endpoint_name}.client.certs.requested')) + clear_flag( + self.expand_name('{endpoint_name}.application.certs.requested')) + + def set_ca(self, certificate_authority): + """ + Publish the CA to all related applications. + """ + for relation in self.relations: + # All the clients get the same CA, so send it to them. + relation.to_publish_raw['ca'] = certificate_authority + + def set_chain(self, chain): + """ + Publish the chain of trust to all related applications. + """ + for relation in self.relations: + # All the clients get the same chain, so send it to them. + relation.to_publish_raw['chain'] = chain + + def set_client_cert(self, cert, key): + """ + Deprecated. This is only for backwards compatibility. + + Publish a globally shared client cert and key. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'client.cert': cert, + 'client.key': key, + }) + + def set_server_cert(self, scope, cert, key): + """ + Deprecated. Use one of the [new_requests][] collections and + `request.set_cert()` instead. + + Set the server cert and key for the request identified by `scope`. + """ + request = self.get_server_requests()[scope] + request.set_cert(cert, key) + + def set_server_multicerts(self, scope): + """ + Deprecated. Done automatically. + """ + pass + + def add_server_cert(self, scope, cn, cert, key): + ''' + Deprecated. Use `request.set_cert()` instead. + ''' + self.set_server_cert(scope, cert, key) + + def get_server_requests(self): + """ + Deprecated. Use the [new_requests][] or [server_requests][] + collections instead. + + One provider can have many requests to generate server certificates. + Return a map of all server request objects indexed by a unique + identifier. + """ + return {req._key: req for req in self.new_server_requests} + + @property + def all_requests(self): + """ + List of all requests that have been made. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('certs.regen', + 'tls.certs.available') + def regen_all_certs(): + tls = endpoint_from_flag('tls.certs.available') + for request in tls.all_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + requests = [] + for unit in self.all_joined_units: + # handle older single server cert request + if unit.received_raw['common_name']: + requests.append(CertificateRequest( + unit, + 'server', + unit.received_raw['certificate_name'], + unit.received_raw['common_name'], + unit.received['sans'], + )) + + # handle mutli server cert requests + reqs = unit.received['cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(CertificateRequest( + unit, + 'server', + common_name, + common_name, + req['sans'], + )) + + # handle client cert requests + reqs = unit.received['client_cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(CertificateRequest( + unit, + 'client', + common_name, + common_name, + req['sans'], + )) + # handle application cert requests + reqs = unit.received['application_cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(ApplicationCertificateRequest( + unit, + 'application', + common_name, + common_name, + req['sans'] + )) + return requests + + @property + def new_requests(self): + """ + Filtered view of [all_requests][] that only includes requests that + haven't been handled. + + Each will be an instance of [CertificateRequest][]. + + This collection can also be further filtered by request type using + [new_server_requests][] or [new_client_requests][]. + + Example usage: + + ```python + @when('tls.certs.requested') + def gen_certs(): + tls = endpoint_from_flag('tls.certs.requested') + for request in tls.new_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.all_requests if not req.is_handled] + + @property + def new_server_requests(self): + """ + Filtered view of [new_requests][] that only includes server cert + requests. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('tls.server.certs.requested') + def gen_server_certs(): + tls = endpoint_from_flag('tls.server.certs.requested') + for request in tls.new_server_requests: + cert, key = generate_server_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.new_requests if req.cert_type == 'server'] + + @property + def new_client_requests(self): + """ + Filtered view of [new_requests][] that only includes client cert + requests. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('tls.client.certs.requested') + def gen_client_certs(): + tls = endpoint_from_flag('tls.client.certs.requested') + for request in tls.new_client_requests: + cert, key = generate_client_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.new_requests if req.cert_type == 'client'] + + @property + def new_application_requests(self): + """ + Filtered view of [new_requests][] that only includes application cert + requests. + + Each will be an instance of [ApplicationCertificateRequest][]. + + Example usage: + + ```python + @when('tls.application.certs.requested') + def gen_application_certs(): + tls = endpoint_from_flag('tls.application.certs.requested') + for request in tls.new_application_requests: + cert, key = generate_application_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + + :returns: List of certificate requests. + :rtype: [CertificateRequest, ] + """ + return [req for req in self.new_requests + if req.cert_type == 'application'] + + @property + def all_published_certs(self): + """ + List of all [Certificate][] instances that this provider has published + for all related applications. + """ + return [req.cert for req in self.all_requests if req.cert] diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/pydocmd.yml b/kubeapi-load-balancer/hooks/relations/tls-certificates/pydocmd.yml new file mode 100644 index 0000000..c568913 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/pydocmd.yml @@ -0,0 +1,19 @@ +site_name: 'TLS Certificates Interface' + +generate: + - requires.md: + - requires + - requires.TlsRequires+ + - provides.md: + - provides + - provides.TlsProvides+ + - common.md: + - tls_certificates_common.CertificateRequest+ + - tls_certificates_common.Certificate+ + +pages: + - Requires: requires.md + - Provides: provides.md + - Common: common.md + +gens_dir: docs diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/requires.py b/kubeapi-load-balancer/hooks/relations/tls-certificates/requires.py new file mode 100644 index 0000000..951f953 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/requires.py @@ -0,0 +1,342 @@ +if not __package__: + # fix relative imports when building docs + import sys + __package__ = sys.modules[''].__name__ + +import uuid + +from charmhelpers.core import hookenv + +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag +from charms.reactive import Endpoint +from charms.reactive import data_changed + +from .tls_certificates_common import Certificate + + +class TlsRequires(Endpoint): + """ + The client's side of the interface protocol. + + The following flags may be set: + + * `{endpoint_name}.available` + Whenever the relation is joined. + + * `{endpoint_name}.ca.available` + When the root CA information is available via the [root_ca_cert][] and + [root_ca_chain][] properties. + + * `{endpoint_name}.ca.changed` + When the root CA information has changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.certs.available` + When the requested server or client certs are available. + + * `{endpoint_name}.certs.changed` + When the requested server or client certs have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.server.certs.available` + When the server certificates requested by [request_server_cert][] are + available via the [server_certs][] collection. + + * `{endpoint_name}.server.certs.changed` + When the requested server certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.client.certs.available` + When the client certificates requested by [request_client_cert][] are + available via the [client_certs][] collection. + + * `{endpoint_name}.client.certs.changed` + When the requested client certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + The following flags have been deprecated: + + * `{endpoint_name}.server.cert.available` + * `{endpoint_name}.client.cert.available` + * `{endpoint_name}.batch.cert.available` + + [Certificate]: common.md#tls_certificates_common.Certificate + [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest + [root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert + [root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain + [request_server_cert]: requires.md#requires.TlsRequires.request_server_cert + [request_client_cert]: requires.md#requires.TlsRequires.request_client_cert + [server_certs]: requires.md#requires.TlsRequires.server_certs + [server_certs_map]: requires.md#requires.TlsRequires.server_certs_map + [client_certs]: requires.md#requires.TlsRequires.server_certs + """ + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + self.relations[0].to_publish_raw['unit_name'] = self._unit_name + prefix = self.expand_name('{endpoint_name}.') + ca_available = self.root_ca_cert + ca_changed = ca_available and data_changed(prefix + 'ca', + self.root_ca_cert) + server_available = self.server_certs + server_changed = server_available and data_changed(prefix + 'servers', + self.server_certs) + client_available = self.client_certs + client_changed = client_available and data_changed(prefix + 'clients', + self.client_certs) + certs_available = server_available or client_available + certs_changed = server_changed or client_changed + + set_flag(prefix + 'available') + toggle_flag(prefix + 'ca.available', ca_available) + toggle_flag(prefix + 'ca.changed', ca_changed) + toggle_flag(prefix + 'server.certs.available', server_available) + toggle_flag(prefix + 'server.certs.changed', server_changed) + toggle_flag(prefix + 'client.certs.available', client_available) + toggle_flag(prefix + 'client.certs.changed', client_changed) + toggle_flag(prefix + 'certs.available', certs_available) + toggle_flag(prefix + 'certs.changed', certs_changed) + # deprecated + toggle_flag(prefix + 'server.cert.available', self.server_certs) + toggle_flag(prefix + 'client.cert.available', self.get_client_cert()) + toggle_flag(prefix + 'batch.cert.available', self.server_certs) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + prefix = self.expand_name('{endpoint_name}.') + clear_flag(prefix + 'available') + clear_flag(prefix + 'ca.available') + clear_flag(prefix + 'ca.changed') + clear_flag(prefix + 'server.certs.available') + clear_flag(prefix + 'server.certs.changed') + clear_flag(prefix + 'client.certs.available') + clear_flag(prefix + 'client.certs.changed') + clear_flag(prefix + 'certs.available') + clear_flag(prefix + 'certs.changed') + # deprecated + clear_flag(prefix + 'server.cert.available') + clear_flag(prefix + 'client.cert.available') + clear_flag(prefix + 'batch.cert.available') + + @property + def _unit_name(self): + return hookenv.local_unit().replace('/', '_') + + @property + def root_ca_cert(self): + """ + Root CA certificate. + """ + # only the leader of the provider should set the CA, or all units + # had better agree + return self.all_joined_units.received_raw['ca'] + + def get_ca(self): + """ + Return the root CA certificate. + + Same as [root_ca_cert][]. + """ + return self.root_ca_cert + + @property + def root_ca_chain(self): + """ + The chain of trust for the root CA. + """ + # only the leader of the provider should set the CA, or all units + # had better agree + return self.all_joined_units.received_raw['chain'] + + def get_chain(self): + """ + Return the chain of trust for the root CA. + + Same as [root_ca_chain][]. + """ + return self.root_ca_chain + + def get_client_cert(self): + """ + Deprecated. Use [request_client_cert][] and the [client_certs][] + collection instead. + + Return a globally shared client certificate and key. + """ + data = self.all_joined_units.received_raw + return (data['client.cert'], data['client.key']) + + def get_server_cert(self): + """ + Deprecated. Use the [server_certs][] collection instead. + + Return the cert and key of the first server certificate requested. + """ + if not self.server_certs: + return (None, None) + cert = self.server_certs[0] + return (cert.cert, cert.key) + + @property + def server_certs(self): + """ + List of [Certificate][] instances for all available server certs. + """ + certs = [] + raw_data = self.all_joined_units.received_raw + json_data = self.all_joined_units.received + + # for backwards compatibility, the first cert goes in its own fields + if self.relations: + common_name = self.relations[0].to_publish_raw['common_name'] + cert = raw_data['{}.server.cert'.format(self._unit_name)] + key = raw_data['{}.server.key'.format(self._unit_name)] + if cert and key: + certs.append(Certificate('server', + common_name, + cert, + key)) + + # subsequent requests go in the collection + field = '{}.processed_requests'.format(self._unit_name) + certs_data = json_data[field] or {} + certs.extend(Certificate('server', + common_name, + cert['cert'], + cert['key']) + for common_name, cert in certs_data.items()) + return certs + + @property + def application_certs(self): + """ + List containg the application Certificate cert. + + :returns: A list containing one certificate + :rtype: [Certificate()] + """ + certs = [] + json_data = self.all_joined_units.received + field = '{}.processed_application_requests'.format(self._unit_name) + certs_data = json_data[field] or {} + app_cert_data = certs_data.get('app_data') + if app_cert_data: + certs = [Certificate( + 'server', + 'app_data', + app_cert_data['cert'], + app_cert_data['key'])] + return certs + + @property + def server_certs_map(self): + """ + Mapping of server [Certificate][] instances by their `common_name`. + """ + return {cert.common_name: cert for cert in self.server_certs} + + def get_batch_requests(self): + """ + Deprecated. Use [server_certs_map][] instead. + + Mapping of server [Certificate][] instances by their `common_name`. + """ + return self.server_certs_map + + @property + def client_certs(self): + """ + List of [Certificate][] instances for all available client certs. + """ + field = '{}.processed_client_requests'.format(self._unit_name) + certs_data = self.all_joined_units.received[field] or {} + return [Certificate('client', + common_name, + cert['cert'], + cert['key']) + for common_name, cert in certs_data.items()] + + @property + def client_certs_map(self): + """ + Mapping of client [Certificate][] instances by their `common_name`. + """ + return {cert.common_name: cert for cert in self.client_certs} + + def request_server_cert(self, cn, sans=None, cert_name=None): + """ + Request a server certificate and key be generated for the given + common name (`cn`) and optional list of alternative names (`sans`). + + The `cert_name` is deprecated and not needed. + + This can be called multiple times to request more than one server + certificate, although the common names must be unique. If called + again with the same common name, it will be ignored. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + to_publish_raw = self.relations[0].to_publish_raw + if to_publish_raw['common_name'] in (None, '', cn): + # for backwards compatibility, first request goes in its own fields + to_publish_raw['common_name'] = cn + to_publish_json['sans'] = sans or [] + cert_name = to_publish_raw.get('certificate_name') or cert_name + if cert_name is None: + cert_name = str(uuid.uuid4()) + to_publish_raw['certificate_name'] = cert_name + else: + # subsequent requests go in the collection + requests = to_publish_json.get('cert_requests', {}) + requests[cn] = {'sans': sans or []} + to_publish_json['cert_requests'] = requests + + def add_request_server_cert(self, cn, sans): + """ + Deprecated. Use [request_server_cert][] instead. + """ + self.request_server_cert(cn, sans) + + def request_server_certs(self): + """ + Deprecated. Just use [request_server_cert][]; this does nothing. + """ + pass + + def request_client_cert(self, cn, sans): + """ + Request a client certificate and key be generated for the given + common name (`cn`) and list of alternative names (`sans`). + + This can be called multiple times to request more than one client + certificate, although the common names must be unique. If called + again with the same common name, it will be ignored. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + requests = to_publish_json.get('client_cert_requests', {}) + requests[cn] = {'sans': sans} + to_publish_json['client_cert_requests'] = requests + + def request_application_cert(self, cn, sans): + """ + Request an application certificate and key be generated for the given + common name (`cn`) and list of alternative names (`sans` ) of this + unit and all peer units. All units will share a single certificates. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + requests = to_publish_json.get('application_cert_requests', {}) + requests[cn] = {'sans': sans} + to_publish_json['application_cert_requests'] = requests diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/tls_certificates_common.py b/kubeapi-load-balancer/hooks/relations/tls-certificates/tls_certificates_common.py new file mode 100644 index 0000000..99a2f8c --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/tls_certificates_common.py @@ -0,0 +1,302 @@ +from charms.reactive import clear_flag, is_data_changed, data_changed + + +class CertificateRequest(dict): + def __init__(self, unit, cert_type, cert_name, common_name, sans): + self._unit = unit + self._cert_type = cert_type + super().__init__({ + 'certificate_name': cert_name, + 'common_name': common_name, + 'sans': sans, + }) + + @property + def _key(self): + return '.'.join((self._unit.relation.relation_id, + self.unit_name, + self.common_name)) + + def resolve_unit_name(self, unit): + """Return name of unit associated with this request. + + unit_name should be provided in the relation data to ensure + compatability with cross-model relations. If the unit name + is absent then fall back to unit_name attribute of the + unit associated with this request. + + :param unit: Unit to extract name from + :type unit: charms.reactive.endpoints.RelatedUnit + :returns: Name of unit + :rtype: str + """ + unit_name = unit.received_raw['unit_name'] + if not unit_name: + unit_name = unit.unit_name + return unit_name + + @property + def unit_name(self): + """Name of this unit. + + :returns: Name of unit + :rtype: str + """ + return self.resolve_unit_name(unit=self._unit).replace('/', '_') + + @property + def application_name(self): + """Name of the application which the request came from. + + :returns: Name of application + :rtype: str + """ + return self.resolve_unit_name(unit=self._unit).split('/')[0] + + @property + def cert_type(self): + """ + Type of certificate, 'server' or 'client', being requested. + """ + return self._cert_type + + @property + def cert_name(self): + return self['certificate_name'] + + @property + def common_name(self): + return self['common_name'] + + @property + def sans(self): + return self['sans'] + + @property + def _publish_key(self): + if self.cert_type == 'server': + return '{}.processed_requests'.format(self.unit_name) + elif self.cert_type == 'client': + return '{}.processed_client_requests'.format(self.unit_name) + raise ValueError('Unknown cert_type: {}'.format(self.cert_type)) + + @property + def _server_cert_key(self): + return '{}.server.cert'.format(self.unit_name) + + @property + def _server_key_key(self): + return '{}.server.key'.format(self.unit_name) + + @property + def _is_top_level_server_cert(self): + return (self.cert_type == 'server' and + self.common_name == self._unit.received_raw['common_name']) + + @property + def cert(self): + """ + The cert published for this request, if any. + """ + cert, key = None, None + if self._is_top_level_server_cert: + tpr = self._unit.relation.to_publish_raw + cert = tpr[self._server_cert_key] + key = tpr[self._server_key_key] + else: + tp = self._unit.relation.to_publish + certs_data = tp.get(self._publish_key, {}) + cert_data = certs_data.get(self.common_name, {}) + cert = cert_data.get('cert') + key = cert_data.get('key') + if cert and key: + return Certificate(self.cert_type, self.common_name, cert, key) + return None + + @property + def is_handled(self): + has_cert = self.cert is not None + same_sans = not is_data_changed(self._key, + sorted(set(self.sans or []))) + return has_cert and same_sans + + def set_cert(self, cert, key): + rel = self._unit.relation + if self._is_top_level_server_cert: + # backwards compatibility; if this is the cert that was requested + # as a single server cert, set it in the response as the single + # server cert + rel.to_publish_raw.update({ + self._server_cert_key: cert, + self._server_key_key: key, + }) + else: + data = rel.to_publish.get(self._publish_key, {}) + data[self.common_name] = { + 'cert': cert, + 'key': key, + } + rel.to_publish[self._publish_key] = data + if not rel.endpoint.new_server_requests: + clear_flag(rel.endpoint.expand_name('{endpoint_name}.server' + '.cert.requested')) + if not rel.endpoint.new_requests: + clear_flag(rel.endpoint.expand_name('{endpoint_name}.' + 'certs.requested')) + data_changed(self._key, sorted(set(self.sans or []))) + + +class ApplicationCertificateRequest(CertificateRequest): + """ + A request for an application consistent certificate. + + This is a request for a certificate that works for all units of an + application. All sans and cns are added together to produce one + certificate and the same certificate and key are sent to all the + units of an application. Only one ApplicationCertificateRequest + is needed per application. + """ + + @property + def _key(self): + """Key to identify this cert. + + :returns: cert key + :rtype: str + """ + return '{}.{}'.format(self._unit.relation.relation_id, 'app_cert') + + @property + def cert(self): + """ + The cert published for this request, if any. + + :returns: Certificate + :rtype: Certificate or None + """ + cert, key = None, None + tp = self._unit.relation.to_publish + certs_data = tp.get(self._publish_key, {}) + cert_data = certs_data.get('app_data', {}) + cert = cert_data.get('cert') + key = cert_data.get('key') + if cert and key: + return Certificate(self.cert_type, self.common_name, cert, key) + return None + + @property + def is_handled(self): + """Whether the certificate has been handled. + + :returns: If the cert has been handled + :rtype: bool + """ + has_cert = self.cert is not None + same_sans = not is_data_changed(self._key, + sorted(set(self.sans or []))) + return has_cert and same_sans + + @property + def sans(self): + """Generate a list of all sans from all units of application + + Examine all units of the application and compile a list of + all sans. CNs are treated as addition san entries. + + :returns: List of sans + :rtype: List[str] + """ + _sans = [] + for unit in self._unit.relation.units: + reqs = unit.received['application_cert_requests'] or {} + for cn, req in reqs.items(): + _sans.append(cn) + _sans.extend(req['sans']) + return sorted(list(set(_sans))) + + @property + def _request_key(self): + """Key used to request cert + + :returns: Key used to request cert + :rtype: str + """ + return 'application_cert_requests' + + def derive_publish_key(self, unit=None): + """Derive the application cert publish key for a unit. + + :param unit: Unit to extract name from + :type unit: charms.reactive.endpoints.RelatedUnit + :returns: publish key + :rtype: str + """ + if not unit: + unit = self._unit + unit_name = self.resolve_unit_name(unit).replace('/', '_') + return '{}.processed_application_requests'.format(unit_name) + + @property + def _publish_key(self): + """Key used to publish cert + + :returns: Key used to publish cert + :rtype: str + """ + return self.derive_publish_key(unit=self._unit) + + def set_cert(self, cert, key): + """Send the cert and key to all units of the application + + :param cert: TLS Certificate + :type cert: str + :param key: TLS Private Key + :type cert: str + """ + rel = self._unit.relation + for unit in self._unit.relation.units: + pub_key = self.derive_publish_key(unit=unit) + data = rel.to_publish.get( + pub_key, + {}) + data['app_data'] = { + 'cert': cert, + 'key': key, + } + rel.to_publish[pub_key] = data + if not rel.endpoint.new_application_requests: + clear_flag(rel.endpoint.expand_name( + '{endpoint_name}.application.certs.requested')) + data_changed(self._key, sorted(set(self.sans or []))) + + +class Certificate(dict): + """ + Represents a created certificate and key. + + The ``cert_type``, ``common_name``, ``cert``, and ``key`` values can + be accessed either as properties or as the contents of the dict. + """ + def __init__(self, cert_type, common_name, cert, key): + super().__init__({ + 'cert_type': cert_type, + 'common_name': common_name, + 'cert': cert, + 'key': key, + }) + + @property + def cert_type(self): + return self['cert_type'] + + @property + def common_name(self): + return self['common_name'] + + @property + def cert(self): + return self['cert'] + + @property + def key(self): + return self['key'] diff --git a/kubeapi-load-balancer/hooks/relations/tls-certificates/tox.ini b/kubeapi-load-balancer/hooks/relations/tls-certificates/tox.ini new file mode 100644 index 0000000..90de9d3 --- /dev/null +++ b/kubeapi-load-balancer/hooks/relations/tls-certificates/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = py3 +skipsdist = true + +[testenv] +basepython=python3 +envdir={toxworkdir}/py3 +deps= + pytest + charms.reactive + pydoc-markdown + +[testenv:docs] +commands=python make_docs + +[flake8] +ignore=E402 diff --git a/kubeapi-load-balancer/hooks/start b/kubeapi-load-balancer/hooks/start new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/start @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/stop b/kubeapi-load-balancer/hooks/stop new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/stop @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/update-status b/kubeapi-load-balancer/hooks/update-status new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/update-status @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/upgrade-charm b/kubeapi-load-balancer/hooks/upgrade-charm new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/upgrade-charm @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/website-relation-broken b/kubeapi-load-balancer/hooks/website-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/website-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/website-relation-changed b/kubeapi-load-balancer/hooks/website-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/website-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/website-relation-created b/kubeapi-load-balancer/hooks/website-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/website-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/website-relation-departed b/kubeapi-load-balancer/hooks/website-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/website-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/hooks/website-relation-joined b/kubeapi-load-balancer/hooks/website-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubeapi-load-balancer/hooks/website-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubeapi-load-balancer/icon.svg b/kubeapi-load-balancer/icon.svg new file mode 100644 index 0000000..7f2998e --- /dev/null +++ b/kubeapi-load-balancer/icon.svg @@ -0,0 +1,412 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kubeapi-load-balancer/layer.yaml b/kubeapi-load-balancer/layer.yaml new file mode 100644 index 0000000..0f3a5b3 --- /dev/null +++ b/kubeapi-load-balancer/layer.yaml @@ -0,0 +1,49 @@ +"includes": +- "layer:options" +- "layer:basic" +- "interface:nrpe-external-master" +- "layer:status" +- "layer:apt" +- "interface:http" +- "layer:debug" +- "interface:tls-certificates" +- "interface:hacluster" +- "layer:kubernetes-common" +- "layer:metrics" +- "layer:nagios" +- "layer:nginx" +- "layer:tls-client" +- "layer:hacluster" +- "interface:public-address" +- "layer:leadership" +"exclude": [".travis.yml", "tests", "tox.ini", "test-requirements.txt", "unit_tests"] +"options": + "apt": + "packages": + - "nginx-full" + "version_package": "" + "full_version": !!bool "false" + "keys": [] + "tls-client": + "ca_certificate_path": "/srv/kubernetes/ca.crt" + "server_certificate_path": "" + "server_key_path": "" + "client_certificate_path": "" + "client_key_path": "" + "hacluster": + "binding_address": "website" + "basic": + "packages": [] + "python_packages": [] + "use_venv": !!bool "true" + "include_system_packages": !!bool "false" + "nagios": {} + "status": + "patch-hookenv": !!bool "true" + "nginx": {} + "debug": {} + "kubernetes-common": {} + "leadership": {} + "kubeapi-load-balancer": {} +"repo": "https://github.com/kubernetes/kubernetes.git" +"is": "kubeapi-load-balancer" diff --git a/kubeapi-load-balancer/lib/.gitkeep b/kubeapi-load-balancer/lib/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/kubeapi-load-balancer/lib/charms/apt.py b/kubeapi-load-balancer/lib/charms/apt.py new file mode 100644 index 0000000..14508c4 --- /dev/null +++ b/kubeapi-load-balancer/lib/charms/apt.py @@ -0,0 +1,209 @@ +# Copyright 2015-2020 Canonical Ltd. +# +# This file is part of the Apt layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +charms.reactive helpers for dealing with deb packages. + +Add apt package sources using add_source(). Queue deb packages for +installation with install(). Configure and work with your software +once the apt.installed.{packagename} flag is set. +''' +import itertools +import re +import subprocess + +from charmhelpers import fetch +from charmhelpers.core import hookenv, unitdata +from charms import layer, reactive +from charms.layer import status +from charms.reactive import flags + + +__all__ = ['add_source', 'update', 'queue_install', 'install_queued', 'installed', 'purge', 'ensure_package_status'] + + +def add_source(source, key=None): + '''Add an apt source. + + Sets the apt.needs_update flag. + + A source may be either a line that can be added directly to + sources.list(5), or in the form ppa:/ for adding + Personal Package Archives, or a distribution component to enable. + + The package signing key should be an ASCII armoured GPG key. While + GPG key ids are also supported, the retrieval mechanism is insecure. + There is no need to specify the package signing key for PPAs or for + the main Ubuntu archives. + ''' + # Maybe we should remember which sources have been added already + # so we don't waste time re-adding them. Is this time significant? + fetch.add_source(source, key) + reactive.set_flag('apt.needs_update') + + +def queue_install(packages, options=None): + """Queue one or more deb packages for install. + + The `apt.installed.{name}` flag is set once the package is installed. + + If a package has already been installed it will not be reinstalled. + + If a package has already been queued it will not be requeued, and + the install options will not be changed. + + Sets the apt.queued_installs flag. + """ + if isinstance(packages, str): + packages = [packages] + # Filter installed packages. + store = unitdata.kv() + queued_packages = store.getrange('apt.install_queue.', strip=True) + packages = { + package: options + for package in packages + if not (package in queued_packages or reactive.is_flag_set('apt.installed.' + package)) + } + if packages: + unitdata.kv().update(packages, prefix='apt.install_queue.') + reactive.set_flag('apt.queued_installs') + + +def installed(): + '''Return the set of deb packages completed install''' + return set(flag.split('.', 2)[2] for flag in flags.get_flags() if flag.startswith('apt.installed.')) + + +def purge(packages): + """Purge one or more deb packages from the system""" + fetch.apt_purge(packages, fatal=True) + store = unitdata.kv() + store.unsetrange(packages, prefix='apt.install_queue.') + for package in packages: + reactive.clear_flag('apt.installed.{}'.format(package)) + + +def update(): + """Update the apt cache. + + Removes the apt.needs_update flag. + """ + status.maintenance('Updating apt cache') + fetch.apt_update(fatal=True) # Friends don't let friends set fatal=False + reactive.clear_flag('apt.needs_update') + + +def install_queued(): + '''Installs queued deb packages. + + Removes the apt.queued_installs flag and sets the apt.installed flag. + + On failure, sets the unit's workload status to 'blocked' and returns + False. Package installs remain queued. + + On success, sets the apt.installed.{packagename} flag for each + installed package and returns True. + ''' + store = unitdata.kv() + queue = sorted((options, package) for package, options in store.getrange('apt.install_queue.', strip=True).items()) + + installed = set() + for options, batch in itertools.groupby(queue, lambda x: x[0]): + packages = [b[1] for b in batch] + try: + status.maintenance('Installing {}'.format(','.join(packages))) + fetch.apt_install(packages, options, fatal=True) + store.unsetrange(packages, prefix='apt.install_queue.') + installed.update(packages) + except subprocess.CalledProcessError: + status.blocked('Unable to install packages {}'.format(','.join(packages))) + return False # Without setting reactive flag. + + for package in installed: + reactive.set_flag('apt.installed.{}'.format(package)) + reactive.clear_flag('apt.queued_installs') + + reset_application_version() + + return True + + +def get_package_version(package, full_version=False): + '''Return the version of an installed package. + + If `full_version` is True, returns the full Debian package version. + Otherwise, returns the shorter 'upstream' version number. + ''' + # Don't use fetch.get_upstream_version, as it depends on python-apt + # and not available if the basic layer's use_site_packages option is off. + cmd = ['dpkg-query', '--show', r'--showformat=${Version}\n', package] + full = subprocess.check_output(cmd, universal_newlines=True).strip() + if not full_version: + # Attempt to strip off Debian style metadata from the end of the + # version number. + m = re.search(r'^([\d.a-z]+)', full, re.I) + if m is not None: + return m.group(1) + return full + + +def reset_application_version(): + '''Set the Juju application version, per settings in layer.yaml''' + # Reset the application version. We call this after installing + # packages to initialize the version. We also call this every + # hook, incase the version has changed (eg. Landscape upgraded + # the package). + opts = layer.options().get('apt', {}) + pkg = opts.get('version_package') + if pkg and pkg in installed(): + ver = get_package_version(pkg, opts.get('full_version', False)) + hookenv.application_version_set(ver) + + +def ensure_package_status(): + '''Hold or unhold packages per the package_status configuration option. + + All packages installed using this module and handlers are affected. + + An mechanism may be added in the future to override this for a + subset of installed packages. + ''' + packages = installed() + if not packages: + return + config = hookenv.config() + package_status = config.get('package_status') or '' + changed = reactive.data_changed('apt.package_status', (package_status, sorted(packages))) + if changed: + if package_status == 'hold': + hookenv.log('Holding packages {}'.format(','.join(packages))) + fetch.apt_hold(packages) + else: + hookenv.log('Unholding packages {}'.format(','.join(packages))) + fetch.apt_unhold(packages) + reactive.clear_flag('apt.needs_hold') + + +def status_set(state, message): + '''DEPRECATED, set the unit's workload status. + + Set state == None to keep the same state and just change the message. + ''' + if state is None: + state = hookenv.status_get()[0] + if state not in ('active', 'waiting', 'blocked'): + state = 'maintenance' # Guess + status.status_set(state, message) diff --git a/kubeapi-load-balancer/lib/charms/layer/__init__.py b/kubeapi-load-balancer/lib/charms/layer/__init__.py new file mode 100644 index 0000000..a8e0c64 --- /dev/null +++ b/kubeapi-load-balancer/lib/charms/layer/__init__.py @@ -0,0 +1,60 @@ +import sys +from importlib import import_module +from pathlib import Path + + +def import_layer_libs(): + """ + Ensure that all layer libraries are imported. + + This makes it possible to do the following: + + from charms import layer + + layer.foo.do_foo_thing() + + Note: This function must be called after bootstrap. + """ + for module_file in Path('lib/charms/layer').glob('*'): + module_name = module_file.stem + if module_name in ('__init__', 'basic', 'execd') or not ( + module_file.suffix == '.py' or module_file.is_dir() + ): + continue + import_module('charms.layer.{}'.format(module_name)) + + +# Terrible hack to support the old terrible interface. +# Try to get people to call layer.options.get() instead so +# that we can remove this garbage. +# Cribbed from https://stackoverfLow.com/a/48100440/4941864 +class OptionsBackwardsCompatibilityHack(sys.modules[__name__].__class__): + def __call__(self, section=None, layer_file=None): + if layer_file is None: + return self.get(section=section) + else: + return self.get(section=section, + layer_file=Path(layer_file)) + + +def patch_options_interface(): + from charms.layer import options + if sys.version_info.minor >= 5: + options.__class__ = OptionsBackwardsCompatibilityHack + else: + # Py 3.4 doesn't support changing the __class__, so we have to do it + # another way. The last line is needed because we already have a + # reference that doesn't get updated with sys.modules. + name = options.__name__ + hack = OptionsBackwardsCompatibilityHack(name) + hack.get = options.get + sys.modules[name] = hack + sys.modules[__name__].options = hack + + +try: + patch_options_interface() +except ImportError: + # This may fail if pyyaml hasn't been installed yet. But in that + # case, the bootstrap logic will try it again once it has. + pass diff --git a/kubeapi-load-balancer/lib/charms/layer/basic.py b/kubeapi-load-balancer/lib/charms/layer/basic.py new file mode 100644 index 0000000..bbdd074 --- /dev/null +++ b/kubeapi-load-balancer/lib/charms/layer/basic.py @@ -0,0 +1,501 @@ +import os +import sys +import re +import shutil +from distutils.version import LooseVersion +from pkg_resources import Requirement +from glob import glob +from subprocess import check_call, check_output, CalledProcessError +from time import sleep + +from charms import layer +from charms.layer.execd import execd_preinstall + + +def _get_subprocess_env(): + env = os.environ.copy() + env['LANG'] = env.get('LANG', 'C.UTF-8') + return env + + +def get_series(): + """ + Return series for a few known OS:es. + Tested as of 2019 november: + * centos6, centos7, rhel6. + * bionic + """ + series = "" + + # Looking for content in /etc/os-release + # works for ubuntu + some centos + if os.path.isfile('/etc/os-release'): + d = {} + with open('/etc/os-release', 'r') as rel: + for l in rel: + if not re.match(r'^\s*$', l): + k, v = l.split('=') + d[k.strip()] = v.strip().replace('"', '') + series = "{ID}{VERSION_ID}".format(**d) + + # Looking for content in /etc/redhat-release + # works for redhat enterprise systems + elif os.path.isfile('/etc/redhat-release'): + with open('/etc/redhat-release', 'r') as redhatlsb: + # CentOS Linux release 7.7.1908 (Core) + line = redhatlsb.readline() + release = int(line.split("release")[1].split()[0][0]) + series = "centos" + str(release) + + # Looking for content in /etc/lsb-release + # works for ubuntu + elif os.path.isfile('/etc/lsb-release'): + d = {} + with open('/etc/lsb-release', 'r') as lsb: + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + series = d['DISTRIB_CODENAME'] + + # This is what happens if we cant figure out the OS. + else: + series = "unknown" + return series + + +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 + + series = get_series() + + # OMG?! is build-essentials needed? + ubuntu_packages = ['python3-pip', + 'python3-setuptools', + 'python3-yaml', + 'python3-dev', + 'python3-wheel', + 'build-essential'] + + # I'm not going to "yum group info "Development Tools" + # omitting above madness + centos_packages = ['python3-pip', + 'python3-setuptools', + 'python3-devel', + 'python3-wheel'] + + packages_needed = [] + if 'centos' in series: + packages_needed = centos_packages + else: + packages_needed = ubuntu_packages + + 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') + hook_name = os.path.basename(sys.argv[0]) + is_bootstrapped = os.path.exists('wheelhouse/.bootstrapped') + is_charm_upgrade = hook_name == 'upgrade-charm' + is_series_upgrade = hook_name == 'post-series-upgrade' + is_post_upgrade = os.path.exists('wheelhouse/.upgraded') + is_upgrade = (not is_post_upgrade and + (is_charm_upgrade or is_series_upgrade)) + if is_bootstrapped and not is_upgrade: + # older subordinates might have downgraded charm-env, so we should + # restore it if necessary + install_or_update_charm_env() + activate_venv() + # the .upgrade file prevents us from getting stuck in a loop + # when re-execing to activate the venv; at this point, we've + # activated the venv, so it's safe to clear it + if is_post_upgrade: + os.unlink('wheelhouse/.upgraded') + return + if os.path.exists(venv): + try: + # focal installs or upgrades prior to PR 160 could leave the venv + # in a broken state which would prevent subsequent charm upgrades + _load_installed_versions(vpip) + except CalledProcessError: + is_broken_venv = True + else: + is_broken_venv = False + if is_upgrade or is_broken_venv: + # All upgrades should do a full clear of the venv, rather than + # just updating it, to bring in updates to Python itself + shutil.rmtree(venv) + if is_upgrade: + if os.path.exists('wheelhouse/.bootstrapped'): + os.unlink('wheelhouse/.bootstrapped') + # bootstrap wheelhouse + if os.path.exists('wheelhouse'): + pre_eoan = series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty', + 'ubuntu16.04', 'xenial', + 'ubuntu18.04', 'bionic') + pydistutils_lines = [ + "[easy_install]\n", + "find_links = file://{}/wheelhouse/\n".format(charm_dir), + "no_index=True\n", + "index_url=\n", # deliberately nothing here; disables it. + ] + if pre_eoan: + pydistutils_lines.append("allow_hosts = ''\n") + 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(pydistutils_lines) + if 'centos' in series: + yum_install(packages_needed) + else: + apt_install(packages_needed) + from charms.layer import options + cfg = options.get('basic') + # include packages defined in layer.yaml + if 'centos' in series: + yum_install(cfg.get('packages', [])) + else: + 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 = get_series() + if series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty'): + apt_install(['python-virtualenv']) + elif 'centos' in series: + yum_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, env=_get_subprocess_env()) + 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') + pre_install_pkgs = ['pip', 'setuptools', 'setuptools-scm'] + # we bundle these packages to work around bugs in older versions (such + # as https://github.com/pypa/pip/issues/56), but if the system already + # provided a newer version, downgrading it can cause other problems + _update_if_newer(pip, pre_install_pkgs) + # install the rest of the wheelhouse deps (extract the pkg names into + # a set so that we can ignore the pre-install packages and let pip + # choose the best version in case there are multiple from layer + # conflicts) + _versions = _load_wheelhouse_versions() + _pkgs = _versions.keys() - set(pre_install_pkgs) + # add back the versions such that each package in pkgs is + # ==. + # This ensures that pip 20.3.4+ will install the packages from the + # wheelhouse without (erroneously) flagging an error. + pkgs = _add_back_versions(_pkgs, _versions) + reinstall_flag = '--force-reinstall' + if not cfg.get('use_venv', True) and pre_eoan: + reinstall_flag = '--ignore-installed' + check_call([pip, 'install', '-U', reinstall_flag, '--no-index', + '--no-cache-dir', '-f', 'wheelhouse'] + list(pkgs), + env=_get_subprocess_env()) + # re-enable installation from pypi + os.remove('/root/.pydistutils.cfg') + + # install pyyaml for centos7, since, unlike the ubuntu image, the + # default image for centos doesn't include pyyaml; see the discussion: + # https://discourse.jujucharms.com/t/charms-for-centos-lets-begin + if 'centos' in series: + check_call([pip, 'install', '-U', 'pyyaml'], + env=_get_subprocess_env()) + + # install python packages from layer options + if cfg.get('python_packages'): + check_call([pip, 'install', '-U'] + cfg.get('python_packages'), + env=_get_subprocess_env()) + 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') + # setup wrappers to ensure envs are used for scripts + install_or_update_charm_env() + for wrapper in ('charms.reactive', 'charms.reactive.sh', + 'chlp', 'layer_option'): + src = os.path.join('/usr/local/sbin', 'charm-env') + dst = os.path.join('/usr/local/sbin', wrapper) + if not os.path.exists(dst): + os.symlink(src, dst) + if cfg.get('use_venv'): + shutil.copy2('bin/layer_option', vbin) + else: + shutil.copy2('bin/layer_option', '/usr/local/bin/') + # re-link the charm copy to the wrapper in case charms + # call bin/layer_option directly (as was the old pattern) + os.remove('bin/layer_option') + os.symlink('/usr/local/sbin/layer_option', 'bin/layer_option') + # flag us as having already bootstrapped so we don't do it again + open('wheelhouse/.bootstrapped', 'w').close() + if is_upgrade: + # flag us as having already upgraded so we don't do it again + open('wheelhouse/.upgraded', '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 _load_installed_versions(pip): + pip_freeze = check_output([pip, 'freeze']).decode('utf8') + versions = {} + for pkg_ver in pip_freeze.splitlines(): + try: + req = Requirement.parse(pkg_ver) + except ValueError: + continue + versions.update({ + req.project_name: LooseVersion(ver) + for op, ver in req.specs if op == '==' + }) + return versions + + +def _load_wheelhouse_versions(): + versions = {} + for wheel in glob('wheelhouse/*'): + pkg, ver = os.path.basename(wheel).rsplit('-', 1) + # nb: LooseVersion ignores the file extension + versions[pkg.replace('_', '-')] = LooseVersion(ver) + return versions + + +def _add_back_versions(pkgs, versions): + """Add back the version strings to each of the packages. + + The versions are LooseVersion() from _load_wheelhouse_versions(). This + function strips the ".zip" or ".tar.gz" from the end of the version string + and adds it back to the package in the form of == + + If a package name is not a key in the versions dictionary, then it is + returned in the list unchanged. + + :param pkgs: A list of package names + :type pkgs: List[str] + :param versions: A map of package to LooseVersion + :type versions: Dict[str, LooseVersion] + :returns: A list of (maybe) versioned packages + :rtype: List[str] + """ + def _strip_ext(s): + """Strip an extension (if it exists) from the string + + :param s: the string to strip an extension off if it exists + :type s: str + :returns: string without an extension of .zip or .tar.gz + :rtype: str + """ + for ending in [".zip", ".tar.gz"]: + if s.endswith(ending): + return s[:-len(ending)] + return s + + def _maybe_add_version(pkg): + """Maybe add back the version number to a package if it exists. + + Adds the version number, if the package exists in the lexically + captured `versions` dictionary, in the form ==. Strips + the extension if it exists. + + :param pkg: the package name to (maybe) add the version number to. + :type pkg: str + """ + try: + return "{}=={}".format(pkg, _strip_ext(str(versions[pkg]))) + except KeyError: + pass + return pkg + + return [_maybe_add_version(pkg) for pkg in pkgs] + + +def _update_if_newer(pip, pkgs): + installed = _load_installed_versions(pip) + wheelhouse = _load_wheelhouse_versions() + for pkg in pkgs: + if pkg not in installed or wheelhouse[pkg] > installed[pkg]: + check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', + pkg], env=_get_subprocess_env()) + + +def install_or_update_charm_env(): + # On Trusty python3-pkg-resources is not installed + try: + from pkg_resources import parse_version + except ImportError: + apt_install(['python3-pkg-resources']) + from pkg_resources import parse_version + + try: + installed_version = parse_version( + check_output(['/usr/local/sbin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + installed_version = parse_version('0.0.0') + try: + bundled_version = parse_version( + check_output(['bin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + bundled_version = parse_version('0.0.0') + if installed_version < bundled_version: + shutil.copy2('bin/charm-env', '/usr/local/sbin/') + + +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. + """ + from charms.layer import options + venv = os.path.abspath('../.venv') + vbin = os.path.join(venv, 'bin') + vpy = os.path.join(vbin, 'python') + use_venv = options.get('basic', 'use_venv') + if use_venv and '.venv' not in sys.executable: + # activate the venv + os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) + reload_interpreter(vpy) + layer.patch_options_interface() + layer.import_layer_libs() + + +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 = _get_subprocess_env() + + 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 + try: + # sometimes apt-get update needs to be run + check_call(['apt-get', 'update'], env=env) + except CalledProcessError: + # sometimes it's a dpkg lock issue + pass + sleep(5) + else: + break + + +def yum_install(packages): + """ Installs packages with yum. + This function largely mimics the apt_install function for consistency. + """ + if packages: + env = os.environ.copy() + cmd = ['yum', '-y', 'install'] + for attempt in range(3): + try: + check_call(cmd + packages, env=env) + except CalledProcessError: + if attempt == 2: + raise + try: + check_call(['yum', 'update'], env=env) + except CalledProcessError: + pass + sleep(5) + else: + break + else: + pass + + +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]) + + +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/kubeapi-load-balancer/lib/charms/layer/execd.py b/kubeapi-load-balancer/lib/charms/layer/execd.py new file mode 100644 index 0000000..438d9a1 --- /dev/null +++ b/kubeapi-load-balancer/lib/charms/layer/execd.py @@ -0,0 +1,114 @@ +# 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 . + +# This module may only import from the Python standard library. +import os +import sys +import subprocess +import time + +''' +execd/preinstall + +Read the layer-basic docs for more info on how to use this feature. +https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#exec-d-support +''' + + +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<>> `get_version('kubelet') + (1, 6, 0) + + """ + cmd = "{} --version".format(bin_name).split() + version_string = subprocess.check_output(cmd).decode("utf-8") + return tuple(int(q) for q in re.findall("[0-9]+", version_string)[:3]) + + +def retry(times, delay_secs): + """Decorator for retrying a method call. + + Args: + times: How many times should we retry before giving up + delay_secs: Delay in secs + + Returns: A callable that would return the last call outcome + """ + + def retry_decorator(func): + """Decorator to wrap the function provided. + + Args: + func: Provided function should return either True od False + + Returns: A callable that would return the last call outcome + + """ + + def _wrapped(*args, **kwargs): + res = func(*args, **kwargs) + attempt = 0 + while not res and attempt < times: + sleep(delay_secs) + res = func(*args, **kwargs) + if res: + break + attempt += 1 + return res + + return _wrapped + + return retry_decorator + + +def calculate_resource_checksum(resource): + """Calculate a checksum for a resource""" + md5 = hashlib.md5() + path = hookenv.resource_get(resource) + if path: + with open(path, "rb") as f: + data = f.read() + md5.update(data) + return md5.hexdigest() + + +def get_resource_checksum_db_key(checksum_prefix, resource): + """Convert a resource name to a resource checksum database key.""" + return checksum_prefix + resource + + +def migrate_resource_checksums(checksum_prefix, snap_resources): + """Migrate resource checksums from the old schema to the new one""" + for resource in snap_resources: + new_key = get_resource_checksum_db_key(checksum_prefix, resource) + if not db.get(new_key): + path = hookenv.resource_get(resource) + if path: + # old key from charms.reactive.helpers.any_file_changed + old_key = "reactive.files_changed." + path + old_checksum = db.get(old_key) + db.set(new_key, old_checksum) + else: + # No resource is attached. Previously, this meant no checksum + # would be calculated and stored. But now we calculate it as if + # it is a 0-byte resource, so let's go ahead and do that. + zero_checksum = hashlib.md5().hexdigest() + db.set(new_key, zero_checksum) + + +def check_resources_for_upgrade_needed(checksum_prefix, snap_resources): + hookenv.status_set("maintenance", "Checking resources") + for resource in snap_resources: + key = get_resource_checksum_db_key(checksum_prefix, resource) + old_checksum = db.get(key) + new_checksum = calculate_resource_checksum(resource) + if new_checksum != old_checksum: + return True + return False + + +def calculate_and_store_resource_checksums(checksum_prefix, snap_resources): + for resource in snap_resources: + key = get_resource_checksum_db_key(checksum_prefix, resource) + checksum = calculate_resource_checksum(resource) + db.set(key, checksum) + + +def get_ingress_address(endpoint_name, ignore_addresses=None): + try: + network_info = hookenv.network_get(endpoint_name) + except NotImplementedError: + network_info = {} + + if not network_info or "ingress-addresses" not in network_info: + # if they don't have ingress-addresses they are running a juju that + # doesn't support spaces, so just return the private address + return hookenv.unit_get("private-address") + + addresses = network_info["ingress-addresses"] + + if ignore_addresses: + hookenv.log("ingress-addresses before filtering: {}".format(addresses)) + iter_filter = filter(lambda item: item not in ignore_addresses, addresses) + addresses = list(iter_filter) + hookenv.log("ingress-addresses after filtering: {}".format(addresses)) + + # Need to prefer non-fan IP addresses due to various issues, e.g. + # https://bugs.launchpad.net/charm-gcp-integrator/+bug/1822997 + # Fan typically likes to use IPs in the 240.0.0.0/4 block, so we'll + # prioritize those last. Not technically correct, but good enough. + try: + sort_key = lambda a: int(a.partition(".")[0]) >= 240 # noqa: E731 + addresses = sorted(addresses, key=sort_key) + except Exception: + hookenv.log(traceback.format_exc()) + + return addresses[0] + + +def get_ingress_address6(endpoint_name): + try: + network_info = hookenv.network_get(endpoint_name) + except NotImplementedError: + network_info = {} + + if not network_info or "ingress-addresses" not in network_info: + return None + + addresses = network_info["ingress-addresses"] + + for addr in addresses: + ip_addr = ipaddress.ip_interface(addr).ip + if ip_addr.version == 6: + return str(ip_addr) + else: + return None + + +def service_restart(service_name): + hookenv.status_set("maintenance", "Restarting {0} service".format(service_name)) + host.service_restart(service_name) + + +def service_start(service_name): + hookenv.log("Starting {0} service.".format(service_name)) + host.service_stop(service_name) + + +def service_stop(service_name): + hookenv.log("Stopping {0} service.".format(service_name)) + host.service_stop(service_name) + + +def arch(): + """Return the package architecture as a string. Raise an exception if the + architecture is not supported by kubernetes.""" + # Get the package architecture for this system. + architecture = check_output(["dpkg", "--print-architecture"]).rstrip() + # Convert the binary result into a string. + architecture = architecture.decode("utf-8") + return architecture + + +def get_service_ip(service, namespace="kube-system", errors_fatal=True): + try: + output = kubectl( + "get", "service", "--namespace", namespace, service, "--output", "json" + ) + except CalledProcessError: + if errors_fatal: + raise + else: + return None + else: + svc = json.loads(output.decode()) + return svc["spec"]["clusterIP"] + + +def kubectl(*args): + """Run a kubectl cli command with a config file. Returns stdout and throws + an error if the command fails.""" + command = ["kubectl", "--kubeconfig=" + kubeclientconfig_path] + list(args) + hookenv.log("Executing {}".format(command)) + return check_output(command) + + +def kubectl_success(*args): + """Runs kubectl with the given args. Returns True if successful, False if + not.""" + try: + kubectl(*args) + return True + except CalledProcessError: + return False + + +def kubectl_manifest(operation, manifest): + """Wrap the kubectl creation command when using filepath resources + :param operation - one of get, create, delete, replace + :param manifest - filepath to the manifest + """ + # Deletions are a special case + if operation == "delete": + # Ensure we immediately remove requested resources with --now + return kubectl_success(operation, "-f", manifest, "--now") + else: + # Guard against an error re-creating the same manifest multiple times + if operation == "create": + # If we already have the definition, its probably safe to assume + # creation was true. + if kubectl_success("get", "-f", manifest): + hookenv.log("Skipping definition for {}".format(manifest)) + return True + # Execute the requested command that did not match any of the special + # cases above + return kubectl_success(operation, "-f", manifest) + + +def get_node_name(): + kubelet_extra_args = parse_extra_args("kubelet-extra-args") + cloud_provider = kubelet_extra_args.get("cloud-provider", "") + if is_state("endpoint.aws.ready"): + cloud_provider = "aws" + elif is_state("endpoint.gcp.ready"): + cloud_provider = "gce" + elif is_state("endpoint.openstack.ready"): + cloud_provider = "openstack" + elif is_state("endpoint.vsphere.ready"): + cloud_provider = "vsphere" + elif is_state("endpoint.azure.ready"): + cloud_provider = "azure" + if cloud_provider == "aws": + return getfqdn().lower() + else: + return gethostname().lower() + + +def create_kubeconfig( + kubeconfig, + server, + ca, + key=None, + certificate=None, + user="ubuntu", + context="juju-context", + cluster="juju-cluster", + password=None, + token=None, + keystone=False, + aws_iam_cluster_id=None, +): + """Create a configuration for Kubernetes based on path using the supplied + arguments for values of the Kubernetes server, CA, key, certificate, user + context and cluster.""" + if not key and not certificate and not password and not token: + raise ValueError("Missing authentication mechanism.") + elif key and not certificate: + raise ValueError("Missing certificate.") + elif not key and certificate: + raise ValueError("Missing key.") + elif token and password: + # token and password are mutually exclusive. Error early if both are + # present. The developer has requested an impossible situation. + # see: kubectl config set-credentials --help + raise ValueError("Token and Password are mutually exclusive.") + + old_kubeconfig = Path(kubeconfig) + new_kubeconfig = Path(str(kubeconfig) + ".new") + + # Create the config file with the address of the master server. + cmd = ( + "kubectl config --kubeconfig={0} set-cluster {1} " + "--server={2} --certificate-authority={3} --embed-certs=true" + ) + check_call(split(cmd.format(new_kubeconfig, cluster, server, ca))) + # Delete old users + cmd = "kubectl config --kubeconfig={0} unset users" + check_call(split(cmd.format(new_kubeconfig))) + # Create the credentials using the client flags. + cmd = "kubectl config --kubeconfig={0} " "set-credentials {1} ".format( + new_kubeconfig, user + ) + + if key and certificate: + cmd = ( + "{0} --client-key={1} --client-certificate={2} " + "--embed-certs=true".format(cmd, key, certificate) + ) + if password: + cmd = "{0} --username={1} --password={2}".format(cmd, user, password) + # This is mutually exclusive from password. They will not work together. + if token: + cmd = "{0} --token={1}".format(cmd, token) + check_call(split(cmd)) + # Create a default context with the cluster. + cmd = "kubectl config --kubeconfig={0} set-context {1} " "--cluster={2} --user={3}" + check_call(split(cmd.format(new_kubeconfig, context, cluster, user))) + # Make the config use this new context. + cmd = "kubectl config --kubeconfig={0} use-context {1}" + check_call(split(cmd.format(new_kubeconfig, context))) + if keystone: + # create keystone user + cmd = "kubectl config --kubeconfig={0} " "set-credentials keystone-user".format( + new_kubeconfig + ) + check_call(split(cmd)) + # create keystone context + cmd = ( + "kubectl config --kubeconfig={0} " + "set-context --cluster={1} " + "--user=keystone-user keystone".format(new_kubeconfig, cluster) + ) + check_call(split(cmd)) + # use keystone context + cmd = "kubectl config --kubeconfig={0} " "use-context keystone".format( + new_kubeconfig + ) + check_call(split(cmd)) + # manually add exec command until kubectl can do it for us + with open(new_kubeconfig, "r") as f: + content = f.read() + content = content.replace( + """- name: keystone-user + user: {}""", + """- name: keystone-user + user: + exec: + command: "/snap/bin/client-keystone-auth" + apiVersion: "client.authentication.k8s.io/v1beta1" +""", + ) + with open(new_kubeconfig, "w") as f: + f.write(content) + if aws_iam_cluster_id: + # create aws-iam context + cmd = ( + "kubectl config --kubeconfig={0} " + "set-context --cluster={1} " + "--user=aws-iam-user aws-iam-authenticator" + ) + check_call(split(cmd.format(new_kubeconfig, cluster))) + + # append a user for aws-iam + cmd = ( + "kubectl --kubeconfig={0} config set-credentials " + "aws-iam-user --exec-command=aws-iam-authenticator " + '--exec-arg="token" --exec-arg="-i" --exec-arg="{1}" ' + '--exec-arg="-r" --exec-arg="<>" ' + "--exec-api-version=client.authentication.k8s.io/v1alpha1" + ) + check_call(split(cmd.format(new_kubeconfig, aws_iam_cluster_id))) + + # not going to use aws-iam context by default since we don't have + # the desired arn. This will make the config not usable if copied. + + # cmd = 'kubectl config --kubeconfig={0} ' \ + # 'use-context aws-iam-authenticator'.format(new_kubeconfig) + # check_call(split(cmd)) + if old_kubeconfig.exists(): + changed = new_kubeconfig.read_text() != old_kubeconfig.read_text() + else: + changed = True + if changed: + new_kubeconfig.rename(old_kubeconfig) + + +def parse_extra_args(config_key): + elements = hookenv.config().get(config_key, "").split() + args = {} + + for element in elements: + if "=" in element: + key, _, value = element.partition("=") + args[key] = value + else: + args[element] = "true" + + return args + + +def configure_kubernetes_service(key, service, base_args, extra_args_key): + db = unitdata.kv() + + prev_args_key = key + service + prev_snap_args = db.get(prev_args_key) or {} + + extra_args = parse_extra_args(extra_args_key) + + args = {} + args.update(base_args) + args.update(extra_args) + + # CIS benchmark action may inject kv config to pass failing tests. Merge + # these after the func args as they should take precedence. + cis_args_key = "cis-" + service + cis_args = db.get(cis_args_key) or {} + args.update(cis_args) + + # Remove any args with 'None' values (all k8s args are 'k=v') and + # construct an arg string for use by 'snap set'. + args = {k: v for k, v in args.items() if v is not None} + args = ['--%s="%s"' % arg for arg in args.items()] + args = " ".join(args) + + snap_opts = {} + for arg in prev_snap_args: + # remove previous args by setting to null + snap_opts[arg] = "null" + snap_opts["args"] = args + snap_opts = ["%s=%s" % opt for opt in snap_opts.items()] + + cmd = ["snap", "set", service] + snap_opts + check_call(cmd) + + # Now that we've started doing snap configuration through the "args" + # option, we should never need to clear previous args again. + db.set(prev_args_key, {}) + + +def _snap_common_path(component): + return Path("/var/snap/{}/common".format(component)) + + +def cloud_config_path(component): + return _snap_common_path(component) / "cloud-config.conf" + + +def _gcp_creds_path(component): + return _snap_common_path(component) / "gcp-creds.json" + + +def _daemon_env_path(component): + return _snap_common_path(component) / "environment" + + +def _cloud_endpoint_ca_path(component): + return _snap_common_path(component) / "cloud-endpoint-ca.crt" + + +def encryption_config_path(): + apiserver_snap_common_path = _snap_common_path("kube-apiserver") + encryption_conf_dir = apiserver_snap_common_path / "encryption" + return encryption_conf_dir / "encryption_config.yaml" + + +def write_gcp_snap_config(component): + # gcp requires additional credentials setup + gcp = endpoint_from_flag("endpoint.gcp.ready") + creds_path = _gcp_creds_path(component) + with creds_path.open("w") as fp: + os.fchmod(fp.fileno(), 0o600) + fp.write(gcp.credentials) + + # create a cloud-config file that sets token-url to nil to make the + # services use the creds env var instead of the metadata server, as + # well as making the cluster multizone + comp_cloud_config_path = cloud_config_path(component) + comp_cloud_config_path.write_text( + "[Global]\n" "token-url = nil\n" "multizone = true\n" + ) + + daemon_env_path = _daemon_env_path(component) + if daemon_env_path.exists(): + daemon_env = daemon_env_path.read_text() + if not daemon_env.endswith("\n"): + daemon_env += "\n" + else: + daemon_env = "" + if gcp_creds_env_key not in daemon_env: + daemon_env += "{}={}\n".format(gcp_creds_env_key, creds_path) + daemon_env_path.parent.mkdir(parents=True, exist_ok=True) + daemon_env_path.write_text(daemon_env) + + +def generate_openstack_cloud_config(): + # openstack requires additional credentials setup + openstack = endpoint_from_flag("endpoint.openstack.ready") + + lines = [ + "[Global]", + "auth-url = {}".format(openstack.auth_url), + "region = {}".format(openstack.region), + "username = {}".format(openstack.username), + "password = {}".format(openstack.password), + "tenant-name = {}".format(openstack.project_name), + "domain-name = {}".format(openstack.user_domain_name), + "tenant-domain-name = {}".format(openstack.project_domain_name), + ] + if openstack.endpoint_tls_ca: + lines.append("ca-file = /etc/config/endpoint-ca.cert") + + lines.extend( + [ + "", + "[LoadBalancer]", + ] + ) + + if openstack.has_octavia in (True, None): + # Newer integrator charm will detect whether underlying OpenStack has + # Octavia enabled so we can set this intelligently. If we're still + # related to an older integrator, though, default to assuming Octavia + # is available. + lines.append("use-octavia = true") + else: + lines.append("use-octavia = false") + lines.append("lb-provider = haproxy") + if openstack.subnet_id: + lines.append("subnet-id = {}".format(openstack.subnet_id)) + if openstack.floating_network_id: + lines.append("floating-network-id = {}".format(openstack.floating_network_id)) + if openstack.lb_method: + lines.append("lb-method = {}".format(openstack.lb_method)) + if openstack.manage_security_groups: + lines.append( + "manage-security-groups = {}".format(openstack.manage_security_groups) + ) + if any( + [openstack.bs_version, openstack.trust_device_path, openstack.ignore_volume_az] + ): + lines.append("") + lines.append("[BlockStorage]") + if openstack.bs_version is not None: + lines.append("bs-version = {}".format(openstack.bs_version)) + if openstack.trust_device_path is not None: + lines.append("trust-device-path = {}".format(openstack.trust_device_path)) + if openstack.ignore_volume_az is not None: + lines.append("ignore-volume-az = {}".format(openstack.ignore_volume_az)) + return "\n".join(lines) + "\n" + + +def write_azure_snap_config(component): + azure = endpoint_from_flag("endpoint.azure.ready") + comp_cloud_config_path = cloud_config_path(component) + comp_cloud_config_path.write_text( + json.dumps( + { + "useInstanceMetadata": True, + "useManagedIdentityExtension": azure.managed_identity, + "subscriptionId": azure.subscription_id, + "resourceGroup": azure.resource_group, + "location": azure.resource_group_location, + "vnetName": azure.vnet_name, + "vnetResourceGroup": azure.vnet_resource_group, + "subnetName": azure.subnet_name, + "securityGroupName": azure.security_group_name, + "loadBalancerSku": "standard", + "securityGroupResourceGroup": azure.security_group_resource_group, + "aadClientId": azure.aad_client_id, + "aadClientSecret": azure.aad_client_secret, + "tenantId": azure.tenant_id, + } + ) + ) + + +def configure_kube_proxy( + configure_prefix, api_servers, cluster_cidr, bind_address=None +): + kube_proxy_opts = {} + kube_proxy_opts["cluster-cidr"] = cluster_cidr + kube_proxy_opts["kubeconfig"] = kubeproxyconfig_path + kube_proxy_opts["logtostderr"] = "true" + kube_proxy_opts["v"] = "0" + num_apis = len(api_servers) + kube_proxy_opts["master"] = api_servers[get_unit_number() % num_apis] + kube_proxy_opts["hostname-override"] = get_node_name() + if bind_address: + kube_proxy_opts["bind-address"] = bind_address + elif is_ipv6(cluster_cidr): + kube_proxy_opts["bind-address"] = "::" + + if host.is_container(): + kube_proxy_opts["conntrack-max-per-core"] = "0" + + if is_dual_stack(cluster_cidr): + kube_proxy_opts["feature-gates"] = "IPv6DualStack=true" + + configure_kubernetes_service( + configure_prefix, "kube-proxy", kube_proxy_opts, "proxy-extra-args" + ) + + +def get_unit_number(): + return int(hookenv.local_unit().split("/")[1]) + + +def cluster_cidr(): + """Return the cluster CIDR provided by the CNI""" + cni = endpoint_from_flag("cni.available") + if not cni: + return None + config = hookenv.config() + if "default-cni" in config: + # master + default_cni = config["default-cni"] + else: + # worker + kube_control = endpoint_from_flag("kube-control.dns.available") + if not kube_control: + return None + default_cni = kube_control.get_default_cni() + return cni.get_config(default=default_cni)["cidr"] + + +def is_dual_stack(cidrs): + """Detect IPv4/IPv6 dual stack from CIDRs""" + return {net.version for net in get_networks(cidrs)} == {4, 6} + + +def is_ipv4(cidrs): + """Detect IPv6 from CIDRs""" + return get_ipv4_network(cidrs) is not None + + +def is_ipv6(cidrs): + """Detect IPv6 from CIDRs""" + return get_ipv6_network(cidrs) is not None + + +def is_ipv6_preferred(cidrs): + """Detect if IPv6 is preffered from CIDRs""" + return get_networks(cidrs)[0].version == 6 + + +def get_networks(cidrs): + """Convert a comma-separated list of CIDRs to a list of networks.""" + if not cidrs: + return [] + return [ipaddress.ip_interface(cidr).network for cidr in cidrs.split(",")] + + +def get_ipv4_network(cidrs): + """Get the IPv4 network from the given CIDRs or None""" + return {net.version: net for net in get_networks(cidrs)}.get(4) + + +def get_ipv6_network(cidrs): + """Get the IPv6 network from the given CIDRs or None""" + return {net.version: net for net in get_networks(cidrs)}.get(6) + + +def enable_ipv6_forwarding(): + """Enable net.ipv6.conf.all.forwarding in sysctl if it is not already.""" + check_call(["sysctl", "net.ipv6.conf.all.forwarding=1"]) + + +def get_bind_addrs(ipv4=True, ipv6=True): + """Get all global-scoped addresses that we might bind to.""" + try: + output = check_output(["ip", "-br", "addr", "show", "scope", "global"]) + except CalledProcessError: + # stderr will have any details, and go to the log + hookenv.log("Unable to determine global addresses", hookenv.ERROR) + return [] + + ignore_interfaces = ("lxdbr", "flannel", "cni", "virbr", "docker") + accept_versions = set() + if ipv4: + accept_versions.add(4) + if ipv6: + accept_versions.add(6) + + addrs = [] + for line in output.decode("utf8").splitlines(): + intf, state, *intf_addrs = line.split() + if state != "UP" or any( + intf.startswith(prefix) for prefix in ignore_interfaces + ): + continue + for addr in intf_addrs: + ip_addr = ipaddress.ip_interface(addr).ip + if ip_addr.version in accept_versions: + addrs.append(str(ip_addr)) + return addrs + + +class InvalidVMwareHost(Exception): + pass + + +def _get_vmware_uuid(): + serial_id_file = "/sys/class/dmi/id/product_serial" + # The serial id from VMWare VMs comes in following format: + # VMware-42 28 13 f5 d4 20 71 61-5d b0 7b 96 44 0c cf 54 + try: + with open(serial_id_file, "r") as f: + serial_string = f.read().strip() + if "VMware-" not in serial_string: + hookenv.log( + "Unable to find VMware ID in " + "product_serial: {}".format(serial_string) + ) + raise InvalidVMwareHost + serial_string = ( + serial_string.split("VMware-")[1].replace(" ", "").replace("-", "") + ) + uuid = "%s-%s-%s-%s-%s" % ( + serial_string[0:8], + serial_string[8:12], + serial_string[12:16], + serial_string[16:20], + serial_string[20:32], + ) + except IOError as err: + hookenv.log("Unable to read UUID from sysfs: {}".format(err)) + uuid = "UNKNOWN" + + return uuid + + +def token_generator(length=32): + """Generate a random token for use in account tokens. + + param: length - the length of the token to generate + """ + alpha = string.ascii_letters + string.digits + token = "".join(random.SystemRandom().choice(alpha) for _ in range(length)) + return token + + +def get_secret_names(): + """Return a dict of 'username: secret_id' for Charmed Kubernetes users.""" + try: + output = kubectl( + "get", + "secrets", + "-n", + AUTH_SECRET_NS, + "--field-selector", + "type={}".format(AUTH_SECRET_TYPE), + "-o", + "json", + ).decode("UTF-8") + except (CalledProcessError, FileNotFoundError): + # The api server may not be up, or we may be trying to run kubelet before + # the snap is installed. Send back an empty dict. + hookenv.log("Unable to get existing secrets", level=hookenv.WARNING) + return {} + + secrets = json.loads(output) + secret_names = {} + if "items" in secrets: + for secret in secrets["items"]: + try: + secret_id = secret["metadata"]["name"] + username_b64 = secret["data"]["username"].encode("UTF-8") + except (KeyError, TypeError): + # CK secrets will have populated 'data', but not all secrets do + continue + secret_names[b64decode(username_b64).decode("UTF-8")] = secret_id + return secret_names + + +def generate_rfc1123(length=10): + """Generate a random string compliant with RFC 1123. + + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names + + param: length - the length of the string to generate + """ + length = 253 if length > 253 else length + valid_chars = string.ascii_lowercase + string.digits + rand_str = "".join(random.SystemRandom().choice(valid_chars) for _ in range(length)) + return rand_str + + +def create_secret(token, username, user, groups=None): + secrets = get_secret_names() + if username in secrets: + # Use existing secret ID if one exists for our username + secret_id = secrets[username] + else: + # secret IDs must be unique and rfc1123 compliant + sani_name = re.sub("[^0-9a-z.-]+", "-", user.lower()) + secret_id = "auth-{}-{}".format(sani_name, generate_rfc1123(10)) + + # The authenticator expects tokens to be in the form user::token + token_delim = "::" + if token_delim not in token: + token = "{}::{}".format(user, token) + + context = { + "type": AUTH_SECRET_TYPE, + "secret_name": secret_id, + "secret_namespace": AUTH_SECRET_NS, + "user": b64encode(user.encode("UTF-8")).decode("utf-8"), + "username": b64encode(username.encode("UTF-8")).decode("utf-8"), + "password": b64encode(token.encode("UTF-8")).decode("utf-8"), + "groups": b64encode(groups.encode("UTF-8")).decode("utf-8") if groups else "", + } + with tempfile.NamedTemporaryFile() as tmp_manifest: + render("cdk.auth-webhook-secret.yaml", tmp_manifest.name, context=context) + + if kubectl_manifest("apply", tmp_manifest.name): + hookenv.log("Created secret for {}".format(username)) + return True + else: + hookenv.log("WARN: Unable to create secret for {}".format(username)) + return False + + +def get_secret_password(username): + """Get the password for the given user from the secret that CK created.""" + try: + output = kubectl( + "get", + "secrets", + "-n", + AUTH_SECRET_NS, + "--field-selector", + "type={}".format(AUTH_SECRET_TYPE), + "-o", + "json", + ).decode("UTF-8") + except CalledProcessError: + # NB: apiserver probably isn't up. This can happen on boostrap or upgrade + # while trying to build kubeconfig files. If we need the 'admin' token during + # this time, pull it directly out of the kubeconfig file if possible. + token = None + if username == "admin": + admin_kubeconfig = Path("/root/.kube/config") + if admin_kubeconfig.exists(): + data = yaml.safe_load(admin_kubeconfig.read_text()) + try: + token = data["users"][0]["user"]["token"] + except (KeyError, IndexError, TypeError): + pass + return token + except FileNotFoundError: + # New deployments may ask for a token before the kubectl snap is installed. + # Give them nothing! + return None + + secrets = json.loads(output) + if "items" in secrets: + for secret in secrets["items"]: + try: + data_b64 = secret["data"] + password_b64 = data_b64["password"].encode("UTF-8") + username_b64 = data_b64["username"].encode("UTF-8") + except (KeyError, TypeError): + # CK authn secrets will have populated 'data', but not all secrets do + continue + + password = b64decode(password_b64).decode("UTF-8") + secret_user = b64decode(username_b64).decode("UTF-8") + if username == secret_user: + return password + return None diff --git a/kubeapi-load-balancer/lib/charms/layer/nagios.py b/kubeapi-load-balancer/lib/charms/layer/nagios.py new file mode 100644 index 0000000..f6ad998 --- /dev/null +++ b/kubeapi-load-balancer/lib/charms/layer/nagios.py @@ -0,0 +1,60 @@ +from pathlib import Path + +NAGIOS_PLUGINS_DIR = '/usr/lib/nagios/plugins' + + +def install_nagios_plugin_from_text(text, plugin_name): + """ Install a nagios plugin. + + Args: + text: Plugin source code (str) + plugin_name: Name of the plugin in nagios + + Returns: Full path to installed plugin + """ + dest_path = Path(NAGIOS_PLUGINS_DIR) / plugin_name + if dest_path.exists(): + # we could complain here, test the files are the same contents, or + # just bail. Idempotency is a big deal in Juju, so I'd like to be + # ok with being called with the same file multiple times, but we + # certainly want to catch the case where multiple layers are using + # the same filename for their nagios checks. + dest = dest_path.read_text() + if dest == text: + # same file + return dest_path + # different file contents! + # maybe someone changed options or something so we need to write + # it again + + dest_path.write_text(text) + dest_path.chmod(0o755) + + return dest_path + + +def install_nagios_plugin_from_file(source_file_path, plugin_name): + """ Install a nagios plugin. + + Args: + source_file_path: Path to plugin source file + plugin_name: Name of the plugin in nagios + + Returns: Full path to installed plugin + """ + + return install_nagios_plugin_from_text(Path(source_file_path).read_text(), + plugin_name) + + +def remove_nagios_plugin(plugin_name): + """ Remove a nagios plugin. + + Args: + plugin_name: Name of the plugin in nagios + + Returns: None + """ + dest_path = Path(NAGIOS_PLUGINS_DIR) / plugin_name + if dest_path.exists(): + dest_path.unlink() diff --git a/kubeapi-load-balancer/lib/charms/layer/nginx.py b/kubeapi-load-balancer/lib/charms/layer/nginx.py new file mode 100644 index 0000000..7194400 --- /dev/null +++ b/kubeapi-load-balancer/lib/charms/layer/nginx.py @@ -0,0 +1,74 @@ +from charmhelpers.core.templating import render +from charmhelpers.core import hookenv +from charmhelpers.core import host + +import toml +import os + + +def load_site(): + if not os.path.isfile('site.toml'): + return {} + + with open('site.toml') as fp: + conf = toml.loads(fp.read()) + + return conf + + +def get_app_path(): + site = load_site() + if 'app_path' in site: + return site['app_path'] + return '/srv/app' + + +def remove_default_site(): + """ + Remove the default enabled + site. + + :return: Boolean + """ + site_path = '/etc/nginx/sites-enabled/default' + if os.path.isfile(site_path): + os.remove(site_path) + host.service_reload('nginx', restart_on_failure=True) + return True + + return False + + +def configure_site(site, template, **kwargs): + """ configures vhost + + Arguments: + site: Site name + template: template to process in templates/ + **kwargs: additional dict items to append to template variables exposed + through the site.toml + """ + hookenv.status_set('maintenance', 'Configuring site {}'.format(site)) + + config = hookenv.config() + context = load_site() + context['host'] = config['host'] + context['port'] = config['port'] + context.update(**kwargs) + conf_path = '/etc/nginx/sites-available/{}'.format(site) + if os.path.exists(conf_path): + os.remove(conf_path) + render(source=template, + target=conf_path, + context=context) + + symlink_path = '/etc/nginx/sites-enabled/{}'.format(site) + if os.path.exists(symlink_path): + os.unlink(symlink_path) + os.symlink(conf_path, symlink_path) + hookenv.log('Wrote vhost config {} to {}'.format(context, template), + 'info') + + if not remove_default_site(): + host.service_reload('nginx', restart_on_failure=True) + hookenv.status_set('active', '') diff --git a/kubeapi-load-balancer/lib/charms/layer/options.py b/kubeapi-load-balancer/lib/charms/layer/options.py new file mode 100644 index 0000000..d3f273f --- /dev/null +++ b/kubeapi-load-balancer/lib/charms/layer/options.py @@ -0,0 +1,26 @@ +import os +from pathlib import Path + +import yaml + + +_CHARM_PATH = Path(os.environ.get('JUJU_CHARM_DIR', '.')) +_DEFAULT_FILE = _CHARM_PATH / 'layer.yaml' +_CACHE = {} + + +def get(section=None, option=None, layer_file=_DEFAULT_FILE): + if option and not section: + raise ValueError('Cannot specify option without section') + + layer_file = (_CHARM_PATH / layer_file).resolve() + if layer_file not in _CACHE: + with layer_file.open() as fp: + _CACHE[layer_file] = yaml.safe_load(fp.read()) + + data = _CACHE[layer_file].get('options', {}) + if section: + data = data.get(section, {}) + if option: + data = data.get(option) + return data diff --git a/kubeapi-load-balancer/lib/charms/layer/status.py b/kubeapi-load-balancer/lib/charms/layer/status.py new file mode 100644 index 0000000..95b2997 --- /dev/null +++ b/kubeapi-load-balancer/lib/charms/layer/status.py @@ -0,0 +1,189 @@ +import inspect +import errno +import subprocess +import yaml +from enum import Enum +from functools import wraps +from pathlib import Path + +from charmhelpers.core import hookenv +from charms import layer + + +_orig_call = subprocess.call +_statuses = {'_initialized': False, + '_finalized': False} + + +class WorkloadState(Enum): + """ + Enum of the valid workload states. + + Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + """ + # note: order here determines precedence of state + MAINTENANCE = 'maintenance' + BLOCKED = 'blocked' + WAITING = 'waiting' + ACTIVE = 'active' + + +def maintenance(message): + """ + Set the status to the `MAINTENANCE` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.MAINTENANCE, message) + + +def maint(message): + """ + Shorthand alias for + [maintenance](status.md#charms.layer.status.maintenance). + + # Parameters + `message` (str): Message to convey to the operator. + """ + maintenance(message) + + +def blocked(message): + """ + Set the status to the `BLOCKED` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.BLOCKED, message) + + +def waiting(message): + """ + Set the status to the `WAITING` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.WAITING, message) + + +def active(message): + """ + Set the status to the `ACTIVE` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.ACTIVE, message) + + +def status_set(workload_state, message): + """ + Set the status to the given workload state with a message. + + # Parameters + `workload_state` (WorkloadState or str): State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. + `message` (str): Message to convey to the operator. + """ + if not isinstance(workload_state, WorkloadState): + workload_state = WorkloadState(workload_state) + if workload_state is WorkloadState.MAINTENANCE: + _status_set_immediate(workload_state, message) + return + layer = _find_calling_layer() + _statuses.setdefault(workload_state, []).append((layer, message)) + if not _statuses['_initialized'] or _statuses['_finalized']: + # We either aren't initialized, so the finalizer may never be run, + # or the finalizer has already run, so it won't run again. In either + # case, we need to manually invoke it to ensure the status gets set. + _finalize() + + +def _find_calling_layer(): + for frame in inspect.stack(): + # switch to .filename when trusty (Python 3.4) is EOL + fn = Path(frame[1]) + if fn.parent.stem not in ('reactive', 'layer', 'charms'): + continue + layer_name = fn.stem + if layer_name == 'status': + continue # skip our own frames + return layer_name + return None + + +def _initialize(): + if not _statuses['_initialized']: + if layer.options.get('status', 'patch-hookenv'): + _patch_hookenv() + hookenv.atexit(_finalize) + _statuses['_initialized'] = True + + +def _finalize(): + if _statuses['_initialized']: + # If we haven't been initialized, we can't truly be finalized. + # This makes things more efficient if an action sets a status + # but subsequently starts the reactive bus. + _statuses['_finalized'] = True + charm_name = hookenv.charm_name() + charm_dir = Path(hookenv.charm_dir()) + with charm_dir.joinpath('layer.yaml').open() as fp: + includes = yaml.safe_load(fp.read()).get('includes', []) + layer_order = includes + [charm_name] + + for workload_state in WorkloadState: + if workload_state not in _statuses: + continue + if not _statuses[workload_state]: + continue + + def _get_key(record): + layer_name, message = record + if layer_name in layer_order: + return layer_order.index(layer_name) + else: + return 0 + + sorted_statuses = sorted(_statuses[workload_state], key=_get_key) + layer_name, message = sorted_statuses[-1] + _status_set_immediate(workload_state, message) + break + + +def _status_set_immediate(workload_state, message): + workload_state = workload_state.value + try: + hookenv.log('status-set: {}: {}'.format(workload_state, message), + hookenv.INFO) + ret = _orig_call(['status-set', workload_state, message]) + if ret == 0: + return + except OSError as e: + # ignore status-set not available on older controllers + if e.errno != errno.ENOENT: + raise + + +def _patch_hookenv(): + # we can't patch hookenv.status_set directly because other layers may have + # already imported it into their namespace, so we have to patch sp.call + subprocess.call = _patched_call + + +@wraps(_orig_call) +def _patched_call(cmd, *args, **kwargs): + if not isinstance(cmd, list) or cmd[0] != 'status-set': + return _orig_call(cmd, *args, **kwargs) + _, workload_state, message = cmd + status_set(workload_state, message) + return 0 # make hookenv.status_set not emit spurious failure logs diff --git a/kubeapi-load-balancer/lib/charms/layer/tls_client.py b/kubeapi-load-balancer/lib/charms/layer/tls_client.py new file mode 100644 index 0000000..b2980dc --- /dev/null +++ b/kubeapi-load-balancer/lib/charms/layer/tls_client.py @@ -0,0 +1,61 @@ +# Copyright 2016-2017 Canonical Ltd. +# +# This file is part of the tls-client 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. + +from charmhelpers.core.hookenv import log +from charmhelpers.core import unitdata + +from charms.reactive import remove_state +from charms.reactive import endpoint_from_flag + + +def reset_certificate_write_flag(cert_type): + """ + Reset the certificate written flag so notification will work on the next + write cert_type must be 'server', 'client', or 'ca' to indicate type of + certificate + """ + if cert_type not in ['server', 'client', 'ca']: + log('Unknown certificate type!') + else: + remove_state('tls_client.{0}.certificate.written'.format(cert_type)) + + +def request_server_cert(common_name, sans=None, crt_path=None, key_path=None): + tls = endpoint_from_flag('certificates.available') + tls.request_server_cert(common_name, sans) + if not crt_path and not key_path: + return + kv = unitdata.kv() + cert_paths = kv.get('layer.tls-client.cert-paths', {}) + cert_paths.setdefault('server', {})[common_name] = { + 'crt': str(crt_path), + 'key': str(key_path), + } + kv.set('layer.tls-client.cert-paths', cert_paths) + + +def request_client_cert(common_name, sans=None, crt_path=None, key_path=None): + tls = endpoint_from_flag('certificates.available') + tls.request_client_cert(common_name, sans) + if not crt_path and not key_path: + return + kv = unitdata.kv() + cert_paths = kv.get('layer.tls-client.cert-paths', {}) + cert_paths.setdefault('client', {})[common_name] = { + 'crt': str(crt_path), + 'key': str(key_path), + } + kv.set('layer.tls-client.cert-paths', cert_paths) diff --git a/kubeapi-load-balancer/lib/charms/leadership.py b/kubeapi-load-balancer/lib/charms/leadership.py new file mode 100644 index 0000000..d2a95fa --- /dev/null +++ b/kubeapi-load-balancer/lib/charms/leadership.py @@ -0,0 +1,68 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Leadership Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.reactive import not_unless + + +__all__ = ['leader_get', 'leader_set'] + + +@not_unless('leadership.is_leader') +def leader_set(*args, **kw): + '''Change leadership settings, per charmhelpers.core.hookenv.leader_set. + + Settings may either be passed in as a single dictionary, or using + keyword arguments. All values must be strings. + + The leadership.set.{key} reactive state will be set while the + leadership hook environment setting remains set. + + Changed leadership settings will set the leadership.changed.{key} + and leadership.changed states. These states will remain set until + the following hook. + + These state changes take effect immediately on the leader, and + in future hooks run on non-leaders. In this way both leaders and + non-leaders can share handlers, waiting on these states. + ''' + if args: + if len(args) > 1: + raise TypeError('leader_set() takes 1 positional argument but ' + '{} were given'.format(len(args))) + else: + settings = dict(args[0]) + else: + settings = {} + settings.update(kw) + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + + for key, value in settings.items(): + if value != previous.get(key): + reactive.set_state('leadership.changed.{}'.format(key)) + reactive.set_state('leadership.changed') + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + hookenv.leader_set(settings) + unitdata.kv().update(settings, prefix='leadership.settings.') + + +def leader_get(attribute=None): + '''Return leadership settings, per charmhelpers.core.hookenv.leader_get.''' + return hookenv.leader_get(attribute) diff --git a/kubeapi-load-balancer/lib/debug_script.py b/kubeapi-load-balancer/lib/debug_script.py new file mode 100644 index 0000000..e156924 --- /dev/null +++ b/kubeapi-load-balancer/lib/debug_script.py @@ -0,0 +1,8 @@ +import os + +dir = os.environ["DEBUG_SCRIPT_DIR"] + + +def open_file(path, *args, **kwargs): + """ Open a file within the debug script dir """ + return open(os.path.join(dir, path), *args, **kwargs) diff --git a/kubeapi-load-balancer/lib/nginxlib.py b/kubeapi-load-balancer/lib/nginxlib.py new file mode 100644 index 0000000..1bd5e73 --- /dev/null +++ b/kubeapi-load-balancer/lib/nginxlib.py @@ -0,0 +1,4 @@ +from warnings import warn +from charms.layer.nginx import * # noqa + +warn('nginxlib is being deprecated, use charms.layer.nginx instead') diff --git a/kubeapi-load-balancer/make_docs b/kubeapi-load-balancer/make_docs new file mode 100644 index 0000000..dcd4c1f --- /dev/null +++ b/kubeapi-load-balancer/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import os +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + sys.path.insert(0, 'lib') + sys.path.insert(1, 'reactive') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') + if os.path.exists('.unit-state.db'): + os.remove('.unit-state.db') diff --git a/kubeapi-load-balancer/metadata.yaml b/kubeapi-load-balancer/metadata.yaml new file mode 100644 index 0000000..44b6e89 --- /dev/null +++ b/kubeapi-load-balancer/metadata.yaml @@ -0,0 +1,38 @@ +"name": "kubeapi-load-balancer" +"summary": |- + Nginx Load Balancer +"maintainers": +- "Tim Van Steenburgh " +- "George Kraft " +- "Rye Terrell " +- "Konstantinos Tsakalozos " +- "Charles Butler " +- "Matthew Bruzek " +"description": | + A round robin Nginx load balancer to distribute traffic for kubernetes apiservers. +"tags": +- "application" +- "nginx" +- "misc" +"series": +- "focal" +- "bionic" +- "xenial" +"requires": + "certificates": + "interface": "tls-certificates" + "ha": + "interface": "hacluster" + "apiserver": + "interface": "http" +"provides": + "nrpe-external-master": + "interface": "nrpe-external-master" + "scope": "container" + "website": + "interface": "http" + "loadbalancer": + "interface": "public-address" + "lb-consumers": + "interface": "loadbalancer" +"subordinate": !!bool "false" diff --git a/kubeapi-load-balancer/metrics.yaml b/kubeapi-load-balancer/metrics.yaml new file mode 100644 index 0000000..0fcb3c1 --- /dev/null +++ b/kubeapi-load-balancer/metrics.yaml @@ -0,0 +1,2 @@ +metrics: + juju-units: {} diff --git a/kubeapi-load-balancer/pydocmd.yml b/kubeapi-load-balancer/pydocmd.yml new file mode 100644 index 0000000..ab3b2ef --- /dev/null +++ b/kubeapi-load-balancer/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'Status Management Layer' + +generate: + - status.md: + - charms.layer.status.WorkloadState + - charms.layer.status.maintenance + - charms.layer.status.maint + - charms.layer.status.blocked + - charms.layer.status.waiting + - charms.layer.status.active + - charms.layer.status.status_set + +pages: + - Status Management Layer: status.md + +gens_dir: docs diff --git a/kubeapi-load-balancer/pyproject.toml b/kubeapi-load-balancer/pyproject.toml new file mode 100644 index 0000000..db0dcd0 --- /dev/null +++ b/kubeapi-load-balancer/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length=120 +target-version=['py35'] diff --git a/kubeapi-load-balancer/reactive/__init__.py b/kubeapi-load-balancer/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubeapi-load-balancer/reactive/apt.py b/kubeapi-load-balancer/reactive/apt.py new file mode 100644 index 0000000..8832296 --- /dev/null +++ b/kubeapi-load-balancer/reactive/apt.py @@ -0,0 +1,158 @@ +# Copyright 2015-2020 Canonical Ltd. +# +# This file is part of the Apt layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +charms.reactive helpers for dealing with deb packages. + +Add apt package sources using add_source(). Queue deb packages for +installation with install(). Configure and work with your software +once the apt.installed.{packagename} flag is set. +''' +import os.path +import subprocess +import re + +from charmhelpers import fetch +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import DEBUG, ERROR, WARNING +from charms import layer +from charms.layer import status +from charms import reactive +from charms.reactive import when, when_not + +import charms.apt + + +@when('apt.needs_update') +def update(): + charms.apt.update() + + +@when('apt.queued_installs') +@when_not('apt.needs_update') +def install_queued(): + charms.apt.install_queued() + + +@when_not('apt.queued_installs') +def ensure_package_status(): + charms.apt.ensure_package_status() + + +def filter_installed_packages(packages): + # Don't use fetch.filter_installed_packages, as it depends on python-apt + # and not available if the basic layer's use_site_packages option is off + cmd = ['dpkg-query', '--show', r'--showformat=${Package}\n'] + installed = set(subprocess.check_output(cmd, universal_newlines=True).split()) + + # list of packages that are not installed + not_installed = set(packages) - installed + + # now we want to check for any regex in the installation of the packages + not_installed_iterable = not_installed.copy() + for pkg in not_installed_iterable: + # grab the pattern that we want to match against the packages + p = re.compile(pkg) + for pkg2 in installed: + matched = p.search(pkg2) + if matched: + not_installed.remove(pkg) + break + + return not_installed + + +def clear_removed_package_flags(): + """On hook startup, clear install flags for removed packages.""" + removed = filter_installed_packages(charms.apt.installed()) + if removed: + hookenv.log('{} missing packages ({})'.format(len(removed), ','.join(removed)), WARNING) + for package in removed: + reactive.clear_flag('apt.installed.{}'.format(package)) + + +def add_implicit_signing_keys(): + """Add keys specified in layer.yaml + + The charm can ship trusted keys, avoiding the need to specify + them in config.yaml. We need to add them before we attempt + to add any custom sources, or apt will block under Bionic + if we attempt to add a source before the key becomes trusted. + """ + opts = layer.options() + if 'apt' not in opts or 'keys' not in opts['apt']: + return + keys = opts['apt']['keys'] + for p in keys: + full_p = os.path.join(hookenv.charm_dir(), p) + if os.path.exists(full_p): + hookenv.log("Adding key {}".format(p), DEBUG) + subprocess.check_call( + ['apt-key', 'add', full_p], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + else: + hookenv.log('Key {!r} does not exist'.format(full_p), ERROR) + + +def configure_sources(): + """Add user specified package sources from the service configuration. + + See charmhelpers.fetch.configure_sources for details. + """ + config = hookenv.config() + + # We don't have enums, so we need to validate this ourselves. + package_status = config.get('package_status') or '' + if package_status not in ('hold', 'install'): + status.blocked('Unknown package_status {}'.format(package_status)) + # Die before further hooks are run. This isn't very nice, but + # there is no other way to inform the operator that they have + # invalid configuration. + raise SystemExit(0) + + sources = config.get('install_sources') or '' + keys = config.get('install_keys') or '' + if reactive.helpers.data_changed('apt.configure_sources', (sources, keys)): + fetch.configure_sources(update=False, sources_var='install_sources', keys_var='install_keys') + reactive.set_flag('apt.needs_update') + + # Clumsy 'config.get() or' per Bug #1641362 + extra_packages = sorted((config.get('extra_packages') or '').split()) + if extra_packages: + charms.apt.queue_install(extra_packages) + + +def queue_layer_packages(): + """Add packages listed in build-time layer options.""" + # Both basic and apt layer. basic layer will have already installed + # its defined packages, but rescheduling it here gets the apt layer + # flag set and they will pinned as any other apt layer installed + # package. + opts = layer.options() + for section in ['basic', 'apt']: + if section in opts and 'packages' in opts[section]: + charms.apt.queue_install(opts[section]['packages']) + + +hookenv.atstart(hookenv.log, 'Initializing Apt Layer') +hookenv.atstart(clear_removed_package_flags) +hookenv.atstart(add_implicit_signing_keys) +hookenv.atstart(configure_sources) +hookenv.atstart(queue_layer_packages) +hookenv.atstart(charms.apt.reset_application_version) diff --git a/kubeapi-load-balancer/reactive/hacluster.py b/kubeapi-load-balancer/reactive/hacluster.py new file mode 100644 index 0000000..f921f76 --- /dev/null +++ b/kubeapi-load-balancer/reactive/hacluster.py @@ -0,0 +1,110 @@ +from charms import layer + +from charms.reactive import hook +from charms.reactive import when, when_not, clear_flag, set_flag, is_flag_set +from charms.reactive import endpoint_from_flag + +from charms.layer.kubernetes_common import get_ingress_address + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +db = unitdata.kv() + + +@hook('upgrade-charm') +def do_upgrade(): + # bump the services from upstart to systemd. :-/ + hacluster = endpoint_from_flag('ha.connected') + if not hacluster: + return + + if not is_flag_set('layer-hacluster.upgraded-systemd'): + services = db.get('layer-hacluster.services', {'current_services': {}, + 'desired_services': {}, + 'deleted_services': {}}) + for name, service in services['current_services'].items(): + hookenv.log("changing service {} to systemd service".format(name)) + hacluster.remove_init_service(name, service) + hacluster.add_systemd_service(name, service) + + # change any pending lsb entries to systemd + for name, service in services['desired_services'].items(): + msg = "changing pending service {} to systemd service" + hookenv.log(msg.format(name)) + hacluster.remove_init_service(name, service) + hacluster.add_systemd_service(name, service) + + clear_flag('layer-hacluster.configured') + set_flag('layer-hacluster.upgraded-systemd') + + +@when('ha.connected', 'layer.hacluster.services_configured') +@when_not('layer-hacluster.configured') +def configure_hacluster(): + """Configure HA resources in corosync""" + hacluster = endpoint_from_flag('ha.connected') + vips = hookenv.config('ha-cluster-vip').split() + dns_record = hookenv.config('ha-cluster-dns') + if vips and dns_record: + set_flag('layer-hacluster.dns_vip.invalid') + msg = "Unsupported configuration. " \ + "ha-cluster-vip and ha-cluster-dns cannot both be set", + hookenv.log(msg) + return + else: + clear_flag('layer-hacluster.dns_vip.invalid') + if vips: + for vip in vips: + hacluster.add_vip(hookenv.application_name(), vip) + elif dns_record: + layer_options = layer.options('hacluster') + binding_address = layer_options.get('binding_address') + ip = get_ingress_address(binding_address) + hacluster.add_dnsha(hookenv.application_name(), ip, dns_record, + 'public') + + services = db.get('layer-hacluster.services', {'current_services': {}, + 'desired_services': {}, + 'deleted_services': {}}) + for name, service in services['deleted_services'].items(): + hacluster.remove_systemd_service(name, service) + for name, service in services['desired_services'].items(): + hacluster.add_systemd_service(name, service) + services['current_services'][name] = service + + services['deleted_services'] = {} + services['desired_services'] = {} + + hacluster.bind_resources() + set_flag('layer-hacluster.configured') + + +@when('config.changed.ha-cluster-vip', + 'ha.connected') +def update_vips(): + hacluster = endpoint_from_flag('ha.connected') + config = hookenv.config() + original_vips = set(config.previous('ha-cluster-vip').split()) + new_vips = set(config['ha-cluster-vip'].split()) + old_vips = original_vips - new_vips + + for vip in old_vips: + hacluster.remove_vip(hookenv.application_name(), vip) + + clear_flag('layer-hacluster.configured') + + +@when('config.changed.ha-cluster-dns', + 'ha.connected') +def update_dns(): + hacluster = endpoint_from_flag('ha.connected') + config = hookenv.config() + original_dns = set(config.previous('ha-cluster-dns').split()) + new_dns = set(config['ha-cluster-dns'].split()) + old_dns = original_dns - new_dns + + for dns in old_dns: + hacluster.remove_dnsha(hookenv.application_name, 'public') + + clear_flag('layer-hacluster.configured') diff --git a/kubeapi-load-balancer/reactive/leadership.py b/kubeapi-load-balancer/reactive/leadership.py new file mode 100644 index 0000000..29c6f3a --- /dev/null +++ b/kubeapi-load-balancer/reactive/leadership.py @@ -0,0 +1,68 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Leadership Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.leadership import leader_get, leader_set + + +__all__ = ['leader_get', 'leader_set'] # Backwards compatibility + + +def initialize_leadership_state(): + '''Initialize leadership.* states from the hook environment. + + Invoked by hookenv.atstart() so states are available in + @hook decorated handlers. + ''' + is_leader = hookenv.is_leader() + if is_leader: + hookenv.log('Initializing Leadership Layer (is leader)') + else: + hookenv.log('Initializing Leadership Layer (is follower)') + + reactive.helpers.toggle_state('leadership.is_leader', is_leader) + + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + current = hookenv.leader_get() + + # Handle deletions. + for key in set(previous.keys()) - set(current.keys()): + current[key] = None + + any_changed = False + for key, value in current.items(): + reactive.helpers.toggle_state('leadership.changed.{}'.format(key), + value != previous.get(key)) + if value != previous.get(key): + any_changed = True + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + reactive.helpers.toggle_state('leadership.changed', any_changed) + + unitdata.kv().update(current, prefix='leadership.settings.') + + +# 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, '_leadership_registered'): + hookenv.atstart(initialize_leadership_state) + reactive._leadership_registered = True diff --git a/kubeapi-load-balancer/reactive/load_balancer.py b/kubeapi-load-balancer/reactive/load_balancer.py new file mode 100644 index 0000000..e6a0b69 --- /dev/null +++ b/kubeapi-load-balancer/reactive/load_balancer.py @@ -0,0 +1,380 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# 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 itertools +import os +import socket +import subprocess + +from pathlib import Path + +from charms.reactive import when, when_any, when_not +from charms.reactive import set_flag, is_state +from charms.reactive import hook +from charms.reactive import clear_flag, endpoint_from_flag, endpoint_from_name +from charmhelpers.core import hookenv +from charmhelpers.core import host +from charmhelpers.contrib.charmsupport import nrpe + +from charms.layer import nginx +from charms.layer import tls_client +from charms.layer import status +from charms.layer import kubernetes_common +from charms.layer.hacluster import add_service_to_hacluster +from charms.layer.hacluster import remove_service_from_hacluster + +from subprocess import Popen +from subprocess import PIPE +from subprocess import STDOUT +from subprocess import CalledProcessError + + +apilb_nginx = """/var/log/nginx.*.log { + daily + missingok + rotate 14 + compress + delaycompress + notifempty + create 0640 www-data adm + sharedscripts + prerotate + if [ -d /etc/logrotate.d/httpd-prerotate ]; then \\ + run-parts /etc/logrotate.d/httpd-prerotate; \\ + fi \\ + endscript + postrotate + invoke-rc.d nginx rotate >/dev/null 2>&1 + endscript +}""" + +cert_dir = Path('/srv/kubernetes/') +server_crt_path = cert_dir / 'server.crt' +server_key_path = cert_dir / 'server.key' + + +@when('certificates.available') +def request_server_certificates(): + '''Send the data that is required to create a server certificate for + this server.''' + # Use the public ip of this unit as the Common Name for the certificate. + common_name = hookenv.unit_public_ip() + + bind_ips = kubernetes_common.get_bind_addrs(ipv4=True, ipv6=True) + + # Create SANs that the tls layer will add to the server cert. + sans = [ + # The CN field is checked as a hostname, so if it's an IP, it + # won't match unless also included in the SANs as an IP field. + common_name, + kubernetes_common.get_ingress_address('website'), + socket.gethostname(), + socket.getfqdn(), + ] + bind_ips + forced_lb_ips = hookenv.config('loadbalancer-ips').split() + if forced_lb_ips: + sans.extend(forced_lb_ips) + else: + hacluster = endpoint_from_flag('ha.connected') + if hacluster: + vips = hookenv.config('ha-cluster-vip').split() + dns_record = hookenv.config('ha-cluster-dns') + if vips: + sans.extend(vips) + elif dns_record: + sans.append(dns_record) + + # maybe they have extra names they want as SANs + extra_sans = hookenv.config('extra_sans') + if extra_sans and not extra_sans == "": + sans.extend(extra_sans.split()) + # Request a server cert with this information. + tls_client.request_server_cert(common_name, sorted(set(sans)), + crt_path=server_crt_path, + key_path=server_key_path) + + +@when('certificates.server.cert.available', + 'nginx.available') +@when_any('tls_client.certs.changed', + 'tls_client.ca.written') +def kick_nginx(tls): + # certificate changed, so sighup nginx + hookenv.log("Certificate information changed, sending SIGHUP to nginx") + host.service_restart('nginx') + clear_flag('tls_client.certs.changed') + clear_flag('tls_client.ca.written') + + +@when('config.changed.port') +def close_old_port(): + config = hookenv.config() + old_port = config.previous('port') + if not old_port: + return + try: + hookenv.close_port(old_port) + except CalledProcessError: + hookenv.log('Port %d already closed, skipping.' % old_port) + + +def maybe_write_apilb_logrotate_config(): + filename = '/etc/logrotate.d/apilb_nginx' + if not os.path.exists(filename): + # Set log rotation for apilb log file + with open(filename, 'w+') as fp: + fp.write(apilb_nginx) + + +@when('nginx.available', + 'tls_client.certs.saved') +@when_any('endpoint.lb-consumers.joined', + 'apiserver.available') +@when_not('upgrade.series.in-progress') +def install_load_balancer(): + ''' Create the default vhost template for load balancing ''' + apiserver = endpoint_from_name('apiserver') + lb_consumers = endpoint_from_name('lb-consumers') + + if not (server_crt_path.exists() and server_key_path.exists()): + hookenv.log('Skipping due to missing cert') + return + if not (apiserver.services() or lb_consumers.all_requests): + hookenv.log('Skipping due to requests not ready') + return + + # At this point the cert and key exist, and they are owned by root. + chown = ['chown', 'www-data:www-data', str(server_crt_path)] + + # Change the owner to www-data so the nginx process can read the cert. + subprocess.call(chown) + chown = ['chown', 'www-data:www-data', str(server_key_path)] + + # Change the owner to www-data so the nginx process can read the key. + subprocess.call(chown) + + servers = {} + if apiserver and apiserver.services(): + servers[hookenv.config('port')] = { + (h['hostname'], h['port']) + for service in apiserver.services() + for h in service['hosts'] + } + for request in lb_consumers.all_requests: + for server_port in request.port_mapping.keys(): + service = servers.setdefault(server_port, set()) + service.update( + (backend, backend_port) + for backend, backend_port in itertools.product( + request.backends, request.port_mapping.values() + ) + ) + nginx.configure_site( + 'apilb', + 'apilb.conf', + servers=servers, + server_certificate=str(server_crt_path), + server_key=str(server_key_path), + proxy_read_timeout=hookenv.config('proxy_read_timeout') + ) + + maybe_write_apilb_logrotate_config() + for listen_port in servers.keys(): + hookenv.open_port(listen_port) + status.active('Loadbalancer ready.') + + +@hook('upgrade-charm') +def upgrade_charm(): + if is_state('certificates.available') and is_state('website.available'): + request_server_certificates() + maybe_write_apilb_logrotate_config() + + +@hook('pre-series-upgrade') +def pre_series_upgrade(): + host.service_pause('nginx') + status.blocked('Series upgrade in progress') + + +@hook('post-series-upgrade') +def post_series_upgrade(): + host.service_resume('nginx') + + +@when('nginx.available') +def set_nginx_version(): + ''' Surface the currently deployed version of nginx to Juju ''' + cmd = 'nginx -v' + p = Popen(cmd, shell=True, + stdin=PIPE, + stdout=PIPE, + stderr=STDOUT, + close_fds=True) + raw = p.stdout.read() + # The version comes back as: + # nginx version: nginx/1.10.0 (Ubuntu) + version = raw.split(b'/')[-1].split(b' ')[0] + hookenv.application_version_set(version.rstrip()) + + +def _get_lb_address(): + hacluster = endpoint_from_flag('ha.connected') + forced_lb_ips = hookenv.config('loadbalancer-ips').split() + address = None + if forced_lb_ips: + address = forced_lb_ips + elif hacluster: + # in the hacluster world, we dump the vip or the dns + # on every unit's data. This is because the + # kubernetes-master charm just grabs the first + # one it sees and uses that ip/dns. + vips = hookenv.config('ha-cluster-vip').split() + dns_record = hookenv.config('ha-cluster-dns') + if vips: + address = vips + elif dns_record: + address = dns_record + return address + + +def _get_lb_port(prefer_private=True): + lb_consumers = endpoint_from_name('lb-consumers') + + # prefer a port from the newer, more explicit relations + public = filter(lambda r: r.public, lb_consumers.all_requests) + private = filter(lambda r: not r.public, lb_consumers.all_requests) + lb_reqs = (private, public) if prefer_private else (public, private) + for lb_req in itertools.chain(*lb_reqs): + return list(lb_req.port_mapping)[0] + + # fall back to the config + return hookenv.config('port') + + +@when('endpoint.lb-consumers.joined', + 'leadership.is_leader') +def provide_lb_consumers(): + '''Respond to any LB requests via the lb-consumers relation. + + This is used in favor for the more complex two relation setup using the + website and loadbalancer relations going forward. + ''' + lb_consumers = endpoint_from_name('lb-consumers') + lb_address = _get_lb_address() + for request in lb_consumers.all_requests: + response = request.response + if request.protocol not in (request.protocols.tcp, + request.protocols.http, + request.protocols.https): + response.error_type = response.error_types.unsupported + response.error_fields = { + 'protocol': 'Protocol must be one of: tcp, http, https' + } + lb_consumers.send_response(request) + continue + if lb_address: + private_address = lb_address + public_address = lb_address + else: + network_info = hookenv.network_get('lb-consumers', + str(request.relation.id)) + private_address = network_info['ingress-addresses'][0] + public_address = hookenv.unit_get('public-address') + if request.public: + response.address = public_address + else: + response.address = private_address + lb_consumers.send_response(request) + + +@when('website.available') +def provide_application_details(): + ''' re-use the nginx layer website relation to relay the hostname/port + to any consuming kubernetes-workers, or other units that require the + kubernetes API ''' + website = endpoint_from_flag('website.available') + lb_address = _get_lb_address() + lb_port = _get_lb_port(prefer_private=True) + if lb_address: + website.configure(port=lb_port, + private_address=lb_address, + hostname=lb_address) + else: + website.configure(port=lb_port) + + +@when('loadbalancer.available') +def provide_loadbalancing(): + '''Send the public address and port to the public-address interface, so + the subordinates can get the public address of this loadbalancer.''' + loadbalancer = endpoint_from_flag('loadbalancer.available') + address = _get_lb_address() + lb_port = _get_lb_port(prefer_private=False) + if not address: + address = hookenv.unit_get('public-address') + loadbalancer.set_address_port(address, lb_port) + + +@when('nrpe-external-master.available') +@when_not('nrpe-external-master.initial-config') +def initial_nrpe_config(nagios=None): + set_flag('nrpe-external-master.initial-config') + update_nrpe_config(nagios) + + +@when('nginx.available') +@when('nrpe-external-master.available') +@when_any('config.changed.nagios_context', + 'config.changed.nagios_servicegroups') +def update_nrpe_config(unused=None): + services = ('nginx',) + + hostname = nrpe.get_nagios_hostname() + current_unit = nrpe.get_nagios_unit_name() + nrpe_setup = nrpe.NRPE(hostname=hostname) + nrpe.add_init_service_checks(nrpe_setup, services, current_unit) + nrpe_setup.write() + + +@when_not('nrpe-external-master.available') +@when('nrpe-external-master.initial-config') +def remove_nrpe_config(nagios=None): + clear_flag('nrpe-external-master.initial-config') + + # List of systemd services for which the checks will be removed + services = ('nginx',) + + # The current nrpe-external-master interface doesn't handle a lot of logic, + # use the charm-helpers code for now. + hostname = nrpe.get_nagios_hostname() + nrpe_setup = nrpe.NRPE(hostname=hostname) + + for service in services: + nrpe_setup.remove_check(shortname=service) + + +@when('nginx.available', 'ha.connected') +def configure_hacluster(): + add_service_to_hacluster('nginx', 'nginx') + set_flag('hacluster-configured') + + +@when_not('ha.connected') +@when('hacluster-configured') +def remove_hacluster(): + remove_service_from_hacluster('nginx', 'nginx') + clear_flag('hacluster-configured') diff --git a/kubeapi-load-balancer/reactive/nginx.py b/kubeapi-load-balancer/reactive/nginx.py new file mode 100644 index 0000000..9a93006 --- /dev/null +++ b/kubeapi-load-balancer/reactive/nginx.py @@ -0,0 +1,33 @@ +from charms.reactive import ( + set_state, + when_not, + when +) + +from charms.layer import nginx + +from charmhelpers.core import hookenv + +config = hookenv.config() + + +# Handlers -------------------------------------------------------------------- +@when('apt.installed.nginx-full') +@when_not('nginx.available') +def nginx_ready(): + nginx.remove_default_site() + hookenv.status_set('active', 'NGINX is ready') + set_state('nginx.available') + + +# Example website.available reaction ------------------------------------------ +""" +This example reaction for an application layer which consumes this nginx layer. +If left here then this reaction may overwrite your top-level reaction depending +on service names, ie., both nginx and ghost have the same reaction method, +however, nginx will execute since it's a higher precedence. + +@when('nginx.available', 'website.available') +def configure_website(website): + website.configure(port=config['port']) +""" diff --git a/kubeapi-load-balancer/reactive/status.py b/kubeapi-load-balancer/reactive/status.py new file mode 100644 index 0000000..2f33f3f --- /dev/null +++ b/kubeapi-load-balancer/reactive/status.py @@ -0,0 +1,4 @@ +from charms import layer + + +layer.status._initialize() diff --git a/kubeapi-load-balancer/reactive/tls_client.py b/kubeapi-load-balancer/reactive/tls_client.py new file mode 100644 index 0000000..afa2228 --- /dev/null +++ b/kubeapi-load-balancer/reactive/tls_client.py @@ -0,0 +1,208 @@ +import os + +from pathlib import Path +from subprocess import check_call + +from charms import layer +from charms.reactive import hook +from charms.reactive import set_state, remove_state +from charms.reactive import when +from charms.reactive import set_flag, clear_flag +from charms.reactive import endpoint_from_flag +from charms.reactive.helpers import data_changed + +from charmhelpers.core import hookenv, unitdata +from charmhelpers.core.hookenv import log + + +@when('certificates.ca.available') +def store_ca(tls): + '''Read the certificate authority from the relation object and install + the ca on this system.''' + # Get the CA from the relationship object. + certificate_authority = tls.get_ca() + if certificate_authority: + layer_options = layer.options('tls-client') + ca_path = layer_options.get('ca_certificate_path') + changed = data_changed('certificate_authority', certificate_authority) + if ca_path: + if changed or not os.path.exists(ca_path): + log('Writing CA certificate to {0}'.format(ca_path)) + # ensure we have a newline at the end of the certificate. + # some things will blow up without one. + # See https://bugs.launchpad.net/charm-kubernetes-master/+bug/1828034 + if not certificate_authority.endswith('\n'): + certificate_authority += '\n' + _write_file(ca_path, certificate_authority) + set_state('tls_client.ca.written') + set_state('tls_client.ca.saved') + if changed: + # Update /etc/ssl/certs and generate ca-certificates.crt + install_ca(certificate_authority) + + +@when('certificates.server.cert.available') +def store_server(tls): + '''Read the server certificate and server key from the relation object + and save them to the certificate directory..''' + server_cert, server_key = tls.get_server_cert() + chain = tls.get_chain() + if chain: + server_cert = server_cert + '\n' + chain + if server_cert and server_key: + layer_options = layer.options('tls-client') + cert_path = layer_options.get('server_certificate_path') + key_path = layer_options.get('server_key_path') + cert_changed = data_changed('server_certificate', server_cert) + key_changed = data_changed('server_key', server_key) + if cert_path: + if cert_changed or not os.path.exists(cert_path): + log('Writing server certificate to {0}'.format(cert_path)) + _write_file(cert_path, server_cert) + set_state('tls_client.server.certificate.written') + set_state('tls_client.server.certificate.saved') + if key_path: + if key_changed or not os.path.exists(key_path): + log('Writing server key to {0}'.format(key_path)) + _write_file(key_path, server_key) + set_state('tls_client.server.key.saved') + + +@when('certificates.client.cert.available') +def store_client(tls): + '''Read the client certificate and client key from the relation object + and copy them to the certificate directory.''' + client_cert, client_key = tls.get_client_cert() + chain = tls.get_chain() + if chain: + client_cert = client_cert + '\n' + chain + if client_cert and client_key: + layer_options = layer.options('tls-client') + cert_path = layer_options.get('client_certificate_path') + key_path = layer_options.get('client_key_path') + cert_changed = data_changed('client_certificate', client_cert) + key_changed = data_changed('client_key', client_key) + if cert_path: + if cert_changed or not os.path.exists(cert_path): + log('Writing client certificate to {0}'.format(cert_path)) + _write_file(cert_path, client_cert) + set_state('tls_client.client.certificate.written') + set_state('tls_client.client.certificate.saved') + if key_path: + if key_changed or not os.path.exists(key_path): + log('Writing client key to {0}'.format(key_path)) + _write_file(key_path, client_key) + set_state('tls_client.client.key.saved') + + +@when('certificates.certs.changed') +def update_certs(): + tls = endpoint_from_flag('certificates.certs.changed') + certs_paths = unitdata.kv().get('layer.tls-client.cert-paths', {}) + all_ready = True + any_changed = False + maps = { + 'server': tls.server_certs_map, + 'client': tls.client_certs_map, + } + + if maps.get('client') == {}: + log( + 'No client certs found using maps. Checking for global \ + client certificates.', + 'WARNING' + ) + # Check for global certs, + # Backwards compatibility https://bugs.launchpad.net/charm-kubernetes-master/+bug/1825819 + cert_pair = tls.get_client_cert() + if cert_pair is not None: + for client_name in certs_paths.get('client', {}).keys(): + maps.get('client').update({ + client_name: cert_pair + }) + + chain = tls.get_chain() + for cert_type in ('server', 'client'): + for common_name, paths in certs_paths.get(cert_type, {}).items(): + cert_pair = maps[cert_type].get(common_name) + if not cert_pair: + all_ready = False + continue + if not data_changed('layer.tls-client.' + '{}.{}'.format(cert_type, common_name), cert_pair): + continue + + cert = None + key = None + if type(cert_pair) is not tuple: + if paths['crt']: + cert = cert_pair.cert + if paths['key']: + key = cert_pair.key + else: + cert, key = cert_pair + + if cert: + if chain: + cert = cert + '\n' + chain + _ensure_directory(paths['crt']) + Path(paths['crt']).write_text(cert) + + if key: + _ensure_directory(paths['key']) + Path(paths['key']).write_text(key) + + any_changed = True + # clear flags first to ensure they are re-triggered if left set + clear_flag('tls_client.{}.certs.changed'.format(cert_type)) + clear_flag('tls_client.{}.cert.{}.changed'.format(cert_type, + common_name)) + set_flag('tls_client.{}.certs.changed'.format(cert_type)) + set_flag('tls_client.{}.cert.{}.changed'.format(cert_type, + common_name)) + if all_ready: + set_flag('tls_client.certs.saved') + if any_changed: + clear_flag('tls_client.certs.changed') + set_flag('tls_client.certs.changed') + clear_flag('certificates.certs.changed') + + +def install_ca(certificate_authority): + '''Install a certificiate authority on the system by calling the + update-ca-certificates command.''' + if certificate_authority: + name = hookenv.service_name() + # Create a path to install CAs on Debian systems. + ca_path = '/usr/local/share/ca-certificates/{0}.crt'.format(name) + log('Writing CA certificate to {0}'.format(ca_path)) + _write_file(ca_path, certificate_authority) + # Update the trusted CAs on this system (a time expensive operation). + check_call(['update-ca-certificates']) + log('Generated ca-certificates.crt for {0}'.format(name)) + set_state('tls_client.ca_installed') + + +@hook('upgrade-charm') +def remove_states(): + remove_state('tls_client.ca.saved') + remove_state('tls_client.server.certificate.saved') + remove_state('tls_client.server.key.saved') + remove_state('tls_client.client.certificate.saved') + remove_state('tls_client.client.key.saved') + + +def _ensure_directory(path): + '''Ensure the parent directory exists creating directories if necessary.''' + directory = os.path.dirname(path) + if not os.path.isdir(directory): + os.makedirs(directory) + os.chmod(directory, 0o770) + + +def _write_file(path, content): + '''Write the path to a file.''' + _ensure_directory(path) + with open(path, 'w') as stream: + stream.write(content) + os.chmod(path, 0o440) diff --git a/kubeapi-load-balancer/requirements.txt b/kubeapi-load-balancer/requirements.txt new file mode 100644 index 0000000..55543d9 --- /dev/null +++ b/kubeapi-load-balancer/requirements.txt @@ -0,0 +1,3 @@ +mock +flake8 +pytest diff --git a/kubeapi-load-balancer/revision b/kubeapi-load-balancer/revision new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/kubeapi-load-balancer/revision @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/kubeapi-load-balancer/templates/.gitkeep b/kubeapi-load-balancer/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/kubeapi-load-balancer/templates/apilb.conf b/kubeapi-load-balancer/templates/apilb.conf new file mode 100644 index 0000000..dc6e56c --- /dev/null +++ b/kubeapi-load-balancer/templates/apilb.conf @@ -0,0 +1,41 @@ +{% for server_port, backends in servers.items() -%} +upstream upstream_{{ server_port }} { + {%- for backend, backend_port in backends %} + server {{ backend }}:{{ backend_port }}; + {%- endfor %} +} + +server { + listen {{ server_port }} ssl http2; + listen [::]:{{ server_port }} ssl http2 ipv6only=on; + server_name server_{{ server_port }}; + + access_log /var/log/nginx.access.log; + error_log /var/log/nginx.error.log; + + ssl on; + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_certificate {{ server_certificate }}; + ssl_certificate_key {{ server_key }}; + ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; + ssl_prefer_server_ciphers on; + + + location / { + proxy_buffering off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto-Version $http2; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_set_header X-Stream-Protocol-Version $http_x_stream_protocol_version; + + add_header X-Stream-Protocol-Version $upstream_http_x_stream_protocol_version; + + proxy_pass https://upstream_{{ server_port }}; + proxy_read_timeout {{ proxy_read_timeout }}; + } +} +{%- endfor %} diff --git a/kubeapi-load-balancer/templates/cdk.auth-webhook-secret.yaml b/kubeapi-load-balancer/templates/cdk.auth-webhook-secret.yaml new file mode 100644 index 0000000..a12c402 --- /dev/null +++ b/kubeapi-load-balancer/templates/cdk.auth-webhook-secret.yaml @@ -0,0 +1,13 @@ +# Manifest for CK secrets that auth-webhook expects +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ secret_name }} + namespace: {{ secret_namespace }} +type: {{ type }} +data: + uid: {{ user }} + username: {{ username }} + password: {{ password }} + groups: '{{ groups }}' diff --git a/kubeapi-load-balancer/templates/vhost.conf.ex b/kubeapi-load-balancer/templates/vhost.conf.ex new file mode 100644 index 0000000..253be36 --- /dev/null +++ b/kubeapi-load-balancer/templates/vhost.conf.ex @@ -0,0 +1,18 @@ +server { + listen 80; + + server_name {{server_name}}; + + location / { + proxy_redirect off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; + proxy_set_header Connection ""; + proxy_http_version 1.1; + proxy_pass http://{{host}}:{{port}}; + + } +} diff --git a/kubeapi-load-balancer/tests/data/bundle.yaml b/kubeapi-load-balancer/tests/data/bundle.yaml new file mode 100644 index 0000000..be515f5 --- /dev/null +++ b/kubeapi-load-balancer/tests/data/bundle.yaml @@ -0,0 +1,107 @@ +description: A minimal two-machine Kubernetes cluster, appropriate for development. +series: focal +machines: + '0': + constraints: cores=2 mem=4G root-disk=16G + series: focal + '1': + constraints: cores=4 mem=4G root-disk=16G + series: focal +services: + containerd: + charm: cs:~containers/containerd + channel: edge + resources: {} + easyrsa: + charm: cs:~containers/easyrsa + channel: edge + num_units: 1 + resources: + easyrsa: 5 + to: + - '1' + etcd: + charm: cs:~containers/etcd + channel: edge + num_units: 1 + options: + channel: 3.4/stable + resources: + core: 0 + etcd: 3 + snapshot: 0 + to: + - '0' + flannel: + charm: cs:~containers/flannel + channel: edge + resources: + flannel-amd64: 653 + flannel-arm64: 650 + flannel-s390x: 637 + kubernetes-master: + charm: cs:~containers/kubernetes-master + channel: edge + constraints: cores=2 mem=4G root-disk=16G + expose: true + num_units: 1 + options: + channel: 1.20/stable + resources: + cdk-addons: 0 + core: 0 + kube-apiserver: 0 + kube-controller-manager: 0 + kube-proxy: 0 + kube-scheduler: 0 + kubectl: 0 + to: + - '0' + kubernetes-worker: + charm: cs:~containers/kubernetes-worker + channel: edge + constraints: cores=4 mem=4G root-disk=16G + num_units: 1 + options: + channel: 1.20/stable + resources: + cni-amd64: 690 + cni-arm64: 681 + cni-s390x: 693 + core: 0 + kube-proxy: 0 + kubectl: 0 + kubelet: 0 + to: + - '1' + kubeapi-load-balancer: + charm: {{k8s_lb_charm}} + num_units: 1 + expose: true +relations: +- - kubeapi-load-balancer:lb-consumers + - kubernetes-master:loadbalancer-internal +- - kubeapi-load-balancer:lb-consumers + - kubernetes-master:loadbalancer-external +- - kubernetes-master:kube-control + - kubernetes-worker:kube-control +- - kubernetes-master:certificates + - easyrsa:client +- - kubeapi-load-balancer:certificates + - easyrsa:client +- - kubernetes-master:etcd + - etcd:db +- - kubernetes-worker:certificates + - easyrsa:client +- - etcd:certificates + - easyrsa:client +- - flannel:etcd + - etcd:db +- - flannel:cni + - kubernetes-master:cni +- - flannel:cni + - kubernetes-worker:cni +- - containerd:containerd + - kubernetes-worker:container-runtime +- - containerd:containerd + - kubernetes-master:container-runtime diff --git a/kubeapi-load-balancer/tests/functional/conftest.py b/kubeapi-load-balancer/tests/functional/conftest.py new file mode 100644 index 0000000..a92e249 --- /dev/null +++ b/kubeapi-load-balancer/tests/functional/conftest.py @@ -0,0 +1,4 @@ +import charms.unit_test + + +charms.unit_test.patch_reactive() diff --git a/kubeapi-load-balancer/tests/functional/test_k8s_common.py b/kubeapi-load-balancer/tests/functional/test_k8s_common.py new file mode 100644 index 0000000..4b867e6 --- /dev/null +++ b/kubeapi-load-balancer/tests/functional/test_k8s_common.py @@ -0,0 +1,90 @@ +from functools import partial + +import pytest +from unittest import mock +from charms.layer import kubernetes_common + + +class TestCreateKubeConfig: + @pytest.fixture(autouse=True) + def _files(self, tmp_path): + self.cfg_file = tmp_path / "config" + self.ca_file = tmp_path / "ca.crt" + self.ca_file.write_text("foo") + self.ckc = partial( + kubernetes_common.create_kubeconfig, + self.cfg_file, + "server", + self.ca_file, + ) + + def test_guard_clauses(self): + with pytest.raises(ValueError): + self.ckc() + assert not self.cfg_file.exists() + with pytest.raises(ValueError): + self.ckc(token="token", password="password") + assert not self.cfg_file.exists() + with pytest.raises(ValueError): + self.ckc(key="key") + assert not self.cfg_file.exists() + + def test_file_creation(self): + self.ckc(password="password") + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert cfg_data_1 + + def test_idempotency(self): + self.ckc(password="password") + cfg_data_1 = self.cfg_file.read_text() + self.ckc(password="password") + cfg_data_2 = self.cfg_file.read_text() + # Verify that calling w/ the same data keeps the same file contents. + assert cfg_data_2 == cfg_data_1 + + def test_efficient_updates(self): + self.ckc(password="old_password") + cfg_stat_1 = self.cfg_file.stat() + self.ckc(password="old_password") + cfg_stat_2 = self.cfg_file.stat() + self.ckc(password="new_password") + cfg_stat_3 = self.cfg_file.stat() + # Verify that calling with the same data doesn't + # modify the file at all, but that new data does + assert cfg_stat_1.st_mtime == cfg_stat_2.st_mtime < cfg_stat_3.st_mtime + + def test_aws_iam(self): + self.ckc(password="password", aws_iam_cluster_id="aws-cluster") + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert "aws-cluster" in cfg_data_1 + + def test_keystone(self): + self.ckc(password="password", keystone=True) + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert "keystone-user" in cfg_data_1 + assert "exec" in cfg_data_1 + + def test_atomic_updates(self): + self.ckc(password="old_password") + with self.cfg_file.open("rt") as f: + # Perform a write in the middle of reading + self.ckc(password="new_password") + # Read data from existing FH after new data was written + cfg_data_1 = f.read() + # Read updated data + cfg_data_2 = self.cfg_file.read_text() + # Verify that the in-progress read didn't get any of the new data + assert cfg_data_1 != cfg_data_2 + assert "old_password" in cfg_data_1 + assert "new_password" in cfg_data_2 + + @mock.patch("charmhelpers.core.hookenv.network_get", autospec=True) + def test_get_ingress_address(self, network_get): + network_get.return_value = {"ingress-addresses": ["1.2.3.4", "5.6.7.8"]} + ingress = kubernetes_common.get_ingress_address("endpoint-name") + assert ingress == "1.2.3.4" + ingress = kubernetes_common.get_ingress_address("endpoint-name", ["1.2.3.4"]) + assert ingress == "5.6.7.8" diff --git a/kubeapi-load-balancer/tests/integration/test_kubeapi-load-balancer_integration.py b/kubeapi-load-balancer/tests/integration/test_kubeapi-load-balancer_integration.py new file mode 100644 index 0000000..554ed94 --- /dev/null +++ b/kubeapi-load-balancer/tests/integration/test_kubeapi-load-balancer_integration.py @@ -0,0 +1,41 @@ +import logging + +import pytest + + +log = logging.getLogger(__name__) + + +def _check_status_messages(ops_test): + """ Validate that the status messages are correct. """ + expected_messages = { + "kubernetes-master": "Kubernetes master running.", + "kubernetes-worker": "Kubernetes worker running.", + "kubeapi-load-balancer": "Loadbalancer ready.", + } + for app, message in expected_messages.items(): + for unit in ops_test.model.applications[app].units: + assert unit.workload_status_message == message + + +@pytest.mark.abort_on_fail +async def test_build_and_deploy(ops_test): + bundle = ops_test.render_bundle( + "tests/data/bundle.yaml", k8s_lb_charm=await ops_test.build_charm(".") + ) + await ops_test.model.deploy(bundle) + await ops_test.model.wait_for_idle(wait_for_active=True, timeout=60 * 60) + _check_status_messages(ops_test) + + +async def test_kube_api_endpoint(ops_test): + """ Validate that using the old MITM-style relation works""" + master = ops_test.model.applications["kubernetes-master"] + worker = ops_test.model.applications["kubernetes-worker"] + await master.remove_relation("loadbalancer-internal", "kubeapi-load-balancer") + await master.remove_relation("loadbalancer-external", "kubeapi-load-balancer") + await master.add_relation("kube-api-endpoint", "kubeapi-load-balancer") + await master.add_relation("loadbalancer", "kubeapi-load-balancer") + await worker.add_relation("kube-api-endpoint", "kubeapi-load-balancer") + await ops_test.model.wait_for_idle(wait_for_active=True, timeout=30 * 60) + _check_status_messages(ops_test) diff --git a/kubeapi-load-balancer/tests/unit/conftest.py b/kubeapi-load-balancer/tests/unit/conftest.py new file mode 100644 index 0000000..a5c275b --- /dev/null +++ b/kubeapi-load-balancer/tests/unit/conftest.py @@ -0,0 +1,4 @@ +import charms.unit_test + +charms.unit_test.patch_reactive() +charms.unit_test.patch_module('subprocess') diff --git a/kubeapi-load-balancer/tests/unit/test_k8s_common.py b/kubeapi-load-balancer/tests/unit/test_k8s_common.py new file mode 100644 index 0000000..0dcad31 --- /dev/null +++ b/kubeapi-load-balancer/tests/unit/test_k8s_common.py @@ -0,0 +1,122 @@ +import json +import string +from subprocess import CalledProcessError +from unittest.mock import Mock + +from charms.layer import kubernetes_common as kc + + +def test_token_generator(): + alphanum = string.ascii_letters + string.digits + token = kc.token_generator(10) + assert len(token) == 10 + unknown_chars = set(token) - set(alphanum) + assert not unknown_chars + + +def test_get_secret_names(monkeypatch): + monkeypatch.setattr(kc, "kubectl", Mock()) + kc.kubectl.side_effect = [ + CalledProcessError(1, "none"), + FileNotFoundError, + "{}".encode("utf8"), + json.dumps( + { + "items": [ + { + "metadata": {"name": "secret-id"}, + "data": {"username": "dXNlcg=="}, + }, + ], + } + ).encode("utf8"), + ] + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {"user": "secret-id"} + + +def test_generate_rfc1123(): + alphanum = string.ascii_letters + string.digits + token = kc.generate_rfc1123(1000) + assert len(token) == 253 + unknown_chars = set(token) - set(alphanum) + assert not unknown_chars + + +def test_create_secret(monkeypatch): + monkeypatch.setattr(kc, "render", Mock()) + monkeypatch.setattr(kc, "kubectl_manifest", Mock()) + monkeypatch.setattr(kc, "get_secret_names", Mock()) + monkeypatch.setattr(kc, "generate_rfc1123", Mock()) + kc.kubectl_manifest.side_effect = [True, False] + kc.get_secret_names.side_effect = [{"username": "secret-id"}, {}] + kc.generate_rfc1123.return_value = "foo" + assert kc.create_secret("token", "username", "user", "groups") + assert kc.render.call_args[1]["context"] == { + "groups": "Z3JvdXBz", + "password": "dXNlcjo6dG9rZW4=", + "secret_name": "secret-id", + "secret_namespace": "kube-system", + "type": "juju.is/token-auth", + "user": "dXNlcg==", + "username": "dXNlcm5hbWU=", + } + assert not kc.create_secret("token", "username", "user", "groups") + assert kc.render.call_args[1]["context"] == { + "groups": "Z3JvdXBz", + "password": "dXNlcjo6dG9rZW4=", + "secret_name": "auth-user-foo", + "secret_namespace": "kube-system", + "type": "juju.is/token-auth", + "user": "dXNlcg==", + "username": "dXNlcm5hbWU=", + } + + +def test_get_secret_password(monkeypatch): + monkeypatch.setattr(kc, "kubectl", Mock()) + monkeypatch.setattr(kc, "Path", Mock()) + monkeypatch.setattr(kc, "yaml", Mock()) + kc.kubectl.side_effect = [ + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + FileNotFoundError, + json.dumps({}).encode("utf8"), + json.dumps({"items": []}).encode("utf8"), + json.dumps({"items": []}).encode("utf8"), + json.dumps({"items": [{}]}).encode("utf8"), + json.dumps({"items": [{"data": {}}]}).encode("utf8"), + json.dumps( + {"items": [{"data": {"username": "Ym9i", "password": "c2VjcmV0"}}]} + ).encode("utf8"), + json.dumps( + {"items": [{"data": {"username": "dXNlcm5hbWU=", "password": "c2VjcmV0"}}]} + ).encode("utf8"), + ] + kc.yaml.safe_load.side_effect = [ + {}, + {"users": None}, + {"users": []}, + {"users": [{"user": {}}]}, + {"users": [{"user": {"token": "secret"}}]}, + ] + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") == "secret" + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") == "secret" diff --git a/kubeapi-load-balancer/tests/unit/test_kubeapi_load_balancer.py b/kubeapi-load-balancer/tests/unit/test_kubeapi_load_balancer.py new file mode 100644 index 0000000..4ea2763 --- /dev/null +++ b/kubeapi-load-balancer/tests/unit/test_kubeapi_load_balancer.py @@ -0,0 +1,17 @@ +from charmhelpers.core import host # patched + +from reactive import load_balancer as handlers + + +def test_series_upgrade(): + assert host.service_pause.call_count == 0 + assert host.service_resume.call_count == 0 + assert handlers.status.blocked.call_count == 0 + handlers.pre_series_upgrade() + assert host.service_pause.call_count == 1 + assert host.service_resume.call_count == 0 + assert handlers.status.blocked.call_count == 1 + handlers.post_series_upgrade() + assert host.service_pause.call_count == 1 + assert host.service_resume.call_count == 1 + assert handlers.status.blocked.call_count == 1 diff --git a/kubeapi-load-balancer/tests/validate-wheelhouse.sh b/kubeapi-load-balancer/tests/validate-wheelhouse.sh new file mode 100755 index 0000000..329dfca --- /dev/null +++ b/kubeapi-load-balancer/tests/validate-wheelhouse.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +build_dir="$(mktemp -d)" +function cleanup { rm -rf "$build_dir"; } +trap cleanup EXIT + +charm build . --build-dir "$build_dir" +pip install -f "$build_dir/kubeapi-load-balancer/wheelhouse" --no-index --no-cache-dir "$build_dir"/kubeapi-load-balancer/wheelhouse/* diff --git a/kubeapi-load-balancer/tox.ini b/kubeapi-load-balancer/tox.ini new file mode 100644 index 0000000..8eee556 --- /dev/null +++ b/kubeapi-load-balancer/tox.ini @@ -0,0 +1,34 @@ +[tox] +skipsdist = True +envlist = lint,unit + +[testenv] +basepython = python3 +setenv = + PYTHONPATH={toxinidir}:{toxinidir}/lib + +[testenv:unit] +deps = + pytest + ipdb + git+https://github.com/juju-solutions/charms.unit_test/#egg=charms.unit_test +commands = pytest --tb native -s --show-capture=no --log-cli-level=INFO {posargs} {toxinidir}/tests/unit + +[testenv:integration] +deps = + pytest + pytest-operator + ipdb +commands = pytest --tb native --show-capture=no --log-cli-level=INFO -s {posargs} {toxinidir}/tests/integration + +[testenv:lint] +deps = + flake8 +commands = flake8 {toxinidir}/reactive {toxinidir}/tests + +[testenv:validate-wheelhouse] +allowlist_externals = {toxinidir}/tests/validate-wheelhouse.sh +commands = {toxinidir}/tests/validate-wheelhouse.sh + +[flake8] +max-line-length = 88 diff --git a/kubeapi-load-balancer/version b/kubeapi-load-balancer/version new file mode 100644 index 0000000..20817dd --- /dev/null +++ b/kubeapi-load-balancer/version @@ -0,0 +1 @@ +ccfa68be \ No newline at end of file diff --git a/kubeapi-load-balancer/wheelhouse.txt b/kubeapi-load-balancer/wheelhouse.txt new file mode 100644 index 0000000..b1fa19e --- /dev/null +++ b/kubeapi-load-balancer/wheelhouse.txt @@ -0,0 +1,24 @@ +# layer:basic +# pip is pinned to <19.0 to avoid https://github.com/pypa/pip/issues/6164 +# even with installing setuptools before upgrading pip ends up with pip seeing +# the older setuptools at the system level if include_system_packages is true +pip>=18.1,<19.0 +# pin Jinja2, PyYAML and MarkupSafe to the last versions supporting python 3.5 +# for trusty +Jinja2<=2.10.1 +PyYAML<=5.2 +MarkupSafe<2.0.0 +setuptools<42 +setuptools-scm<=1.17.0 +charmhelpers>=0.4.0,<1.0.0 +charms.reactive>=0.1.0,<2.0.0 +wheel<0.34 +# pin netaddr to avoid pulling importlib-resources +netaddr<=0.7.19 + +# layer:nginx +toml + +# kubeapi-load-balancer +loadbalancer-interface + diff --git a/kubeapi-load-balancer/wheelhouse/Jinja2-2.10.1.tar.gz b/kubeapi-load-balancer/wheelhouse/Jinja2-2.10.1.tar.gz new file mode 100644 index 0000000..ffd1054 Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/Jinja2-2.10.1.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/MarkupSafe-1.1.1.tar.gz b/kubeapi-load-balancer/wheelhouse/MarkupSafe-1.1.1.tar.gz new file mode 100644 index 0000000..a6dad8e Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/MarkupSafe-1.1.1.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/PyYAML-5.2.tar.gz b/kubeapi-load-balancer/wheelhouse/PyYAML-5.2.tar.gz new file mode 100644 index 0000000..666d12a Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/PyYAML-5.2.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/Tempita-0.5.2.tar.gz b/kubeapi-load-balancer/wheelhouse/Tempita-0.5.2.tar.gz new file mode 100644 index 0000000..755befc Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/Tempita-0.5.2.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/cached-property-1.5.2.tar.gz b/kubeapi-load-balancer/wheelhouse/cached-property-1.5.2.tar.gz new file mode 100644 index 0000000..501f2c0 Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/cached-property-1.5.2.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/charmhelpers-0.20.23.tar.gz b/kubeapi-load-balancer/wheelhouse/charmhelpers-0.20.23.tar.gz new file mode 100644 index 0000000..8fbc8ec Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/charmhelpers-0.20.23.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/charms.reactive-1.4.1.tar.gz b/kubeapi-load-balancer/wheelhouse/charms.reactive-1.4.1.tar.gz new file mode 100644 index 0000000..03bc1fe Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/charms.reactive-1.4.1.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/loadbalancer_interface-1.1.1.tar.gz b/kubeapi-load-balancer/wheelhouse/loadbalancer_interface-1.1.1.tar.gz new file mode 100644 index 0000000..265133c Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/loadbalancer_interface-1.1.1.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/marshmallow-3.14.0.tar.gz b/kubeapi-load-balancer/wheelhouse/marshmallow-3.14.0.tar.gz new file mode 100644 index 0000000..14e4efc Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/marshmallow-3.14.0.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/marshmallow-enum-1.5.1.tar.gz b/kubeapi-load-balancer/wheelhouse/marshmallow-enum-1.5.1.tar.gz new file mode 100644 index 0000000..642941a Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/marshmallow-enum-1.5.1.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/netaddr-0.7.19.tar.gz b/kubeapi-load-balancer/wheelhouse/netaddr-0.7.19.tar.gz new file mode 100644 index 0000000..cc31d9d Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/netaddr-0.7.19.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/ops-1.2.0.tar.gz b/kubeapi-load-balancer/wheelhouse/ops-1.2.0.tar.gz new file mode 100644 index 0000000..2cb4358 Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/ops-1.2.0.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/ops_reactive_interface-1.0.1.tar.gz b/kubeapi-load-balancer/wheelhouse/ops_reactive_interface-1.0.1.tar.gz new file mode 100644 index 0000000..14f5ded Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/ops_reactive_interface-1.0.1.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/pbr-5.6.0.tar.gz b/kubeapi-load-balancer/wheelhouse/pbr-5.6.0.tar.gz new file mode 100644 index 0000000..0d5c965 Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/pbr-5.6.0.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/pip-18.1.tar.gz b/kubeapi-load-balancer/wheelhouse/pip-18.1.tar.gz new file mode 100644 index 0000000..a18192d Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/pip-18.1.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/pyaml-21.10.1.tar.gz b/kubeapi-load-balancer/wheelhouse/pyaml-21.10.1.tar.gz new file mode 100644 index 0000000..b19aad3 Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/pyaml-21.10.1.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/setuptools-41.6.0.zip b/kubeapi-load-balancer/wheelhouse/setuptools-41.6.0.zip new file mode 100644 index 0000000..3345759 Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/setuptools-41.6.0.zip differ diff --git a/kubeapi-load-balancer/wheelhouse/setuptools_scm-1.17.0.tar.gz b/kubeapi-load-balancer/wheelhouse/setuptools_scm-1.17.0.tar.gz new file mode 100644 index 0000000..43b16c7 Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/setuptools_scm-1.17.0.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/six-1.16.0.tar.gz b/kubeapi-load-balancer/wheelhouse/six-1.16.0.tar.gz new file mode 100644 index 0000000..5bf3a27 Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/six-1.16.0.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/toml-0.10.2.tar.gz b/kubeapi-load-balancer/wheelhouse/toml-0.10.2.tar.gz new file mode 100644 index 0000000..41dd278 Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/toml-0.10.2.tar.gz differ diff --git a/kubeapi-load-balancer/wheelhouse/wheel-0.33.6.tar.gz b/kubeapi-load-balancer/wheelhouse/wheel-0.33.6.tar.gz new file mode 100644 index 0000000..c922c4e Binary files /dev/null and b/kubeapi-load-balancer/wheelhouse/wheel-0.33.6.tar.gz differ diff --git a/kubernetes-master/.build.manifest b/kubernetes-master/.build.manifest new file mode 100644 index 0000000..0b538db --- /dev/null +++ b/kubernetes-master/.build.manifest @@ -0,0 +1,2771 @@ +{ + "layers": [ + { + "branch": "refs/heads/master", + "rev": "fcdcea4e5de3e1556c24e6704607862d0ba00a56", + "url": "layer:options" + }, + { + "branch": "refs/heads/master", + "rev": "a3ff62c32c993d80417f6e093e3ef95e42f62083", + "url": "layer:basic" + }, + { + "branch": "refs/heads/master", + "rev": "d3acdf209cbaf5b732e9aba621778a0f56dbaeb9", + "url": "layer:snap" + }, + { + "branch": "refs/heads/master", + "rev": "527dd64fc4b9a6b0f8d80a3c2c0b865155050275", + "url": "layer:debug" + }, + { + "branch": "refs/heads/master", + "rev": "fb46dec78d390571753d21876bbba689bbbca9e4", + "url": "layer:tls-client" + }, + { + "branch": "refs/heads/master", + "rev": "cc5bd3f49b2fa5e6c3ab2336763c313ec8bf083f", + "url": "layer:leadership" + }, + { + "branch": "refs/heads/master", + "rev": "f491ebe32b503c9712d2f8cd602dcce18f4aab46", + "url": "layer:metrics" + }, + { + "branch": "refs/heads/master", + "rev": "47dfcd4920ef6317850a4837ef0057ab0092a18e", + "url": "layer:nagios" + }, + { + "branch": "refs/heads/master", + "rev": "b60102068c6f0ddbeaf8a308549a3e88cfa35688", + "url": "layer:cdk-service-kicker" + }, + { + "branch": "refs/heads/master", + "rev": "023c67941e18663a4df49f53edba809f43ba5069", + "url": "layer:cis-benchmark" + }, + { + "branch": "refs/heads/master", + "rev": "fa27fc93e0b08000963e83a6bfe49812d890dfcf", + "url": "layer:coordinator" + }, + { + "branch": "refs/heads/master", + "rev": "bbeabfee52c4442cdaf3a34e5e35530a3bd71156", + "url": "layer:kubernetes-common" + }, + { + "branch": "refs/heads/master", + "rev": "809f06c6f6521be59e21859eaebeccd13f4d8c28", + "url": "layer:kubernetes-master-worker-base" + }, + { + "branch": "refs/heads/master", + "rev": "e22c18b133070ce354cebbda864a5aa8a4b60398", + "url": "layer:vault-kv" + }, + { + "branch": "refs/heads/master", + "rev": "a7d7b6423db37a47611310039e6ed1929c0a2eab", + "url": "layer:status" + }, + { + "branch": "refs/heads/master", + "rev": "76bddfb640ab8767fc7e4a4b73a4a4e781948f34", + "url": "layer:apt" + }, + { + "branch": "refs/heads/master", + "rev": "2c4c16cd9e4254494d79aac1d17eacf1620d1b0f", + "url": "layer:vaultlocker" + }, + { + "branch": "refs/heads/master", + "rev": "a8f88f16bb7771807a0f7fdb17ee16b0e310fc2b", + "url": "layer:hacluster" + }, + { + "branch": "refs/heads/master", + "rev": "31070c6182c76824c1541c571df334454f4fabaf", + "url": "kubernetes-master" + }, + { + "branch": "refs/heads/master", + "rev": "d9850016d930a6d507b9fd45e2598d327922b140", + "url": "interface:tls-certificates" + }, + { + "branch": "refs/heads/master", + "rev": "2e0e1fdea6d83b55078200aacb537d60013ec5bc", + "url": "interface:nrpe-external-master" + }, + { + "branch": "refs/heads/master", + "rev": "6f927f10b97f45c566481cf57a29d433f17373e1", + "url": "interface:container-runtime" + }, + { + "branch": "refs/heads/master", + "rev": "dceab99ac3739cc7265e386287f100f1bfebc47f", + "url": "interface:vault-kv" + }, + { + "branch": "refs/heads/master", + "rev": "8125a7baecccf9b0869e515b92300dde3a86f31b", + "url": "interface:hacluster" + }, + { + "branch": "refs/heads/master", + "rev": "c1061a29297084fa53c2474ba371671186ff3389", + "url": "interface:ceph-admin" + }, + { + "branch": "(HEAD detached at ccfa68be)", + "rev": "ccfa68bef24ab3e7f9f1e85c082390de1d62707d", + "url": "interface:ceph-client" + }, + { + "branch": "refs/heads/master", + "rev": "44f244cbd08b86bf2b68bd71c3fb34c7c070c382", + "url": "interface:etcd" + }, + { + "branch": "refs/heads/master", + "rev": "632131b1f122daf6fb601fd4c9f1e4dbb1a92e09", + "url": "interface:http" + }, + { + "branch": "refs/heads/master", + "rev": "88b1e8fad78d06efdbf512cd75eaa0bb308eb1c1", + "url": "interface:kubernetes-cni" + }, + { + "branch": "refs/heads/master", + "rev": "9bc32742b7720a755ada9526424e5d80092e1536", + "url": "interface:kube-dns" + }, + { + "branch": "refs/heads/master", + "rev": "534310f5bca8edde02cadaf6ac42231cea0b040b", + "url": "interface:kube-control" + }, + { + "branch": "refs/heads/master", + "rev": "8e486e329dd12f70c4220874c795c0f0280d99ae", + "url": "interface:kube-masters" + }, + { + "branch": "refs/heads/master", + "rev": "5021f8a23f6e6e4cc449d2d02f2d8cb99763ec27", + "url": "interface:public-address" + }, + { + "branch": "refs/heads/master", + "rev": "d8d8c7ef17c99ad53383f3cabf4cf5c8191d16f7", + "url": "interface:aws-integration" + }, + { + "branch": "refs/heads/master", + "rev": "d8f093cb2930edf5f93678253dca2da70b73b4fb", + "url": "interface:gcp-integration" + }, + { + "branch": "refs/heads/master", + "rev": "0d7a994f04b9e92ed847829ce8349b1a9c672e47", + "url": "interface:openstack-integration" + }, + { + "branch": "refs/heads/master", + "rev": "d5caea55ced6785f391215ee457c3a964eaf3f4b", + "url": "interface:vsphere-integration" + }, + { + "branch": "refs/heads/master", + "rev": "8d2202e433d7c188de4df2fd4bddb355193e93ac", + "url": "interface:azure-integration" + }, + { + "branch": "refs/heads/master", + "rev": "53e93b8820899f2251d207ed5d5c3b212ceb64de", + "url": "interface:keystone-credentials" + }, + { + "branch": "refs/heads/master", + "rev": "3f775242c16d53243c993d7ba0c896169ad1639e", + "url": "interface:prometheus-manual" + }, + { + "branch": "refs/heads/master", + "rev": "e64261e281f012a00d374c6779ec52e488cb8713", + "url": "interface:grafana-dashboard" + }, + { + "branch": "refs/heads/master", + "rev": "e9a8c168b81b687790119dd6df2e7a4c1f729c41", + "url": "interface:aws-iam" + } + ], + "signatures": { + ".build.manifest": [ + "build", + "dynamic", + "unchecked" + ], + ".github/workflows/main.yml": [ + "kubernetes-master", + "static", + "ac395c5924c9a8f884b7db92f4bef24e3dd8fd8b4d084976a0994adbc020a349" + ], + ".gitignore": [ + "kubernetes-master", + "static", + "2d275519f0da994b9db61e63c8010917da1f4779199f18f1012f23f9b16b353e" + ], + ".travis.yml": [ + "layer:cis-benchmark", + "static", + "b6dbe144aa288b8a89caf1119b9835b407b234c9b32a1c81013b12a0593a8be2" + ], + "CONTRIBUTING.md": [ + "kubernetes-master", + "static", + "dc83e4e868d1dbe5b1404faf736d556895a7d4ca9be3bff2d1fdebc0036993d6" + ], + "LICENSE": [ + "kubernetes-master", + "static", + "58d1e17ffe5109a7ae296caafcadfdbe6a7d176f0bc4ab01e12a689b0499d8bd" + ], + "Makefile": [ + "layer:basic", + "static", + "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301" + ], + "README.md": [ + "kubernetes-master", + "static", + "7ef5c5ab3a462939739fe83c1d7f70154ec763d452e33b7fb4f1af595b67b2cd" + ], + "actions.yaml": [ + "kubernetes-master", + "dynamic", + "2c09f607e00ff7b65d316088676af7d8df8bf7cf2c86f1f32590d57fe048837a" + ], + "actions/apply-manifest": [ + "kubernetes-master", + "static", + "1e2058489bd361db33cbb5bd66915d2747518d424dad7fac5011bf10c3fe070d" + ], + "actions/cis-benchmark": [ + "layer:cis-benchmark", + "static", + "fd3c1b8ba478b7f933605897ace8ae9f3ee102d9992f46f1e36d95eb1b094b84" + ], + "actions/debug": [ + "layer:debug", + "static", + "db0a42dae4c5045b2c06385bf22209dfe0e2ded55822ef847d84b01d9ff2b046" + ], + "actions/get-kubeconfig": [ + "kubernetes-master", + "static", + "1e2058489bd361db33cbb5bd66915d2747518d424dad7fac5011bf10c3fe070d" + ], + "actions/kubectl-actions.py": [ + "kubernetes-master", + "static", + "1e2058489bd361db33cbb5bd66915d2747518d424dad7fac5011bf10c3fe070d" + ], + "actions/namespace-create": [ + "kubernetes-master", + "static", + "fc25a90c3bdecc883028f789b5061980591a7bc26398666b8dc3e24e09c9be1c" + ], + "actions/namespace-delete": [ + "kubernetes-master", + "static", + "fc25a90c3bdecc883028f789b5061980591a7bc26398666b8dc3e24e09c9be1c" + ], + "actions/namespace-list": [ + "kubernetes-master", + "static", + "fc25a90c3bdecc883028f789b5061980591a7bc26398666b8dc3e24e09c9be1c" + ], + "actions/restart": [ + "kubernetes-master", + "static", + "72cb46d4971f057fdbbc901599a735a7ce3d61e7ae9b2687c9e9b4cd478e26d0" + ], + "actions/upgrade": [ + "kubernetes-master", + "static", + "a155a181b259f9c70f265a73851f8cecec1174f6b810a3fdd9a0749c2a91e5a5" + ], + "actions/user-create": [ + "kubernetes-master", + "static", + "2ffaf34bef1888ea11e0dba1df82e1fcf10dbe4a9a5c59e407772310f094f2a2" + ], + "actions/user-delete": [ + "kubernetes-master", + "static", + "2ffaf34bef1888ea11e0dba1df82e1fcf10dbe4a9a5c59e407772310f094f2a2" + ], + "actions/user-list": [ + "kubernetes-master", + "static", + "2ffaf34bef1888ea11e0dba1df82e1fcf10dbe4a9a5c59e407772310f094f2a2" + ], + "actions/user_actions.py": [ + "kubernetes-master", + "static", + "2ffaf34bef1888ea11e0dba1df82e1fcf10dbe4a9a5c59e407772310f094f2a2" + ], + "bin/charm-env": [ + "layer:basic", + "static", + "fb6a20fac4102a6a4b6ffe903fcf666998f9a95a3647e6f9af7a1eeb44e58fd5" + ], + "bin/layer_option": [ + "layer:options", + "static", + "e959bf29da4c5edff28b2602c24113c4df9e25cdc9f2aa3b5d46c8577b2a40cc" + ], + "config.yaml": [ + "kubernetes-master", + "dynamic", + "dbd9cf8913f4a35f21e1eb2f12c8c4a6777b653513c478b66d3c00725f9addc1" + ], + "copyright": [ + "kubernetes-master", + "static", + "1eedc4e165789729bc492abd80e34ac85dcb0ec429eebdf225129b9b0bfc3502" + ], + "copyright.layer-apt": [ + "layer:apt", + "static", + "5123b2d0220fefb4424a463216fb41a6dd7cfad49c9799ba7037f1e74a2fd6bc" + ], + "copyright.layer-basic": [ + "layer:basic", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-coordinator": [ + "layer:coordinator", + "static", + "7d212a095a6143559fb51f26bc40c2ba24b977190f65c7e5c835104f54d5dfc5" + ], + "copyright.layer-leadership": [ + "layer:leadership", + "static", + "8ce407829378fc0f72ce44c7f624e4951c7ccb3db1cfb949bee026b701728cc9" + ], + "copyright.layer-metrics": [ + "layer:metrics", + "static", + "08509dcbade4c20761ba4382ef23c831744dbab1d4a8dd94a1c2b4d4e913334c" + ], + "copyright.layer-nagios": [ + "layer:nagios", + "static", + "47b2363574909e748bcc471d9004780ac084b301c154905654b5b6f088474749" + ], + "copyright.layer-options": [ + "layer:options", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-snap": [ + "layer:snap", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "copyright.layer-status": [ + "layer:status", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "copyright.layer-vault-kv": [ + "layer:vault-kv", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "copyright.layer-vaultlocker": [ + "layer:vaultlocker", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "debug-scripts/auth-webhook": [ + "kubernetes-master", + "static", + "08185f547fe131bf7ecd7d65fd7cfaa246f613e9ccd6fcf666eb02f6d987a7e8" + ], + "debug-scripts/charm-unitdata": [ + "layer:debug", + "static", + "c952b9d31f3942e4e722cb3e70f5119707b69b8e76cc44e2e906bc6d9aef49b7" + ], + "debug-scripts/filesystem": [ + "layer:debug", + "static", + "d29cc8687f4422d024001c91b1ac756ee6bf8a2a125bc98db1199ba775eb8fd7" + ], + "debug-scripts/juju-logs": [ + "layer:debug", + "static", + "d260b35753a917368cb8c64c1312546a0a40ef49cba84c75bc6369549807c55e" + ], + "debug-scripts/juju-network-get": [ + "layer:debug", + "static", + "6d849a1f8e6569bd0d5ea38299f7937cb8b36a5f505e3532f6c756eabeb8b6c5" + ], + "debug-scripts/kubectl": [ + "kubernetes-master", + "static", + "696848b11b760ab278b02b650ffda2adc8ba75c6701d574bdec0a7a1a75aea7e" + ], + "debug-scripts/kubernetes-master-services": [ + "kubernetes-master", + "static", + "f9930483765f715098c7e6a6b21e08105aff7dea4ecddc68fb8b6480951242d3" + ], + "debug-scripts/network": [ + "layer:debug", + "static", + "714afae5dcb45554ff1f05285501e3b7fcc656c8de51217e263b93dab25a9d2e" + ], + "debug-scripts/packages": [ + "layer:debug", + "static", + "e8177102dc2ca853cb9272c1257cf2cfd5253d2a074e602d07c8bc4ea8e27c75" + ], + "debug-scripts/sysctl": [ + "layer:debug", + "static", + "990035b320e09cc2228e1f2f880e795d51118b2959339eacddff9cbb74349c6a" + ], + "debug-scripts/systemd": [ + "layer:debug", + "static", + "23ddf533198bf5b1ce723acde31ada806aab8539292b514c721d8ec08af74106" + ], + "debug-scripts/tls-certs": [ + "layer:tls-client", + "static", + "ebf7f23ef6e39fb8e664bac2e9429e32aaeb673b4a51751724b835c007e85d3b" + ], + "docs/README": [ + "kubernetes-master", + "static", + "ea099038f01227b2907a915aa9e93d9ed73d85f9b446edcbe079c8a8de21e0cf" + ], + "docs/index.md": [ + "kubernetes-master", + "static", + "86a64daf0d25da6fb4d3bead5110a35c3ac302f5e5642b6a8bb748fd9ae9871b" + ], + "docs/status.md": [ + "layer:status", + "static", + "975dec9f8c938196e102e954a80226bda293407c4e5ae857c118bf692154702a" + ], + "docs/vault-kv.md": [ + "layer:vault-kv", + "static", + "96d97a5ff204f4ce12efdecea33c1a118deee383c2c067bfcce760b56e00c635" + ], + "docs/vaultlocker.md": [ + "layer:vaultlocker", + "static", + "a4dfe20b9ca14895d3b98658f5848dac61eefa62b2ea6f317ab2c2e65d151372" + ], + "exec.d/docker-compose/charm-pre-install": [ + "layer:kubernetes-master-worker-base", + "static", + "32482c2a88209cbe512990db5fb4deabdcff88282bf7c7dd71a265383139fc77" + ], + "exec.d/vmware-patch/charm-pre-install": [ + "kubernetes-master", + "static", + "9f98f70669ddd949ff83c7b408b678ae170bf41e4faa2828b4d66bd47acca93e" + ], + "hooks/aws-iam-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-iam-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-iam-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-iam-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-iam-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/azure-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/azure-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/azure-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/azure-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/azure-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ceph-client-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ceph-client-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ceph-client-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ceph-client-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ceph-client-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ceph-storage-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ceph-storage-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ceph-storage-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ceph-storage-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ceph-storage-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/collect-metrics": [ + "layer:metrics", + "static", + "139fe18ce4cf2bed2155d3d0fce1c3b4cf1bc2598242cda42b3d772ec9bf8558" + ], + "hooks/config-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/container-runtime-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/container-runtime-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/container-runtime-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/container-runtime-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/container-runtime-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/coordinator-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/coordinator-relation-changed": [ + "layer:coordinator", + "static", + "e5138d13492aa9a90379e8fce4a85c612481e7bc27a49958edbbfcaaf06f03a6" + ], + "hooks/coordinator-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/coordinator-relation-departed": [ + "layer:coordinator", + "static", + "e5138d13492aa9a90379e8fce4a85c612481e7bc27a49958edbbfcaaf06f03a6" + ], + "hooks/coordinator-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/dns-provider-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/dns-provider-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/dns-provider-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/dns-provider-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/dns-provider-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/etcd-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/etcd-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/etcd-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/etcd-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/etcd-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/gcp-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/gcp-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/gcp-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/gcp-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/gcp-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/grafana-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/grafana-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/grafana-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/grafana-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/grafana-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ha-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ha-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ha-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ha-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ha-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/hook.template": [ + "layer:basic", + "static", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/install": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/keystone-credentials-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/keystone-credentials-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/keystone-credentials-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/keystone-credentials-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/keystone-credentials-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-api-endpoint-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-api-endpoint-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-api-endpoint-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-api-endpoint-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-api-endpoint-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-control-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-control-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-control-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-control-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-control-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-masters-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-masters-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-masters-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-masters-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-masters-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-elected": [ + "layer:coordinator", + "static", + "e5138d13492aa9a90379e8fce4a85c612481e7bc27a49958edbbfcaaf06f03a6" + ], + "hooks/leader-settings-changed": [ + "layer:coordinator", + "static", + "e5138d13492aa9a90379e8fce4a85c612481e7bc27a49958edbbfcaaf06f03a6" + ], + "hooks/loadbalancer-external-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-external-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-external-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-external-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-external-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-internal-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-internal-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-internal-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-internal-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-internal-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/loadbalancer-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/openstack-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/openstack-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/openstack-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/openstack-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/openstack-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/post-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/pre-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/prometheus-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/prometheus-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/prometheus-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/prometheus-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/prometheus-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/relations/aws-iam/LICENSE": [ + "interface:aws-iam", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "hooks/relations/aws-iam/README.md": [ + "interface:aws-iam", + "static", + "18f660ddbbfe1f4b27733397391d994b95a5b3d94d75dcaa519482a2af76e9d2" + ], + "hooks/relations/aws-iam/__init__.py": [ + "interface:aws-iam", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/aws-iam/interface.yaml": [ + "interface:aws-iam", + "static", + "d8d9324026cfe5b3a1e2df4d6e8570bd2b1e8279629946170014fee352c25b12" + ], + "hooks/relations/aws-iam/provides.py": [ + "interface:aws-iam", + "static", + "42d6f71a30608e26eccb80743948c45be2ad002bce7fd670d343953bdc639509" + ], + "hooks/relations/aws-iam/requires.py": [ + "interface:aws-iam", + "static", + "65fab5cb68b1ba2d2cee5c11e74a4ed0002321079af4019a8dde00f83d6c8188" + ], + "hooks/relations/aws-integration/.gitignore": [ + "interface:aws-integration", + "static", + "315971ad9cc5d6ada2391f0940e1800149b211a18be3c7a8f396735d7978702b" + ], + "hooks/relations/aws-integration/LICENSE": [ + "interface:aws-integration", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/aws-integration/README.md": [ + "interface:aws-integration", + "static", + "1585d72b136158ce0741fc2ce0d7710c1ec55662f846afe2e768a4708c51057e" + ], + "hooks/relations/aws-integration/__init__.py": [ + "interface:aws-integration", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/aws-integration/copyright": [ + "interface:aws-integration", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/aws-integration/docs/provides.md": [ + "interface:aws-integration", + "static", + "a7669f49156173c27ede87105f6e65a07e1e5e41f3c154a24e1a82f307f65073" + ], + "hooks/relations/aws-integration/docs/requires.md": [ + "interface:aws-integration", + "static", + "09553e5f07f216e5234125fdf38a21af00ab11349cdb788b21703ae72b0aeed1" + ], + "hooks/relations/aws-integration/interface.yaml": [ + "interface:aws-integration", + "static", + "4449f48e5aaa99c0bb3e8e1c9833d11d3b20fc5f81ae1f15b6442af5ec873167" + ], + "hooks/relations/aws-integration/make_docs": [ + "interface:aws-integration", + "static", + "b471fefc7eaa5c377d47b2b63481d6c8f4c5e9d224428efe93c5abbd13a0817d" + ], + "hooks/relations/aws-integration/provides.py": [ + "interface:aws-integration", + "static", + "ee8f91b281d9112999f3d0e1d2ac17964fca3af5102fe5b072f3f3659b932ab7" + ], + "hooks/relations/aws-integration/pydocmd.yml": [ + "interface:aws-integration", + "static", + "8c242cde2b2517c74de8ad6b1b90d2f6d97b2eb86c54edaf2eb8a8f7d32913e8" + ], + "hooks/relations/aws-integration/requires.py": [ + "interface:aws-integration", + "static", + "3006d6a2607bc15507bec3e6144093c6938a51a22eee1f550d714ff702728c39" + ], + "hooks/relations/azure-integration/.gitignore": [ + "interface:azure-integration", + "static", + "9653f2820c79d92ac3518eedd0e1f43ffec128d5df9216c25d906fcba8ee46b8" + ], + "hooks/relations/azure-integration/LICENSE": [ + "interface:azure-integration", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/azure-integration/README.md": [ + "interface:azure-integration", + "static", + "c7799dba9471709e086dcd2ea272ad7a6e33f5058d875ce2bf5b3a6939d4a1e7" + ], + "hooks/relations/azure-integration/__init__.py": [ + "interface:azure-integration", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/azure-integration/copyright": [ + "interface:azure-integration", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/azure-integration/docs/provides.md": [ + "interface:azure-integration", + "static", + "60ae63187cac32c00d9f462f1723c9487960c728beae871f1a409c92196cc1f5" + ], + "hooks/relations/azure-integration/docs/requires.md": [ + "interface:azure-integration", + "static", + "b01e313c8ce3d02093e851bd84d5e8b7ae77b300c4b06b5048bddc78c1ad3eb3" + ], + "hooks/relations/azure-integration/interface.yaml": [ + "interface:azure-integration", + "static", + "cea5bfd87c278bd3f2e8dc00e654930f06d2bd91ef731a063edea14b04d9128a" + ], + "hooks/relations/azure-integration/make_docs": [ + "interface:azure-integration", + "static", + "e76f4a64c2fdc4a9f97a57d6515b4a25f9404d7043f2792db5206bc44213927c" + ], + "hooks/relations/azure-integration/provides.py": [ + "interface:azure-integration", + "static", + "33af701c7abd51e869de945c1f032749136c66560bb604e8e72521dc9d7e495b" + ], + "hooks/relations/azure-integration/pydocmd.yml": [ + "interface:azure-integration", + "static", + "4c17085efb4ec328891b49257413eed4d9a552eeea8e589509e48081effe51ed" + ], + "hooks/relations/azure-integration/requires.py": [ + "interface:azure-integration", + "static", + "2e60fecf8bc65d84124742d0833afc90d2e839f5dfa2923e8d1849063c51f47a" + ], + "hooks/relations/ceph-admin/.gitignore": [ + "interface:ceph-admin", + "static", + "38da8f2fbf99eb7b9ec38ea900ed13681803bbfa3482929cfeeaec86c591aa50" + ], + "hooks/relations/ceph-admin/README.md": [ + "interface:ceph-admin", + "static", + "805e4836c511fd78ac54e9377ac20430b736bcb96baf4d1106c6779c7c2ae4f4" + ], + "hooks/relations/ceph-admin/__init__.py": [ + "interface:ceph-admin", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/ceph-admin/interface.yaml": [ + "interface:ceph-admin", + "static", + "c9dc8e16173423a4a13dbfa247c48d587c08097529a7060e7cd64b75ef53e19c" + ], + "hooks/relations/ceph-admin/requires.py": [ + "interface:ceph-admin", + "static", + "3ccb57e3d033b0f281a0ebc60d64e1bc43e6e3fd008ba089c36b40955731a372" + ], + "hooks/relations/ceph-client/README.md": [ + "interface:ceph-client", + "static", + "475c8bff2d3041b7e22f4870bb6c8d73ccd88a53f53471dddae8ec5572b6caa2" + ], + "hooks/relations/ceph-client/__init__.py": [ + "interface:ceph-client", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/ceph-client/interface.yaml": [ + "interface:ceph-client", + "static", + "963f7b670b81d9ef1acc4c54a9ee4593f33c1864e199cfcb6cbf9deb15a0f0c4" + ], + "hooks/relations/ceph-client/lib/base_provides.py": [ + "interface:ceph-client", + "static", + "749435e1ea8794722f72838c97536090bc89f423c852040c2131dfb9dc71e0f8" + ], + "hooks/relations/ceph-client/lib/base_requires.py": [ + "interface:ceph-client", + "static", + "105fd680689b85516e0768da7e114dd5fc3b5fb7970ab7bb6d00122c81f7b3e1" + ], + "hooks/relations/ceph-client/provides.py": [ + "interface:ceph-client", + "static", + "ede8c70822bca0fd8ec5da9586ae390afa7e14878e158081fbe2c7ce8bc2f270" + ], + "hooks/relations/ceph-client/requires.py": [ + "interface:ceph-client", + "static", + "ddeebe898592169ffc8b54f8536ed1387981401cf43e40d90972d46bc5353dc6" + ], + "hooks/relations/container-runtime/.gitignore": [ + "interface:container-runtime", + "static", + "a2ebfecdb6c1b58267fbe97e6e2ac02c2b963df7673fc1047270f0f0cff16732" + ], + "hooks/relations/container-runtime/LICENSE": [ + "interface:container-runtime", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "hooks/relations/container-runtime/README.md": [ + "interface:container-runtime", + "static", + "44273265818229d2c858c3af0e0eee3a7df05aaa9ab20d28c3872190d4b48611" + ], + "hooks/relations/container-runtime/__init__.py": [ + "interface:container-runtime", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/container-runtime/interface.yaml": [ + "interface:container-runtime", + "static", + "e5343dcb11a6817a6050df4ea1c463eeaa0dd4777098566d4e27b056775426c6" + ], + "hooks/relations/container-runtime/provides.py": [ + "interface:container-runtime", + "static", + "4e818da222f507604179a828629787a1250083c847277f6b5b8e028cfbbb6d06" + ], + "hooks/relations/container-runtime/requires.py": [ + "interface:container-runtime", + "static", + "95285168b02f1f70be15c03098833a85e60fa1658ed72a46acd42e8e85ded761" + ], + "hooks/relations/coordinator/peers.py": [ + "layer:coordinator", + "static", + "d615c442396422a30a0c5f7639750d15bb59247ae5d9362c4f5dc8dd2cc7fff2" + ], + "hooks/relations/etcd/.gitignore": [ + "interface:etcd", + "static", + "cf237c7aff44efbe6e502e645c3e06da03a69d7bdeb43392108ef3348143417e" + ], + "hooks/relations/etcd/README.md": [ + "interface:etcd", + "static", + "93873d073f5f5302d352e09321aaf87458556e9730f89e1c682699c1d0db2386" + ], + "hooks/relations/etcd/__init__.py": [ + "interface:etcd", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/etcd/interface.yaml": [ + "interface:etcd", + "static", + "ba9f723b57a434f7efb2c06abec4167cd412c16da5f496a477dd7691e9a715be" + ], + "hooks/relations/etcd/peers.py": [ + "interface:etcd", + "static", + "99419c3d139fb5bb90021e0482f9e7ac2cfb776fb7af79b46209c6a75b36e834" + ], + "hooks/relations/etcd/provides.py": [ + "interface:etcd", + "static", + "3db1f644ab669e2dec59d59b61de63b721bc05b38fe646e525fff8f0d60982f9" + ], + "hooks/relations/etcd/requires.py": [ + "interface:etcd", + "static", + "8ffc1a094807fd36a1d1428b0a07b2428074134d46086066ecd6c0acd9fcd13e" + ], + "hooks/relations/gcp-integration/.gitignore": [ + "interface:gcp-integration", + "static", + "9653f2820c79d92ac3518eedd0e1f43ffec128d5df9216c25d906fcba8ee46b8" + ], + "hooks/relations/gcp-integration/LICENSE": [ + "interface:gcp-integration", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/gcp-integration/README.md": [ + "interface:gcp-integration", + "static", + "dab3f4a03f02dec0095883054780e3e3f1bf63262b06a9fd499364a3db8b1e97" + ], + "hooks/relations/gcp-integration/__init__.py": [ + "interface:gcp-integration", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/gcp-integration/copyright": [ + "interface:gcp-integration", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/gcp-integration/docs/provides.md": [ + "interface:gcp-integration", + "static", + "a67cda4094b4d601c8de63cf099ba2e83fecf3a8382e88f44e58b98be8872fa6" + ], + "hooks/relations/gcp-integration/docs/requires.md": [ + "interface:gcp-integration", + "static", + "d7e6d7dc90b74d35bf2bd10b00b3ba289ab856dc79ec51046508a85b9dda35a3" + ], + "hooks/relations/gcp-integration/interface.yaml": [ + "interface:gcp-integration", + "static", + "368e8ade9267b905dcb2e6843e7ed61bd6d246f0b0c18942e729f546d5db2260" + ], + "hooks/relations/gcp-integration/make_docs": [ + "interface:gcp-integration", + "static", + "5bf011da5045c31da97a67b8633d30ea90adc6c0d4d823f839fce6e07e5fe222" + ], + "hooks/relations/gcp-integration/provides.py": [ + "interface:gcp-integration", + "static", + "839f15cf978cf94343772889846ad3e2b8375372ef25ed08036207e5608b1f48" + ], + "hooks/relations/gcp-integration/pydocmd.yml": [ + "interface:gcp-integration", + "static", + "2d5a524cbde5ccf732b67382a85deb7c26dfb92315c30d26c2b2d5632a2a8f38" + ], + "hooks/relations/gcp-integration/requires.py": [ + "interface:gcp-integration", + "static", + "79c75c6c76b37bc5ac486ac2e14f853223c4c603850d2f231f187ab255cbdbf0" + ], + "hooks/relations/grafana-dashboard/.gitignore": [ + "interface:grafana-dashboard", + "static", + "5567034242cd31b5fb3a0d7e1f4cee8a2bb7454d4b35d4051f333145b09ff881" + ], + "hooks/relations/grafana-dashboard/LICENSE": [ + "interface:grafana-dashboard", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/grafana-dashboard/README.md": [ + "interface:grafana-dashboard", + "static", + "d46e6c55423b4f0e28f803702632739582f3c0fad5d0427346f210eba8879685" + ], + "hooks/relations/grafana-dashboard/__init__.py": [ + "interface:grafana-dashboard", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/grafana-dashboard/common.py": [ + "interface:grafana-dashboard", + "static", + "965f19c07d3475d7fe5a21235dc0cf1a27f11da9dad498d0cd1a51260b999aa3" + ], + "hooks/relations/grafana-dashboard/copyright": [ + "interface:grafana-dashboard", + "static", + "ee9809231ae81b9efc2b44b52aab2f6c8e4800319fdce5acad537b0eac556de4" + ], + "hooks/relations/grafana-dashboard/docs/common.md": [ + "interface:grafana-dashboard", + "static", + "ab69cc6e293b66175dfeee09707f8d02659ae5ba5b9aa4c441295a1025db12f7" + ], + "hooks/relations/grafana-dashboard/docs/provides.md": [ + "interface:grafana-dashboard", + "static", + "626b5655ce1e9f7733c86379fe67709e840b760046d899e5d761b034f94d939e" + ], + "hooks/relations/grafana-dashboard/docs/requires.md": [ + "interface:grafana-dashboard", + "static", + "4f78cff5a0395aff8477267e925066bfa93654eaeb4ba812c682f968171cca55" + ], + "hooks/relations/grafana-dashboard/interface.yaml": [ + "interface:grafana-dashboard", + "static", + "97e4c9a33360708668aa0330323fe9e9e5e95fa5a1e02d4f6b8e8dc60e155b52" + ], + "hooks/relations/grafana-dashboard/provides.py": [ + "interface:grafana-dashboard", + "static", + "cd63928094e6d34be92944ce65cb5b01ff9ba2bd9646036d006fa743a3c0fdb5" + ], + "hooks/relations/grafana-dashboard/requires.py": [ + "interface:grafana-dashboard", + "static", + "b071b9e66a3206351f563d7a4d160499b13a6af29d80930cb01720b5974e1dd2" + ], + "hooks/relations/hacluster/.stestr.conf": [ + "interface:hacluster", + "static", + "46965969e6df6ac729b7dac68d57bc4e677e9f4d79d445be77f54ca3b9e58774" + ], + "hooks/relations/hacluster/README.md": [ + "interface:hacluster", + "static", + "7fad91e409c6e559cdb76d11c89c325531adc25679049a629a28c4f890755f1f" + ], + "hooks/relations/hacluster/__init__.py": [ + "interface:hacluster", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/hacluster/copyright": [ + "interface:hacluster", + "static", + "7a296596102da98cecee289a195e00d6af44241911321699b3d4d4af93f11893" + ], + "hooks/relations/hacluster/interface.yaml": [ + "interface:hacluster", + "static", + "5f4e6c8d7b2884bdceeee422821f4db7163dbfa7994d86cb405ffef2c3dea43c" + ], + "hooks/relations/hacluster/interface_hacluster/__init__.py": [ + "interface:hacluster", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/hacluster/interface_hacluster/common.py": [ + "interface:hacluster", + "static", + "abcc0d2940d142976ccfa3fa7518227549ee13041292af7ad61101a7d0c02f7e" + ], + "hooks/relations/hacluster/requires.py": [ + "interface:hacluster", + "static", + "68cf3ed22af30e42f34fc70ca484e8e4eeaedac6410bd3f228677cc791e6f46c" + ], + "hooks/relations/hacluster/test-requirements.txt": [ + "interface:hacluster", + "static", + "63756e4b1c67bc161cee0d30d460dbb83911b2c064dc1c55454a30c1ab877616" + ], + "hooks/relations/http/.gitignore": [ + "interface:http", + "static", + "83b4ca18cc39800b1d260b5633cd0252e21501b21e7c33e718db44f1a68a09b8" + ], + "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", + "8c72cd8a5a6ea24f53b6dba11f4353c75265bfa7d3ecc2dd096c8963eab8c877" + ], + "hooks/relations/http/requires.py": [ + "interface:http", + "static", + "76cc886368eaf9c2403a6dc46b40531c3f4eaf67b08829f890c57cb645430abd" + ], + "hooks/relations/keystone-credentials/.gitignore": [ + "interface:keystone-credentials", + "static", + "ddc61d479977d318682280fa2b18bcb6cb9a1b0e0e7897cea3d14d5c8d222e68" + ], + "hooks/relations/keystone-credentials/.gitreview": [ + "interface:keystone-credentials", + "static", + "79122a6758c1a504d6caa55ca329e9028caf5d9a52516a4a77be2a1e676d45c8" + ], + "hooks/relations/keystone-credentials/.stestr.conf": [ + "interface:keystone-credentials", + "static", + "46965969e6df6ac729b7dac68d57bc4e677e9f4d79d445be77f54ca3b9e58774" + ], + "hooks/relations/keystone-credentials/.zuul.yaml": [ + "interface:keystone-credentials", + "static", + "c240e43920d05095cf5a0a9aa648685676c12bdcbb3874b79bbec5b5e7b18b7c" + ], + "hooks/relations/keystone-credentials/__init__.py": [ + "interface:keystone-credentials", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/keystone-credentials/copyright": [ + "interface:keystone-credentials", + "static", + "7a296596102da98cecee289a195e00d6af44241911321699b3d4d4af93f11893" + ], + "hooks/relations/keystone-credentials/interface.yaml": [ + "interface:keystone-credentials", + "static", + "daa50ddd8948bdd6d6f8838498aa4251219f3bbe23344a05477764e6fc5ca33f" + ], + "hooks/relations/keystone-credentials/provides.py": [ + "interface:keystone-credentials", + "static", + "67b853e714b2f43cbd671a4d6c1b85330938a6d8e24da9bf88236efcbe033499" + ], + "hooks/relations/keystone-credentials/requires.py": [ + "interface:keystone-credentials", + "static", + "92d591067b288de5336e6228a2c84be5839354bfd050d7ce84df62c03a813785" + ], + "hooks/relations/keystone-credentials/test-requirements.txt": [ + "interface:keystone-credentials", + "static", + "38a6e3c379a0689eb8f95d0107865847d528c020561669aad4287e1108df6ca7" + ], + "hooks/relations/kube-control/.travis.yml": [ + "interface:kube-control", + "static", + "c2bd1b88f26c88b883696cca155c28671359a256ed48b90a9ea724b376f2a829" + ], + "hooks/relations/kube-control/README.md": [ + "interface:kube-control", + "static", + "66ee58f59efceefa21f7f2d7f88c1d75c07a16bbec8d09a83a7fda6373eab421" + ], + "hooks/relations/kube-control/__init__.py": [ + "interface:kube-control", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/kube-control/interface.yaml": [ + "interface:kube-control", + "static", + "07e3d781283ecbb59c780cc8e4aeb9f030f22d2db6c28d731b74a36ab126960d" + ], + "hooks/relations/kube-control/provides.py": [ + "interface:kube-control", + "static", + "08e090bb3ad51e5825590ad0dee077288648b171764480afc20205c740fa15be" + ], + "hooks/relations/kube-control/requires.py": [ + "interface:kube-control", + "static", + "a064ad0b75081439faeda7fb948934bfd86a7ab2079d25b2ad47aa5fa32c2a6f" + ], + "hooks/relations/kube-dns/README.md": [ + "interface:kube-dns", + "static", + "f02265c0931c5582cbad911050ee1578c370e4ecaffdbf56d11505f97ce44fee" + ], + "hooks/relations/kube-dns/__init__.py": [ + "interface:kube-dns", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/kube-dns/interface.yaml": [ + "interface:kube-dns", + "static", + "e4ca8faafe4cce43eed862d35346780df4cba4eb243baaf5aecd891514deb26d" + ], + "hooks/relations/kube-dns/provides.py": [ + "interface:kube-dns", + "static", + "f0ea4f0610779a70860d5257f0760f62ea2ec682c5f005ba5afff92c9824aa36" + ], + "hooks/relations/kube-dns/requires.py": [ + "interface:kube-dns", + "static", + "38b819b7ee98c3c38142d2cc8122dedd9d8c0f34767c5cc11392a564f38db370" + ], + "hooks/relations/kube-masters/README.md": [ + "interface:kube-masters", + "static", + "37f61924210be49fc9c66595512a307e9112d09ded09c628506571c3b4f6961c" + ], + "hooks/relations/kube-masters/__init__.py": [ + "interface:kube-masters", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/kube-masters/interface.yaml": [ + "interface:kube-masters", + "static", + "55041c3c142523c10e82ece780b178de041f1b5a1d7c2c106370d81b6cad1d42" + ], + "hooks/relations/kube-masters/peers.py": [ + "interface:kube-masters", + "static", + "2237030141571ef6acb1934a724f1620164bb2ddf08450aab23d14b0dc7b84b7" + ], + "hooks/relations/kubernetes-cni/.github/workflows/tests.yaml": [ + "interface:kubernetes-cni", + "static", + "d0015cd49675976ff87832f5ef7ea20ffca961786379c72bb6acdbdeddd9137c" + ], + "hooks/relations/kubernetes-cni/.gitignore": [ + "interface:kubernetes-cni", + "static", + "0594213ebf9c6ef87827b30405ee67d847f73f4185a865e0e5e9c0be9d29eabe" + ], + "hooks/relations/kubernetes-cni/README.md": [ + "interface:kubernetes-cni", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/kubernetes-cni/__init__.py": [ + "interface:kubernetes-cni", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/kubernetes-cni/interface.yaml": [ + "interface:kubernetes-cni", + "static", + "03affdaf7e879adfdf8c434aa31d40faa6d2872faa7dfd93a5d3a1ebae02487d" + ], + "hooks/relations/kubernetes-cni/provides.py": [ + "interface:kubernetes-cni", + "static", + "e436e187f2bab6e73add2b897cd43a2f000fde4726e40b772b66f27786c85dee" + ], + "hooks/relations/kubernetes-cni/requires.py": [ + "interface:kubernetes-cni", + "static", + "45398af27246eaf2005115bd3f270b78fc830d4345b02cc0c4d438711b7cd9fe" + ], + "hooks/relations/nrpe-external-master/README.md": [ + "interface:nrpe-external-master", + "static", + "d8ed3bc7334f6581b12b6091923f58e6f5ef62075a095a4e78fb8f434a948636" + ], + "hooks/relations/nrpe-external-master/__init__.py": [ + "interface:nrpe-external-master", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/nrpe-external-master/interface.yaml": [ + "interface:nrpe-external-master", + "static", + "894f24ba56148044dae5b7febf874b427d199239bcbe1f2f55c3db06bb77b5f0" + ], + "hooks/relations/nrpe-external-master/provides.py": [ + "interface:nrpe-external-master", + "static", + "e6ba708d05b227b139a86be59c83ed95a2bad030bc81e5819167ba5e1e67ecd4" + ], + "hooks/relations/nrpe-external-master/requires.py": [ + "interface:nrpe-external-master", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/openstack-integration/.gitignore": [ + "interface:openstack-integration", + "static", + "9653f2820c79d92ac3518eedd0e1f43ffec128d5df9216c25d906fcba8ee46b8" + ], + "hooks/relations/openstack-integration/LICENSE": [ + "interface:openstack-integration", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/openstack-integration/README.md": [ + "interface:openstack-integration", + "static", + "ca58e21bd973f6e65f7a8a06b4aeabd50bf137ab6fab9c8defa8789b02df3aa5" + ], + "hooks/relations/openstack-integration/__init__.py": [ + "interface:openstack-integration", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/openstack-integration/copyright": [ + "interface:openstack-integration", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/openstack-integration/docs/provides.md": [ + "interface:openstack-integration", + "static", + "ec4b81da3dfeac892f94053d753b56e504f5fd9c6ec4e743efa40efade3aa651" + ], + "hooks/relations/openstack-integration/docs/requires.md": [ + "interface:openstack-integration", + "static", + "95424fe767a26e3208800b4099f8768212b0a72b989ee145f181b67d678e3bbe" + ], + "hooks/relations/openstack-integration/interface.yaml": [ + "interface:openstack-integration", + "static", + "11b07a41bd2e24765231c4b7c7218da15f2173398d8d73698ecb210e599d02f6" + ], + "hooks/relations/openstack-integration/make_docs": [ + "interface:openstack-integration", + "static", + "a564aac288cc0bf4ff14418a341f11b065988c2b64adf93ec451e09dd92dcea5" + ], + "hooks/relations/openstack-integration/provides.py": [ + "interface:openstack-integration", + "static", + "ad09fc79fa5eb7a142477d5bf7f48b53f6ede389708de0bb297c6d009aba502b" + ], + "hooks/relations/openstack-integration/pydocmd.yml": [ + "interface:openstack-integration", + "static", + "3568f8a3c1446dfd736f31050e2b470bf125cc41717d156a4b866c7ea53861be" + ], + "hooks/relations/openstack-integration/requires.py": [ + "interface:openstack-integration", + "static", + "a15f5a7ffa2391f75da6bde0007700ee75f058e62430924312ff39efc6ecea6b" + ], + "hooks/relations/prometheus-manual/.gitignore": [ + "interface:prometheus-manual", + "static", + "5567034242cd31b5fb3a0d7e1f4cee8a2bb7454d4b35d4051f333145b09ff881" + ], + "hooks/relations/prometheus-manual/LICENSE": [ + "interface:prometheus-manual", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/prometheus-manual/README.md": [ + "interface:prometheus-manual", + "static", + "506d4a334ebbe40905c76fc74e4ab5285d836ac28c7d1087b85b5a304960be2e" + ], + "hooks/relations/prometheus-manual/__init__.py": [ + "interface:prometheus-manual", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/prometheus-manual/common.py": [ + "interface:prometheus-manual", + "static", + "013107b3bc8f148779ada8097db725ac9c3d22c605a5794cb8bae95cace9fa4c" + ], + "hooks/relations/prometheus-manual/copyright": [ + "interface:prometheus-manual", + "static", + "ee9809231ae81b9efc2b44b52aab2f6c8e4800319fdce5acad537b0eac556de4" + ], + "hooks/relations/prometheus-manual/docs/common.md": [ + "interface:prometheus-manual", + "static", + "91b9e9300a2fef2ce1112cdc57a224ee06ab513ea127edc8a59b6ce9c715cd25" + ], + "hooks/relations/prometheus-manual/docs/provides.md": [ + "interface:prometheus-manual", + "static", + "6b226c2587dbf5b304e6466f2b31bbb208512896b2ab057b11b646cf3501e292" + ], + "hooks/relations/prometheus-manual/docs/requires.md": [ + "interface:prometheus-manual", + "static", + "0100bdc38afd892336747eac005260bc9656ffc1a40f9fb0faef824ab07c1021" + ], + "hooks/relations/prometheus-manual/interface.yaml": [ + "interface:prometheus-manual", + "static", + "4a268318ee2adcc8a5a3482d49595d3805f94bf8976bd1ee4a4f7f9db89e472e" + ], + "hooks/relations/prometheus-manual/provides.py": [ + "interface:prometheus-manual", + "static", + "232917934637d8905ddcd448ce51c2c30dcb9217e043592be356d510c09190c4" + ], + "hooks/relations/prometheus-manual/requires.py": [ + "interface:prometheus-manual", + "static", + "0492a9f1037f39479f2e607162aa48ca67451e00124541a7d56f7e0a920903e0" + ], + "hooks/relations/public-address/README.md": [ + "interface:public-address", + "static", + "7225effe61bfd8571447b8b685a2ecb52be17431b3066a5306330954c4cb064d" + ], + "hooks/relations/public-address/__init__.py": [ + "interface:public-address", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/public-address/interface.yaml": [ + "interface:public-address", + "static", + "49d6777a54aa84c7d3be8d531be237564e90f2e4cb2be05ef5617a372a382340" + ], + "hooks/relations/public-address/provides.py": [ + "interface:public-address", + "static", + "7c99b0fe987d38773ed3e67c0378fdb78748c04d6895489cd4bca40aaeb051b2" + ], + "hooks/relations/public-address/requires.py": [ + "interface:public-address", + "static", + "d6a7c6c0762d29a5db19afb4cf82af50812988d5e19a3a48fcbe8b0f6fec12a5" + ], + "hooks/relations/tls-certificates/.gitignore": [ + "interface:tls-certificates", + "static", + "b485e74def213c534676224e655e9276b62d401ebc643508ddc545dd335cb6dc" + ], + "hooks/relations/tls-certificates/README.md": [ + "interface:tls-certificates", + "static", + "6851227de8fcca7edfd504159dbe3e3af31080af64df46f3d3b345da7630827a" + ], + "hooks/relations/tls-certificates/__init__.py": [ + "interface:tls-certificates", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/tls-certificates/docs/common.md": [ + "interface:tls-certificates", + "static", + "5e91d6637fc0ccc50af2776de9e59a0f8098244b627816b2e18fabb266e980ff" + ], + "hooks/relations/tls-certificates/docs/provides.md": [ + "interface:tls-certificates", + "static", + "5c12dfca99b5c15ba10b4e7f7cff4cb4c9b621b198deba5f2397d3c837d035fe" + ], + "hooks/relations/tls-certificates/docs/requires.md": [ + "interface:tls-certificates", + "static", + "148dd1de163d75253f0a9d3c35e108dcaacbc9bdf97e47186743e6c82a67b62e" + ], + "hooks/relations/tls-certificates/interface.yaml": [ + "interface:tls-certificates", + "static", + "e412e54b1d327bad15a882f7f0bf996212090db576b863cc9cff7a68afc0e4fa" + ], + "hooks/relations/tls-certificates/make_docs": [ + "interface:tls-certificates", + "static", + "3671543bddc9d277171263310e404df3f11660429582cb27b39b7e7ec8757a37" + ], + "hooks/relations/tls-certificates/provides.py": [ + "interface:tls-certificates", + "static", + "be2a4b9a411c770989c529fd887070ad91649481a13f5239cfd8751f234b637c" + ], + "hooks/relations/tls-certificates/pydocmd.yml": [ + "interface:tls-certificates", + "static", + "48a233f60a89f87d56e9bc715e05766f5d39bbea2bc8741ed31f67b30c8cfcb8" + ], + "hooks/relations/tls-certificates/requires.py": [ + "interface:tls-certificates", + "static", + "442d773112079bc674d3e6be75b00323fcad7efd2f03613a1972b575dd438dba" + ], + "hooks/relations/tls-certificates/tls_certificates_common.py": [ + "interface:tls-certificates", + "static", + "068bd32ba69bfa514e1da386919d18b348ee678b40c372f275c9110f2cc4677c" + ], + "hooks/relations/vault-kv/.gitignore": [ + "interface:vault-kv", + "static", + "996ad92a4713473baf27997a048901fdfa0039b9497bcc916f3f50b9000c1b96" + ], + "hooks/relations/vault-kv/README.md": [ + "interface:vault-kv", + "static", + "30082282d57b9a7c1d0bc0311ea0a2b9d50dd8f74829696b413524a4bbffb635" + ], + "hooks/relations/vault-kv/__init__.py": [ + "interface:vault-kv", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/vault-kv/copyright": [ + "interface:vault-kv", + "static", + "d72972f963502ab390e2b3cdb72cc0f49afa0ef9e7d6e589607d260e6f9a577f" + ], + "hooks/relations/vault-kv/interface.yaml": [ + "interface:vault-kv", + "static", + "e021758bd6e3536c2cbc30f08354dd23c11e2a7cc4d3b93584d3646fa64c331d" + ], + "hooks/relations/vault-kv/provides.py": [ + "interface:vault-kv", + "static", + "82d6f62f8e92f12fe43a8803b17be29731c7e4e4b94ca53f6f141d2a3f0a5df4" + ], + "hooks/relations/vault-kv/requires.py": [ + "interface:vault-kv", + "static", + "eaa5e8eb962fcf9d3f655d88f3e27958ac3b2b87a16904bca7d426fb6136ac27" + ], + "hooks/relations/vault-kv/test-requirements.txt": [ + "interface:vault-kv", + "static", + "41b5d0f807a3166c534aa01f773dbdfbefcc9af37e369159a9dba6f0a8c75a78" + ], + "hooks/relations/vsphere-integration/.gitignore": [ + "interface:vsphere-integration", + "static", + "9653f2820c79d92ac3518eedd0e1f43ffec128d5df9216c25d906fcba8ee46b8" + ], + "hooks/relations/vsphere-integration/LICENSE": [ + "interface:vsphere-integration", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/vsphere-integration/README.md": [ + "interface:vsphere-integration", + "static", + "8de815f0f938cb8f58c536899ed87e55aac507a782093bd50d50bd3c1d6add1c" + ], + "hooks/relations/vsphere-integration/__init__.py": [ + "interface:vsphere-integration", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/vsphere-integration/copyright": [ + "interface:vsphere-integration", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/vsphere-integration/docs/provides.md": [ + "interface:vsphere-integration", + "static", + "daa3c44a2df6d774adc60bde1160f1e307129be9d696f018eab4a7e713ee775a" + ], + "hooks/relations/vsphere-integration/docs/requires.md": [ + "interface:vsphere-integration", + "static", + "4e79bb1b151f1de63b423d39a6e1831efbb6f767fe5b84963162f62c6bbb9123" + ], + "hooks/relations/vsphere-integration/interface.yaml": [ + "interface:vsphere-integration", + "static", + "20295b882dfb9a1750d8e988eaa3383cd3109fae510785ba4e415d7fa9b118af" + ], + "hooks/relations/vsphere-integration/make_docs": [ + "interface:vsphere-integration", + "static", + "cd9d91049ee3c6e6148f4bd9204a34463dde905ce665cff25be014ffc1b81b89" + ], + "hooks/relations/vsphere-integration/provides.py": [ + "interface:vsphere-integration", + "static", + "8ccb09c4a3009b59caea227ef40395fb063d3e8ce983338060fb59bbe74138c0" + ], + "hooks/relations/vsphere-integration/pydocmd.yml": [ + "interface:vsphere-integration", + "static", + "9f8eb566569977f10955da67def28886737e80914ae000e4acfae1313d08f105" + ], + "hooks/relations/vsphere-integration/requires.py": [ + "interface:vsphere-integration", + "static", + "d56702f60037f06259752d3bd7882f7ee46f60a4ce7b6d1071520d69ec9351f9" + ], + "hooks/start": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/stop": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/update-status": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/upgrade-charm": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vault-kv-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vault-kv-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vault-kv-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vault-kv-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vault-kv-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vsphere-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vsphere-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vsphere-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vsphere-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vsphere-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "icon.svg": [ + "kubernetes-master", + "static", + "d543b4638e3efc01d5e79b1bc0a0f8f5d42389470fe16f9cdd843f24a26aa560" + ], + "layer.yaml": [ + "kubernetes-master", + "dynamic", + "787461ca8aa81ec65452cc479f363ee5ad14dfb54ea7333af25f3139c731cdac" + ], + "lib/charms/apt.py": [ + "layer:apt", + "static", + "c7613992eb33ac94d83fbf02f467b614ea5112eaf561c4715def90989cefa531" + ], + "lib/charms/coordinator.py": [ + "layer:coordinator", + "static", + "6dbacc87605be8efcbf19ec05341e4eb210327724495c79998a46947e034dbea" + ], + "lib/charms/layer/__init__.py": [ + "layer:basic", + "static", + "dfe0d26c6bf409767de6e2546bc648f150e1b396243619bad3aa0553ab7e0e6f" + ], + "lib/charms/layer/basic.py": [ + "layer:basic", + "static", + "98b47134770ed6e4c0b2d4aad73cd5bc200bec84aa9c1c4e075fd70c3222a0c9" + ], + "lib/charms/layer/execd.py": [ + "layer:basic", + "static", + "fda8bd491032db1db8ddaf4e99e7cc878c6fb5432efe1f91cadb5b34765d076d" + ], + "lib/charms/layer/hacluster.py": [ + "layer:hacluster", + "static", + "f58e0c1503187247f858ff3c9a1166d59107afd1557ba89e4878ec2e79304f8a" + ], + "lib/charms/layer/kubernetes_common.py": [ + "layer:kubernetes-common", + "static", + "29cedffd490e6295273d195a7c9bace2fcdf149826e7427f2af9698f7f75055b" + ], + "lib/charms/layer/kubernetes_master.py": [ + "kubernetes-master", + "static", + "b84b823a2198833b2dcaae1b1a04c7f64cfb24838fa0d3e43e2ef106be6e8af0" + ], + "lib/charms/layer/nagios.py": [ + "layer:nagios", + "static", + "0246710bdbea844356007a64409907d93e6e94a289d83266e8b7c5d921fb3a6c" + ], + "lib/charms/layer/options.py": [ + "layer:options", + "static", + "8ae7a07d22542fc964f2d2bee8219d1c78a68dace70a1b38d36d4aea47b1c3b2" + ], + "lib/charms/layer/snap.py": [ + "layer:snap", + "static", + "f278a3b06a1604e1c59f107d2ff3e9f5705e3c6c7be7a012c1a500d0fc8925df" + ], + "lib/charms/layer/status.py": [ + "layer:status", + "static", + "d560a5e07b2e5f2b0f25f30e1f0278b06f3f90c01e4dbad5c83d71efc79018c6" + ], + "lib/charms/layer/tls_client.py": [ + "layer:tls-client", + "static", + "34531c3980777b661b913d77c432fc371ed10425473c2eb365b1dd5540c2ec6e" + ], + "lib/charms/layer/vault_kv.py": [ + "layer:vault-kv", + "static", + "bd902b2cbe0be6cab87e991066aa2d31a2aac429de2e168686469cd8a3ad6a46" + ], + "lib/charms/layer/vaultlocker.py": [ + "layer:vaultlocker", + "static", + "fc2ae363cc3c8a9b7d46b9ec1b96b53b97c357087a8de9ae90786586584b7eb5" + ], + "lib/charms/leadership.py": [ + "layer:leadership", + "static", + "20ffcbbc08147506759726ad51567420659ffb8a2e0121079240b8706658e332" + ], + "lib/debug_script.py": [ + "layer:debug", + "static", + "a4d56f2d3e712b1b5cadb657c7195c6268d0aac6d228991049fd769e0ddaf453" + ], + "lxd-profile.yaml": [ + "kubernetes-master", + "static", + "e62700f1993721652d83756f89e1f8b33c5d0dec6fb27554f61aaf96ccd4e379" + ], + "make_docs": [ + "layer:vaultlocker", + "static", + "c990f55c8e879793a62ed8464ee3d7e0d7d2225fdecaf17af24b0df0e2daa8c1" + ], + "metadata.yaml": [ + "kubernetes-master", + "dynamic", + "246c439bfb9fb05310ff536411c37c644fe013506d2648a259d801a3f899bd43" + ], + "metrics.yaml": [ + "kubernetes-master", + "static", + "51805e00187180beb34a06c6c9d08b4a6889e02aec3e9b01043146f0002c8b51" + ], + "pydocmd.yml": [ + "layer:vaultlocker", + "static", + "145103565659638229fec4c2d6ad7161746a75f13167d1aa16c5cb66081faf82" + ], + "pyproject.toml": [ + "layer:apt", + "static", + "19689509a5fb9bfc90ed1e873122ac0a90f22533b7f40055c38fdd587fe297de" + ], + "reactive/__init__.py": [ + "layer:coordinator", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "reactive/apt.py": [ + "layer:apt", + "static", + "6fe40f18eb84a910a71a4acb7ec74856128de846de6029b4fc297a875692c837" + ], + "reactive/cdk_service_kicker.py": [ + "layer:cdk-service-kicker", + "static", + "cc2648443016a18324ecb26acb71d69c71610ba23df235f280383552136f7efc" + ], + "reactive/coordinator.py": [ + "layer:coordinator", + "static", + "18cda7ddf00ae0e47578d489fc3ebb376b4428cd0559797a87ddbead54360d02" + ], + "reactive/hacluster.py": [ + "layer:hacluster", + "static", + "0b34980232eec9866c85b55070db7e72a04689f92b338207c5839531abd0eadc" + ], + "reactive/kubernetes_master.py": [ + "kubernetes-master", + "static", + "59cc8fa74c76d0e33185e4733c569853871a090184227a2f1f09bd03bc9ad170" + ], + "reactive/kubernetes_master_worker_base.py": [ + "layer:kubernetes-master-worker-base", + "static", + "4a2d098f2e54f96b7ecef19b9485534b2da911a4a62104bd0efa40e4cb8bb519" + ], + "reactive/leadership.py": [ + "layer:leadership", + "static", + "e2b233cf861adc3b2d9e9c062134ce2f104953f03283cdddd88f49efee652e8f" + ], + "reactive/snap.py": [ + "layer:snap", + "static", + "e4625ff4190ed33625f50d94343eda100871052ef133028f5f0ff1edfa5a23c3" + ], + "reactive/status.py": [ + "layer:status", + "static", + "30207fc206f24e91def5252f1c7f7c8e23c0aed0e93076babf5e03c05296d207" + ], + "reactive/tls_client.py": [ + "layer:tls-client", + "static", + "08e850e401d2004523dca6b5e6bc47c33d558bf575dd55969491e11cd3ed98c8" + ], + "reactive/vault_kv.py": [ + "layer:vault-kv", + "static", + "5eaf9dc71ea652dc02a5d4d752c1cbde1541c08b269c7409145b225cc1dbf139" + ], + "reactive/vaultlocker.py": [ + "layer:vaultlocker", + "static", + "28e31d57017933a3b7e44dfd1913bbb3525fa7910b9a43eb6ad320d098160a4f" + ], + "requirements.txt": [ + "layer:basic", + "static", + "a00f75d80849e5b4fc5ad2e7536f947c25b1a4044b341caa8ee87a92d3a4c804" + ], + "setup.py": [ + "layer:snap", + "static", + "b219c8c6cb138a2f70a8ef9136d1cc3fe6210bd1e28c99fccb5e7ae90d547164" + ], + "templates/cdk-service-kicker": [ + "layer:cdk-service-kicker", + "static", + "b17adff995310e14d1b510337efa0af0531b55e2c487210168829e0dc1a6f99b" + ], + "templates/cdk-service-kicker.service": [ + "layer:cdk-service-kicker", + "static", + "c2d3977fa89d453f0f13a8a823621c44bb642ec7392d8b7462b631864f665029" + ], + "templates/cdk.auth-webhook-secret.yaml": [ + "layer:kubernetes-common", + "static", + "efaf34c12a5c961fa7843199070945ba05717b3656a0f3acc3327f45334bcaec" + ], + "templates/cdk.master.auth-webhook-conf.yaml": [ + "kubernetes-master", + "static", + "11df8c0c1a4157e7a552b864188df1dcdc99153a8b359667b640937251bad678" + ], + "templates/cdk.master.auth-webhook.logrotate": [ + "kubernetes-master", + "static", + "826249ef09b9a2fe70a1b0c905e987d427d07fb1c4b82b23edee930cf8de2317" + ], + "templates/cdk.master.auth-webhook.py": [ + "kubernetes-master", + "static", + "1e4958fa0273d5754e6fa9703ee4256bdc31ac0fc7d46ce85277e17f4b8c738d" + ], + "templates/cdk.master.auth-webhook.service": [ + "kubernetes-master", + "static", + "960c8d38e928ef3f1c9f29958226656c6a339a4834b30664b032c10f98eb7f62" + ], + "templates/cdk.master.leader.file-watcher.path": [ + "kubernetes-master", + "static", + "f5698867fafd661f270c41a7ddb3dccf14f974dff65decc59148888c4bc5a9d3" + ], + "templates/cdk.master.leader.file-watcher.service": [ + "kubernetes-master", + "static", + "aeceb87e99da56744159e36ae1657551b570fcae1c452fb8a28b660134b7ee1b" + ], + "templates/cdk.master.leader.file-watcher.sh": [ + "kubernetes-master", + "static", + "99a5398879580b219664447f58384244688fad9923f5aed7477403ac4cc813c5" + ], + "templates/ceph-secret.yaml": [ + "kubernetes-master", + "static", + "cf38a4f47dd3399f5a07f16e889dfc755fcf63541e0b8e0e6936b5993f360b44" + ], + "templates/ceph.conf": [ + "kubernetes-master", + "static", + "63d2a3751ea00ae8ccce61d642735f949031595e1a170aa1830105f1cd3c7266" + ], + "templates/create-namespace.yaml.j2": [ + "kubernetes-master", + "static", + "8ace952fc7f873b0ec2c0f9843a3e39023e20da5397d799c442a78749aa96239" + ], + "templates/grafana/autoload/kubernetes.json": [ + "kubernetes-master", + "static", + "085ee057337177c9ea31153b9c910eb7eabd6ca3536cc864ab19b047fd4b2e80" + ], + "templates/grafana/conditional/prometheus.json": [ + "kubernetes-master", + "static", + "f066e89b01609616aca4da14439f51afe8bde0046d254273fb746a9ea582a3c5" + ], + "templates/grafana/conditional/telegraf.json": [ + "kubernetes-master", + "static", + "845f3b66e6899693e53b09dca874d36d387e99c6714f525f6823ab27f81ef220" + ], + "templates/keystone-api-server-webhook.yaml": [ + "kubernetes-master", + "static", + "57d856c3e55fbfddf08a3952c3a4864713c345f6a4fe42aa4861429c85af8de8" + ], + "templates/kube-keystone.sh": [ + "kubernetes-master", + "static", + "f346f743809da597a37b6b3531cfb525de7cd7196827e818770ae57781f7f47b" + ], + "templates/kube-proxy-iptables-fix.sh": [ + "kubernetes-master", + "static", + "62313fd28f76cfc6e5f2dd426c3ca7a728c91bf064d532e39d8e8fb51a115bbb" + ], + "templates/nagios_plugin.py": [ + "kubernetes-master", + "static", + "0627dc0fe546a6262a9e8b0ca265d783d76ef18b546bb2966b4ff4114db1b392" + ], + "templates/prometheus/k8s-api-endpoints.yaml.j2": [ + "kubernetes-master", + "static", + "78af8a158956011c8abfb11895fab3e67c2d7d6a092c09fa0b2ddead1ee9549b" + ], + "templates/prometheus/kube-state-metrics.yaml.j2": [ + "kubernetes-master", + "static", + "cd01643061d21fc061fde4dacd28bd5dda9938ce531868c9c5a529a390ad29ec" + ], + "templates/prometheus/kube-state-telemetry.yaml.j2": [ + "kubernetes-master", + "static", + "278e98c6abe9312053c08e72160181c968fcf447b3902bb608c4971deca7b192" + ], + "templates/prometheus/kubernetes-cadvisor.yaml.j2": [ + "kubernetes-master", + "static", + "df55e745681353b08029262e4e806f6deed99add34c19950ccd0aa7dabcd226d" + ], + "templates/prometheus/kubernetes-nodes.yaml.j2": [ + "kubernetes-master", + "static", + "6f22e3b7a6a87d7d50b1ad3099fccf08677acc434273f032952d8bf7f548c612" + ], + "templates/rbac-pod-security-policy.yaml": [ + "kubernetes-master", + "static", + "b4e7b7c0976f1a0175c0e60b458e3e9d8bd486849033c4cff7d2684793aa603a" + ], + "templates/rbac-proxy.yaml": [ + "kubernetes-master", + "static", + "abb77f196e008fc636c254c89672bb889ca34a91103972c11a5e2e59aa608400" + ], + "templates/service-always-restart.systemd-229.conf": [ + "kubernetes-master", + "static", + "516958fbf8b9a05cc86f6700d0de7bdc6b2ba1847d69fbe1214e23b52e00b064" + ], + "templates/service-always-restart.systemd-latest.conf": [ + "kubernetes-master", + "static", + "37de98817682363d48b3dd2b635f5cfb281533aaa9d3836d1af44f9d6a59984c" + ], + "templates/service-iptables-fix.service": [ + "kubernetes-master", + "static", + "3f8a29c719c175e17a7a69756223babf0e2b56f8a8d69dbbd81e8e0889863669" + ], + "templates/system-monitoring-rbac-role.yaml": [ + "kubernetes-master", + "static", + "a50f45a1e978ffeaf872f961c2f8ff95fbc144462baef42bcdda3c51da03f54f" + ], + "templates/vaultlocker-loop@.service": [ + "layer:vaultlocker", + "static", + "57d81403c04033d382094b3c8a60c4728eb0fad146746921fe7e770b4c49f758" + ], + "templates/vaultlocker.conf.j2": [ + "layer:vaultlocker", + "static", + "7428fcfb91731d37be14a0f8d4c5923cc95a28bd28579c5a013928ab147b0beb" + ], + "tests/functional/conftest.py": [ + "layer:kubernetes-common", + "static", + "fd53e0c38b4dda0c18096167889cd0d85b98b0a13225f9f8853261241e94078c" + ], + "tests/functional/test_k8s_common.py": [ + "layer:kubernetes-common", + "static", + "680a53724154771dd78422bbaf24b151788d86dd07960712c5d9e0d758499b50" + ], + "tests/unit/conftest.py": [ + "layer:kubernetes-common", + "static", + "fd53e0c38b4dda0c18096167889cd0d85b98b0a13225f9f8853261241e94078c" + ], + "tests/unit/test_k8s_common.py": [ + "layer:kubernetes-common", + "static", + "da9bcea8e75160311a4055c1cbf577b497ddd45dc00223c5f1667598f94d9be4" + ], + "tox.ini": [ + "layer:vaultlocker", + "static", + "716854030c843efa3afd32b8742807f1515b5fc705d8ae81aac9b447ffae411f" + ], + "version": [ + "kubernetes-master", + "dynamic", + "8b6f22cae011f81db35e9721985b8a6310f5d247c83e98368afd0c1ac7dce419" + ], + "wheelhouse.txt": [ + "kubernetes-master", + "dynamic", + "9308d42bcacaf5feb0e156848796f550fac48780743f72d41fd2cf75e877b67e" + ], + "wheelhouse/Jinja2-2.10.1.tar.gz": [ + "layer:basic", + "dynamic", + "065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013" + ], + "wheelhouse/MarkupSafe-1.1.1.tar.gz": [ + "layer:basic", + "dynamic", + "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b" + ], + "wheelhouse/PyYAML-5.2.tar.gz": [ + "layer:basic", + "dynamic", + "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c" + ], + "wheelhouse/Tempita-0.5.2.tar.gz": [ + "__pip__", + "dynamic", + "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c" + ], + "wheelhouse/aiohttp-3.7.4.post0.tar.gz": [ + "kubernetes-master", + "dynamic", + "493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf" + ], + "wheelhouse/async-timeout-3.0.1.tar.gz": [ + "__pip__", + "dynamic", + "0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f" + ], + "wheelhouse/attrs-21.2.0.tar.gz": [ + "__pip__", + "dynamic", + "ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "wheelhouse/cached-property-1.5.2.tar.gz": [ + "__pip__", + "dynamic", + "9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130" + ], + "wheelhouse/certifi-2021.10.8.tar.gz": [ + "__pip__", + "dynamic", + "78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872" + ], + "wheelhouse/chardet-4.0.0.tar.gz": [ + "__pip__", + "dynamic", + "0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa" + ], + "wheelhouse/charmhelpers-0.20.23.tar.gz": [ + "layer:basic", + "dynamic", + "59a9776594e91cd3e3e000043f8668b4d7b279422dbb17e320f01dc16385b80e" + ], + "wheelhouse/charms.reactive-1.4.1.tar.gz": [ + "layer:basic", + "dynamic", + "bba21b4fd40b26c240c9ef2aa10c6fdf73592031c68591da4e7ccc46ca9cb616" + ], + "wheelhouse/charset-normalizer-2.0.7.tar.gz": [ + "__pip__", + "dynamic", + "e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0" + ], + "wheelhouse/gunicorn-20.1.0.tar.gz": [ + "kubernetes-master", + "dynamic", + "e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + ], + "wheelhouse/hvac-0.11.2.tar.gz": [ + "layer:vault-kv", + "dynamic", + "f905c59d32d88d3f67571fe5a8a78de4659e04798ad809de439f667247d13626" + ], + "wheelhouse/idna-3.3.tar.gz": [ + "__pip__", + "dynamic", + "9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "wheelhouse/idna-ssl-1.1.0.tar.gz": [ + "__pip__", + "dynamic", + "a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c" + ], + "wheelhouse/loadbalancer_interface-1.1.1.tar.gz": [ + "kubernetes-master", + "dynamic", + "c71d50bb66286d6e15a5f2975c0a316a3cd43c2042428258c96d1b4b95e5706b" + ], + "wheelhouse/marshmallow-3.14.0.tar.gz": [ + "__pip__", + "dynamic", + "bba1a940985c052c5cc7849f97da196ebc81f3b85ec10c56ef1f3228aa9cbe74" + ], + "wheelhouse/marshmallow-enum-1.5.1.tar.gz": [ + "__pip__", + "dynamic", + "38e697e11f45a8e64b4a1e664000897c659b60aa57bfa18d44e226a9920b6e58" + ], + "wheelhouse/multidict-5.2.0.tar.gz": [ + "__pip__", + "dynamic", + "0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce" + ], + "wheelhouse/netaddr-0.7.19.tar.gz": [ + "layer:basic", + "dynamic", + "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd" + ], + "wheelhouse/netifaces-0.11.0.tar.gz": [ + "layer:vault-kv", + "dynamic", + "043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32" + ], + "wheelhouse/ops-1.2.0.tar.gz": [ + "__pip__", + "dynamic", + "3deb00ad7952b203502290a79bf8c8ce9b70e4f34fec3307fd45133c97a45824" + ], + "wheelhouse/ops_reactive_interface-1.0.1.tar.gz": [ + "__pip__", + "dynamic", + "9ed351c42fc187299c23125975aa3dfee9f6aaae0c9d49bce8904ac079255dba" + ], + "wheelhouse/pbr-5.6.0.tar.gz": [ + "__pip__", + "dynamic", + "42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd" + ], + "wheelhouse/pip-18.1.tar.gz": [ + "layer:basic", + "dynamic", + "c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1" + ], + "wheelhouse/psutil-5.8.0.tar.gz": [ + "layer:vault-kv", + "dynamic", + "0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6" + ], + "wheelhouse/pyaml-21.10.1.tar.gz": [ + "__pip__", + "dynamic", + "c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383" + ], + "wheelhouse/requests-2.26.0.tar.gz": [ + "__pip__", + "dynamic", + "b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + ], + "wheelhouse/setuptools-41.6.0.zip": [ + "layer:basic", + "dynamic", + "6afa61b391dcd16cb8890ec9f66cc4015a8a31a6e1c2b4e0c464514be1a3d722" + ], + "wheelhouse/setuptools_scm-1.17.0.tar.gz": [ + "layer:basic", + "dynamic", + "70a4cf5584e966ae92f54a764e6437af992ba42ac4bca7eb37cc5d02b98ec40a" + ], + "wheelhouse/six-1.16.0.tar.gz": [ + "__pip__", + "dynamic", + "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926" + ], + "wheelhouse/tenacity-5.0.3.tar.gz": [ + "layer:snap", + "dynamic", + "24b7f302a1caa1801e58b39ea557129c095966e64e5b1ddad3c93a6cb033e38b" + ], + "wheelhouse/typing_extensions-3.10.0.2.tar.gz": [ + "__pip__", + "dynamic", + "49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e" + ], + "wheelhouse/urllib3-1.26.7.tar.gz": [ + "__pip__", + "dynamic", + "4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece" + ], + "wheelhouse/wheel-0.33.6.tar.gz": [ + "layer:basic", + "dynamic", + "10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646" + ], + "wheelhouse/yarl-1.7.0.tar.gz": [ + "__pip__", + "dynamic", + "8e7ebaf62e19c2feb097ffb7c94deb0f0c9fab52590784c8cd679d30ab009162" + ] + } +} \ No newline at end of file diff --git a/kubernetes-master/.github/workflows/main.yml b/kubernetes-master/.github/workflows/main.yml new file mode 100644 index 0000000..27f47a8 --- /dev/null +++ b/kubernetes-master/.github/workflows/main.yml @@ -0,0 +1,47 @@ +name: Test Suite +on: [pull_request] + +jobs: + lint-unit-wheelhouse: + name: Lint, Unit, Wheelhouse + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + sudo snap install charm --classic + - name: Lint + run: tox -vve lint + - name: Unit Tests + run: tox -vve unit + - name: Validate Wheelhouse + run: tox -vve validate-wheelhouse + integration-test: + name: Integration test with VMWare + runs-on: self-hosted + timeout-minutes: 360 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@main + with: + provider: vsphere + credentials-yaml: ${{ secrets.CREDENTIALS_YAML }} + clouds-yaml: ${{ secrets.CLOUDS_YAML }} + bootstrap-options: "--model-default datastore=vsanDatastore --model-default primary-network=VLAN_2764" + - name: Run test + run: tox -e integration diff --git a/kubernetes-master/.gitignore b/kubernetes-master/.gitignore new file mode 100644 index 0000000..4701d96 --- /dev/null +++ b/kubernetes-master/.gitignore @@ -0,0 +1,5 @@ +.tox/ +__pycache__/ +*.pyc +placeholders/ +*.charm diff --git a/kubernetes-master/.travis.yml b/kubernetes-master/.travis.yml new file mode 100644 index 0000000..66d8e1f --- /dev/null +++ b/kubernetes-master/.travis.yml @@ -0,0 +1,7 @@ +language: python +python: + - "3.5" +install: + - pip install tox-travis +script: + - tox diff --git a/kubernetes-master/CONTRIBUTING.md b/kubernetes-master/CONTRIBUTING.md new file mode 100644 index 0000000..f0d8d31 --- /dev/null +++ b/kubernetes-master/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributor Guide + +This Juju charm is open source ([Apache License 2.0](./LICENSE)) and we actively seek any community contibutions +for code, suggestions and documentation. +This page details a few notes, workflows and suggestions for how to make contributions most effective and help us +all build a better charm - please give them a read before working on any contributions. + +## Licensing + +This charm has been created under the [Apache License 2.0](./LICENSE), which will cover any contributions you may +make to this project. Please familiarise yourself with the terms of the license. + +Additionally, this charm uses the Harmony CLA agreement. It’s the easiest way for you to give us permission to +use your contributions. +In effect, you’re giving us a license, but you still own the copyright — so you retain the right to modify your +code and use it in other projects. Please [sign the CLA here](https://ubuntu.com/legal/contributors/agreement) before +making any contributions. + +## Code of conduct + +We have adopted the Ubuntu code of Conduct. You can read this in full [here](https://ubuntu.com/community/code-of-conduct). + +## Contributing code + +To contribute code to this project, pleas euse the following workflow: + +1. [Submit a bug](https://bugs.launchpad.net/charm-etcd/+filebug) to explain the need for and track the change. +2. Create a branch on your fork of the repo with your changes, including a unit test covering the new or modified code. +3. Submit a PR. The PR description should include a link to the bug on Launchpad. +4. Update the Launchpad bug to include a link to the PR and the `review-needed` tag. +5. Once reviewed and merged, the change will become available on the edge channel and assigned to an appropriate milestone + for further release according to priority. + +## Documentation + +Documentation for this charm is currently maintained as part of the Charmed Kubernetes docs. +See [this page](https://github.com/charmed-kubernetes/kubernetes-docs/blob/master/pages/k8s/charm-etcd.md) diff --git a/kubernetes-master/LICENSE b/kubernetes-master/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/kubernetes-master/LICENSE @@ -0,0 +1,202 @@ + + 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. \ No newline at end of file diff --git a/kubernetes-master/Makefile b/kubernetes-master/Makefile new file mode 100644 index 0000000..a1ad3a5 --- /dev/null +++ b/kubernetes-master/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/kubernetes-master/README.md b/kubernetes-master/README.md new file mode 100644 index 0000000..02f24a6 --- /dev/null +++ b/kubernetes-master/README.md @@ -0,0 +1,53 @@ +# Kubernetes-master + +[Kubernetes](http://kubernetes.io/) is an open source system for managing +application containers across a cluster of hosts. The Kubernetes project was +started by Google in 2014, combining the experience of running production +workloads combined with best practices from the community. + +The Kubernetes project defines some new terms that may be unfamiliar to users +or operators. For more information please refer to the concept guide in the +[getting started guide](https://kubernetes.io/docs/home/). + +This charm is an encapsulation of the Kubernetes master processes and the +operations to run on any cloud for the entire lifecycle of the cluster. + +This charm is built from other charm layers using the Juju reactive framework. +The other layers focus on specific subset of operations making this layer +specific to operations of Kubernetes master processes. + +# Charmed Kubernetes + +This charm is not fully functional when deployed by itself. It requires other +charms to model a complete Kubernetes cluster. A Kubernetes cluster needs a +distributed key value store such as [Etcd](https://coreos.com/etcd/) and the +kubernetes-worker charm which delivers the Kubernetes node services. A cluster +also requires a Software Defined Network (SDN), a Container Runtime such as +[containerd](https://jaas.ai/u/containers/containerd), and Transport Layer +Security (TLS) so the components in a cluster communicate securely. + +Please take a look at the [Charmed Kubernetes](https://jaas.ai/charmed-kubernetes) +or the [Kubernetes core](https://jaas.ai/kubernetes-core) bundles for +examples of complete models of Kubernetes clusters. + +For full install instructions, please see the [Charmed Kubernetes documentation](https://ubuntu.com/kubernetes/docs/quickstart). + +For details on configuring and operating this charm, see the [kubernetes-master documentation](https://ubuntu.com/kubernetes/docs/charm-kubernetes-master) on the same site. + +# Developers + +## Building the charm + +``` +make charm +``` + +## Testing the charm + +``` +tox +``` + +Note that the unit tests use [`charms.unit_test`](https://pypi.org/project/charms.unit-test/) +so all charms.reactive helpers are automatically patched with fakes and little manual +patching needs to be done. Things like `set_flag` and `is_flag_set` can be used directly. diff --git a/kubernetes-master/actions.yaml b/kubernetes-master/actions.yaml new file mode 100644 index 0000000..3dfa89f --- /dev/null +++ b/kubernetes-master/actions.yaml @@ -0,0 +1,109 @@ +"debug": + "description": "Collect debug data" +"cis-benchmark": + "description": | + Run the CIS Kubernetes Benchmark against snap-based components. + "params": + "apply": + "type": "string" + "default": "none" + "description": | + Apply remediations to address benchmark failures. The default, 'none', + will not attempt to fix any reported failures. Set to 'conservative' + to resolve simple failures. Set to 'dangerous' to attempt to resolve + all failures. + + Note: Applying any remediation may result in an unusable cluster. + "config": + "type": "string" + "default": "https://github.com/charmed-kubernetes/kube-bench-config/archive/cis-1.5.zip#sha1=811f21dbf6c841bafdbfbd8a21f912ad67582f46" + "description": | + Archive containing configuration files to use when running kube-bench. + The default value is known to be compatible with snap components. When + using a custom URL, append '#=' to verify the + archive integrity when downloaded. + "release": + "type": "string" + "default": "https://github.com/aquasecurity/kube-bench/releases/download/v0.3.1/kube-bench_0.3.1_linux_amd64.tar.gz#sha256=6616f1373987259285e2f676a225d4a3885cd62b7e7a116102ff2fb445724281" + "description": | + Archive containing the 'kube-bench' binary to run. The default value + points to a stable upstream release. When using a custom URL, append + '#=' to verify the archive integrity when + downloaded. + + This may also be set to the special keyword 'upstream'. In this case, + the action will compile and use a local kube-bench binary built from + the master branch of the upstream repository: + https://github.com/aquasecurity/kube-bench + +"restart": + "description": "Restart the Kubernetes master services on demand." +"namespace-list": + "description": "List existing k8s namespaces" +"namespace-create": + "description": "Create new namespace" + "params": + "name": + "type": "string" + "description": "Namespace name eg. staging" + "minLength": !!int "2" + "required": + - "name" +"namespace-delete": + "description": "Delete namespace" + "params": + "name": + "type": "string" + "description": "Namespace name eg. staging" + "minLength": !!int "2" + "required": + - "name" +"upgrade": + "description": "Upgrade the kubernetes snaps" + "params": + "fix-cluster-name": + "type": "boolean" + "default": !!bool "true" + "description": >- + If using the OpenStack cloud provider, whether to fix the cluster + name sent to it to include the cluster tag. This fixes an issue + with load balancers conflicting with other clusters in the same + project but will cause new load balancers to be created which will + require manual intervention to resolve. +"user-create": + "description": "Create a new user" + "params": + "name": + "type": "string" + "description": | + Username for the new user. This value must only contain alphanumeric + characters, ':', '@', '-' or '.'. + "minLength": !!int "2" + "groups": + "type": "string" + "description": | + Optional comma-separated list of groups eg. 'system:masters,managers' + "required": + - "name" +"user-delete": + "description": "Delete an existing user" + "params": + "name": + "type": "string" + "description": "Username of the user to delete" + "minLength": !!int "2" + "required": + - "name" +"user-list": + "description": "List existing users" +"get-kubeconfig": + "description": "Retrieve Kubernetes cluster config, including credentials" +"apply-manifest": + "description": "Apply JSON formatted Kubernetes manifest to cluster" + "params": + "json": + "type": "string" + "description": "The content of the manifest to deploy in JSON format" + "minLength": !!int "2" + "required": + - "json" diff --git a/kubernetes-master/actions/apply-manifest b/kubernetes-master/actions/apply-manifest new file mode 100755 index 0000000..60e67eb --- /dev/null +++ b/kubernetes-master/actions/apply-manifest @@ -0,0 +1,75 @@ +#!/usr/local/sbin/charm-env python3 +import os +import json +import tempfile +import subprocess +from charmhelpers.core.hookenv import action_get, action_set, action_fail, action_name + + +def _kubectl(args): + """ + Executes kubectl with args as arguments + """ + snap_bin = os.path.join(os.sep, "snap", "bin") + env = os.environ.copy() + env["PATH"] = os.pathsep.join([snap_bin, env["PATH"]]) + cmd = ["kubectl", "--kubeconfig=/home/ubuntu/config"] + cmd.extend(args) + return subprocess.check_output( + cmd, + env=env, + stderr=subprocess.STDOUT, + ) + + +def get_kubeconfig(): + """ + Read the kubeconfig on the master and return it as JSON + """ + try: + result = _kubectl(["config", "view", "-o", "json", "--raw"]) + # JSON format verification + kubeconfig = json.dumps(json.loads(result)) + action_set({"kubeconfig": kubeconfig}) + except json.JSONDecodeError as e: + action_fail("Failed to parse kubeconfig: {}".format(str(e))) + except Exception as e: + action_fail("Failed to retrieve kubeconfig: {}".format(str(e))) + + +def apply_manifest(): + """ + Applies a user defined manifest with kubectl + """ + _, apply_path = tempfile.mkstemp(suffix=".json") + try: + manifest = json.loads(action_get("json")) + with open(apply_path, "w") as manifest_file: + json.dump(manifest, manifest_file) + output = _kubectl(["apply", "-f", apply_path]) + + action_set( + { + "summary": "Manifest applied.", + "output": output.decode("utf-8"), + } + ) + except subprocess.CalledProcessError as e: + action_fail( + "kubectl failed with exit code {} and message: {}".format( + e.returncode, e.output + ) + ) + except json.JSONDecodeError as e: + action_fail("Failed to parse JSON manifest: {}".format(str(e))) + except Exception as e: + action_fail("Failed to apply manifest: {}".format(str(e))) + finally: + os.unlink(apply_path) + + +action = action_name() +if action == "get-kubeconfig": + get_kubeconfig() +elif action == "apply-manifest": + apply_manifest() diff --git a/kubernetes-master/actions/cis-benchmark b/kubernetes-master/actions/cis-benchmark new file mode 100755 index 0000000..3f91dea --- /dev/null +++ b/kubernetes-master/actions/cis-benchmark @@ -0,0 +1,371 @@ +#!/usr/local/sbin/charm-env python3 +import os +import json +import shlex +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +import charms.layer +import charms.reactive +from charmhelpers.core import hookenv, unitdata +from charmhelpers.fetch.archiveurl import ArchiveUrlFetchHandler +from charms.layer import snap +from charms.reactive import clear_flag, is_flag_set, set_flag + + +BENCH_HOME = '/home/ubuntu/kube-bench' +BENCH_BIN = '{}/kube-bench'.format(BENCH_HOME) +BENCH_CFG = '{}/cfg-ck'.format(BENCH_HOME) +GO_PKG = 'github.com/aquasecurity/kube-bench' +RESULTS_DIR = '/home/ubuntu/kube-bench-results' + +# Remediation dicts associate a failing test with a tuple to fix it. +# Conservative fixes will probably leave the cluster in a good state. +# Dangerous fixes will likely break the cluster. +# Tuple examples: +# {'1.2.3': ('manual -- we don't know how to auto fix this', None, None)} +# {'1.2.3': ('cli', 'command to run', None)} +# {'1.2.3': ('kv', 'snap', {cfg_key: value})} +CONSERVATIVE = { + '0.0.0': ('cli', 'echo "this is fine"', None), + + # etcd (no known failures with a default install) + + # k8s-master + '1.2.21': ('kv', 'kube-apiserver', {'profiling': 'false'}), + '1.2.23': ('kv', 'kube-apiserver', {'audit-log-maxage': '30'}), + '1.2.24': ('kv', 'kube-apiserver', {'audit-log-maxbackup': '10'}), + '1.3.1': ('kv', 'kube-controller-manager', {'terminated-pod-gc-threshold': '500'}), + '1.3.2': ('kv', 'kube-controller-manager', {'profiling': 'false'}), + '1.4.1': ('kv', 'kube-scheduler', {'profiling': 'false'}), + + # k8s-worker + '4.2.2': ('kv', 'kubelet', {'authorization-mode': 'Webhook'}), + '4.2.4': ('kv', 'kubelet', {'read-only-port': '0'}), + '4.2.6': ('kv', 'kubelet', {'protect-kernel-defaults': 'true'}), +} +ADMISSION_PLUGINS = {'enable-admission-plugins': ('PersistentVolumeLabel', + 'PodSecurityPolicy,' + 'ServiceAccount,' + 'NodeRestriction')} +DANGEROUS = { + '0.0.0': ('cli', 'echo "this is fine"', None), + + # etcd (no known failures with a default install) + + # k8s-master + '1.2.2': ('kv', 'kube-apiserver', {'basic-auth-file': None}), + '1.2.3': ('kv', 'kube-apiserver', {'token-auth-file': None}), + '1.2.7': ('kv', 'kube-apiserver', {'authorization-mode': 'RBAC,Node'}), + '1.2.8': ('kv', 'kube-apiserver', {'authorization-mode': 'RBAC,Node'}), + '1.2.9': ('kv', 'kube-apiserver', {'authorization-mode': 'RBAC,Node'}), + '1.2.14': ('kv', 'kube-apiserver', ADMISSION_PLUGINS), + '1.2.16': ('kv', 'kube-apiserver', ADMISSION_PLUGINS), + '1.2.17': ('kv', 'kube-apiserver', ADMISSION_PLUGINS), + '1.2.18': ('kv', 'kube-apiserver', {'insecure-bind-address': None}), + '1.2.19': ('kv', 'kube-apiserver', {'insecure-port': '0'}), + '1.2.33': ('manual', None, None), + '1.3.6': ('kv', 'kube-controller-manager', + {'feature-gates': 'RotateKubeletServerCertificate=true'}), + + # k8s-worker + '4.2.12': ('kv', 'kubelet', + {'feature-gates': 'RotateKubeletServerCertificate=true'}), +} + + +def _fail(msg): + '''Fail the action with a given message.''' + hookenv.action_fail(msg) + sys.exit() + + +def _move_matching_parent(dirpath, filename, dest): + '''Move a parent directory that contains a specific file. + + Helper function that walks a directory looking for a given file. If found, + the file's parent directory is moved to the given destination. + + :param: dirpath: String path to search + :param: filename: String file to find + :param: dest: String destination of the found parent directory + ''' + for root, _, files in os.walk(dirpath): + for name in files: + if name == filename: + hookenv.log('Moving {} to {}'.format(root, dest)) + shutil.move(root, dest) + return + else: + _fail('Could not find {} in {}'.format(filename, dirpath)) + + +def _restart_charm(): + '''Set charm-specific flags and call reactive.main().''' + app = hookenv.charm_name() or 'unknown' + if 'master' in app: + hookenv.log('Restarting master') + clear_flag('kubernetes-master.components.started') + elif 'worker' in app: + hookenv.log('Restarting worker') + set_flag('kubernetes-worker.restart-needed') + elif 'etcd' in app: + hookenv.log('No-op: etcd does not need to be restarted') + return + else: + _fail('Unable to determine the charm to restart: {}'.format(app)) + + # Invoke reactive so the charm will react to the flags we just managed + charms.layer.import_layer_libs() + charms.reactive.main() + + +def install(release, config): + '''Install kube-bench and related configuration. + + Release and configuration are set via action params. If installing an + upstream release, this method will also install 'go' if needed. + + :param: release: Archive URI or 'upstream' + :param: config: Archive URI of configuration files + ''' + if Path(BENCH_HOME).exists(): + shutil.rmtree(BENCH_HOME) + fetcher = ArchiveUrlFetchHandler() + + if release == 'upstream': + Path(BENCH_HOME).mkdir(parents=True, exist_ok=True) + + # Setup the 'go' environment + env = os.environ.copy() + go_bin = shutil.which('go', path='{}:/snap/bin'.format(env['PATH'])) + if not go_bin: + snap.install('go', channel='stable', classic=True) + go_bin = '/snap/bin/go' + go_cache = os.getenv('GOCACHE', '/var/snap/go/common/cache') + go_path = os.getenv('GOPATH', '/var/snap/go/common') + env['GOCACHE'] = go_cache + env['GOPATH'] = go_path + Path(go_path).mkdir(parents=True, exist_ok=True) + + # From https://github.com/aquasecurity/kube-bench#installing-from-sources + go_cmd = ('{bin} get {pkg} ' + 'github.com/golang/dep/cmd/dep'.format(bin=go_bin, pkg=GO_PKG)) + try: + subprocess.check_call(shlex.split(go_cmd), cwd=go_path, env=env) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(go_cmd)) + + go_cmd = ('{bin} build -o {out} {base}/src/{pkg}'.format( + bin=go_bin, out=BENCH_BIN, base=go_path, pkg=GO_PKG)) + try: + subprocess.check_call(shlex.split(go_cmd), cwd=go_path, env=env) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(go_cmd)) + else: + # Fetch the release URI and put it in the right place. + archive_path = fetcher.install(source=release) + # NB: We may not know the structure of the archive, but we know the + # directory containing 'kube-bench' belongs in our BENCH_HOME. + _move_matching_parent( + dirpath=archive_path, filename='kube-bench', dest=BENCH_HOME) + + # Fetch the config URI and put it in the right place. + archive_dir = fetcher.install(source=config) + # NB: We may not know the structure of the archive, but we know the + # directory containing 'config.yaml' belongs in our BENCH_CFG. + _move_matching_parent( + dirpath=archive_dir, filename='config.yaml', dest=BENCH_CFG) + + +def apply(remediations=None): + '''Apply remediations to address benchmark failures. + + :param: remediations: either 'conservative' or 'dangerous' + ''' + applied_fixes = 0 + danger = True if remediations == 'dangerous' else False + db = unitdata.kv() + + json_log = report(log_format='json') + hookenv.log('Loading JSON from: {}'.format(json_log)) + try: + with open(json_log, 'r') as f: + full_json = json.load(f) + except Exception: + _fail('Failed to load: {}'.format(json_log)) + + for test in full_json.get('tests', {}): + for result in test.get('results', {}): + test_num = result.get('test_number') + test_remediation = result.get('remediation') + test_status = result.get('status', '') + + if test_status.lower() == 'fail': + test_remedy = CONSERVATIVE.get(test_num) + if not test_remedy and danger: + # no conservative remedy, check dangerous if user wants + test_remedy = DANGEROUS.get(test_num) + if isinstance(test_remedy, tuple): + if test_remedy[0] == 'manual': + # we don't know how to autofix; log remediation text + hookenv.log('Test {}: unable to auto-apply remedy.\n' + 'Manual steps:\n{}'.format(test_num, + test_remediation)) + elif test_remedy[0] == 'cli': + cmd = shlex.split(test_remedy[1]) + try: + out = subprocess.check_output(cmd) + except subprocess.CalledProcessError: + _fail('Test {}: failed to run: {}'.format(test_num, cmd)) + else: + hookenv.log('Test {}: applied remedy: {}\n' + 'Output: {}'.format(test_num, cmd, out)) + applied_fixes += 1 + elif test_remedy[0] == 'kv': + cfg_key = 'cis-' + test_remedy[1] + cfg = db.get(cfg_key) or {} + cfg.update(test_remedy[2]) + db.set(cfg_key, cfg) + + hookenv.log('Test {}: updated configuration: {}\n'.format( + test_num, cfg)) + applied_fixes += 1 + else: + hookenv.log('Test {}: remediation is missing'.format(test_num)) + + # CLI and KV changes will require a charm restart; do it. + if applied_fixes > 0: + _restart_charm() + + msg = ('Applied {} remediations. Re-run with "apply=none" to generate a ' + 'new report.').format(applied_fixes) + hookenv.action_set({'summary': msg}) + + +def reset(): + '''Reset any remediations we applied to unitdata.kv(). + + This action does not track individual remediations to reset. Therefore, + this function unconditionally unsets all 'cis-' prefixed arguments that + this action may have set and restarts the relevant charm. + ''' + db = unitdata.kv() + + db.unset('cis-kube-apiserver') + db.unset('cis-kube-scheduler') + db.unset('cis-kube-controller-manager') + db.unset('cis-kubelet') + _restart_charm() + + hookenv.action_set({'summary': ('Reset is complete. Re-run with ' + '"apply=none" to generate a new report.')}) + + +def report(log_format='text'): + '''Run kube-bench and report results. + + By default, save the full plain-text results to our RESULTS_DIR and set + action output with a summary. This function can also save full results in + a machine-friendly json format. + + :param: log_format: String determines if output is text or json + :returns: Path to results log + ''' + Path(RESULTS_DIR).mkdir(parents=True, exist_ok=True) + + # Node type is different depending on the charm + app = hookenv.charm_name() or 'unknown' + version = 'cis-1.5' + if 'master' in app: + target = 'master' + elif 'worker' in app: + target = 'node' + elif 'etcd' in app: + target = 'etcd' + else: + _fail('Unable to determine the target to benchmark: {}'.format(app)) + + # Commands and log names are different depending on the format + if log_format == 'json': + log_prefix = 'results-json-' + verbose_cmd = ('{bin} -D {cfg} --benchmark {ver} --json run ' + '--targets {target}').format( + bin=BENCH_BIN, cfg=BENCH_CFG, ver=version, target=target) + else: + log_prefix = 'results-text-' + verbose_cmd = ('{bin} -D {cfg} --benchmark {ver} run ' + '--targets {target}').format( + bin=BENCH_BIN, cfg=BENCH_CFG, ver=version, target=target) + + summary_cmd = ('{bin} -D {cfg} --benchmark {ver} ' + '--noremediations --noresults run --targets {target}').format( + bin=BENCH_BIN, cfg=BENCH_CFG, ver=version, target=target) + + # Store full results for future consumption + with tempfile.NamedTemporaryFile(mode='w+b', prefix=log_prefix, + dir=RESULTS_DIR, delete=False) as res_file: + try: + subprocess.call(shlex.split(verbose_cmd), stdout=res_file) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(verbose_cmd)) + else: + # remember the filename for later (and make it readable, why not?) + Path(res_file.name).chmod(0o644) + log = res_file.name + + # When making a summary, we also have a verbose report. Set action output + # so operators can see everything related to this run. + try: + out = subprocess.check_output(shlex.split(summary_cmd), + universal_newlines=True) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(summary_cmd)) + else: + fetch_cmd = 'juju scp {unit}:{file} .'.format(unit=hookenv.local_unit(), + file=log) + hookenv.action_set({'cmd': summary_cmd, + 'report': fetch_cmd, + 'summary': out}) + + return log or None + + +if __name__ == '__main__': + if not (is_flag_set('snap.installed.etcd') or + is_flag_set('kubernetes-master.snaps.installed') or + is_flag_set('kubernetes-worker.snaps.installed')): + msg = 'Snaps are not yet installed on this unit.' + _fail(msg) + + # Validate action params + release = hookenv.action_get('release') or 'upstream' + config = hookenv.action_get('config') + if not config: + msg = 'Missing "config" parameter' + _fail(msg) + remediations = hookenv.action_get('apply') + if remediations not in ['none', 'conservative', 'dangerous', 'reset']: + msg = 'Invalid "apply" parameter: {}'.format(remediations) + _fail(msg) + + # TODO: may want an option to overwrite an existing install + if Path(BENCH_BIN).exists() and Path(BENCH_CFG).exists(): + hookenv.log('{} exists; skipping install'.format(BENCH_HOME)) + else: + hookenv.log('Installing benchmark from: {}'.format(release)) + install(release, config) + + # Reset, remediate, or report + if remediations == 'reset': + hookenv.log('Attempting to remove all remediations') + reset() + elif remediations != 'none': + hookenv.log('Applying "{}" remediations'.format(remediations)) + apply(remediations) + else: + hookenv.log('Report only; no remediations were requested') + report(log_format='text') diff --git a/kubernetes-master/actions/debug b/kubernetes-master/actions/debug new file mode 100755 index 0000000..8ba160e --- /dev/null +++ b/kubernetes-master/actions/debug @@ -0,0 +1,102 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import tarfile +import tempfile +import traceback +from contextlib import contextmanager +from datetime import datetime +from charmhelpers.core.hookenv import action_set, local_unit + +archive_dir = None +log_file = None + + +@contextmanager +def archive_context(): + """ Open a context with a new temporary directory. + + When the context closes, the directory is archived, and the archive + location is added to Juju action output. """ + global archive_dir + global log_file + with tempfile.TemporaryDirectory() as temp_dir: + name = "debug-" + datetime.now().strftime("%Y%m%d%H%M%S") + archive_dir = os.path.join(temp_dir, name) + os.makedirs(archive_dir) + with open("%s/debug.log" % archive_dir, "w") as log_file: + yield + os.chdir(temp_dir) + tar_path = "/home/ubuntu/%s.tar.gz" % name + with tarfile.open(tar_path, "w:gz") as f: + f.add(name) + action_set({ + "path": tar_path, + "command": "juju scp %s:%s ." % (local_unit(), tar_path), + "message": " ".join([ + "Archive has been created on unit %s." % local_unit(), + "Use the juju scp command to copy it to your local machine." + ]) + }) + + +def log(msg): + """ Log a message that will be included in the debug archive. + + Must be run within archive_context """ + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for line in str(msg).splitlines(): + log_file.write(timestamp + " | " + line.rstrip() + "\n") + + +def run_script(script): + """ Run a single script. Must be run within archive_context """ + log("Running script: " + script) + script_dir = os.path.join(archive_dir, script) + os.makedirs(script_dir) + env = os.environ.copy() + env["PYTHONPATH"] = "lib" # allow same imports as reactive code + env["DEBUG_SCRIPT_DIR"] = script_dir + with open(script_dir + "/stdout", "w") as stdout: + with open(script_dir + "/stderr", "w") as stderr: + process = subprocess.Popen( + "debug-scripts/" + script, + stdout=stdout, stderr=stderr, env=env + ) + try: + exit_code = process.wait(timeout=300) + except subprocess.TimeoutExpired: + log("ERROR: still running, terminating") + process.terminate() + try: + exit_code = process.wait(timeout=10) + except subprocess.TimeoutExpired: + log("ERROR: still running, killing") + process.kill() + exit_code = process.wait(timeout=10) + if exit_code != 0: + log("ERROR: %s failed with exit code %d" % (script, exit_code)) + + +def run_all_scripts(): + """ Run all scripts. For the sake of robustness, log and ignore any + exceptions that occur. + + Must be run within archive_context """ + scripts = os.listdir("debug-scripts") + for script in scripts: + try: + run_script(script) + except: + log(traceback.format_exc()) + + +def main(): + """ Open an archive context and run all scripts. """ + with archive_context(): + run_all_scripts() + + +if __name__ == "__main__": + main() diff --git a/kubernetes-master/actions/get-kubeconfig b/kubernetes-master/actions/get-kubeconfig new file mode 100755 index 0000000..60e67eb --- /dev/null +++ b/kubernetes-master/actions/get-kubeconfig @@ -0,0 +1,75 @@ +#!/usr/local/sbin/charm-env python3 +import os +import json +import tempfile +import subprocess +from charmhelpers.core.hookenv import action_get, action_set, action_fail, action_name + + +def _kubectl(args): + """ + Executes kubectl with args as arguments + """ + snap_bin = os.path.join(os.sep, "snap", "bin") + env = os.environ.copy() + env["PATH"] = os.pathsep.join([snap_bin, env["PATH"]]) + cmd = ["kubectl", "--kubeconfig=/home/ubuntu/config"] + cmd.extend(args) + return subprocess.check_output( + cmd, + env=env, + stderr=subprocess.STDOUT, + ) + + +def get_kubeconfig(): + """ + Read the kubeconfig on the master and return it as JSON + """ + try: + result = _kubectl(["config", "view", "-o", "json", "--raw"]) + # JSON format verification + kubeconfig = json.dumps(json.loads(result)) + action_set({"kubeconfig": kubeconfig}) + except json.JSONDecodeError as e: + action_fail("Failed to parse kubeconfig: {}".format(str(e))) + except Exception as e: + action_fail("Failed to retrieve kubeconfig: {}".format(str(e))) + + +def apply_manifest(): + """ + Applies a user defined manifest with kubectl + """ + _, apply_path = tempfile.mkstemp(suffix=".json") + try: + manifest = json.loads(action_get("json")) + with open(apply_path, "w") as manifest_file: + json.dump(manifest, manifest_file) + output = _kubectl(["apply", "-f", apply_path]) + + action_set( + { + "summary": "Manifest applied.", + "output": output.decode("utf-8"), + } + ) + except subprocess.CalledProcessError as e: + action_fail( + "kubectl failed with exit code {} and message: {}".format( + e.returncode, e.output + ) + ) + except json.JSONDecodeError as e: + action_fail("Failed to parse JSON manifest: {}".format(str(e))) + except Exception as e: + action_fail("Failed to apply manifest: {}".format(str(e))) + finally: + os.unlink(apply_path) + + +action = action_name() +if action == "get-kubeconfig": + get_kubeconfig() +elif action == "apply-manifest": + apply_manifest() diff --git a/kubernetes-master/actions/kubectl-actions.py b/kubernetes-master/actions/kubectl-actions.py new file mode 100755 index 0000000..60e67eb --- /dev/null +++ b/kubernetes-master/actions/kubectl-actions.py @@ -0,0 +1,75 @@ +#!/usr/local/sbin/charm-env python3 +import os +import json +import tempfile +import subprocess +from charmhelpers.core.hookenv import action_get, action_set, action_fail, action_name + + +def _kubectl(args): + """ + Executes kubectl with args as arguments + """ + snap_bin = os.path.join(os.sep, "snap", "bin") + env = os.environ.copy() + env["PATH"] = os.pathsep.join([snap_bin, env["PATH"]]) + cmd = ["kubectl", "--kubeconfig=/home/ubuntu/config"] + cmd.extend(args) + return subprocess.check_output( + cmd, + env=env, + stderr=subprocess.STDOUT, + ) + + +def get_kubeconfig(): + """ + Read the kubeconfig on the master and return it as JSON + """ + try: + result = _kubectl(["config", "view", "-o", "json", "--raw"]) + # JSON format verification + kubeconfig = json.dumps(json.loads(result)) + action_set({"kubeconfig": kubeconfig}) + except json.JSONDecodeError as e: + action_fail("Failed to parse kubeconfig: {}".format(str(e))) + except Exception as e: + action_fail("Failed to retrieve kubeconfig: {}".format(str(e))) + + +def apply_manifest(): + """ + Applies a user defined manifest with kubectl + """ + _, apply_path = tempfile.mkstemp(suffix=".json") + try: + manifest = json.loads(action_get("json")) + with open(apply_path, "w") as manifest_file: + json.dump(manifest, manifest_file) + output = _kubectl(["apply", "-f", apply_path]) + + action_set( + { + "summary": "Manifest applied.", + "output": output.decode("utf-8"), + } + ) + except subprocess.CalledProcessError as e: + action_fail( + "kubectl failed with exit code {} and message: {}".format( + e.returncode, e.output + ) + ) + except json.JSONDecodeError as e: + action_fail("Failed to parse JSON manifest: {}".format(str(e))) + except Exception as e: + action_fail("Failed to apply manifest: {}".format(str(e))) + finally: + os.unlink(apply_path) + + +action = action_name() +if action == "get-kubeconfig": + get_kubeconfig() +elif action == "apply-manifest": + apply_manifest() diff --git a/kubernetes-master/actions/namespace-create b/kubernetes-master/actions/namespace-create new file mode 100755 index 0000000..50e8650 --- /dev/null +++ b/kubernetes-master/actions/namespace-create @@ -0,0 +1,60 @@ +#!/usr/local/sbin/charm-env python3 +import os +from yaml import safe_load as load +from charmhelpers.core.hookenv import ( + action_get, + action_set, + action_fail, + action_name +) +from charmhelpers.core.templating import render +from subprocess import check_output + + +os.environ['PATH'] += os.pathsep + os.path.join(os.sep, 'snap', 'bin') + + +def kubectl(args): + cmd = ["kubectl", "--kubeconfig=/home/ubuntu/config"] + cmd.extend(args) + return check_output(cmd) + + +def namespace_list(): + y = load(kubectl(['get', 'namespaces', '-o', 'yaml'])) + ns = [i['metadata']['name'] for i in y['items']] + action_set({'namespaces': ', '.join(ns)+'.'}) + return ns + + +def namespace_create(): + name = action_get('name') + if name in namespace_list(): + action_fail('Namespace "{}" already exists.'.format(name)) + return + + render('create-namespace.yaml.j2', '/etc/kubernetes/addons/create-namespace.yaml', + context={'name': name}) + kubectl(['create', '-f', '/etc/kubernetes/addons/create-namespace.yaml']) + action_set({'msg': 'Namespace "{}" created.'.format(name)}) + + +def namespace_delete(): + name = action_get('name') + if name in ['default', 'kube-system']: + action_fail('Not allowed to delete "{}".'.format(name)) + return + if name not in namespace_list(): + action_fail('Namespace "{}" does not exist.'.format(name)) + return + kubectl(['delete', 'ns/'+name]) + action_set({'msg': 'Namespace "{}" deleted.'.format(name)}) + + +action = action_name().replace('namespace-', '') +if action == 'create': + namespace_create() +elif action == 'list': + namespace_list() +elif action == 'delete': + namespace_delete() diff --git a/kubernetes-master/actions/namespace-delete b/kubernetes-master/actions/namespace-delete new file mode 100755 index 0000000..50e8650 --- /dev/null +++ b/kubernetes-master/actions/namespace-delete @@ -0,0 +1,60 @@ +#!/usr/local/sbin/charm-env python3 +import os +from yaml import safe_load as load +from charmhelpers.core.hookenv import ( + action_get, + action_set, + action_fail, + action_name +) +from charmhelpers.core.templating import render +from subprocess import check_output + + +os.environ['PATH'] += os.pathsep + os.path.join(os.sep, 'snap', 'bin') + + +def kubectl(args): + cmd = ["kubectl", "--kubeconfig=/home/ubuntu/config"] + cmd.extend(args) + return check_output(cmd) + + +def namespace_list(): + y = load(kubectl(['get', 'namespaces', '-o', 'yaml'])) + ns = [i['metadata']['name'] for i in y['items']] + action_set({'namespaces': ', '.join(ns)+'.'}) + return ns + + +def namespace_create(): + name = action_get('name') + if name in namespace_list(): + action_fail('Namespace "{}" already exists.'.format(name)) + return + + render('create-namespace.yaml.j2', '/etc/kubernetes/addons/create-namespace.yaml', + context={'name': name}) + kubectl(['create', '-f', '/etc/kubernetes/addons/create-namespace.yaml']) + action_set({'msg': 'Namespace "{}" created.'.format(name)}) + + +def namespace_delete(): + name = action_get('name') + if name in ['default', 'kube-system']: + action_fail('Not allowed to delete "{}".'.format(name)) + return + if name not in namespace_list(): + action_fail('Namespace "{}" does not exist.'.format(name)) + return + kubectl(['delete', 'ns/'+name]) + action_set({'msg': 'Namespace "{}" deleted.'.format(name)}) + + +action = action_name().replace('namespace-', '') +if action == 'create': + namespace_create() +elif action == 'list': + namespace_list() +elif action == 'delete': + namespace_delete() diff --git a/kubernetes-master/actions/namespace-list b/kubernetes-master/actions/namespace-list new file mode 100755 index 0000000..50e8650 --- /dev/null +++ b/kubernetes-master/actions/namespace-list @@ -0,0 +1,60 @@ +#!/usr/local/sbin/charm-env python3 +import os +from yaml import safe_load as load +from charmhelpers.core.hookenv import ( + action_get, + action_set, + action_fail, + action_name +) +from charmhelpers.core.templating import render +from subprocess import check_output + + +os.environ['PATH'] += os.pathsep + os.path.join(os.sep, 'snap', 'bin') + + +def kubectl(args): + cmd = ["kubectl", "--kubeconfig=/home/ubuntu/config"] + cmd.extend(args) + return check_output(cmd) + + +def namespace_list(): + y = load(kubectl(['get', 'namespaces', '-o', 'yaml'])) + ns = [i['metadata']['name'] for i in y['items']] + action_set({'namespaces': ', '.join(ns)+'.'}) + return ns + + +def namespace_create(): + name = action_get('name') + if name in namespace_list(): + action_fail('Namespace "{}" already exists.'.format(name)) + return + + render('create-namespace.yaml.j2', '/etc/kubernetes/addons/create-namespace.yaml', + context={'name': name}) + kubectl(['create', '-f', '/etc/kubernetes/addons/create-namespace.yaml']) + action_set({'msg': 'Namespace "{}" created.'.format(name)}) + + +def namespace_delete(): + name = action_get('name') + if name in ['default', 'kube-system']: + action_fail('Not allowed to delete "{}".'.format(name)) + return + if name not in namespace_list(): + action_fail('Namespace "{}" does not exist.'.format(name)) + return + kubectl(['delete', 'ns/'+name]) + action_set({'msg': 'Namespace "{}" deleted.'.format(name)}) + + +action = action_name().replace('namespace-', '') +if action == 'create': + namespace_create() +elif action == 'list': + namespace_list() +elif action == 'delete': + namespace_delete() diff --git a/kubernetes-master/actions/restart b/kubernetes-master/actions/restart new file mode 100755 index 0000000..d130733 --- /dev/null +++ b/kubernetes-master/actions/restart @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set +ex + +# Restart the apiserver, controller-manager, and scheduler + +systemctl restart snap.kube-apiserver.daemon +action-set apiserver.status='restarted' + +systemctl restart snap.kube-controller-manager.daemon +action-set controller-manager.status='restarted' + +systemctl restart snap.kube-scheduler.daemon +action-set kube-scheduler.status='restarted' diff --git a/kubernetes-master/actions/upgrade b/kubernetes-master/actions/upgrade new file mode 100755 index 0000000..ecff71f --- /dev/null +++ b/kubernetes-master/actions/upgrade @@ -0,0 +1,9 @@ +#!/bin/bash +set -eux + +if [[ "$(action-get fix-cluster-name)" == "true" ]]; then + charms.reactive set_state 'kubernetes-master.cdk-addons.unique-cluster-tag' +fi + +charms.reactive set_state kubernetes-master.upgrade-specified +exec hooks/config-changed diff --git a/kubernetes-master/actions/user-create b/kubernetes-master/actions/user-create new file mode 100755 index 0000000..7e7785a --- /dev/null +++ b/kubernetes-master/actions/user-create @@ -0,0 +1,105 @@ +#!/usr/local/sbin/charm-env python3 +import os +import re +import sys +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import action_get, action_set, action_fail, action_name +from charms import layer + +os.environ["PATH"] += os.pathsep + os.path.join(os.sep, "snap", "bin") + +# Import charm layers and start reactive +layer.import_layer_libs() +hookenv._run_atstart() + + +def protect_resources(name): + """Do not allow the action to operate on names used by Charmed Kubernetes.""" + protected_names = [ + "admin", + "system:kube-controller-manager", + "kube-controller-manager", + "system:kube-proxy", + "kube-proxy", + "system:kube-scheduler", + "kube-scheduler", + "system:monitoring", + ] + if name.startswith("kubelet") or name in protected_names: + action_fail('Not allowed to {} "{}".'.format(action, name)) + sys.exit(0) + + +def user_list(): + """Return a dict of 'username: secret_id' for Charmed Kubernetes users.""" + secrets = layer.kubernetes_common.get_secret_names() + action_set({"users": ", ".join(list(secrets))}) + return secrets + + +def user_create(): + user = action_get("name") + groups = action_get("groups") or "" + protect_resources(user) + + users = user_list() + if user in list(users): + action_fail('User "{}" already exists.'.format(user)) + return + + # Validate the name + if re.search("[^0-9A-Za-z:@.-]+", user): + msg = "User name may only contain alphanumeric characters, ':', '@', '-' or '.'" + action_fail(msg) + return + + # Create the secret + # TODO: make the token format less magical so it doesn't get out of + # sync with the function that creates secrets in k8s-master.py. + token = "{}::{}".format(user, layer.kubernetes_master.token_generator()) + if not layer.kubernetes_common.create_secret(token, user, user, groups): + action_fail("Failed to create secret for: {}".format(user)) + return + + # Create a kubeconfig + ca_crt = layer.kubernetes_common.ca_crt_path + kubeconfig_path = "/home/ubuntu/{}-kubeconfig".format(user) + public_address, public_port = layer.kubernetes_master.get_api_endpoint() + public_server = "https://{0}:{1}".format(public_address, public_port) + + layer.kubernetes_common.create_kubeconfig( + kubeconfig_path, public_server, ca_crt, token=token, user=user + ) + os.chmod(kubeconfig_path, 0o644) + + # Tell the people what they've won + fetch_cmd = "juju scp {}:{} .".format(hookenv.local_unit(), kubeconfig_path) + action_set({"msg": 'User "{}" created.'.format(user)}) + action_set({"users": ", ".join(list(users) + [user])}) + action_set({"kubeconfig": fetch_cmd}) + + +def user_delete(): + user = action_get("name") + protect_resources(user) + + users = user_list() + if user not in list(users): + action_fail('User "{}" does not exist.'.format(user)) + return + + # Delete the secret + secret_id = users[user] + layer.kubernetes_master.delete_secret(secret_id) + + action_set({"msg": 'User "{}" deleted.'.format(user)}) + action_set({"users": ", ".join(u for u in list(users) if u != user)}) + + +action = action_name().replace("user-", "") +if action == "create": + user_create() +elif action == "list": + user_list() +elif action == "delete": + user_delete() diff --git a/kubernetes-master/actions/user-delete b/kubernetes-master/actions/user-delete new file mode 100755 index 0000000..7e7785a --- /dev/null +++ b/kubernetes-master/actions/user-delete @@ -0,0 +1,105 @@ +#!/usr/local/sbin/charm-env python3 +import os +import re +import sys +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import action_get, action_set, action_fail, action_name +from charms import layer + +os.environ["PATH"] += os.pathsep + os.path.join(os.sep, "snap", "bin") + +# Import charm layers and start reactive +layer.import_layer_libs() +hookenv._run_atstart() + + +def protect_resources(name): + """Do not allow the action to operate on names used by Charmed Kubernetes.""" + protected_names = [ + "admin", + "system:kube-controller-manager", + "kube-controller-manager", + "system:kube-proxy", + "kube-proxy", + "system:kube-scheduler", + "kube-scheduler", + "system:monitoring", + ] + if name.startswith("kubelet") or name in protected_names: + action_fail('Not allowed to {} "{}".'.format(action, name)) + sys.exit(0) + + +def user_list(): + """Return a dict of 'username: secret_id' for Charmed Kubernetes users.""" + secrets = layer.kubernetes_common.get_secret_names() + action_set({"users": ", ".join(list(secrets))}) + return secrets + + +def user_create(): + user = action_get("name") + groups = action_get("groups") or "" + protect_resources(user) + + users = user_list() + if user in list(users): + action_fail('User "{}" already exists.'.format(user)) + return + + # Validate the name + if re.search("[^0-9A-Za-z:@.-]+", user): + msg = "User name may only contain alphanumeric characters, ':', '@', '-' or '.'" + action_fail(msg) + return + + # Create the secret + # TODO: make the token format less magical so it doesn't get out of + # sync with the function that creates secrets in k8s-master.py. + token = "{}::{}".format(user, layer.kubernetes_master.token_generator()) + if not layer.kubernetes_common.create_secret(token, user, user, groups): + action_fail("Failed to create secret for: {}".format(user)) + return + + # Create a kubeconfig + ca_crt = layer.kubernetes_common.ca_crt_path + kubeconfig_path = "/home/ubuntu/{}-kubeconfig".format(user) + public_address, public_port = layer.kubernetes_master.get_api_endpoint() + public_server = "https://{0}:{1}".format(public_address, public_port) + + layer.kubernetes_common.create_kubeconfig( + kubeconfig_path, public_server, ca_crt, token=token, user=user + ) + os.chmod(kubeconfig_path, 0o644) + + # Tell the people what they've won + fetch_cmd = "juju scp {}:{} .".format(hookenv.local_unit(), kubeconfig_path) + action_set({"msg": 'User "{}" created.'.format(user)}) + action_set({"users": ", ".join(list(users) + [user])}) + action_set({"kubeconfig": fetch_cmd}) + + +def user_delete(): + user = action_get("name") + protect_resources(user) + + users = user_list() + if user not in list(users): + action_fail('User "{}" does not exist.'.format(user)) + return + + # Delete the secret + secret_id = users[user] + layer.kubernetes_master.delete_secret(secret_id) + + action_set({"msg": 'User "{}" deleted.'.format(user)}) + action_set({"users": ", ".join(u for u in list(users) if u != user)}) + + +action = action_name().replace("user-", "") +if action == "create": + user_create() +elif action == "list": + user_list() +elif action == "delete": + user_delete() diff --git a/kubernetes-master/actions/user-list b/kubernetes-master/actions/user-list new file mode 100755 index 0000000..7e7785a --- /dev/null +++ b/kubernetes-master/actions/user-list @@ -0,0 +1,105 @@ +#!/usr/local/sbin/charm-env python3 +import os +import re +import sys +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import action_get, action_set, action_fail, action_name +from charms import layer + +os.environ["PATH"] += os.pathsep + os.path.join(os.sep, "snap", "bin") + +# Import charm layers and start reactive +layer.import_layer_libs() +hookenv._run_atstart() + + +def protect_resources(name): + """Do not allow the action to operate on names used by Charmed Kubernetes.""" + protected_names = [ + "admin", + "system:kube-controller-manager", + "kube-controller-manager", + "system:kube-proxy", + "kube-proxy", + "system:kube-scheduler", + "kube-scheduler", + "system:monitoring", + ] + if name.startswith("kubelet") or name in protected_names: + action_fail('Not allowed to {} "{}".'.format(action, name)) + sys.exit(0) + + +def user_list(): + """Return a dict of 'username: secret_id' for Charmed Kubernetes users.""" + secrets = layer.kubernetes_common.get_secret_names() + action_set({"users": ", ".join(list(secrets))}) + return secrets + + +def user_create(): + user = action_get("name") + groups = action_get("groups") or "" + protect_resources(user) + + users = user_list() + if user in list(users): + action_fail('User "{}" already exists.'.format(user)) + return + + # Validate the name + if re.search("[^0-9A-Za-z:@.-]+", user): + msg = "User name may only contain alphanumeric characters, ':', '@', '-' or '.'" + action_fail(msg) + return + + # Create the secret + # TODO: make the token format less magical so it doesn't get out of + # sync with the function that creates secrets in k8s-master.py. + token = "{}::{}".format(user, layer.kubernetes_master.token_generator()) + if not layer.kubernetes_common.create_secret(token, user, user, groups): + action_fail("Failed to create secret for: {}".format(user)) + return + + # Create a kubeconfig + ca_crt = layer.kubernetes_common.ca_crt_path + kubeconfig_path = "/home/ubuntu/{}-kubeconfig".format(user) + public_address, public_port = layer.kubernetes_master.get_api_endpoint() + public_server = "https://{0}:{1}".format(public_address, public_port) + + layer.kubernetes_common.create_kubeconfig( + kubeconfig_path, public_server, ca_crt, token=token, user=user + ) + os.chmod(kubeconfig_path, 0o644) + + # Tell the people what they've won + fetch_cmd = "juju scp {}:{} .".format(hookenv.local_unit(), kubeconfig_path) + action_set({"msg": 'User "{}" created.'.format(user)}) + action_set({"users": ", ".join(list(users) + [user])}) + action_set({"kubeconfig": fetch_cmd}) + + +def user_delete(): + user = action_get("name") + protect_resources(user) + + users = user_list() + if user not in list(users): + action_fail('User "{}" does not exist.'.format(user)) + return + + # Delete the secret + secret_id = users[user] + layer.kubernetes_master.delete_secret(secret_id) + + action_set({"msg": 'User "{}" deleted.'.format(user)}) + action_set({"users": ", ".join(u for u in list(users) if u != user)}) + + +action = action_name().replace("user-", "") +if action == "create": + user_create() +elif action == "list": + user_list() +elif action == "delete": + user_delete() diff --git a/kubernetes-master/actions/user_actions.py b/kubernetes-master/actions/user_actions.py new file mode 100755 index 0000000..7e7785a --- /dev/null +++ b/kubernetes-master/actions/user_actions.py @@ -0,0 +1,105 @@ +#!/usr/local/sbin/charm-env python3 +import os +import re +import sys +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import action_get, action_set, action_fail, action_name +from charms import layer + +os.environ["PATH"] += os.pathsep + os.path.join(os.sep, "snap", "bin") + +# Import charm layers and start reactive +layer.import_layer_libs() +hookenv._run_atstart() + + +def protect_resources(name): + """Do not allow the action to operate on names used by Charmed Kubernetes.""" + protected_names = [ + "admin", + "system:kube-controller-manager", + "kube-controller-manager", + "system:kube-proxy", + "kube-proxy", + "system:kube-scheduler", + "kube-scheduler", + "system:monitoring", + ] + if name.startswith("kubelet") or name in protected_names: + action_fail('Not allowed to {} "{}".'.format(action, name)) + sys.exit(0) + + +def user_list(): + """Return a dict of 'username: secret_id' for Charmed Kubernetes users.""" + secrets = layer.kubernetes_common.get_secret_names() + action_set({"users": ", ".join(list(secrets))}) + return secrets + + +def user_create(): + user = action_get("name") + groups = action_get("groups") or "" + protect_resources(user) + + users = user_list() + if user in list(users): + action_fail('User "{}" already exists.'.format(user)) + return + + # Validate the name + if re.search("[^0-9A-Za-z:@.-]+", user): + msg = "User name may only contain alphanumeric characters, ':', '@', '-' or '.'" + action_fail(msg) + return + + # Create the secret + # TODO: make the token format less magical so it doesn't get out of + # sync with the function that creates secrets in k8s-master.py. + token = "{}::{}".format(user, layer.kubernetes_master.token_generator()) + if not layer.kubernetes_common.create_secret(token, user, user, groups): + action_fail("Failed to create secret for: {}".format(user)) + return + + # Create a kubeconfig + ca_crt = layer.kubernetes_common.ca_crt_path + kubeconfig_path = "/home/ubuntu/{}-kubeconfig".format(user) + public_address, public_port = layer.kubernetes_master.get_api_endpoint() + public_server = "https://{0}:{1}".format(public_address, public_port) + + layer.kubernetes_common.create_kubeconfig( + kubeconfig_path, public_server, ca_crt, token=token, user=user + ) + os.chmod(kubeconfig_path, 0o644) + + # Tell the people what they've won + fetch_cmd = "juju scp {}:{} .".format(hookenv.local_unit(), kubeconfig_path) + action_set({"msg": 'User "{}" created.'.format(user)}) + action_set({"users": ", ".join(list(users) + [user])}) + action_set({"kubeconfig": fetch_cmd}) + + +def user_delete(): + user = action_get("name") + protect_resources(user) + + users = user_list() + if user not in list(users): + action_fail('User "{}" does not exist.'.format(user)) + return + + # Delete the secret + secret_id = users[user] + layer.kubernetes_master.delete_secret(secret_id) + + action_set({"msg": 'User "{}" deleted.'.format(user)}) + action_set({"users": ", ".join(u for u in list(users) if u != user)}) + + +action = action_name().replace("user-", "") +if action == "create": + user_create() +elif action == "list": + user_list() +elif action == "delete": + user_delete() diff --git a/kubernetes-master/bin/charm-env b/kubernetes-master/bin/charm-env new file mode 100755 index 0000000..d211ce9 --- /dev/null +++ b/kubernetes-master/bin/charm-env @@ -0,0 +1,107 @@ +#!/bin/bash + +VERSION="1.0.0" + + +find_charm_dirs() { + # Hopefully, $JUJU_CHARM_DIR is set so which venv to use in unambiguous. + if [[ -n "$JUJU_CHARM_DIR" || -n "$CHARM_DIR" ]]; then + if [[ -z "$JUJU_CHARM_DIR" ]]; then + # accept $CHARM_DIR to be more forgiving + export JUJU_CHARM_DIR="$CHARM_DIR" + fi + if [[ -z "$CHARM_DIR" ]]; then + # set CHARM_DIR as well to help with backwards compatibility + export CHARM_DIR="$JUJU_CHARM_DIR" + fi + return + fi + # Try to guess the value for JUJU_CHARM_DIR by looking for a non-subordinate + # (because there's got to be at least one principle) charm directory; + # if there are several, pick the first by alpha order. + agents_dir="/var/lib/juju/agents" + if [[ -d "$agents_dir" ]]; then + desired_charm="$1" + found_charm_dir="" + if [[ -n "$desired_charm" ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + charm_name="$(grep -o '^['\''"]\?name['\''"]\?:.*' $charm_dir/metadata.yaml 2> /dev/null | sed -e 's/.*: *//' -e 's/['\''"]//g')" + if [[ "$charm_name" == "$desired_charm" ]]; then + if [[ -n "$found_charm_dir" ]]; then + >&2 echo "Ambiguous possibilities for JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + found_charm_dir="$charm_dir" + fi + done + if [[ -z "$found_charm_dir" ]]; then + >&2 echo "Unable to determine JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + export JUJU_CHARM_DIR="$found_charm_dir" + export CHARM_DIR="$found_charm_dir" + return + fi + # shellcheck disable=SC2126 + non_subordinates="$(grep -L 'subordinate"\?:.*true' "$agents_dir"/unit-*/charm/metadata.yaml | wc -l)" + if [[ "$non_subordinates" -gt 1 ]]; then + >&2 echo 'Ambiguous possibilities for JUJU_CHARM_DIR; please use --charm or run within a Juju hook context' + exit 1 + elif [[ "$non_subordinates" -eq 1 ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + if grep -q 'subordinate"\?:.*true' "$charm_dir/metadata.yaml"; then + continue + fi + export JUJU_CHARM_DIR="$charm_dir" + export CHARM_DIR="$charm_dir" + return + done + fi + fi + >&2 echo 'Unable to determine JUJU_CHARM_DIR; please run within a Juju hook context' + exit 1 +} + +try_activate_venv() { + if [[ -d "$JUJU_CHARM_DIR/../.venv" ]]; then + . "$JUJU_CHARM_DIR/../.venv/bin/activate" + fi +} + +find_wrapped() { + PATH="${PATH/\/usr\/local\/sbin:}" which "$(basename "$0")" +} + + +if [[ "$1" == "--version" || "$1" == "-v" ]]; then + echo "$VERSION" + exit 0 +fi + + +# allow --charm option to hint which JUJU_CHARM_DIR to choose when ambiguous +# NB: --charm option must come first +# NB: option must be processed outside find_charm_dirs to modify $@ +charm_name="" +if [[ "$1" == "--charm" ]]; then + charm_name="$2" + shift; shift +fi + +find_charm_dirs "$charm_name" +try_activate_venv +export PYTHONPATH="$JUJU_CHARM_DIR/lib:$PYTHONPATH" + +if [[ "$(basename "$0")" == "charm-env" ]]; then + # being used as a shebang + exec "$@" +elif [[ "$0" == "$BASH_SOURCE" ]]; then + # being invoked as a symlink wrapping something to find in the venv + exec "$(find_wrapped)" "$@" +elif [[ "$(basename "$BASH_SOURCE")" == "charm-env" ]]; then + # being sourced directly; do nothing + /bin/true +else + # being sourced for wrapped bash helpers + . "$(find_wrapped)" +fi diff --git a/kubernetes-master/bin/layer_option b/kubernetes-master/bin/layer_option new file mode 100755 index 0000000..3253ef8 --- /dev/null +++ b/kubernetes-master/bin/layer_option @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from charms import layer + + +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 = layer.options.get(args.section, 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/kubernetes-master/config.yaml b/kubernetes-master/config.yaml new file mode 100644 index 0000000..e776521 --- /dev/null +++ b/kubernetes-master/config.yaml @@ -0,0 +1,448 @@ +# 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 and snap_proxy_url have been deprecated for some time. + # If your charm still needs them, add these config items manually + # to your charm's config.yaml. + # snap_proxy: + # description: > + # DEPRECATED. Use snap-http-proxy and snap-https-proxy model configuration settings. + # HTTP/HTTPS web proxy for Snappy to use when accessing the snap store. + # type: string + # default: "" + # snap_proxy_url: + # default: "" + # type: string + # description: > + # DEPRECATED. Use snap-store-proxy model configuration setting. + # The address of a Snap Store Proxy to use for snaps e.g. http://snap-proxy.example.com + "snapd_refresh": + "default": "max" + "type": "string" + "description": | + How often snapd handles updates for installed snaps. Setting an empty + string will check 4x per day. Set to "max" to delay the refresh as long + as possible. You may also set a custom string as described in the + 'refresh.timer' section here: + https://forum.snapcraft.io/t/system-options/87 + + DEPRECATED in 1.19: Manage installed snap versions with the snap-store-proxy model config. + See: https://snapcraft.io/snap-store-proxy and https://juju.is/docs/offline-mode-strategies#heading--snap-specific-proxy + "nagios_context": + "default": "juju" + "type": "string" + "description": | + Used by the nrpe subordinate charms. + A string that will be prepended to instance name to set the host name + in nagios. So for instance the hostname would be something like: + juju-myservice-0 + If you're running multiple environments with the same services in them + this allows you to differentiate between them. + "nagios_servicegroups": + "default": "" + "type": "string" + "description": | + A comma-separated list of nagios servicegroups. + If left empty, the nagios_context will be used as the servicegroup + "sysctl": + "type": "string" + "default": "{ net.ipv4.conf.all.forwarding : 1, net.ipv4.neigh.default.gc_thresh1\ + \ : 128, net.ipv4.neigh.default.gc_thresh2 : 28672, net.ipv4.neigh.default.gc_thresh3\ + \ : 32768, net.ipv6.neigh.default.gc_thresh1 : 128, net.ipv6.neigh.default.gc_thresh2\ + \ : 28672, net.ipv6.neigh.default.gc_thresh3 : 32768, fs.inotify.max_user_instances\ + \ : 8192, fs.inotify.max_user_watches : 1048576, kernel.panic : 10, kernel.panic_on_oops:\ + \ 1, vm.overcommit_memory : 1 }" + "description": | + YAML formatted associative array of sysctl values, e.g.: + '{kernel.pid_max : 4194303 }'. Note that kube-proxy handles + the conntrack settings. The proper way to alter them is to + use the proxy-extra-args config to set them, e.g.: + juju config kubernetes-master proxy-extra-args="conntrack-min=1000000 conntrack-max-per-core=250000" + juju config kubernetes-worker proxy-extra-args="conntrack-min=1000000 conntrack-max-per-core=250000" + The proxy-extra-args conntrack-min and conntrack-max-per-core can be set to 0 to ignore + kube-proxy's settings and use the sysctl settings instead. Note the fundamental difference between + the setting of conntrack-max-per-core vs nf_conntrack_max. + "proxy-extra-args": + "type": "string" + "default": "" + "description": | + Space separated list of flags and key=value pairs that will be passed as arguments to + kube-proxy. For example a value like this: + runtime-config=batch/v2alpha1=true profiling=true + will result in kube-apiserver being run with the following options: + --runtime-config=batch/v2alpha1=true --profiling=true + "extra_packages": + "description": > + Space separated list of extra deb packages to install. + "type": "string" + "default": "" + "package_status": + "default": "install" + "type": "string" + "description": > + The status of service-affecting packages will be set to this + value in the dpkg database. Valid values are "install" and "hold". + "install_sources": + "description": > + List of extra apt sources, per charm-helpers standard + format (a yaml list of strings encoded as a string). Each source + may be either a line that can be added directly to + sources.list(5), or in the form ppa:/ for adding + Personal Package Archives, or a distribution component to enable. + "type": "string" + "default": "" + "install_keys": + "description": > + List of signing keys for install_sources package sources, per + charmhelpers standard format (a yaml list of strings encoded as + a string). The keys should be the full ASCII armoured GPG public + keys. While GPG key ids are also supported and looked up on a + keyserver, operators should be aware that this mechanism is + insecure. null can be used if a standard package signing key is + used that will already be installed on the machine, and for PPA + sources where the package signing key is securely retrieved from + Launchpad. + "type": "string" + "default": "" + "ha-cluster-vip": + "type": "string" + "description": | + Virtual IP for the charm to use with the HA Cluster subordinate charm + Mutually exclusive with ha-cluster-dns. Multiple virtual IPs are + separated by spaces. + "default": "" + "ha-cluster-dns": + "type": "string" + "description": | + DNS entry to use with the HA Cluster subordinate charm. + Mutually exclusive with ha-cluster-vip. + "default": "" + "audit-policy": + "type": "string" + "default": | + apiVersion: audit.k8s.io/v1 + kind: Policy + rules: + # Don't log read-only requests from the apiserver + - level: None + users: ["system:apiserver"] + verbs: ["get", "list", "watch"] + # Don't log kube-proxy watches + - level: None + users: ["system:kube-proxy"] + verbs: ["watch"] + resources: + - resources: ["endpoints", "services"] + # Don't log nodes getting their own status + - level: None + userGroups: ["system:nodes"] + verbs: ["get"] + resources: + - resources: ["nodes"] + # Don't log kube-controller-manager and kube-scheduler getting endpoints + - level: None + users: ["system:unsecured"] + namespaces: ["kube-system"] + verbs: ["get"] + resources: + - resources: ["endpoints"] + # Log everything else at the Request level. + - level: Request + omitStages: + - RequestReceived + "description": | + Audit policy passed to kube-apiserver via --audit-policy-file. + For more info, please refer to the upstream documentation at + https://kubernetes.io/docs/tasks/debug-application-cluster/audit/ + "audit-webhook-config": + "type": "string" + "default": "" + "description": | + Audit webhook config passed to kube-apiserver via --audit-webhook-config-file. + For more info, please refer to the upstream documentation at + https://kubernetes.io/docs/tasks/debug-application-cluster/audit/ + "image-registry": + "type": "string" + "default": "rocks.canonical.com:443/cdk" + "description": | + Container image registry to use for CDK. This includes addons like the Kubernetes dashboard, + metrics server, ingress, and dns along with non-addon images including the pause + container and default backend image. + "enable-dashboard-addons": + "type": "boolean" + "default": !!bool "true" + "description": "Deploy the Kubernetes Dashboard" + "dns-provider": + "type": "string" + "default": "auto" + "description": | + DNS provider addon to use. Can be "auto", "core-dns", "kube-dns", or + "none". + + CoreDNS is only supported on Kubernetes 1.14+. + + When set to "auto", the behavior is as follows: + - New deployments of Kubernetes 1.14+ will use CoreDNS + - New deployments of Kubernetes 1.13 or older will use KubeDNS + - Upgraded deployments will continue to use whichever provider was + previously used. + "dns_domain": + "type": "string" + "default": "cluster.local" + "description": "The local domain for cluster dns" + "extra_sans": + "type": "string" + "default": "" + "description": | + Space-separated list of extra SAN entries to add to the x509 certificate + created for the master nodes. + "service-cidr": + "type": "string" + "default": "10.152.183.0/24" + "description": | + CIDR to use for Kubernetes services. After deployment it is + only possible to increase the size of the IP range. It is not possible to + change or shrink the address range after deployment. + "allow-privileged": + "type": "string" + "default": "auto" + "description": | + Allow kube-apiserver to run in privileged mode. Supported values are + "true", "false", and "auto". If "true", kube-apiserver will run in + privileged mode by default. If "false", kube-apiserver will never run in + privileged mode. If "auto", kube-apiserver will not run in privileged + mode by default, but will switch to privileged mode if gpu hardware is + detected on a worker node. + "enable-nvidia-plugin": + "type": "string" + "default": "auto" + "description": | + Load the nvidia device plugin daemonset. Supported values are + "auto" and "false". When "auto", the daemonset will be loaded + only if GPUs are detected. When "false" the nvidia device plugin + will not be loaded. + "channel": + "type": "string" + "default": "1.23/edge" + "description": | + Snap channel to install Kubernetes master services from + "client_password": + "type": "string" + "default": "" + "description": | + Password to be used for admin user (leave empty for random password). + "api-extra-args": + "type": "string" + "default": "" + "description": | + Space separated list of flags and key=value pairs that will be passed as arguments to + kube-apiserver. For example a value like this: + runtime-config=batch/v2alpha1=true profiling=true + will result in kube-apiserver being run with the following options: + --runtime-config=batch/v2alpha1=true --profiling=true + "controller-manager-extra-args": + "type": "string" + "default": "" + "description": | + Space separated list of flags and key=value pairs that will be passed as arguments to + kube-controller-manager. For example a value like this: + runtime-config=batch/v2alpha1=true profiling=true + will result in kube-controller-manager being run with the following options: + --runtime-config=batch/v2alpha1=true --profiling=true + "scheduler-extra-args": + "type": "string" + "default": "" + "description": | + Space separated list of flags and key=value pairs that will be passed as arguments to + kube-scheduler. For example a value like this: + runtime-config=batch/v2alpha1=true profiling=true + will result in kube-scheduler being run with the following options: + --runtime-config=batch/v2alpha1=true --profiling=true + "authorization-mode": + "type": "string" + "default": "Node,RBAC" + "description": | + Comma separated authorization modes. Allowed values are + "RBAC", "Node", "Webhook", "ABAC", "AlwaysDeny" and "AlwaysAllow". + "require-manual-upgrade": + "type": "boolean" + "default": !!bool "true" + "description": | + When true, master nodes will not be upgraded until the user triggers + it manually by running the upgrade action. + "storage-backend": + "type": "string" + "default": "auto" + "description": | + The storage backend for kube-apiserver persistence. Can be "etcd2", "etcd3", or + "auto". Auto mode will select etcd3 on new installations, or etcd2 on upgrades. + "enable-metrics": + "type": "boolean" + "default": !!bool "true" + "description": | + If true the metrics server for Kubernetes will be deployed onto the cluster. + "default-storage": + "type": "string" + "default": "auto" + "description": | + The storage class to make the default storage class. Allowed values are "auto", + "none", "ceph-xfs", "ceph-ext4", "cephfs". Note: Only works in Kubernetes >= 1.10 + "cephfs-mounter": + "type": "string" + "default": "default" + "description": | + The client driver used for cephfs based storage. Options are "fuse", "kernel" and "default". + "keystone-policy": + "default": | + apiVersion: v1 + kind: ConfigMap + metadata: + name: k8s-auth-policy + namespace: kube-system + labels: + k8s-app: k8s-keystone-auth + data: + policies: | + [ + { + "resource": { + "verbs": ["get", "list", "watch"], + "resources": ["*"], + "version": "*", + "namespace": "*" + }, + "match": [ + { + "type": "role", + "values": ["k8s-viewers"] + }, + { + "type": "project", + "values": ["k8s"] + } + ] + }, + { + "resource": { + "verbs": ["*"], + "resources": ["*"], + "version": "*", + "namespace": "default" + }, + "match": [ + { + "type": "role", + "values": ["k8s-users"] + }, + { + "type": "project", + "values": ["k8s"] + } + ] + }, + { + "resource": { + "verbs": ["*"], + "resources": ["*"], + "version": "*", + "namespace": "*" + }, + "match": [ + { + "type": "role", + "values": ["k8s-admins"] + }, + { + "type": "project", + "values": ["k8s"] + } + ] + } + ] + "type": "string" + "description": | + Policy for Keystone authorization. This is used when a Keystone charm is + related to kubernetes-master in order to provide authorization + for Keystone users on the Kubernetes cluster. + "enable-keystone-authorization": + "type": "boolean" + "default": !!bool "false" + "description": | + If true and the Keystone charm is related, users will authorize against + the Keystone server. Note that if related, users will always authenticate + against Keystone. + "keystone-ssl-ca": + "type": "string" + "description": | + Keystone certificate authority encoded in base64 for securing communications to Keystone. + For example: `juju config kubernetes-master keystone-ssl-ca=$(base64 /path/to/ca.crt)` + "default": "" + "dashboard-auth": + "type": "string" + "description": | + Method of authentication for the Kubernetes dashboard. Allowed values are "auto", + "basic", and "token". If set to "auto", basic auth is used unless Keystone is + related to kubernetes-master, in which case token auth is used. + + DEPRECATED: this option has no effect on Kubernetes 1.19 and above. + "default": "auto" + "loadbalancer-ips": + "type": "string" + "description": | + Space separated list of IP addresses of loadbalancers in front of the control plane. + These can be either virtual IP addresses that have been floated in front of the control + plane or the IP of a loadbalancer appliance such as an F5. Workers will alternate IP + addresses from this list to distribute load - for example If you have 2 IPs and 4 workers, + each IP will be used by 2 workers. Note that this will only work if kubeapi-load-balancer + is not in use and there is a relation between kubernetes-master:kube-api-endpoint and + kubernetes-worker:kube-api-endpoint. If using the kubeapi-load-balancer, see the + loadbalancer-ips configuration variable on the kubeapi-load-balancer charm. + "default": "" + "default-cni": + "type": "string" + "description": | + Default CNI network to use when multiple CNI subordinates are related. + + The value of this config should be the application name of a related CNI + subordinate. For example: + + juju config kubernetes-master default-cni=flannel + + If unspecified, then the default CNI network is chosen alphabetically. + "default": "" + "authn-webhook-endpoint": + "type": "string" + "default": "" + "description": | + Custom endpoint to check when authenticating kube-apiserver requests. + This must be an https url accessible by the k8s-master units. For example: + + https://your.server:8443/authenticate + + When a JSON-serialized TokenReview object is POSTed to this endpoint, it must + respond with appropriate authentication details. For more info, please refer + to the upstream documentation at + https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication + "pod-security-policy": + "type": "string" + "default": "" + "description": |- + Default RBAC pod security policy [0] and privileged cluster roles formatted + as a YAML file as a string. + A good example of a PSP policy can be found here [1]. + + [0] https://kubernetes.io/docs/concepts/policy/pod-security-policy/ + [1] https://github.com/kubernetes/examples/blob/master/staging/podsecuritypolicy/rbac/policies.yaml diff --git a/kubernetes-master/copyright b/kubernetes-master/copyright new file mode 100644 index 0000000..8aec8ec --- /dev/null +++ b/kubernetes-master/copyright @@ -0,0 +1,13 @@ +Copyright 2016 The Kubernetes Authors. + +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/kubernetes-master/copyright.layer-apt b/kubernetes-master/copyright.layer-apt new file mode 100644 index 0000000..0814dc1 --- /dev/null +++ b/kubernetes-master/copyright.layer-apt @@ -0,0 +1,15 @@ +Copyright 2015-2016 Canonical Ltd. + +This file is part of the Apt layer for Juju. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/kubernetes-master/copyright.layer-basic b/kubernetes-master/copyright.layer-basic new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/kubernetes-master/copyright.layer-basic @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/copyright.layer-coordinator b/kubernetes-master/copyright.layer-coordinator new file mode 100644 index 0000000..b8518aa --- /dev/null +++ b/kubernetes-master/copyright.layer-coordinator @@ -0,0 +1,15 @@ +Copyright 2015-2016 Canonical Ltd. + +This file is part of the Coordinator Layer for Juju. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/kubernetes-master/copyright.layer-leadership b/kubernetes-master/copyright.layer-leadership new file mode 100644 index 0000000..08b983f --- /dev/null +++ b/kubernetes-master/copyright.layer-leadership @@ -0,0 +1,15 @@ +Copyright 2015-2016 Canonical Ltd. + +This file is part of the Leadership Layer for Juju. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/kubernetes-master/copyright.layer-metrics b/kubernetes-master/copyright.layer-metrics new file mode 100644 index 0000000..2df15bd --- /dev/null +++ b/kubernetes-master/copyright.layer-metrics @@ -0,0 +1,13 @@ +Copyright 2016 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. diff --git a/kubernetes-master/copyright.layer-nagios b/kubernetes-master/copyright.layer-nagios new file mode 100644 index 0000000..c80db95 --- /dev/null +++ b/kubernetes-master/copyright.layer-nagios @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2016, Canonical Ltd. +License: GPL-3 + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranties of + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . diff --git a/kubernetes-master/copyright.layer-options b/kubernetes-master/copyright.layer-options new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/kubernetes-master/copyright.layer-options @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/copyright.layer-snap b/kubernetes-master/copyright.layer-snap new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/kubernetes-master/copyright.layer-snap @@ -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/kubernetes-master/copyright.layer-status b/kubernetes-master/copyright.layer-status new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-master/copyright.layer-status @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/copyright.layer-vault-kv b/kubernetes-master/copyright.layer-vault-kv new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-master/copyright.layer-vault-kv @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/copyright.layer-vaultlocker b/kubernetes-master/copyright.layer-vaultlocker new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-master/copyright.layer-vaultlocker @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/debug-scripts/auth-webhook b/kubernetes-master/debug-scripts/auth-webhook new file mode 100755 index 0000000..befa79e --- /dev/null +++ b/kubernetes-master/debug-scripts/auth-webhook @@ -0,0 +1,7 @@ +#!/bin/sh +set -ux + +systemctl status cdk.master.auth-webhook.service > $DEBUG_SCRIPT_DIR/auth-webhook-systemctl-status + +AUTH_LOG=/root/cdk/auth-webhook/auth-webhook.log +test -f $AUTH_LOG && cp $AUTH_LOG $DEBUG_SCRIPT_DIR diff --git a/kubernetes-master/debug-scripts/charm-unitdata b/kubernetes-master/debug-scripts/charm-unitdata new file mode 100755 index 0000000..d2aac60 --- /dev/null +++ b/kubernetes-master/debug-scripts/charm-unitdata @@ -0,0 +1,12 @@ +#!/usr/local/sbin/charm-env python3 + +import debug_script +import json +from charmhelpers.core import unitdata + +kv = unitdata.kv() +data = kv.getrange("") + +with debug_script.open_file("unitdata.json", "w") as f: + json.dump(data, f, indent=2) + f.write("\n") diff --git a/kubernetes-master/debug-scripts/filesystem b/kubernetes-master/debug-scripts/filesystem new file mode 100755 index 0000000..c5ec6d8 --- /dev/null +++ b/kubernetes-master/debug-scripts/filesystem @@ -0,0 +1,17 @@ +#!/bin/sh +set -ux + +# report file system disk space usage +df -hT > $DEBUG_SCRIPT_DIR/df-hT +# estimate file space usage +du -h / 2>&1 > $DEBUG_SCRIPT_DIR/du-h +# list the mounted filesystems +mount > $DEBUG_SCRIPT_DIR/mount +# list the mounted systems with ascii trees +findmnt -A > $DEBUG_SCRIPT_DIR/findmnt +# list block devices +lsblk > $DEBUG_SCRIPT_DIR/lsblk +# list open files +lsof 2>&1 > $DEBUG_SCRIPT_DIR/lsof +# list local system locks +lslocks > $DEBUG_SCRIPT_DIR/lslocks diff --git a/kubernetes-master/debug-scripts/juju-logs b/kubernetes-master/debug-scripts/juju-logs new file mode 100755 index 0000000..d27c458 --- /dev/null +++ b/kubernetes-master/debug-scripts/juju-logs @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +cp -v /var/log/juju/* $DEBUG_SCRIPT_DIR diff --git a/kubernetes-master/debug-scripts/juju-network-get b/kubernetes-master/debug-scripts/juju-network-get new file mode 100755 index 0000000..983c8c4 --- /dev/null +++ b/kubernetes-master/debug-scripts/juju-network-get @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import yaml +import debug_script + +with open('metadata.yaml') as f: + metadata = yaml.load(f) + +relations = [] +for key in ['requires', 'provides', 'peers']: + relations += list(metadata.get(key, {}).keys()) + +os.mkdir(os.path.join(debug_script.dir, 'relations')) + +for relation in relations: + path = 'relations/' + relation + with debug_script.open_file(path, 'w') as f: + cmd = ['network-get', relation] + subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) diff --git a/kubernetes-master/debug-scripts/kubectl b/kubernetes-master/debug-scripts/kubectl new file mode 100755 index 0000000..216231d --- /dev/null +++ b/kubernetes-master/debug-scripts/kubectl @@ -0,0 +1,15 @@ +#!/bin/sh +set -ux + +export PATH=$PATH:/snap/bin + +alias kubectl="kubectl --kubeconfig=/root/.kube/config" + +kubectl cluster-info > $DEBUG_SCRIPT_DIR/cluster-info +kubectl cluster-info dump > $DEBUG_SCRIPT_DIR/cluster-info-dump +for obj in pods svc ingress secrets pv pvc rc; do + kubectl describe $obj --all-namespaces > $DEBUG_SCRIPT_DIR/describe-$obj +done +for obj in nodes; do + kubectl describe $obj > $DEBUG_SCRIPT_DIR/describe-$obj +done diff --git a/kubernetes-master/debug-scripts/kubernetes-master-services b/kubernetes-master/debug-scripts/kubernetes-master-services new file mode 100755 index 0000000..59d646b --- /dev/null +++ b/kubernetes-master/debug-scripts/kubernetes-master-services @@ -0,0 +1,9 @@ +#!/bin/sh +set -ux + +for service in kube-apiserver kube-controller-manager kube-scheduler kube-proxy; do + systemctl status snap.$service.daemon > $DEBUG_SCRIPT_DIR/$service-systemctl-status + journalctl -u snap.$service.daemon > $DEBUG_SCRIPT_DIR/$service-journal +done + +# FIXME: grab snap config or something diff --git a/kubernetes-master/debug-scripts/network b/kubernetes-master/debug-scripts/network new file mode 100755 index 0000000..944a355 --- /dev/null +++ b/kubernetes-master/debug-scripts/network @@ -0,0 +1,11 @@ +#!/bin/sh +set -ux + +ifconfig -a > $DEBUG_SCRIPT_DIR/ifconfig +cp -v /etc/resolv.conf $DEBUG_SCRIPT_DIR/resolv.conf +cp -v /etc/network/interfaces $DEBUG_SCRIPT_DIR/interfaces +netstat -planut > $DEBUG_SCRIPT_DIR/netstat +route -n > $DEBUG_SCRIPT_DIR/route +iptables-save > $DEBUG_SCRIPT_DIR/iptables-save +dig google.com > $DEBUG_SCRIPT_DIR/dig-google +ping -w 2 -i 0.1 google.com > $DEBUG_SCRIPT_DIR/ping-google diff --git a/kubernetes-master/debug-scripts/packages b/kubernetes-master/debug-scripts/packages new file mode 100755 index 0000000..b60a9cf --- /dev/null +++ b/kubernetes-master/debug-scripts/packages @@ -0,0 +1,7 @@ +#!/bin/sh +set -ux + +dpkg --list > $DEBUG_SCRIPT_DIR/dpkg-list +snap list > $DEBUG_SCRIPT_DIR/snap-list +pip2 list > $DEBUG_SCRIPT_DIR/pip2-list +pip3 list > $DEBUG_SCRIPT_DIR/pip3-list diff --git a/kubernetes-master/debug-scripts/sysctl b/kubernetes-master/debug-scripts/sysctl new file mode 100755 index 0000000..a86a6c8 --- /dev/null +++ b/kubernetes-master/debug-scripts/sysctl @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +sysctl -a > $DEBUG_SCRIPT_DIR/sysctl diff --git a/kubernetes-master/debug-scripts/systemd b/kubernetes-master/debug-scripts/systemd new file mode 100755 index 0000000..8bb9b6f --- /dev/null +++ b/kubernetes-master/debug-scripts/systemd @@ -0,0 +1,9 @@ +#!/bin/sh +set -ux + +systemctl --all > $DEBUG_SCRIPT_DIR/systemctl +journalctl > $DEBUG_SCRIPT_DIR/journalctl +systemd-analyze time > $DEBUG_SCRIPT_DIR/systemd-analyze-time +systemd-analyze blame > $DEBUG_SCRIPT_DIR/systemd-analyze-blame +systemd-analyze critical-chain > $DEBUG_SCRIPT_DIR/systemd-analyze-critical-chain +systemd-analyze dump > $DEBUG_SCRIPT_DIR/systemd-analyze-dump diff --git a/kubernetes-master/debug-scripts/tls-certs b/kubernetes-master/debug-scripts/tls-certs new file mode 100755 index 0000000..2692e51 --- /dev/null +++ b/kubernetes-master/debug-scripts/tls-certs @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import shutil +import traceback +import debug_script +from charms import layer + +options = layer.options.get('tls-client') + +def copy_cert(source_key, name): + try: + source = options[source_key] + dest = os.path.join(debug_script.dir, name) + shutil.copy(source, dest) + except Exception: + traceback.print_exc() + +copy_cert('client_certificate_path', 'client.crt') +copy_cert('server_certificate_path', 'server.crt') +copy_cert('ca_certificate_path', 'ca.crt') diff --git a/kubernetes-master/docs/README b/kubernetes-master/docs/README new file mode 100644 index 0000000..9973bb8 --- /dev/null +++ b/kubernetes-master/docs/README @@ -0,0 +1 @@ +This docs directory is currently experimental. Please do not make changes to the docs here as any edits may be lost diff --git a/kubernetes-master/docs/index.md b/kubernetes-master/docs/index.md new file mode 100644 index 0000000..f644122 --- /dev/null +++ b/kubernetes-master/docs/index.md @@ -0,0 +1,832 @@ + + +This charm is an encapsulation of the Kubernetes master processes and the +operations to run on any cloud for the entire lifecycle of the cluster. + +This charm is built from other charm layers using the Juju reactive framework. +The other layers focus on specific subset of operations making this layer +specific to operations of Kubernetes master processes. + +# Deployment + +This charm is not fully functional when deployed by itself. It requires other +charms to model a complete Kubernetes cluster. A Kubernetes cluster needs a +distributed key value store such as [Etcd](https://coreos.com/etcd/) and the +kubernetes-worker charm which delivers the Kubernetes node services. A cluster +requires a Software Defined Network (SDN), a Container Runtime such as +[containerd](https://jaas.ai/u/containers/containerd), and Transport Layer +Security (TLS) so the components in a cluster communicate securely. + +Please take a look at the [Charmed Kubernetes]( https://jaas.ai/charmed-kubernetes) +or the [Kubernetes core](https://jaas.ai/kubernetes-core) bundles for +examples of complete models of Kubernetes clusters. + +# Resources + +The kubernetes-master charm takes advantage of the [Juju Resources](https://jaas.ai/docs/juju-resources) +feature to deliver the Kubernetes software. + +In deployments on public clouds the Charm Store provides the resource to the +charm automatically with no user intervention. Some environments with strict +firewall rules may not be able to contact the Charm Store. In these network +restricted environments the resource can be uploaded to the model by the Juju +operator. + +#### Snap Refresh + +The kubernetes resources used by this charm are snap packages. When not +specified during deployment, these resources come from the public store. By +default, the `snapd` daemon will refresh all snaps installed from the store +four (4) times per day. A charm configuration option is provided for operators +to control this refresh frequency. + +>NOTE: this is a global configuration option and will affect the refresh +time for all snaps installed on a system. + +Examples: + +```sh +## refresh kubernetes-master snaps every tuesday +juju config kubernetes-master snapd_refresh="tue" + +## refresh snaps at 11pm on the last (5th) friday of the month +juju config kubernetes-master snapd_refresh="fri5,23:00" + +## delay the refresh as long as possible +juju config kubernetes-master snapd_refresh="max" + +## use the system default refresh timer +juju config kubernetes-master snapd_refresh="" +``` + +For more information, see the [snap documentation](/kubernetes/docs/snap-refresh). + +## Configuration + +This charm supports some configuration options to set up a Kubernetes cluster +that works in your environment, detailed in the section below. + +For some specific Kubernetes service configuration tasks, please also see the +section on [configuring K8s services](#k8s-services). + + + + + + +| name | type | Default | Description | +|------|--------|--------------|-------------------------------------------| +| allow-privileged | string | auto | [See notes](#allow-privileged-description) | +| api-extra-args | string | | [See notes](#api-extra-args-description) | +| audit-policy | string | [See notes](#audit-policy-default) | Audit policy passed to kube-apiserver via --audit-policy-file. For more info, please refer to the upstream documentation at https://kubernetes.io/docs/tasks/debug-application-cluster/audit/ | +| audit-webhook-config | string | | Audit webhook config passed to kube-apiserver via --audit-webhook-config-file. For more info, please refer to the upstream documentation at https://kubernetes.io/docs/tasks/debug-application-cluster/audit/ | +| authorization-mode | string | AlwaysAllow | Comma separated authorization modes. Allowed values are "RBAC", "Node", "Webhook", "ABAC", "AlwaysDeny" and "AlwaysAllow". | +| channel | string | 1.17/stable | Snap channel to install Kubernetes master services from | +| client_password | string | | Password to be used for admin user (leave empty for random password). | +| controller-manager-extra-args | string | | [See notes](#controller-manager-extra-args-description) | +| dashboard-auth | string | auto | [See notes](#dashboard-auth-description) | +| default-storage | string | auto | The storage class to make the default storage class. Allowed values are "auto", "none", "ceph-xfs", "ceph-ext4". Note: Only works in Kubernetes >= 1.10 | +| dns-provider | string | auto | [See notes](#dns-provider-description) | +| dns_domain | string | cluster.local | The local domain for cluster dns | +| enable-dashboard-addons | boolean | True | Deploy the Kubernetes Dashboard and Heapster addons | +| enable-keystone-authorization | boolean | False | If true and the Keystone charm is related, users will authorize against the Keystone server. Note that if related, users will always authenticate against Keystone. | +| enable-metrics | boolean | True | If true the metrics server for Kubernetes will be deployed onto the cluster. | +| enable-nvidia-plugin | string | auto | Load the nvidia device plugin daemonset. Supported values are "auto" and "false". When "auto", the daemonset will be loaded only if GPUs are detected. When "false" the nvidia device plugin will not be loaded. | +| extra_packages | string | | Space separated list of extra deb packages to install. | +| extra_sans | string | | Space-separated list of extra SAN entries to add to the x509 certificate created for the master nodes. | +| ha-cluster-dns | string | | DNS entry to use with the HA Cluster subordinate charm. Mutually exclusive with ha-cluster-vip. | +| ha-cluster-vip | string | | Virtual IP for the charm to use with the HA Cluster subordinate charm Mutually exclusive with ha-cluster-dns. Multiple virtual IPs are separated by spaces. | +| image-registry | string | [See notes](#image-registry-default) | Container image registry to use for CDK. This includes addons like the Kubernetes dashboard, metrics server, ingress, and dns along with non-addon images including the pause container and default backend image. | +| install_keys | string | | [See notes](#install_keys-description) | +| install_sources | string | | [See notes](#install_sources-description) | +| keystone-policy | string | [See notes](#keystone-policy-default) | Policy for Keystone authorization. This is used when a Keystone charm is related to kubernetes-master in order to provide authorization for Keystone users on the Kubernetes cluster. | +| keystone-ssl-ca | string | | Keystone certificate authority encoded in base64 for securing communications to Keystone. For example: `juju config kubernetes-master keystone-ssl-ca=$(base64 /path/to/ca.crt)` | +| loadbalancer-ips | string | | [See notes](#loadbalancer-ips-description) | +| nagios_context | string | juju | [See notes](#nagios_context-description) | +| nagios_servicegroups | string | | A comma-separated list of nagios servicegroups. If left empty, the nagios_context will be used as the servicegroup | +| package_status | string | install | The status of service-affecting packages will be set to this value in the dpkg database. Valid values are "install" and "hold". | +| proxy-extra-args | string | | [See notes](#proxy-extra-args-description) | +| require-manual-upgrade | boolean | True | When true, master nodes will not be upgraded until the user triggers it manually by running the upgrade action. | +| scheduler-extra-args | string | | [See notes](#scheduler-extra-args-description) | +| service-cidr | string | 10.152.183.0/24 | CIDR to user for Kubernetes services. Cannot be changed after deployment. | +| snap_proxy | string | | DEPRECATED. Use snap-http-proxy and snap-https-proxy model configuration settings. HTTP/HTTPS web proxy for Snappy to use when accessing the snap store. | +| snap_proxy_url | string | | DEPRECATED. Use snap-store-proxy model configuration setting. The address of a Snap Store Proxy to use for snaps e.g. http://snap-proxy.example.com | +| snapd_refresh | string | max | [See notes](#snapd_refresh-description) | +| storage-backend | string | auto | The storage backend for kube-apiserver persistence. Can be "etcd2", "etcd3", or "auto". Auto mode will select etcd3 on new installations, or etcd2 on upgrades. | +| sysctl | string | [See notes](#sysctl-default) | [See notes](#sysctl-description) | + +--- + +### allow-privileged + + + +**Description:** + +Allow kube-apiserver to run in privileged mode. Supported values are +"true", "false", and "auto". If "true", kube-apiserver will run in +privileged mode by default. If "false", kube-apiserver will never run in +privileged mode. If "auto", kube-apiserver will not run in privileged +mode by default, but will switch to privileged mode if gpu hardware is +detected on a worker node. + +[Back to table](#table-allow-privileged) + + +### api-extra-args + + + +**Description:** + +Space separated list of flags and key=value pairs that will be passed as arguments to +kube-apiserver. For example a value like this: + +``` + runtime-config=batch/v2alpha1=true profiling=true +``` + +will result in kube-apiserver being run with the following options: + --runtime-config=batch/v2alpha1=true --profiling=true + +[Back to table](#table-api-extra-args) + + +### audit-policy + + + +**Default:** + +``` +apiVersion: audit.k8s.io/v1beta1 +kind: Policy +rules: +# Don't log read-only requests from the apiserver +- level: None + users: ["system:apiserver"] + verbs: ["get", "list", "watch"] +# Don't log kube-proxy watches +- level: None + users: ["system:kube-proxy"] + verbs: ["watch"] + resources: + - resources: ["endpoints", "services"] +# Don't log nodes getting their own status +- level: None + userGroups: ["system:nodes"] + verbs: ["get"] + resources: + - resources: ["nodes"] +# Don't log kube-controller-manager and kube-scheduler getting endpoints +- level: None + users: ["system:unsecured"] + namespaces: ["kube-system"] + verbs: ["get"] + resources: + - resources: ["endpoints"] +# Log everything else at the Request level. +- level: Request + omitStages: + - RequestReceived + +``` + + +[Back to table](#table-audit-policy) + + +### controller-manager-extra-args + + + +**Description:** + +Space separated list of flags and key=value pairs that will be passed as arguments to +kube-controller-manager. For example a value like this: + +``` + runtime-config=batch/v2alpha1=true profiling=true +``` + +will result in kube-controller-manager being run with the following options: + --runtime-config=batch/v2alpha1=true --profiling=true + +[Back to table](#table-controller-manager-extra-args) + + +### dashboard-auth + + + +**Description:** + +Method of authentication for the Kubernetes dashboard. Allowed values are "auto", +"basic", and "token". If set to "auto", basic auth is used unless Keystone is +related to kubernetes-master, in which case token auth is used. + +[Back to table](#table-dashboard-auth) + + +### dns-provider + + + +**Description:** + +DNS provider addon to use. Can be "auto", "core-dns", "kube-dns", or +"none". + +CoreDNS is only supported on Kubernetes 1.14+. + +When set to "auto", the behavior is as follows: +- New deployments of Kubernetes 1.14+ will use CoreDNS +- New deployments of Kubernetes 1.13 or older will use KubeDNS +- Upgraded deployments will continue to use whichever provider was +previously used. + +[Back to table](#table-dns-provider) + + +### image-registry + + + +**Default:** + +``` +rocks.canonical.com:443/cdk +``` + + +[Back to table](#table-image-registry) + + +### install_keys + + + +**Description:** + +List of signing keys for install_sources package sources, per charmhelpers standard format (a yaml list of strings encoded as a string). The keys should be the full ASCII armoured GPG public keys. While GPG key ids are also supported and looked up on a keyserver, operators should be aware that this mechanism is insecure. null can be used if a standard package signing key is used that will already be installed on the machine, and for PPA sources where the package signing key is securely retrieved from Launchpad. + +[Back to table](#table-install_keys) + + +### install_sources + + + +**Description:** + +List of extra apt sources, per charm-helpers standard format (a yaml list of strings encoded as a string). Each source may be either a line that can be added directly to sources.list(5), or in the form ppa:/ for adding Personal Package Archives, or a distribution component to enable. + +[Back to table](#table-install_sources) + + +### keystone-policy + + + +**Default:** + +``` +apiVersion: v1 +kind: ConfigMap +metadata: + name: k8s-auth-policy + namespace: kube-system + labels: + k8s-app: k8s-keystone-auth +data: + policies: | + [ + { + "resource": { + "verbs": ["get", "list", "watch"], + "resources": ["*"], + "version": "*", + "namespace": "*" + }, + "match": [ + { + "type": "role", + "values": ["k8s-viewers"] + }, + { + "type": "project", + "values": ["k8s"] + } + ] + }, + { + "resource": { + "verbs": ["*"], + "resources": ["*"], + "version": "*", + "namespace": "default" + }, + "match": [ + { + "type": "role", + "values": ["k8s-users"] + }, + { + "type": "project", + "values": ["k8s"] + } + ] + }, + { + "resource": { + "verbs": ["*"], + "resources": ["*"], + "version": "*", + "namespace": "*" + }, + "match": [ + { + "type": "role", + "values": ["k8s-admins"] + }, + { + "type": "project", + "values": ["k8s"] + } + ] + } + ] + +``` + + +[Back to table](#table-keystone-policy) + + +### loadbalancer-ips + + + +**Description:** + +Space separated list of IP addresses of loadbalancers in front of the control plane. +These can be either virtual IP addresses that have been floated in front of the control +plane or the IP of a loadbalancer appliance such as an F5. Workers will alternate IP +addresses from this list to distribute load - for example If you have 2 IPs and 4 workers, +each IP will be used by 2 workers. Note that this will only work if kubeapi-load-balancer +is not in use and there is a relation between kubernetes-master:kube-api-endpoint and +kubernetes-worker:kube-api-endpoint. If using the kubeapi-load-balancer, see the +loadbalancer-ips configuration variable on the kubeapi-load-balancer charm. + +[Back to table](#table-loadbalancer-ips) + + +### nagios_context + + + +**Description:** + +Used by the nrpe subordinate charms. +A string that will be prepended to instance name to set the host name +in nagios. So for instance the hostname would be something like: + +``` + juju-myservice-0 +``` + +If you're running multiple environments with the same services in them +this allows you to differentiate between them. + +[Back to table](#table-nagios_context) + + +### proxy-extra-args + + + +**Description:** + +Space separated list of flags and key=value pairs that will be passed as arguments to +kube-proxy. For example a value like this: + +``` + runtime-config=batch/v2alpha1=true profiling=true +``` + +will result in kube-apiserver being run with the following options: + --runtime-config=batch/v2alpha1=true --profiling=true + +[Back to table](#table-proxy-extra-args) + + +### scheduler-extra-args + + + +**Description:** + +Space separated list of flags and key=value pairs that will be passed as arguments to +kube-scheduler. For example a value like this: + +``` + runtime-config=batch/v2alpha1=true profiling=true +``` + +will result in kube-scheduler being run with the following options: + --runtime-config=batch/v2alpha1=true --profiling=true + +[Back to table](#table-scheduler-extra-args) + + +### snapd_refresh + + + +**Description:** + +How often snapd handles updates for installed snaps. Setting an empty +string will check 4x per day. Set to "max" to delay the refresh as long +as possible. You may also set a custom string as described in the +'refresh.timer' section here: + https://forum.snapcraft.io/t/system-options/87 + +[Back to table](#table-snapd_refresh) + + +### sysctl + + + +**Default:** + +``` +{ net.ipv4.conf.all.forwarding : 1, net.ipv4.neigh.default.gc_thresh1 : 128, net.ipv4.neigh.default.gc_thresh2 : 28672, net.ipv4.neigh.default.gc_thresh3 : 32768, net.ipv6.neigh.default.gc_thresh1 : 128, net.ipv6.neigh.default.gc_thresh2 : 28672, net.ipv6.neigh.default.gc_thresh3 : 32768, fs.inotify.max_user_instances : 8192, fs.inotify.max_user_watches: 1048576 } +``` + + +[Back to table](#table-sysctl) + + + +**Description:** + +YAML formatted associative array of sysctl values, e.g.: +'{kernel.pid_max : 4194303 }'. Note that kube-proxy handles +the conntrack settings. The proper way to alter them is to +use the proxy-extra-args config to set them, e.g.: + +``` + juju config kubernetes-master proxy-extra-args="conntrack-min=1000000 conntrack-max-per-core=250000" + juju config kubernetes-worker proxy-extra-args="conntrack-min=1000000 conntrack-max-per-core=250000" +``` + +The proxy-extra-args conntrack-min and conntrack-max-per-core can be set to 0 to ignore +kube-proxy's settings and use the sysctl settings instead. Note the fundamental difference between +the setting of conntrack-max-per-core vs nf_conntrack_max. + +[Back to table](#table-sysctl) + + + + + + +# Configuring K8s services + +**Charmed Kubernetes** ships with sensible, tested default configurations to +ensure a reliable Kubernetes experience, but of course these can be changed to +reflect the purpose and resources of your cluster. +The configuration section above details all available configuration options, +this section deals with specific, commonly used settings. +You may wish to also read the [Addons page][] for information on the extra +services installed with **Charmed Kubernetes**. + + +## IPVS (IP Virtual Server) + +IPVS implements transport-layer load balancing as part of the Linux kernel, and +can be used by the `kube-proxy` service to handle service routing. By default +`kube-proxy` uses a solution based on iptables, but this can cause a lot of +overhead in systems with large numbers of nodes. There is more information on +this in the upstream Kubernetes [IPVS deep dive][] documentation. + +IPVS is an extra option for kube-proxy, and can be enabled by changing the +configuration: + +``` +juju config kubernetes-master proxy-extra-args="proxy-mode=ipvs" +``` + +It is also necessary to change this configuration option on the worker: + +``` +juju config kubernetes-worker proxy-extra-args="proxy-mode=ipvs" +``` + +## Admission controls + +As with other aspects of the Kubernetes API, admission controls can be +enabled by adding extra values to the charm's +[api-extra-args](#api-extra-args-description) configuration. + +For admission controls, it may be useful to refer to the +[Kubernetes blog][blog-admission] for more information on the options, but +for example, to add the `PodSecurityPolicy` admission controller: + +1. Check any current config settings for `api-extra-args` (there are none by default): + ```bash + juju config kubernetes-master api-extra-args + ``` +2. Append the desired config option to the previous output and apply: + ```bash + juju config kubernetes-master api-extra-args="enable-admission-plugins=PodSecurityPolicy" + ``` + +Note that prior to Kubernetes 1.16 (kubernetes-master revision 778), the config +setting was `admission-control`, rather than `enable-admission-plugins`. + + +## Adding SANs and certificate regeneration + +As explained in the [Certificates and trust overview][certs-and-trust], the +[`extra_sans`](#table-extra_sans) configuration settings can be used to add +SANs and regenerate x509 certificate(s) for the API server running on the +Kubernetes master node(s), and for the load balancer. When this configuration is +changed, the master node(s) will regenerate its certificate and restart the API +server to update the certificate used for communication. Note: This is +disruptive and restarts the API server. + +The process is the same for both the `kubernetes-master` and the +`kubeapi-load-balancer`. The configuration option takes a space-separated list +of extra entries: + +```bash +juju config kubernetes-master extra_sans="master.mydomain.com lb.mydomain.com" +juju config kubeapi-load-balancer extra_sans="master.mydomain.com lb.mydomain.com" +``` +To clear the entries out of the certificate, use an empty string: + +```bash +juju config kubernetes-master extra_sans="" +juju config kubeapi-load-balancer extra_sans="" +``` + +## DNS for the cluster + +The DNS add-on allows pods to have DNS names in addition to IP addresses. +The Kubernetes cluster DNS server (based on the SkyDNS library) supports +forward lookups (A records), service lookups (SRV records) and reverse IP +address lookups (PTR records). More information about the DNS can be obtained +from the [Kubernetes DNS admin guide](http://kubernetes.io/docs/admin/dns/). + +# Actions + + + + +You can run an action with the following + +```bash +juju run-action kubernetes-master ACTION [parameters] [--wait] +``` +
+
+
+ apply-manifest +
+
+
+

+ Apply JSON formatted Kubernetes manifest to cluster +

+
+
+
+
+
+

+ This action has the following parameters: +

+
+
json
+

+ The content of the manifest to deploy in JSON format +

+

+ Default: +


+
+
+
+
+
+
+ cis-benchmark +
+
+
+

+ Run the CIS Kubernetes Benchmark against snap-based components. +

+
+
+
+
+
+

+ This action has the following parameters: +

+
+
apply
+

+ Apply remediations to address benchmark failures. The default, 'none', will not attempt to fix any reported failures. Set to 'conservative' to resolve simple failures. Set to 'dangerous' to attempt to resolve all failures. Note: Applying any remediation may result in an unusable cluster. +

+

+ Default: none +


+
config
+

+ Archive containing configuration files to use when running kube-bench. The default value is known to be compatible with snap components. When using a custom URL, append '#<hash_type>=<checksum>' to verify the archive integrity when downloaded. +

+

+ Default: https://github.com/charmed-kubernetes/kube-bench-c onfig/archive/cis-1.5.zip#sha1=cb8e78712ee5bfeab87 d0ed7c139a83e88915530 +


+
release
+

+ Set the kube-bench release to run. If set to 'upstream', the action will compile and use a local kube-bench binary built from the master branch of the upstream repository: https://github.com/aquasecurity/kube-bench This value may also be set to an accessible archive containing a pre-built kube-bench binary, for example: https://github.com/aquasecurity/kube- bench/releases/download/v0.0.34/kube-bench_0.0.34_ linux_amd64.tar.gz#sha256=f96d1fcfb84b18324f1299db 074d41ef324a25be5b944e79619ad1a079fca077 +

+

+ Default: https://github.com/aquasecurity/kube- bench/releases/download/v0.2.3/kube-bench_0.2.3_li nux_amd64.tar.gz#sha256=429a1db271689aafec009434de d1dea07a6685fee85a1deea638097c8512d548 +


+
+
+
+
+
+
+ debug +
+
+
+

+ Collect debug data +

+
+
+
+
+
+
+ get-kubeconfig +
+
+
+

+ Retrieve Kubernetes cluster config, including credentials +

+
+
+
+
+
+
+ namespace-create +
+
+
+

+ Create new namespace +

+
+
+
+
+
+

+ This action has the following parameters: +

+
+
name
+

+ Namespace name eg. staging +

+

+ Default: +


+
+
+
+
+
+
+ namespace-delete +
+
+
+

+ Delete namespace +

+
+
+
+
+
+

+ This action has the following parameters: +

+
+
name
+

+ Namespace name eg. staging +

+

+ Default: +


+
+
+
+
+
+
+ namespace-list +
+
+
+

+ List existing k8s namespaces +

+
+
+
+
+
+
+ restart +
+
+
+

+ Restart the Kubernetes master services on demand. +

+
+
+
+
+
+
+ upgrade +
+
+
+

+ Upgrade the kubernetes snaps +

+
+
+
+
+
+

+ This action has the following parameters: +

+
+
fix-cluster-name
+

+ If using the OpenStack cloud provider, whether to fix the cluster name sent to it to include the cluster tag. This fixes an issue with load balancers conflicting with other clusters in the same project but will cause new load balancers to be created which will require manual intervention to resolve. +

+

+ Default: True +


+
+
+
+ + + + + + +# More information + +- [Kubernetes github project](https://github.com/kubernetes/kubernetes) +- [Kubernetes issue tracker](https://github.com/kubernetes/kubernetes/issues) +- [Kubernetes documentation](http://kubernetes.io/docs/) +- [Kubernetes releases](https://github.com/kubernetes/kubernetes/releases) + + +[IPVS deep dive]: https://kubernetes.io/blog/2018/07/09/ipvs-based-in-cluster-load-balancing-deep-dive/ +[blog-admission]: https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/ +[Addons page]: /kubernetes/docs/cdk-addons +[certs-and-trust]: /kubernetes/docs/certs-and-trust diff --git a/kubernetes-master/docs/status.md b/kubernetes-master/docs/status.md new file mode 100644 index 0000000..c6cceab --- /dev/null +++ b/kubernetes-master/docs/status.md @@ -0,0 +1,91 @@ +

WorkloadState

+ +```python +WorkloadState(self, /, *args, **kwargs) +``` + +Enum of the valid workload states. + +Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + +

maintenance

+ +```python +maintenance(message) +``` + +Set the status to the `MAINTENANCE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

maint

+ +```python +maint(message) +``` + +Shorthand alias for +[maintenance](status.md#charms.layer.status.maintenance). + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

blocked

+ +```python +blocked(message) +``` + +Set the status to the `BLOCKED` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

waiting

+ +```python +waiting(message) +``` + +Set the status to the `WAITING` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

active

+ +```python +active(message) +``` + +Set the status to the `ACTIVE` state with the given operator message. + +__Parameters__ + +- __`message` (str)__: Message to convey to the operator. + +

status_set

+ +```python +status_set(workload_state, message) +``` + +Set the status to the given workload state with a message. + +__Parameters__ + +- __`workload_state` (WorkloadState or str)__: State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. +- __`message` (str)__: Message to convey to the operator. + diff --git a/kubernetes-master/docs/vault-kv.md b/kubernetes-master/docs/vault-kv.md new file mode 100644 index 0000000..8408256 --- /dev/null +++ b/kubernetes-master/docs/vault-kv.md @@ -0,0 +1,98 @@ +

charms.layer.vault_kv

+ + +

VaultNotReady

+ +```python +VaultNotReady(self, /, *args, **kwargs) +``` + +Exception indicating that Vault was accessed before it was ready. + +

VaultUnitKV

+ +```python +VaultUnitKV(self) +``` + +A simplified interface for storing data in Vault, with the data scoped to +the current unit. + +Keys must be strings, but data can be structured as long as it is +JSON-serializable. + +This class can be used as a dict, or you can use `self.get` and `self.set` +for a more KV-like interface. When values are set, via either style, they +are immediately persisted to Vault. Values are also cached in memory. + +Note: This class is a singleton. + +

VaultAppKV

+ +```python +VaultAppKV(self) +``` + +A simplified interface for storing data in Vault, with data shared by every +unit of the application. + +Keys must be strings, but data can be structured as long as it is +JSON-serializable. + +This class can be used as a dict, or you can use `self.get` and `self.set` +for a more KV-like interface. When values are set, via either style, they +are immediately persisted to Vault. Values are also cached in memory. + +Note: This class is a singleton. + +

is_changed

+ +```python +VaultAppKV.is_changed(self, key) +``` + +Determine if the value for the given key has changed since the last +time `self.update_hashes()` has been called. + +In order to detect changes, hashes of the values are also sotred +in Vault. + +

update_hashes

+ +```python +VaultAppKV.update_hashes(self) +``` + +Update the hashes in Vault, thus marking all fields as unchanged. + +This is done automatically at exit. + +

get_vault_config

+ +```python +get_vault_config() +``` + +Get the config data needed for this application to access Vault. + +This is only needed if you're using another application, such as +VaultLocker, using the secrets backend provided by this layer. + +Returns a dictionary containing the following keys: + + * vault_url + * secret_backend + * role_id + * secret_id + +Note: This data is cached in [UnitData][] so anything with access to that +could access Vault as this application. + +If any of this data changes (such as the secret_id being rotated), this +layer will set the `layer.vault-kv.config.changed` flag. + +If this is called before the Vault relation is available, it will raise +`VaultNotReady`. + +[UnitData]: https://charm-helpers.readthedocs.io/en/latest/api/charmhelpers.core.unitdata.html + diff --git a/kubernetes-master/docs/vaultlocker.md b/kubernetes-master/docs/vaultlocker.md new file mode 100644 index 0000000..e30f255 --- /dev/null +++ b/kubernetes-master/docs/vaultlocker.md @@ -0,0 +1,49 @@ +

charms.layer.vaultlocker

+ + +

encrypt_storage

+ +```python +encrypt_storage(storage_name, mountbase=None) +``` + +Set up encryption for the given Juju storage entry, and optionally create +and mount XFS filesystems on the encrypted storage entry location(s). + +Note that the storage entry **must** be defined with ``type: block``. + +If ``mountbase`` is not given, the location(s) will not be formatted or +mounted. When interacting with or mounting the location(s) manually, the +name returned by :func:`decrypted_device` called on the storage entry's +location should be used in place of the raw location. + +If the storage is defined as ``multiple``, the individual locations +will be mounted at ``{mountbase}/{storage_name}/{num}`` where ``{num}`` +is based on the storage ID. Otherwise, the storage will mounted at +``{mountbase}/{storage_name}``. + +

encrypt_device

+ +```python +encrypt_device(device, mountpoint=None) +``` + +Set up encryption for the given block device, and optionally create and +mount an XFS filesystem on the encrypted device. + +If ``mountpoint`` is not given, the device will not be formatted or +mounted. When interacting with or mounting the device manually, the +name returned by :func:`decrypted_device` called on the device name +should be used in place of the raw device name. + +

decrypted_device

+ +```python +decrypted_device(device) +``` + +Returns the mapped device name for the decrypted version of the encrypted +device. + +This mapped device name is what should be used for mounting the device. + diff --git a/kubernetes-master/exec.d/docker-compose/charm-pre-install b/kubernetes-master/exec.d/docker-compose/charm-pre-install new file mode 100644 index 0000000..f0202c5 --- /dev/null +++ b/kubernetes-master/exec.d/docker-compose/charm-pre-install @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# This stubs out charm-pre-install coming from layer-docker as a workaround for +# offline installs until https://github.com/juju/charm-tools/issues/301 is fixed. diff --git a/kubernetes-master/exec.d/vmware-patch/charm-pre-install b/kubernetes-master/exec.d/vmware-patch/charm-pre-install new file mode 100755 index 0000000..b5e6d97 --- /dev/null +++ b/kubernetes-master/exec.d/vmware-patch/charm-pre-install @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +MY_HOSTNAME=$(hostname) + +: ${JUJU_UNIT_NAME:=`uuidgen`} + + +if [ "${MY_HOSTNAME}" == "ubuntuguest" ]; then + juju-log "Detected broken vsphere integration. Applying hostname override" + + FRIENDLY_HOSTNAME=$(echo $JUJU_UNIT_NAME | tr / -) + juju-log "Setting hostname to $FRIENDLY_HOSTNAME" + if [ ! -f /etc/hostname.orig ]; then + mv /etc/hostname /etc/hostname.orig + fi + echo "${FRIENDLY_HOSTNAME}" > /etc/hostname + hostname $FRIENDLY_HOSTNAME +fi diff --git a/kubernetes-master/hooks/aws-iam-relation-broken b/kubernetes-master/hooks/aws-iam-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/aws-iam-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/aws-iam-relation-changed b/kubernetes-master/hooks/aws-iam-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/aws-iam-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/aws-iam-relation-created b/kubernetes-master/hooks/aws-iam-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/aws-iam-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/aws-iam-relation-departed b/kubernetes-master/hooks/aws-iam-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/aws-iam-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/aws-iam-relation-joined b/kubernetes-master/hooks/aws-iam-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/aws-iam-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/aws-relation-broken b/kubernetes-master/hooks/aws-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/aws-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/aws-relation-changed b/kubernetes-master/hooks/aws-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/aws-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/aws-relation-created b/kubernetes-master/hooks/aws-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/aws-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/aws-relation-departed b/kubernetes-master/hooks/aws-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/aws-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/aws-relation-joined b/kubernetes-master/hooks/aws-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/aws-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/azure-relation-broken b/kubernetes-master/hooks/azure-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/azure-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/azure-relation-changed b/kubernetes-master/hooks/azure-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/azure-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/azure-relation-created b/kubernetes-master/hooks/azure-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/azure-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/azure-relation-departed b/kubernetes-master/hooks/azure-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/azure-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/azure-relation-joined b/kubernetes-master/hooks/azure-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/azure-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ceph-client-relation-broken b/kubernetes-master/hooks/ceph-client-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ceph-client-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ceph-client-relation-changed b/kubernetes-master/hooks/ceph-client-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ceph-client-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ceph-client-relation-created b/kubernetes-master/hooks/ceph-client-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ceph-client-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ceph-client-relation-departed b/kubernetes-master/hooks/ceph-client-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ceph-client-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ceph-client-relation-joined b/kubernetes-master/hooks/ceph-client-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ceph-client-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ceph-storage-relation-broken b/kubernetes-master/hooks/ceph-storage-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ceph-storage-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ceph-storage-relation-changed b/kubernetes-master/hooks/ceph-storage-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ceph-storage-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ceph-storage-relation-created b/kubernetes-master/hooks/ceph-storage-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ceph-storage-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ceph-storage-relation-departed b/kubernetes-master/hooks/ceph-storage-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ceph-storage-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ceph-storage-relation-joined b/kubernetes-master/hooks/ceph-storage-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ceph-storage-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/certificates-relation-broken b/kubernetes-master/hooks/certificates-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/certificates-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/certificates-relation-changed b/kubernetes-master/hooks/certificates-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/certificates-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/certificates-relation-created b/kubernetes-master/hooks/certificates-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/certificates-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/certificates-relation-departed b/kubernetes-master/hooks/certificates-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/certificates-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/certificates-relation-joined b/kubernetes-master/hooks/certificates-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/certificates-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/cni-relation-broken b/kubernetes-master/hooks/cni-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/cni-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/cni-relation-changed b/kubernetes-master/hooks/cni-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/cni-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/cni-relation-created b/kubernetes-master/hooks/cni-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/cni-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/cni-relation-departed b/kubernetes-master/hooks/cni-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/cni-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/cni-relation-joined b/kubernetes-master/hooks/cni-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/cni-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/collect-metrics b/kubernetes-master/hooks/collect-metrics new file mode 100755 index 0000000..8a27863 --- /dev/null +++ b/kubernetes-master/hooks/collect-metrics @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +import yaml +import os +from subprocess import check_output, check_call, CalledProcessError + + +def build_command(doc): + values = {} + metrics = doc.get("metrics", {}) + for metric, mdoc in metrics.items(): + if not mdoc: + continue + cmd = mdoc.get("command") + if cmd: + try: + value = check_output(cmd, shell=True, universal_newlines=True) + except CalledProcessError as e: + check_call(['juju-log', '-lERROR', + 'Error collecting metric {}:\n{}'.format( + metric, e.output)]) + continue + value = value.strip() + if value: + values[metric] = value + + if not values: + return None + command = ["add-metric"] + for metric, value in values.items(): + command.append("%s=%s" % (metric, value)) + return command + + +if __name__ == '__main__': + charm_dir = os.path.dirname(os.path.abspath(os.path.join(__file__, ".."))) + metrics_yaml = os.path.join(charm_dir, "metrics.yaml") + with open(metrics_yaml) as f: + doc = yaml.load(f) + command = build_command(doc) + if command: + check_call(command) diff --git a/kubernetes-master/hooks/config-changed b/kubernetes-master/hooks/config-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/config-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/container-runtime-relation-broken b/kubernetes-master/hooks/container-runtime-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/container-runtime-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/container-runtime-relation-changed b/kubernetes-master/hooks/container-runtime-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/container-runtime-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/container-runtime-relation-created b/kubernetes-master/hooks/container-runtime-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/container-runtime-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/container-runtime-relation-departed b/kubernetes-master/hooks/container-runtime-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/container-runtime-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/container-runtime-relation-joined b/kubernetes-master/hooks/container-runtime-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/container-runtime-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/coordinator-relation-broken b/kubernetes-master/hooks/coordinator-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/coordinator-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/coordinator-relation-changed b/kubernetes-master/hooks/coordinator-relation-changed new file mode 100755 index 0000000..fe39f65 --- /dev/null +++ b/kubernetes-master/hooks/coordinator-relation-changed @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer.basic import bootstrap_charm_deps +bootstrap_charm_deps() + + +# This will load and run the appropriate @hook and other decorated +# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive, +# and $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/kubernetes-master/hooks/coordinator-relation-created b/kubernetes-master/hooks/coordinator-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/coordinator-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/coordinator-relation-departed b/kubernetes-master/hooks/coordinator-relation-departed new file mode 100755 index 0000000..fe39f65 --- /dev/null +++ b/kubernetes-master/hooks/coordinator-relation-departed @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer.basic import bootstrap_charm_deps +bootstrap_charm_deps() + + +# This will load and run the appropriate @hook and other decorated +# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive, +# and $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/kubernetes-master/hooks/coordinator-relation-joined b/kubernetes-master/hooks/coordinator-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/coordinator-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/dns-provider-relation-broken b/kubernetes-master/hooks/dns-provider-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/dns-provider-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/dns-provider-relation-changed b/kubernetes-master/hooks/dns-provider-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/dns-provider-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/dns-provider-relation-created b/kubernetes-master/hooks/dns-provider-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/dns-provider-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/dns-provider-relation-departed b/kubernetes-master/hooks/dns-provider-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/dns-provider-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/dns-provider-relation-joined b/kubernetes-master/hooks/dns-provider-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/dns-provider-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/etcd-relation-broken b/kubernetes-master/hooks/etcd-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/etcd-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/etcd-relation-changed b/kubernetes-master/hooks/etcd-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/etcd-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/etcd-relation-created b/kubernetes-master/hooks/etcd-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/etcd-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/etcd-relation-departed b/kubernetes-master/hooks/etcd-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/etcd-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/etcd-relation-joined b/kubernetes-master/hooks/etcd-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/etcd-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/gcp-relation-broken b/kubernetes-master/hooks/gcp-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/gcp-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/gcp-relation-changed b/kubernetes-master/hooks/gcp-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/gcp-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/gcp-relation-created b/kubernetes-master/hooks/gcp-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/gcp-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/gcp-relation-departed b/kubernetes-master/hooks/gcp-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/gcp-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/gcp-relation-joined b/kubernetes-master/hooks/gcp-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/gcp-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/grafana-relation-broken b/kubernetes-master/hooks/grafana-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/grafana-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/grafana-relation-changed b/kubernetes-master/hooks/grafana-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/grafana-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/grafana-relation-created b/kubernetes-master/hooks/grafana-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/grafana-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/grafana-relation-departed b/kubernetes-master/hooks/grafana-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/grafana-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/grafana-relation-joined b/kubernetes-master/hooks/grafana-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/grafana-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ha-relation-broken b/kubernetes-master/hooks/ha-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ha-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ha-relation-changed b/kubernetes-master/hooks/ha-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ha-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ha-relation-created b/kubernetes-master/hooks/ha-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ha-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ha-relation-departed b/kubernetes-master/hooks/ha-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ha-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/ha-relation-joined b/kubernetes-master/hooks/ha-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/ha-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/hook.template b/kubernetes-master/hooks/hook.template new file mode 100644 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/hook.template @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/install b/kubernetes-master/hooks/install new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/install @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/keystone-credentials-relation-broken b/kubernetes-master/hooks/keystone-credentials-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/keystone-credentials-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/keystone-credentials-relation-changed b/kubernetes-master/hooks/keystone-credentials-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/keystone-credentials-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/keystone-credentials-relation-created b/kubernetes-master/hooks/keystone-credentials-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/keystone-credentials-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/keystone-credentials-relation-departed b/kubernetes-master/hooks/keystone-credentials-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/keystone-credentials-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/keystone-credentials-relation-joined b/kubernetes-master/hooks/keystone-credentials-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/keystone-credentials-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-api-endpoint-relation-broken b/kubernetes-master/hooks/kube-api-endpoint-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-api-endpoint-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-api-endpoint-relation-changed b/kubernetes-master/hooks/kube-api-endpoint-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-api-endpoint-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-api-endpoint-relation-created b/kubernetes-master/hooks/kube-api-endpoint-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-api-endpoint-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-api-endpoint-relation-departed b/kubernetes-master/hooks/kube-api-endpoint-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-api-endpoint-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-api-endpoint-relation-joined b/kubernetes-master/hooks/kube-api-endpoint-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-api-endpoint-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-control-relation-broken b/kubernetes-master/hooks/kube-control-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-control-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-control-relation-changed b/kubernetes-master/hooks/kube-control-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-control-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-control-relation-created b/kubernetes-master/hooks/kube-control-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-control-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-control-relation-departed b/kubernetes-master/hooks/kube-control-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-control-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-control-relation-joined b/kubernetes-master/hooks/kube-control-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-control-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-masters-relation-broken b/kubernetes-master/hooks/kube-masters-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-masters-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-masters-relation-changed b/kubernetes-master/hooks/kube-masters-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-masters-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-masters-relation-created b/kubernetes-master/hooks/kube-masters-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-masters-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-masters-relation-departed b/kubernetes-master/hooks/kube-masters-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-masters-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/kube-masters-relation-joined b/kubernetes-master/hooks/kube-masters-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/kube-masters-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/leader-elected b/kubernetes-master/hooks/leader-elected new file mode 100755 index 0000000..fe39f65 --- /dev/null +++ b/kubernetes-master/hooks/leader-elected @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer.basic import bootstrap_charm_deps +bootstrap_charm_deps() + + +# This will load and run the appropriate @hook and other decorated +# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive, +# and $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/kubernetes-master/hooks/leader-settings-changed b/kubernetes-master/hooks/leader-settings-changed new file mode 100755 index 0000000..fe39f65 --- /dev/null +++ b/kubernetes-master/hooks/leader-settings-changed @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer.basic import bootstrap_charm_deps +bootstrap_charm_deps() + + +# This will load and run the appropriate @hook and other decorated +# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive, +# and $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/kubernetes-master/hooks/loadbalancer-external-relation-broken b/kubernetes-master/hooks/loadbalancer-external-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-external-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-external-relation-changed b/kubernetes-master/hooks/loadbalancer-external-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-external-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-external-relation-created b/kubernetes-master/hooks/loadbalancer-external-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-external-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-external-relation-departed b/kubernetes-master/hooks/loadbalancer-external-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-external-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-external-relation-joined b/kubernetes-master/hooks/loadbalancer-external-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-external-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-internal-relation-broken b/kubernetes-master/hooks/loadbalancer-internal-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-internal-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-internal-relation-changed b/kubernetes-master/hooks/loadbalancer-internal-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-internal-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-internal-relation-created b/kubernetes-master/hooks/loadbalancer-internal-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-internal-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-internal-relation-departed b/kubernetes-master/hooks/loadbalancer-internal-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-internal-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-internal-relation-joined b/kubernetes-master/hooks/loadbalancer-internal-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-internal-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-relation-broken b/kubernetes-master/hooks/loadbalancer-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-relation-changed b/kubernetes-master/hooks/loadbalancer-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-relation-created b/kubernetes-master/hooks/loadbalancer-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-relation-departed b/kubernetes-master/hooks/loadbalancer-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/loadbalancer-relation-joined b/kubernetes-master/hooks/loadbalancer-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/loadbalancer-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/nrpe-external-master-relation-broken b/kubernetes-master/hooks/nrpe-external-master-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/nrpe-external-master-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/nrpe-external-master-relation-changed b/kubernetes-master/hooks/nrpe-external-master-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/nrpe-external-master-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/nrpe-external-master-relation-created b/kubernetes-master/hooks/nrpe-external-master-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/nrpe-external-master-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/nrpe-external-master-relation-departed b/kubernetes-master/hooks/nrpe-external-master-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/nrpe-external-master-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/nrpe-external-master-relation-joined b/kubernetes-master/hooks/nrpe-external-master-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/nrpe-external-master-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/openstack-relation-broken b/kubernetes-master/hooks/openstack-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/openstack-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/openstack-relation-changed b/kubernetes-master/hooks/openstack-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/openstack-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/openstack-relation-created b/kubernetes-master/hooks/openstack-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/openstack-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/openstack-relation-departed b/kubernetes-master/hooks/openstack-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/openstack-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/openstack-relation-joined b/kubernetes-master/hooks/openstack-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/openstack-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/post-series-upgrade b/kubernetes-master/hooks/post-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/post-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/pre-series-upgrade b/kubernetes-master/hooks/pre-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/pre-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/prometheus-relation-broken b/kubernetes-master/hooks/prometheus-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/prometheus-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/prometheus-relation-changed b/kubernetes-master/hooks/prometheus-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/prometheus-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/prometheus-relation-created b/kubernetes-master/hooks/prometheus-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/prometheus-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/prometheus-relation-departed b/kubernetes-master/hooks/prometheus-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/prometheus-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/prometheus-relation-joined b/kubernetes-master/hooks/prometheus-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/prometheus-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/relations/aws-iam/LICENSE b/kubernetes-master/hooks/relations/aws-iam/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-iam/LICENSE @@ -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/kubernetes-master/hooks/relations/aws-iam/README.md b/kubernetes-master/hooks/relations/aws-iam/README.md new file mode 100644 index 0000000..5a54cd8 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-iam/README.md @@ -0,0 +1,47 @@ +# aws-iam interface + +This interface provides communication between +[kubernetes-master](https://github.com/charmed-kubernetes/charm-kubernetes-master) +and [aws-iam](https://github.com/charmed-kubernetes/charm-aws-iam) +subordinate. + +It allows the requires side, aws-iam, to know when the api server is +up and available and to tell the api server when the webhook.yaml +file is written so that it may restart and use the webhook. + +## Provides (kubernetes-master side) + +### States + * `aws-iam.available` + Indicates that there are one or more units on the other side + of the relation + * `aws-iam.ready` + Indicates that the webhook status has been set. This is used + to indicate it is time to restart the API server to pick up + the webhook config on the Kubernetes side. +### Methods + * `get_cluster_id` + The AWS-IAM charm generates a random cluster ID for the cluster + that is needed in the kubectl configuration file. This is + retrieved from the relation here. + * `set_api_server_status` + This is set to indicate if the Kubernetes API server is up and + ready for connections. This is needed because the aws-iam charm + needs to set up the service it will use in order to add the IP + to the extra sans in the ssl certificate used to secure + communication between the master and the service. + +## Requires (aws-iam side) + +### States + * `aws-iam.available` + Indicates that there are one or more units on the other + side of the relation +### Methods + * `set_cluster_id` + The AWS-IAM charm generates a random cluster ID for the + cluster that is needed in the kubectl configuration file. + This is passed over the relation here. + * `set_webhook_status` + Called to set that the webhook configuration has been written + to disk. \ No newline at end of file diff --git a/kubernetes-master/hooks/relations/aws-iam/__init__.py b/kubernetes-master/hooks/relations/aws-iam/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/aws-iam/interface.yaml b/kubernetes-master/hooks/relations/aws-iam/interface.yaml new file mode 100644 index 0000000..d824eaa --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-iam/interface.yaml @@ -0,0 +1,4 @@ +name: aws-iam +summary: Used to integrate AWS IAM into kubernetes-master charm +version: 1 +maintainer: "Mike Wilson " diff --git a/kubernetes-master/hooks/relations/aws-iam/provides.py b/kubernetes-master/hooks/relations/aws-iam/provides.py new file mode 100644 index 0000000..2cffae7 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-iam/provides.py @@ -0,0 +1,35 @@ +from charms.reactive import Endpoint +from charms.reactive import toggle_flag + + +# kubernetes-master side +class AWSIAMProvides(Endpoint): + + # called automagically before any decorated handlers, but after + # flags are set + def manage_flags(self): + # we want to make sure all the templates and stuff are written + # and pods started before we switch the API server over to + # use the webhook. This is critical for the webhook template + # since the API server will crash if the file isn't there. + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined) + toggle_flag(self.expand_name('endpoint.{endpoint_name}.ready'), + self.is_joined and all(unit.received['webhook_status'] + for unit in self.all_joined_units)) + + def get_cluster_id(self): + """ Gets randomly generated cluster ID. """ + + return self.all_joined_units.received['cluster_id'] + + def set_api_server_status(self, status): + """ Sets the status of the Kubernetes API server. + + Args: + status: Boolean value. True when API server is started + and ready to receive requests. + """ + + for relation in self.relations: + relation.to_publish['api_server_state'] = status diff --git a/kubernetes-master/hooks/relations/aws-iam/requires.py b/kubernetes-master/hooks/relations/aws-iam/requires.py new file mode 100644 index 0000000..960c265 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-iam/requires.py @@ -0,0 +1,36 @@ +from charms.reactive import Endpoint +from charms.reactive import toggle_flag + + +# aws-iam side +class AWSIAMRequires(Endpoint): + + # called automagically before any decorated handlers, but after + # flags are set + def manage_flags(self): + # kubectl is used to deploy the webhook pod. This means that + # the api server needs to be up in order to do that. So we + # wait until the cluster is up before trying. + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined and all(unit.received['api_server_state'] + for unit in self.all_joined_units)) + + def set_webhook_status(self, status): + """ Sets the status of the webhook configuration file. + + Args: + status: Boolean value. True when webhook configuration has been + written to disk and the API server can be configured to + pick that up and restart. + """ + for relation in self.relations: + relation.to_publish['webhook_status'] = status + + def set_cluster_id(self, id): + """ Sets the randomly generated cluster id. The cluster ID is just + a unique value to identify this cluster for AWS-IAM. It is needed + by the API server for the kubectl configuration file. + """ + + for relation in self.relations: + relation.to_publish['cluster_id'] = id diff --git a/kubernetes-master/hooks/relations/aws-integration/.gitignore b/kubernetes-master/hooks/relations/aws-integration/.gitignore new file mode 100644 index 0000000..ba1431e --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/.gitignore @@ -0,0 +1,2 @@ +.tox +__pycache__ diff --git a/kubernetes-master/hooks/relations/aws-integration/LICENSE b/kubernetes-master/hooks/relations/aws-integration/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-master/hooks/relations/aws-integration/README.md b/kubernetes-master/hooks/relations/aws-integration/README.md new file mode 100644 index 0000000..59abfcf --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/README.md @@ -0,0 +1,28 @@ +# Overview + +This layer encapsulates the `aws-integration` interface communciation protocol +and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:aws-integration` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:aws-integration'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `aws-integration` interface protocol: + +```yaml +requires: + aws: + interface: aws-integration +``` + +For documentation on how to use the API for this interface, see: + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) (this will only be used by the aws-integrator charm) diff --git a/kubernetes-master/hooks/relations/aws-integration/__init__.py b/kubernetes-master/hooks/relations/aws-integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/aws-integration/copyright b/kubernetes-master/hooks/relations/aws-integration/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/hooks/relations/aws-integration/docs/provides.md b/kubernetes-master/hooks/relations/aws-integration/docs/provides.md new file mode 100644 index 0000000..57ecb25 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/docs/provides.md @@ -0,0 +1,179 @@ +

provides

+ + +This is the provides side of the interface layer, for use only by the AWS +integrator charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for AWS integration features. + The AWS integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. + +

AWSIntegrationProvides

+ +```python +AWSIntegrationProvides(self, endpoint_name, relation_ids=None) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag +from charms import layer + +@when('endpoint.aws.requested') +def handle_requests(): + aws = endpoint_from_flag('endpoint.aws.requested') + for request in aws.requests: + if request.instance_tags: + tag_instance( + request.instance_id, + request.region, + request.instance_tags) + if request.requested_load_balancer_management: + layer.aws.enable_load_balancer_management( + request.application_name, + request.instance_id, + request.region, + ) + # ... + request.mark_completed() +``` + +

application_names

+ + +Set of names of all applications that are still joined. + +

requests

+ + +A list of the new or updated `IntegrationRequests` that +have been made. + +

unit_instances

+ + +Mapping of unit names to instance IDs and regions for all joined units. + +

IntegrationRequest

+ +```python +IntegrationRequest(self, unit) +``` + +A request for integration from a single remote unit. + +

application_name

+ + +The name of the application making the request. + +

changed

+ + +Whether this request has changed since the last time it was +marked completed. + +

hash

+ + +SHA hash of the data for this request. + +

instance_id

+ + +The instance ID reported for this request. + +

instance_security_group_tags

+ + +Mapping of tag names to values (or `None`) to apply to this instance's +machine-specific security group (firewall). + +

instance_subnet_tags

+ + +Mapping of tag names to values (or `None`) to apply to this instance's +subnet. + +

instance_tags

+ + +Mapping of tag names to values (or `None`) to apply to this instance. + +

object_storage_access_patterns

+ + +List of patterns to which to restrict object storage access. + +

object_storage_management_patterns

+ + +List of patterns to which to restrict object storage management. + +

region

+ + +The region reported for this request. + +

requested_block_storage_management

+ + +Flag indicating whether block storage management was requested. + +

requested_dns_management

+ + +Flag indicating whether DNS management was requested. + +

requested_instance_inspection

+ + +Flag indicating whether the ability to inspect instances was requested. + +

requested_load_balancer_management

+ + +Flag indicating whether load balancer management was requested. + +

requested_network_management

+ + +Flag indicating whether the ability to manage networking (firewalls, +subnets, etc) was requested. + +

requested_object_storage_access

+ + +Flag indicating whether object storage access was requested. + +

requested_object_storage_management

+ + +Flag indicating whether object storage management was requested. + +

unit_name

+ + +The name of the unit making the request. + +

mark_completed

+ +```python +IntegrationRequest.mark_completed(self) +``` + +Mark this request as having been completed. + +

clear

+ +```python +IntegrationRequest.clear(self) +``` + +Clear this request's cached data. + diff --git a/kubernetes-master/hooks/relations/aws-integration/docs/requires.md b/kubernetes-master/hooks/relations/aws-integration/docs/requires.md new file mode 100644 index 0000000..41607f4 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/docs/requires.md @@ -0,0 +1,178 @@ +

requires

+ + +This is the requires side of the interface layer, for use in charms that +wish to request integration with AWS native features. The integration will +be provided by the AWS integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of AWS specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific AWS features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the AWS instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. + +

AWSIntegrationRequires

+ +```python +AWSIntegrationRequires(self, *args, **kwargs) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag + +@when('endpoint.aws.joined') +def request_aws_integration(): + aws = endpoint_from_flag('endpoint.aws.joined') + aws.request_instance_tags({ + 'tag1': 'value1', + 'tag2': None, + }) + aws.request_load_balancer_management() + # ... + +@when('endpoint.aws.ready') +def aws_integration_ready(): + update_config_enable_aws() +``` + +

instance_id

+ + +This unit's instance-id. + +

region

+ + +The region this unit is in. + +

tag_instance

+ +```python +AWSIntegrationRequires.tag_instance(self, tags) +``` + +Request that the given tags be applied to this instance. + +__Parameters__ + +- __`tags` (dict)__: Mapping of tag names to values (or `None`). + +

tag_instance_security_group

+ +```python +AWSIntegrationRequires.tag_instance_security_group(self, tags) +``` + +Request that the given tags be applied to this instance's +machine-specific security group (firewall) created by Juju. + +__Parameters__ + +- __`tags` (dict)__: Mapping of tag names to values (or `None`). + +

tag_instance_subnet

+ +```python +AWSIntegrationRequires.tag_instance_subnet(self, tags) +``` + +Request that the given tags be applied to this instance's subnet. + +__Parameters__ + +- __`tags` (dict)__: Mapping of tag names to values (or `None`). + +

enable_acm_readonly

+ +```python +AWSIntegrationRequires.enable_acm_readonly(self) +``` + +Request readonly for ACM. + +

enable_acm_fullaccess

+ +```python +AWSIntegrationRequires.enable_acm_fullaccess(self) +``` + +Request fullaccess for ACM. + +

enable_instance_inspection

+ +```python +AWSIntegrationRequires.enable_instance_inspection(self) +``` + +Request the ability to inspect instances. + +

enable_network_management

+ +```python +AWSIntegrationRequires.enable_network_management(self) +``` + +Request the ability to manage networking (firewalls, subnets, etc). + +

enable_load_balancer_management

+ +```python +AWSIntegrationRequires.enable_load_balancer_management(self) +``` + +Request the ability to manage load balancers. + +

enable_block_storage_management

+ +```python +AWSIntegrationRequires.enable_block_storage_management(self) +``` + +Request the ability to manage block storage. + +

enable_dns_management

+ +```python +AWSIntegrationRequires.enable_dns_management(self) +``` + +Request the ability to manage DNS. + +

enable_object_storage_access

+ +```python +AWSIntegrationRequires.enable_object_storage_access(self, patterns=None) +``` + +Request the ability to access object storage. + +__Parameters__ + +- __`patterns` (list)__: If given, restrict access to the resources matching + the patterns. If patterns do not start with the S3 ARN prefix +- __(`arn__:aws:s3:::`), it will be prepended. + +

enable_object_storage_management

+ +```python +AWSIntegrationRequires.enable_object_storage_management(self, patterns=None) +``` + +Request the ability to manage object storage. + +__Parameters__ + +- __`patterns` (list)__: If given, restrict management to the resources + matching the patterns. If patterns do not start with the S3 ARN +- __prefix (`arn__:aws:s3:::`), it will be prepended. + diff --git a/kubernetes-master/hooks/relations/aws-integration/interface.yaml b/kubernetes-master/hooks/relations/aws-integration/interface.yaml new file mode 100644 index 0000000..fe3da6d --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/interface.yaml @@ -0,0 +1,4 @@ +name: aws-integration +summary: Interface for connecting to the AWS integrator charm. +version: 1 +maintainer: Cory Johns diff --git a/kubernetes-master/hooks/relations/aws-integration/make_docs b/kubernetes-master/hooks/relations/aws-integration/make_docs new file mode 100644 index 0000000..72b69c2 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'aws': {'interface': 'aws-integration'}}, + 'provides': {'aws': {'interface': 'aws-integration'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-master/hooks/relations/aws-integration/provides.py b/kubernetes-master/hooks/relations/aws-integration/provides.py new file mode 100644 index 0000000..ae94211 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/provides.py @@ -0,0 +1,288 @@ +""" +This is the provides side of the interface layer, for use only by the AWS +integrator charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for AWS integration features. + The AWS integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. +""" + +import json +from hashlib import sha256 + +from charmhelpers.core import unitdata + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class AWSIntegrationProvides(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + + @when('endpoint.aws.requested') + def handle_requests(): + aws = endpoint_from_flag('endpoint.aws.requested') + for request in aws.requests: + if request.instance_tags: + tag_instance( + request.instance_id, + request.region, + request.instance_tags) + if request.requested_load_balancer_management: + layer.aws.enable_load_balancer_management( + request.application_name, + request.instance_id, + request.region, + ) + # ... + request.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + requests = self.requests + toggle_flag(self.expand_name('requested'), len(requests) > 0) + clear_flag(self.expand_name('changed')) + + @when('endpoint.{endpoint_name}.departed') + def cleanup(self): + for unit in self.all_departed_units: + request = IntegrationRequest(unit) + request.clear() + self.all_departed_units.clear() + clear_flag(self.expand_name('departed')) + + @property + def requests(self): + """ + A list of the new or updated #IntegrationRequests that + have been made. + """ + return [request for request in self.all_requests if request.changed] + + @property + def all_requests(self): + """ + A list of all the #IntegrationRequests that have been made, + even if unchanged. + """ + return [IntegrationRequest(unit) for unit in self.all_joined_units] + + @property + def application_names(self): + """ + Set of names of all applications that are still joined. + """ + return {unit.application_name for unit in self.all_joined_units} + + @property + def unit_instances(self): + """ + Mapping of unit names to instance IDs and regions for all joined units. + """ + return { + unit.unit_name: { + 'instance-id': unit.received['instance-id'], + 'region': unit.received['region'], + } for unit in self.all_joined_units + } + + +class IntegrationRequest: + """ + A request for integration from a single remote unit. + """ + def __init__(self, unit): + self._unit = unit + self._hash = sha256(json.dumps(dict(unit.received), + sort_keys=True).encode('utf8') + ).hexdigest() + + @property + def hash(self): + """ + SHA hash of the data for this request. + """ + return self._hash + + @property + def _hash_key(self): + endpoint = self._unit.relation.endpoint + return endpoint.expand_name('request.{}'.format(self.instance_id)) + + @property + def changed(self): + """ + Whether this request has changed since the last time it was + marked completed. + """ + if not (self.instance_id and self._requested): + return False + saved_hash = unitdata.kv().get(self._hash_key) + result = saved_hash != self.hash + return result + + def mark_completed(self): + """ + Mark this request as having been completed. + """ + completed = self._unit.relation.to_publish.get('completed', {}) + completed[self.instance_id] = self.hash + unitdata.kv().set(self._hash_key, self.hash) + self._unit.relation.to_publish['completed'] = completed + + def clear(self): + """ + Clear this request's cached data. + """ + unitdata.kv().unset(self._hash_key) + + @property + def unit_name(self): + """ + The name of the unit making the request. + """ + return self._unit.unit_name + + @property + def application_name(self): + """ + The name of the application making the request. + """ + return self._unit.application_name + + @property + def _requested(self): + return self._unit.received['requested'] + + @property + def instance_id(self): + """ + The instance ID reported for this request. + """ + return self._unit.received['instance-id'] + + @property + def region(self): + """ + The region reported for this request. + """ + return self._unit.received['region'] + + @property + def instance_tags(self): + """ + Mapping of tag names to values (or `None`) to apply to this instance. + """ + # uses dict() here to make a copy, just to be safe + return dict(self._unit.received.get('instance-tags', {})) + + @property + def instance_security_group_tags(self): + """ + Mapping of tag names to values (or `None`) to apply to this instance's + machine-specific security group (firewall). + """ + # uses dict() here to make a copy, just to be safe + return dict(self._unit.received.get('instance-security-group-tags', + {})) + + @property + def instance_subnet_tags(self): + """ + Mapping of tag names to values (or `None`) to apply to this instance's + subnet. + """ + # uses dict() here to make a copy, just to be safe + return dict(self._unit.received.get('instance-subnet-tags', {})) + + @property + def requested_instance_inspection(self): + """ + Flag indicating whether the ability to inspect instances was requested. + """ + return bool(self._unit.received['enable-instance-inspection']) + + @property + def requested_acm_readonly(self): + """ + Flag indicating whether acm readonly was requested. + """ + return bool(self._unit.received['enable-acm-readonly']) + + @property + def requested_acm_fullaccess(self): + """ + Flag indicating whether acm fullaccess was requested. + """ + return bool(self._unit.received['enable-acm-fullaccess']) + + @property + def requested_network_management(self): + """ + Flag indicating whether the ability to manage networking (firewalls, + subnets, etc) was requested. + """ + return bool(self._unit.received['enable-network-management']) + + @property + def requested_load_balancer_management(self): + """ + Flag indicating whether load balancer management was requested. + """ + return bool(self._unit.received['enable-load-balancer-management']) + + @property + def requested_block_storage_management(self): + """ + Flag indicating whether block storage management was requested. + """ + return bool(self._unit.received['enable-block-storage-management']) + + @property + def requested_dns_management(self): + """ + Flag indicating whether DNS management was requested. + """ + return bool(self._unit.received['enable-dns-management']) + + @property + def requested_object_storage_access(self): + """ + Flag indicating whether object storage access was requested. + """ + return bool(self._unit.received['enable-object-storage-access']) + + @property + def object_storage_access_patterns(self): + """ + List of patterns to which to restrict object storage access. + """ + return list( + self._unit.received['object-storage-access-patterns'] or []) + + @property + def requested_object_storage_management(self): + """ + Flag indicating whether object storage management was requested. + """ + return bool(self._unit.received['enable-object-storage-management']) + + @property + def object_storage_management_patterns(self): + """ + List of patterns to which to restrict object storage management. + """ + return list( + self._unit.received['object-storage-management-patterns'] or []) diff --git a/kubernetes-master/hooks/relations/aws-integration/pydocmd.yml b/kubernetes-master/hooks/relations/aws-integration/pydocmd.yml new file mode 100644 index 0000000..70a2e75 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'AWS Integration Interface' + +generate: + - requires.md: + - requires + - requires.AWSIntegrationRequires+ + - provides.md: + - provides + - provides.AWSIntegrationProvides+ + - provides.IntegrationRequest+ + +pages: + - Requires: requires.md + - Provides: provides.md + +gens_dir: docs diff --git a/kubernetes-master/hooks/relations/aws-integration/requires.py b/kubernetes-master/hooks/relations/aws-integration/requires.py new file mode 100644 index 0000000..c457e02 --- /dev/null +++ b/kubernetes-master/hooks/relations/aws-integration/requires.py @@ -0,0 +1,262 @@ +""" +This is the requires side of the interface layer, for use in charms that +wish to request integration with AWS native features. The integration will +be provided by the AWS integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of AWS specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific AWS features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the AWS instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. +""" + + +import json +import string +from hashlib import sha256 +from urllib.parse import urljoin +from urllib.request import urlopen + +from charmhelpers.core import unitdata + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import clear_flag, toggle_flag + + +# block size to read data from AWS metadata service +# (realistically, just needs to be bigger than ~20 chars) +READ_BLOCK_SIZE = 2048 + + +class AWSIntegrationRequires(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + + @when('endpoint.aws.joined') + def request_aws_integration(): + aws = endpoint_from_flag('endpoint.aws.joined') + aws.request_instance_tags({ + 'tag1': 'value1', + 'tag2': None, + }) + aws.request_load_balancer_management() + # ... + + @when('endpoint.aws.ready') + def aws_integration_ready(): + update_config_enable_aws() + ``` + """ + # the IP is the AWS metadata service, documented here: + # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html + _metadata_url = 'http://169.254.169.254/latest/meta-data/' + _instance_id_url = urljoin(_metadata_url, 'instance-id') + _az_url = urljoin(_metadata_url, 'placement/availability-zone') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._instance_id = None + self._region = None + + @property + def _received(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single AWS integration application with a + single unit. + """ + return self.relations[0].joined_units.received + + @property + def _to_publish(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single AWS integration application with a + single unit. + """ + return self.relations[0].to_publish + + @when('endpoint.{endpoint_name}.joined') + def send_instance_info(self): + self._to_publish['instance-id'] = self.instance_id + self._to_publish['region'] = self.region + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + completed = self._received.get('completed', {}) + actual_hash = completed.get(self.instance_id) + # My middle name is ready. No, that doesn't sound right. + # I eat ready for breakfast. + toggle_flag(self.expand_name('ready'), + self._requested and actual_hash == self._expected_hash) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + @property + def instance_id(self): + """ + This unit's instance-id. + """ + if self._instance_id is None: + cache_key = self.expand_name('instance-id') + cached = unitdata.kv().get(cache_key) + if cached: + self._instance_id = cached + else: + with urlopen(self._instance_id_url) as fd: + self._instance_id = fd.read(READ_BLOCK_SIZE).decode('utf8') + unitdata.kv().set(cache_key, self._instance_id) + return self._instance_id + + @property + def region(self): + """ + The region this unit is in. + """ + if self._region is None: + cache_key = self.expand_name('region') + cached = unitdata.kv().get(cache_key) + if cached: + self._region = cached + else: + with urlopen(self._az_url) as fd: + az = fd.read(READ_BLOCK_SIZE).decode('utf8') + self._region = az.rstrip(string.ascii_lowercase) + unitdata.kv().set(cache_key, self._region) + return self._region + + @property + def _expected_hash(self): + return sha256(json.dumps(dict(self._to_publish), + sort_keys=True).encode('utf8')).hexdigest() + + @property + def _requested(self): + # whether or not a request has been issued + return self._to_publish['requested'] + + def _request(self, keyvals): + self._to_publish.update(keyvals) + self._to_publish['requested'] = True + clear_flag(self.expand_name('ready')) + + def tag_instance(self, tags): + """ + Request that the given tags be applied to this instance. + + # Parameters + `tags` (dict): Mapping of tag names to values (or `None`). + """ + self._request({'instance-tags': dict(tags)}) + + def tag_instance_security_group(self, tags): + """ + Request that the given tags be applied to this instance's + machine-specific security group (firewall) created by Juju. + + # Parameters + `tags` (dict): Mapping of tag names to values (or `None`). + """ + self._request({'instance-security-group-tags': dict(tags)}) + + def tag_instance_subnet(self, tags): + """ + Request that the given tags be applied to this instance's subnet. + + # Parameters + `tags` (dict): Mapping of tag names to values (or `None`). + """ + self._request({'instance-subnet-tags': dict(tags)}) + + def enable_acm_readonly(self): + """ + Request readonly for ACM. + """ + self._request({'enable-acm-readonly': True}) + + def enable_acm_fullaccess(self): + """ + Request fullaccess for ACM. + """ + self._request({'enable-acm-fullaccess': True}) + + def enable_instance_inspection(self): + """ + Request the ability to inspect instances. + """ + self._request({'enable-instance-inspection': True}) + + def enable_network_management(self): + """ + Request the ability to manage networking (firewalls, subnets, etc). + """ + self._request({'enable-network-management': True}) + + def enable_load_balancer_management(self): + """ + Request the ability to manage load balancers. + """ + self._request({'enable-load-balancer-management': True}) + + def enable_block_storage_management(self): + """ + Request the ability to manage block storage. + """ + self._request({'enable-block-storage-management': True}) + + def enable_dns_management(self): + """ + Request the ability to manage DNS. + """ + self._request({'enable-dns-management': True}) + + def enable_object_storage_access(self, patterns=None): + """ + Request the ability to access object storage. + + # Parameters + `patterns` (list): If given, restrict access to the resources matching + the patterns. If patterns do not start with the S3 ARN prefix + (`arn:aws:s3:::`), it will be prepended. + """ + if patterns: + for i, pattern in enumerate(patterns): + if not pattern.startswith('arn:aws:s3:::'): + patterns[i] = 'arn:aws:s3:::{}'.format(pattern) + self._request({ + 'enable-object-storage-access': True, + 'object-storage-access-patterns': patterns, + }) + + def enable_object_storage_management(self, patterns=None): + """ + Request the ability to manage object storage. + + # Parameters + `patterns` (list): If given, restrict management to the resources + matching the patterns. If patterns do not start with the S3 ARN + prefix (`arn:aws:s3:::`), it will be prepended. + """ + if patterns: + for i, pattern in enumerate(patterns): + if not pattern.startswith('arn:aws:s3:::'): + patterns[i] = 'arn:aws:s3:::{}'.format(pattern) + self._request({ + 'enable-object-storage-management': True, + 'object-storage-management-patterns': patterns, + }) diff --git a/kubernetes-master/hooks/relations/azure-integration/.gitignore b/kubernetes-master/hooks/relations/azure-integration/.gitignore new file mode 100644 index 0000000..5f9f2c5 --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/.gitignore @@ -0,0 +1,3 @@ +.tox +__pycache__ +*.pyc diff --git a/kubernetes-master/hooks/relations/azure-integration/LICENSE b/kubernetes-master/hooks/relations/azure-integration/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-master/hooks/relations/azure-integration/README.md b/kubernetes-master/hooks/relations/azure-integration/README.md new file mode 100644 index 0000000..ddcae26 --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/README.md @@ -0,0 +1,28 @@ +# Overview + +This layer encapsulates the `azure-integration` interface communciation +protocol and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:azure-integration` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:azure-integration'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `azure-integration` interface protocol: + +```yaml +requires: + azure: + interface: azure-integration +``` + +For documentation on how to use the API for this interface, see: + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) (this will only be used by the azure-integrator charm) diff --git a/kubernetes-master/hooks/relations/azure-integration/__init__.py b/kubernetes-master/hooks/relations/azure-integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/azure-integration/copyright b/kubernetes-master/hooks/relations/azure-integration/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/hooks/relations/azure-integration/docs/provides.md b/kubernetes-master/hooks/relations/azure-integration/docs/provides.md new file mode 100644 index 0000000..4348dff --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/docs/provides.md @@ -0,0 +1,175 @@ +

provides

+ + +This is the provides side of the interface layer, for use only by the Azure +integrator charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for Azure integration features. + The Azure integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. + +

AzureIntegrationProvides

+ +```python +AzureIntegrationProvides(self, endpoint_name, relation_ids=None) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag +from charms import layer + +@when('endpoint.azure.requests-pending') +def handle_requests(): + azure = endpoint_from_flag('endpoint.azure.requests-pending') + for request in azure.requests: + if request.instance_tags: + layer.azure.tag_instance( + request.vm_name, + request.resource_group, + request.instance_tags) + if request.requested_load_balancer_management: + layer.azure.enable_load_balancer_management( + request.charm, + request.vm_name, + request.resource_group, + ) + # ... + azure.mark_completed() +``` + +

relation_ids

+ + +A list of the IDs of all established relations. + +

requests

+ + +A list of the new or updated `IntegrationRequests` that +have been made. + +

get_departed_charms

+ +```python +AzureIntegrationProvides.get_departed_charms(self) +``` + +Get a list of all charms that have had all units depart since the +last time this was called. + +

mark_completed

+ +```python +AzureIntegrationProvides.mark_completed(self) +``` + +Mark all requests as completed and remove the `requests-pending` flag. + +

IntegrationRequest

+ +```python +IntegrationRequest(self, unit) +``` + +A request for integration from a single remote unit. + +

application_name

+ + +The name of the application making the request. + +

charm

+ + +The charm name reported for this request. + +

instance_tags

+ + +Mapping of tag names to values to apply to this instance. + +

is_changed

+ + +Whether this request has changed since the last time it was +marked completed (if ever). + +

model_uuid

+ + +The UUID of the model containing the application making this request. + +

relation_id

+ + +The ID of the relation for the unit making the request. + +

requested_block_storage_management

+ + +Flag indicating whether block storage management was requested. + +

requested_dns_management

+ + +Flag indicating whether DNS management was requested. + +

requested_instance_inspection

+ + +Flag indicating whether the ability to inspect instances was requested. + +

requested_network_management

+ + +Flag indicating whether the ability to manage networking was requested. + +

requested_object_storage_access

+ + +Flag indicating whether object storage access was requested. + +

requested_object_storage_management

+ + +Flag indicating whether object storage management was requested. + +

requested_security_management

+ + +Flag indicating whether security management was requested. + +

resource_group

+ + +The resource group reported for this request. + +

unit_name

+ + +The name of the unit making the request. + +

vm_id

+ + +The instance ID reported for this request. + +

vm_name

+ + +The instance name reported for this request. + +

mark_completed

+ +```python +IntegrationRequest.mark_completed(self) +``` + +Mark this request as having been completed. + diff --git a/kubernetes-master/hooks/relations/azure-integration/docs/requires.md b/kubernetes-master/hooks/relations/azure-integration/docs/requires.md new file mode 100644 index 0000000..608b4ee --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/docs/requires.md @@ -0,0 +1,145 @@ +

requires

+ + +This is the requires side of the interface layer, for use in charms that +wish to request integration with Azure native features. The integration will +be provided by the Azure integrator charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of Azure specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific Azure features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the Azure instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. + +

AzureIntegrationRequires

+ +```python +AzureIntegrationRequires(self, *args, **kwargs) +``` + +Interface to request integration access. + +Note that due to resource limits and permissions granularity, policies are +limited to being applied at the charm level. That means that, if any +permissions are requested (i.e., any of the enable methods are called), +what is granted will be the sum of those ever requested by any instance of +the charm on this cloud. + +Labels, on the other hand, will be instance specific. + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag + +@when('endpoint.azure.joined') +def request_azure_integration(): + azure = endpoint_from_flag('endpoint.azure.joined') + azure.tag_instance({ + 'tag1': 'value1', + 'tag2': None, + }) + azure.request_load_balancer_management() + # ... + +@when('endpoint.azure.ready') +def azure_integration_ready(): + update_config_enable_azure() +``` + +

is_ready

+ + +Whether or not the request for this instance has been completed. + +

resource_group

+ + +The resource group this unit is in. + +

vm_id

+ + +This unit's instance ID. + +

vm_name

+ + +This unit's instance name. + +

tag_instance

+ +```python +AzureIntegrationRequires.tag_instance(self, tags) +``` + +Request that the given tags be applied to this instance. + +__Parameters__ + +- __`tags` (dict)__: Mapping of tags names to values. + +

enable_instance_inspection

+ +```python +AzureIntegrationRequires.enable_instance_inspection(self) +``` + +Request the ability to inspect instances. + +

enable_network_management

+ +```python +AzureIntegrationRequires.enable_network_management(self) +``` + +Request the ability to manage networking. + +

enable_security_management

+ +```python +AzureIntegrationRequires.enable_security_management(self) +``` + +Request the ability to manage security (e.g., firewalls). + +

enable_block_storage_management

+ +```python +AzureIntegrationRequires.enable_block_storage_management(self) +``` + +Request the ability to manage block storage. + +

enable_dns_management

+ +```python +AzureIntegrationRequires.enable_dns_management(self) +``` + +Request the ability to manage DNS. + +

enable_object_storage_access

+ +```python +AzureIntegrationRequires.enable_object_storage_access(self) +``` + +Request the ability to access object storage. + +

enable_object_storage_management

+ +```python +AzureIntegrationRequires.enable_object_storage_management(self) +``` + +Request the ability to manage object storage. + diff --git a/kubernetes-master/hooks/relations/azure-integration/interface.yaml b/kubernetes-master/hooks/relations/azure-integration/interface.yaml new file mode 100644 index 0000000..a77a7cb --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/interface.yaml @@ -0,0 +1,4 @@ +name: azure-integration +summary: Interface for connecting to the Azure integrator charm. +version: 1 +maintainer: Cory Johns diff --git a/kubernetes-master/hooks/relations/azure-integration/make_docs b/kubernetes-master/hooks/relations/azure-integration/make_docs new file mode 100644 index 0000000..84df5ee --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'azure': {'interface': 'azure-integration'}}, + 'provides': {'azure': {'interface': 'azure-integration'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-master/hooks/relations/azure-integration/provides.py b/kubernetes-master/hooks/relations/azure-integration/provides.py new file mode 100644 index 0000000..5ff7d3a --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/provides.py @@ -0,0 +1,275 @@ +""" +This is the provides side of the interface layer, for use only by the Azure +integrator charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for Azure integration features. + The Azure integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. +""" + +from operator import attrgetter + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class AzureIntegrationProvides(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + + @when('endpoint.azure.requests-pending') + def handle_requests(): + azure = endpoint_from_flag('endpoint.azure.requests-pending') + for request in azure.requests: + if request.instance_tags: + layer.azure.tag_instance( + request.vm_name, + request.resource_group, + request.instance_tags) + if request.requested_load_balancer_management: + layer.azure.enable_load_balancer_management( + request.charm, + request.vm_name, + request.resource_group, + ) + # ... + azure.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + toggle_flag(self.expand_name('requests-pending'), + len(self.requests) > 0) + clear_flag(self.expand_name('changed')) + + @property + def requests(self): + """ + A list of the new or updated #IntegrationRequests that + have been made. + """ + if not hasattr(self, '_requests'): + all_requests = [IntegrationRequest(unit) + for unit in self.all_joined_units] + is_changed = attrgetter('is_changed') + self._requests = list(filter(is_changed, all_requests)) + return self._requests + + @property + def relation_ids(self): + """ + A list of the IDs of all established relations. + """ + return [relation.relation_id for relation in self.relations] + + def get_departed_charms(self): + """ + Get a list of all charms that have had all units depart since the + last time this was called. + """ + joined_charms = {unit.received['charm'] + for unit in self.all_joined_units + if unit.received['charm']} + departed_charms = [unit.received['charm'] + for unit in self.all_departed_units + if unit.received['charm'] not in joined_charms] + self.all_departed_units.clear() + return departed_charms + + def mark_completed(self): + """ + Mark all requests as completed and remove the `requests-pending` flag. + """ + for request in self.requests: + request.mark_completed() + clear_flag(self.expand_name('requests-pending')) + self._requests = [] + + +class IntegrationRequest: + """ + A request for integration from a single remote unit. + """ + def __init__(self, unit): + self._unit = unit + + @property + def _to_publish(self): + return self._unit.relation.to_publish + + @property + def _completed(self): + return self._to_publish.get('completed', {}) + + @property + def _requested(self): + return self._unit.received['requested'] + + @property + def is_changed(self): + """ + Whether this request has changed since the last time it was + marked completed (if ever). + """ + if not all([self.charm, self.vm_id, self.vm_name, + self.resource_group, self._requested]): + return False + return self._completed.get(self.vm_id) != self._requested + + def mark_completed(self): + """ + Mark this request as having been completed. + """ + completed = self._completed + completed[self.vm_id] = self._requested + self._to_publish['completed'] = completed # have to explicitly update + + def send_additional_metadata(self, resource_group_location, + vnet_name, vnet_resource_group, + subnet_name, security_group_name, + security_group_resource_group, + use_managed_identity=True, aad_client=None, + aad_secret=None, tenant_id=None): + self._to_publish.update({ + 'resource-group-location': resource_group_location, + 'vnet-name': vnet_name, + 'vnet-resource-group': vnet_resource_group, + 'subnet-name': subnet_name, + 'security-group-name': security_group_name, + 'security-group-resource-group': security_group_resource_group, + 'use-managed-identity': use_managed_identity, + 'aad-client': aad_client, + 'aad-client-secret': aad_secret, + 'tenant-id': tenant_id + }) + + @property + def relation_id(self): + """ + The ID of the relation for the unit making the request. + """ + return self._unit.relation.relation_id + + @property + def unit_name(self): + """ + The name of the unit making the request. + """ + return self._unit.unit_name + + @property + def application_name(self): + """ + The name of the application making the request. + """ + return self._unit.application_name + + @property + def charm(self): + """ + The charm name reported for this request. + """ + return self._unit.received['charm'] + + @property + def vm_id(self): + """ + The instance ID reported for this request. + """ + return self._unit.received['vm-id'] + + @property + def vm_name(self): + """ + The instance name reported for this request. + """ + return self._unit.received['vm-name'] + + @property + def resource_group(self): + """ + The resource group reported for this request. + """ + return self._unit.received['res-group'] + + @property + def model_uuid(self): + """ + The UUID of the model containing the application making this request. + """ + return self._unit.received['model-uuid'] + + @property + def instance_tags(self): + """ + Mapping of tag names to values to apply to this instance. + """ + # uses dict() here to make a copy, just to be safe + return dict(self._unit.received.get('instance-tags', {})) + + @property + def requested_instance_inspection(self): + """ + Flag indicating whether the ability to inspect instances was requested. + """ + return bool(self._unit.received['enable-instance-inspection']) + + @property + def requested_network_management(self): + """ + Flag indicating whether the ability to manage networking was requested. + """ + return bool(self._unit.received['enable-network-management']) + + @property + def requested_loadbalancer_management(self): + """ + Flag indicating whether the ability to manage networking was requested. + """ + return bool(self._unit.received['enable-loadbalancer-management']) + + + @property + def requested_security_management(self): + """ + Flag indicating whether security management was requested. + """ + return bool(self._unit.received['enable-security-management']) + + @property + def requested_block_storage_management(self): + """ + Flag indicating whether block storage management was requested. + """ + return bool(self._unit.received['enable-block-storage-management']) + + @property + def requested_dns_management(self): + """ + Flag indicating whether DNS management was requested. + """ + return bool(self._unit.received['enable-dns-management']) + + @property + def requested_object_storage_access(self): + """ + Flag indicating whether object storage access was requested. + """ + return bool(self._unit.received['enable-object-storage-access']) + + @property + def requested_object_storage_management(self): + """ + Flag indicating whether object storage management was requested. + """ + return bool(self._unit.received['enable-object-storage-management']) diff --git a/kubernetes-master/hooks/relations/azure-integration/pydocmd.yml b/kubernetes-master/hooks/relations/azure-integration/pydocmd.yml new file mode 100644 index 0000000..6414c29 --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'Azure Integration Interface' + +generate: + - requires.md: + - requires + - requires.AzureIntegrationRequires+ + - provides.md: + - provides + - provides.AzureIntegrationProvides+ + - provides.IntegrationRequest+ + +pages: + - Requires: requires.md + - Provides: provides.md + +gens_dir: docs diff --git a/kubernetes-master/hooks/relations/azure-integration/requires.py b/kubernetes-master/hooks/relations/azure-integration/requires.py new file mode 100644 index 0000000..600d69e --- /dev/null +++ b/kubernetes-master/hooks/relations/azure-integration/requires.py @@ -0,0 +1,298 @@ +""" +This is the requires side of the interface layer, for use in charms that +wish to request integration with Azure native features. The integration will +be provided by the Azure integrator charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of Azure specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific Azure features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the Azure instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. +""" + + +import json +import os +import random +import string +from urllib.request import urlopen, Request + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import clear_flag, toggle_flag + + +# block size to read data from Azure metadata service +# (realistically, just needs to be bigger than ~20 chars) +READ_BLOCK_SIZE = 2048 + + +class AzureIntegrationRequires(Endpoint): + """ + Interface to request integration access. + + Note that due to resource limits and permissions granularity, policies are + limited to being applied at the charm level. That means that, if any + permissions are requested (i.e., any of the enable methods are called), + what is granted will be the sum of those ever requested by any instance of + the charm on this cloud. + + Labels, on the other hand, will be instance specific. + + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + + @when('endpoint.azure.joined') + def request_azure_integration(): + azure = endpoint_from_flag('endpoint.azure.joined') + azure.tag_instance({ + 'tag1': 'value1', + 'tag2': None, + }) + azure.request_load_balancer_management() + # ... + + @when('endpoint.azure.ready') + def azure_integration_ready(): + update_config_enable_azure() + ``` + """ + # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service + _metadata_url = 'http://169.254.169.254/metadata/instance?api-version=2017-12-01' # noqa + _metadata_headers = {'Metadata': 'true'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._vm_metadata = None + + @property + def _received(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single Azure integration application with a + single unit. + """ + return self.relations[0].joined_units.received + + @property + def _to_publish(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single Azure integration application with a + single unit. + """ + return self.relations[0].to_publish + + @when('endpoint.{endpoint_name}.joined') + def send_instance_info(self): + self._to_publish['charm'] = hookenv.charm_name() + self._to_publish['vm-id'] = self.vm_id + self._to_publish['vm-name'] = self.vm_name + self._to_publish['res-group'] = self.resource_group + self._to_publish['model-uuid'] = os.environ['JUJU_MODEL_UUID'] + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + # My middle name is ready. No, that doesn't sound right. + # I eat ready for breakfast. + toggle_flag(self.expand_name('ready'), self.is_ready) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + @property + def vm_metadata(self): + if self._vm_metadata is None: + cache_key = self.expand_name('vm-metadata') + cached = unitdata.kv().get(cache_key) + if cached: + self._vm_metadata = cached + else: + req = Request(self._metadata_url, + headers=self._metadata_headers) + with urlopen(req) as fd: + metadata = fd.read(READ_BLOCK_SIZE).decode('utf8').strip() + self._vm_metadata = json.loads(metadata) + unitdata.kv().set(cache_key, self._vm_metadata) + return self._vm_metadata + + @property + def vm_id(self): + """ + This unit's instance ID. + """ + return self.vm_metadata['compute']['vmId'] + + @property + def vm_name(self): + """ + This unit's instance name. + """ + return self.vm_metadata['compute']['name'] + + @property + def vm_location(self): + """ + The location (region) the instance is running in. + """ + return self.vm_metadata['compute']['location'] + + @property + def resource_group(self): + """ + The resource group this unit is in. + """ + return self.vm_metadata['compute']['resourceGroupName'] + + @property + def resource_group_location(self): + """ + The location (region) the resource group is in. + """ + return self._received['resource-group-location'] + + @property + def subscription_id(self): + """ + The ID of the Azure Subscription this unit is in. + """ + return self.vm_metadata['compute']['subscriptionId'] + + @property + def vnet_name(self): + """ + The name of the virtual network the instance is in. + """ + return self._received['vnet-name'] + + @property + def vnet_resource_group(self): + """ + The name of the virtual network the instance is in. + """ + return self._received['vnet-resource-group'] + + @property + def subnet_name(self): + """ + The name of the subnet the instance is in. + """ + return self._received['subnet-name'] + + @property + def security_group_name(self): + """ + The name of the security group attached to the cluster's subnet. + """ + return self._received['security-group-name'] + + @property + def is_ready(self): + """ + Whether or not the request for this instance has been completed. + """ + requested = self._to_publish['requested'] + completed = self._received.get('completed', {}).get(self.vm_id) + return requested and requested == completed + + @property + def security_group_resource_group(self): + return self._received['security-group-resource-group'] + + @property + def managed_identity(self): + return self._received['use-managed-identity'] + + @property + def aad_client_id(self): + return self._received['aad-client'] + + @property + def aad_client_secret(self): + return self._received['aad-client-secret'] + + @property + def tenant_id(self): + return self._received['tenant-id'] + + def _request(self, keyvals): + alphabet = string.ascii_letters + string.digits + nonce = ''.join(random.choice(alphabet) for _ in range(8)) + self._to_publish.update(keyvals) + self._to_publish['requested'] = nonce + clear_flag(self.expand_name('ready')) + + def tag_instance(self, tags): + """ + Request that the given tags be applied to this instance. + + # Parameters + `tags` (dict): Mapping of tags names to values. + """ + self._request({'instance-tags': dict(tags)}) + + def enable_instance_inspection(self): + """ + Request the ability to inspect instances. + """ + self._request({'enable-instance-inspection': True}) + + def enable_network_management(self): + """ + Request the ability to manage networking. + """ + self._request({'enable-network-management': True}) + + def enable_loadbalancer_management(self): + """ + Request the ability to manage networking. + """ + self._request({'enable-loadbalancer-management': True}) + + + def enable_security_management(self): + """ + Request the ability to manage security (e.g., firewalls). + """ + self._request({'enable-security-management': True}) + + def enable_block_storage_management(self): + """ + Request the ability to manage block storage. + """ + self._request({'enable-block-storage-management': True}) + + def enable_dns_management(self): + """ + Request the ability to manage DNS. + """ + self._request({'enable-dns': True}) + + def enable_object_storage_access(self): + """ + Request the ability to access object storage. + """ + self._request({'enable-object-storage-access': True}) + + def enable_object_storage_management(self): + """ + Request the ability to manage object storage. + """ + self._request({'enable-object-storage-management': True}) + + diff --git a/kubernetes-master/hooks/relations/ceph-admin/.gitignore b/kubernetes-master/hooks/relations/ceph-admin/.gitignore new file mode 100644 index 0000000..ca3c9ea --- /dev/null +++ b/kubernetes-master/hooks/relations/ceph-admin/.gitignore @@ -0,0 +1,2 @@ +.idea +*.swp diff --git a/kubernetes-master/hooks/relations/ceph-admin/README.md b/kubernetes-master/hooks/relations/ceph-admin/README.md new file mode 100644 index 0000000..b0c89db --- /dev/null +++ b/kubernetes-master/hooks/relations/ceph-admin/README.md @@ -0,0 +1,41 @@ +# Overview + +**WARNING**: This is an unofficial, untested, and experimental layer from +the community. + +This interface layer handles the communication between the Ceph Monitor +and a client that requires an admin key. + +# Usage + +## Requires + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.available` The ceph client has been related to a provider. + The following accessors will be available: + - key - The admin cephx key + - auth - Whether or not strict auth is supported + - mon_hosts - The public addresses list of the monitor cluster + + +Client example: + +```python +@when('ceph-admin.available') +def ceph_connected(ceph_info): + charm_ceph_conf = os.path.join(os.sep, 'etc', 'ceph', 'ceph.conf') + cephx_key = os.path.join(os.sep, 'etc', 'ceph', 'ceph.client.admin.keyring') + + ceph_context = { + 'auth_supported': ceph_client.auth, + 'mon_hosts': ceph_client.mon_hosts, + } + + with open(charm_ceph_conf, 'w') as cephconf: + cephconf.write(render_template('ceph.conf', ceph_context)) + + # Write out the cephx_key also + with open(cephx_key, 'w') as cephconf: + cephconf.write(ceph_client.key) +``` diff --git a/kubernetes-master/hooks/relations/ceph-admin/__init__.py b/kubernetes-master/hooks/relations/ceph-admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/ceph-admin/interface.yaml b/kubernetes-master/hooks/relations/ceph-admin/interface.yaml new file mode 100644 index 0000000..45dd6f4 --- /dev/null +++ b/kubernetes-master/hooks/relations/ceph-admin/interface.yaml @@ -0,0 +1,3 @@ +name: ceph-admin +summary: Ceph Admin Client Interface +version: 1 diff --git a/kubernetes-master/hooks/relations/ceph-admin/requires.py b/kubernetes-master/hooks/relations/ceph-admin/requires.py new file mode 100644 index 0000000..ba07b51 --- /dev/null +++ b/kubernetes-master/hooks/relations/ceph-admin/requires.py @@ -0,0 +1,23 @@ +from charms.reactive import Endpoint +from charms.reactive import toggle_flag + + +class CephClient(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.available'), + all([self.key(), + self.fsid(), + self.auth(), + self.mon_hosts()])) + + def key(self): + return self.all_joined_units.received_raw['key'] + + def fsid(self): + return self.all_joined_units.received_raw['fsid'] + + def auth(self): + return self.all_joined_units.received_raw['auth'] + + def mon_hosts(self): + return self.all_joined_units.received_raw['mon_hosts'] diff --git a/kubernetes-master/hooks/relations/ceph-client/README.md b/kubernetes-master/hooks/relations/ceph-client/README.md new file mode 100644 index 0000000..18076bd --- /dev/null +++ b/kubernetes-master/hooks/relations/ceph-client/README.md @@ -0,0 +1,43 @@ +# Overview + +This interface layer handles the communication between the Ceph Monitor +cluster and a client that requires an access key and a pool to use. + +# Usage + +## Requires + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.available` The ceph client has been related to a provider. + +The following accessors will be available: + + - key - The cephx access key + - auth - Whether or not strict auth is supported + - mon_hosts - The public addresses list of the monitor cluster + +Client example: + +```python +@when('ceph-client.connected') +def ceph_connected(ceph_client): + ceph_client.create_pool('newpool') + +@when('ceph-client.available') +def ceph_ready(ceph_client): + charm_ceph_conf= os.path.join(os.sep, 'etc', 'ceph', 'ceph.conf') + cephx_key = os.path.join(os.sep, 'etc', 'ceph', 'ceph.client.charm.keyring') + + ceph_context = { + 'auth_supported': ceph_client.auth, + 'mon_hosts': ceph_client.mon_hosts, + } + + with open(charm_ceph_conf, 'w') as cephconf: + cephconf.write(render_template('ceph.conf', ceph_context)) + + # Write out the cephx_key also + with open(cephx_key, 'w') as cephconf: + cephconf.write(ceph_client.key) +``` diff --git a/kubernetes-master/hooks/relations/ceph-client/__init__.py b/kubernetes-master/hooks/relations/ceph-client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/ceph-client/interface.yaml b/kubernetes-master/hooks/relations/ceph-client/interface.yaml new file mode 100644 index 0000000..8578859 --- /dev/null +++ b/kubernetes-master/hooks/relations/ceph-client/interface.yaml @@ -0,0 +1,13 @@ +name: ceph-client +summary: Ceph Client Interface +version: 1 +maintainer: OpenStack Charmers +ignore: + - 'unit_tests' + - 'Makefile' + - '.testr.conf' + - 'test-requirements.txt' + - 'tox.ini' + - '.gitignore' + - '.gitreview' + - '.unit-state.db' \ No newline at end of file diff --git a/kubernetes-master/hooks/relations/ceph-client/lib/base_provides.py b/kubernetes-master/hooks/relations/ceph-client/lib/base_provides.py new file mode 100644 index 0000000..32ebfd8 --- /dev/null +++ b/kubernetes-master/hooks/relations/ceph-client/lib/base_provides.py @@ -0,0 +1,97 @@ +# from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import ( + relation_set, +) +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes +# from charms.reactive import is_state +# from charms.reactive import not_unless + + +class CephProvides(RelationBase): + scope = scopes.UNIT + + @hook('{provides:ceph-client}-relation-{joined,changed}') + def changed(self): + self.set_state('{relation_name}.connected') + # service = hookenv.remote_service_name() + conversation = self.conversation() + if conversation.get_remote('broker_req'): + self.set_state('{relation_name}.broker_requested') + + def provide_auth(self, service, key, auth_supported, public_address): + """ + Provide a token to a requesting service. + :param str service: The service which requested the key + :param str key: The key to access Ceph + :param str auth_supported: Supported auth methods + :param str public_address: Ceph's public address + """ + conversation = self.conversation(scope=service) + # print("Conversation is ", conversation) + # key is a keyword argument to the set_remote function so we have to + # set it separately. + relation_set( + relation_id=conversation.namespace, + relation_settings={'key': key}) + opts = { + 'auth': auth_supported, + 'ceph-public-address': public_address, + } + conversation.set_remote(**opts) + + def requested_keys(self): + """ + Return a list of tuples mapping a service name to the key name + requested by that service. + Example usage:: + for service, key in ceph.requested_keys(): + ceph.provide_auth(service, key, auth, public_address) + """ + for conversation in self.conversations(): + service = conversation.scope + key = self.requested_key(service) + if key is None: + yield service + + def requested_key(self, service): + """ + Return the key provided to the requesting service. + """ + return self.conversation(scope=service).get_remote('key') + + def provide_broker_token(self, service, unit_response_key, token): + """ + Provide a token to a requesting service. + :param str service: The service which requested the key + :param str unit_response_key: The unique key for the unit + :param str token: Broker token top provide + """ + conversation = self.conversation(scope=service) + + # broker_rsp is being left for backward compatibility, + # unit_response_key superscedes it + conversation.set_remote(**{ + 'broker_rsp': token, + unit_response_key: token, + }) + + def requested_tokens(self): + """ + Return a list of tuples mapping a service name to the token name + requested by that service. + Example usage:: + for service, token in ceph.requested_tokens(): + ceph.provide_auth(service, token, auth, public_address) + """ + for conversation in self.conversations(): + service = conversation.scope + token = self.requested_token(service) + yield service, token + + def requested_token(self, service): + """ + Return the token provided to the requesting service. + """ + return self.conversation(scope=service).get_remote('broker_req') diff --git a/kubernetes-master/hooks/relations/ceph-client/lib/base_requires.py b/kubernetes-master/hooks/relations/ceph-client/lib/base_requires.py new file mode 100644 index 0000000..6c8bb70 --- /dev/null +++ b/kubernetes-master/hooks/relations/ceph-client/lib/base_requires.py @@ -0,0 +1,331 @@ +# 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 json + +import charms.reactive as reactive + +from charmhelpers.core.hookenv import ( + application_name, + local_unit, + log, +) +from charmhelpers.contrib.network.ip import format_ipv6_addr + +from charmhelpers.contrib.storage.linux.ceph import ( + CephBrokerRq, + is_request_complete, + is_request_sent, +) + + +class CephRequires(reactive.Endpoint): + + def joined(self): + reactive.set_flag(self.expand_name('{endpoint_name}.connected')) + + @property + def key(self): + return self._key() + + def _key(self): + return self.all_joined_units.received.get('key') + + @property + def auth(self): + return self._auth() + + def _auth(self): + return self.all_joined_units.received.get('auth') + + @property + def relation_name(self): + return self.expand_name('{endpoint_name}') + + def initial_ceph_response(self): + raise NotImplementedError + + def changed(self): + data = self.initial_ceph_response() + if all(data.values()): + reactive.set_flag(self.expand_name('{endpoint_name}.available')) + + rq = self.get_current_request() + if rq: + log("changed broker_req: {}".format(rq.ops)) + + if rq and is_request_complete(rq, relation=self.relation_name): + log("Setting ceph-client.pools.available") + reactive.set_flag( + self.expand_name('{endpoint_name}.pools.available')) + else: + log("incomplete request. broker_req not found") + + def broken(self): + reactive.clear_flag( + self.expand_name('{endpoint_name}.available')) + reactive.clear_flag( + self.expand_name('{endpoint_name}.connected')) + reactive.clear_flag( + self.expand_name('{endpoint_name}.pools.available')) + + def create_replicated_pool(self, name, replicas=3, weight=None, + pg_num=None, group=None, namespace=None, + app_name=None, **kwargs): + """ + Request pool setup + + :param name: Name of pool to create + :type name: str + :param replicas: Number of replicas for supporting pools + :type replicas: int + :param weight: The percentage of data the pool makes up + :type weight: Optional[float] + :param pg_num: If not provided, this value will be calculated by the + broker based on how many OSDs are in the cluster at the + time of creation. Note that, if provided, this value + will be capped at the current available maximum. + :type pg_num: Optional[int] + :param group: Group to add pool to. + :type group: Optional[str] + :param namespace: A group can optionally have a namespace defined that + will be used to further restrict pool access. + :type namespace: Optional[str] + :param app_name: (Optional) Tag pool with application name. Note that + there is certain protocols emerging upstream with + regard to meaningful application names to use. + Examples are ``rbd`` and ``rgw``. + :type app_name: Optional[str] + :param kwargs: Additional keyword arguments subject to validation. + Refer to CephBrokerRq.add_op_create_replicated_pool + method for documentation. + :type kwargs: Dict[str,any] + """ + rq = self.get_current_request() or CephBrokerRq() + kwargs.update({ + 'name': name, + 'replica_count': replicas, + 'pg_num': pg_num, + 'weight': weight, + 'group': group, + 'namespace': namespace, + 'app_name': app_name, + }) + rq.add_op_create_replicated_pool(**kwargs) + self.send_request_if_needed(rq) + reactive.clear_flag( + self.expand_name('{endpoint_name}.pools.available')) + + def create_pool(self, name, replicas=3, weight=None, pg_num=None, + group=None, namespace=None): + """ + Request pool setup -- deprecated. Please use create_replicated_pool + or create_erasure_pool(which doesn't exist yet) + + @param name: Name of pool to create + @param replicas: Number of replicas for supporting pools + @param weight: The percentage of data the pool makes up + @param pg_num: If not provided, this value will be calculated by the + broker based on how many OSDs are in the cluster at the + time of creation. Note that, if provided, this value + will be capped at the current available maximum. + @param group: Group to add pool to. + @param namespace: A group can optionally have a namespace defined that + will be used to further restrict pool access. + """ + self.create_replicated_pool(name, replicas, weight, pg_num, group, + namespace) + + def create_erasure_pool(self, name, erasure_profile=None, + weight=None, group=None, app_name=None, + max_bytes=None, max_objects=None, + allow_ec_overwrites=False, + **kwargs): + """ + Request erasure coded pool setup + + :param name: Name of pool to create + :type name: str + :param erasure_profile: Name of erasure profile for pool + :type erasure_profile: str + :param weight: The percentage of data the pool makes up + :type weight: Optional[float] + :param group: Group to add pool to. + :type group: Optional[str] + :param app_name: Name of application using pool + :type app_name: Optional[str] + :param max_bytes: Maximum bytes of quota to apply + :type max_bytes: Optional[int] + :param max_objects: Maximum object quota to apply + :type max_objects: Optional[int] + :param allow_ec_overwrites: Allow EC pools to be overwritten + :type allow_ec_overwrites: bool + :param kwargs: Additional keyword arguments subject to validation. + Refer to CephBrokerRq.add_op_create_replicated_pool + method for documentation. + :type kwargs: Dict[str,any] + """ + rq = self.get_current_request() or CephBrokerRq() + kwargs.update({ + 'name': name, + 'erasure_profile': erasure_profile, + 'weight': weight, + 'group': group, + 'app_name': app_name, + 'max_bytes': max_bytes, + 'max_objects': max_objects, + 'allow_ec_overwrites': allow_ec_overwrites, + }) + rq.add_op_create_erasure_pool(**kwargs) + self.send_request_if_needed(rq) + reactive.clear_flag( + self.expand_name('{endpoint_name}.pools.available')) + + def create_erasure_profile(self, name, + erasure_type='jerasure', + erasure_technique=None, + k=None, m=None, + failure_domain=None, + lrc_locality=None, + shec_durability_estimator=None, + clay_helper_chunks=None, + device_class=None, + clay_scalar_mds=None, + lrc_crush_locality=None): + """ + Create erasure coding profile + + @param name: Name of erasure coding profile + @param erasure_type: Erasure coding plugin to use + @param erasure_technique: Erasure coding technique to use + @param k: Number of data chunks + @param m: Number of coding chunks + @param failure_domain: Failure domain to use for PG placement + @param lrc_locality: + Group the coding and data chunks into sets + of size locality (lrc plugin) + @param shec_durability_estimator: + The number of parity chuncks each of which includes + a data chunk in its calculation range (shec plugin) + @param clay_helper_chunks: + The number of helper chunks to use for recovery operations + (clay plugin) + @param device_class: + Device class to use for profile (ssd, hdd, nvme) + @param clay_scalar_mds: + Plugin to use for CLAY layered construction + (jerasure|isa|shec) + @param lrc_crush_locality: + Type of crush bucket in which set of chunks + defined by lrc_locality will be stored. + """ + rq = self.get_current_request() or CephBrokerRq() + rq.add_op_create_erasure_profile( + name=name, + erasure_type=erasure_type, + erasure_technique=erasure_technique, + k=k, m=m, + failure_domain=failure_domain, + lrc_locality=lrc_locality, + shec_durability_estimator=shec_durability_estimator, + clay_helper_chunks=clay_helper_chunks, + device_class=device_class, + clay_scalar_mds=clay_scalar_mds, + lrc_crush_locality=lrc_crush_locality + ) + self.send_request_if_needed(rq) + reactive.clear_flag( + self.expand_name('{endpoint_name}.pools.available')) + + def request_access_to_group(self, name, namespace=None, permission=None, + key_name=None, + object_prefix_permissions=None): + """ + Adds the requested permissions to service's Ceph key + + Adds the requested permissions to the current service's Ceph key, + allowing the key to access only the specified pools or + object prefixes. object_prefix_permissions should be a dictionary + keyed on the permission with the corresponding value being a list + of prefixes to apply that permission to. + { + 'rwx': ['prefix1', 'prefix2'], + 'class-read': ['prefix3']} + @param name: Target group name for permissions request. + @param namespace: namespace to further restrict pool access. + @param permission: Permission to be requested against pool + @param key_name: userid to grant permission to + @param object_prefix_permissions: Add object_prefix permissions. + """ + current_request = self.get_current_request() or CephBrokerRq() + current_request.add_op_request_access_to_group( + name, + namespace=namespace, + permission=permission, + key_name=key_name, + object_prefix_permissions=object_prefix_permissions) + self.send_request_if_needed(current_request) + + def send_request_if_needed(self, request): + """Send broker request if an equivalent request has not been sent + + @param request: A CephBrokerRq object + """ + if is_request_sent(request, relation=self.relation_name): + log('Request already sent but not complete, ' + 'not sending new request') + else: + for relation in self.relations: + relation.to_publish['broker_req'] = json.loads( + request.request) + relation.to_publish_raw[ + 'application-name'] = application_name() + relation.to_publish_raw['unit-name'] = local_unit() + + def get_current_request(self): + broker_reqs = [] + for relation in self.relations: + broker_req = relation.to_publish.get('broker_req', {}) + if broker_req: + rq = CephBrokerRq() + rq.set_ops(broker_req['ops']) + broker_reqs.append(rq) + # Check that if there are multiple requests then they are the same. + assert all(x == broker_reqs[0] for x in broker_reqs) + if broker_reqs: + return broker_reqs[0] + + def get_remote_all(self, key, default=None): + """Return a list of all values presented by remote units for key""" + values = [] + for relation in self.relations: + for unit in relation.units: + value = unit.received.get(key, default) + if value: + values.append(value) + return list(set(values)) + + def mon_hosts(self): + """List of all monitor host public addresses""" + hosts = [] + addrs = self.get_remote_all('ceph-public-address') + for ceph_addrs in addrs: + # NOTE(jamespage): This looks odd but deals with + # use with ceph-proxy which + # presents all monitors in + # a single space delimited field. + for addr in ceph_addrs.split(' '): + hosts.append(format_ipv6_addr(addr) or addr) + hosts.sort() + return hosts diff --git a/kubernetes-master/hooks/relations/ceph-client/provides.py b/kubernetes-master/hooks/relations/ceph-client/provides.py new file mode 100644 index 0000000..a8fbd05 --- /dev/null +++ b/kubernetes-master/hooks/relations/ceph-client/provides.py @@ -0,0 +1,20 @@ +# Copyright 2020 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .lib import base_provides + + +class CephClientProvider(base_provides.CephProvides): + + pass diff --git a/kubernetes-master/hooks/relations/ceph-client/requires.py b/kubernetes-master/hooks/relations/ceph-client/requires.py new file mode 100644 index 0000000..f542246 --- /dev/null +++ b/kubernetes-master/hooks/relations/ceph-client/requires.py @@ -0,0 +1,46 @@ +# Copyright 2020 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .lib import base_requires + +from charms.reactive import ( + when, +) + + +class CephClientRequires(base_requires.CephRequires): + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + super().joined() + + @when('endpoint.{endpoint_name}.changed') + def changed(self): + super().changed() + + @when('endpoint.{endpoint_name}.departed') + def departed(self): + super().changed() + + @when('endpoint.{endpoint_name}.broken') + def broken(self): + super().broken() + + def initial_ceph_response(self): + data = { + 'key': self.key, + 'auth': self.auth, + 'mon_hosts': self.mon_hosts() + } + return data diff --git a/kubernetes-master/hooks/relations/container-runtime/.gitignore b/kubernetes-master/hooks/relations/container-runtime/.gitignore new file mode 100644 index 0000000..894a44c --- /dev/null +++ b/kubernetes-master/hooks/relations/container-runtime/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/kubernetes-master/hooks/relations/container-runtime/LICENSE b/kubernetes-master/hooks/relations/container-runtime/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/kubernetes-master/hooks/relations/container-runtime/LICENSE @@ -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/kubernetes-master/hooks/relations/container-runtime/README.md b/kubernetes-master/hooks/relations/container-runtime/README.md new file mode 100644 index 0000000..4620013 --- /dev/null +++ b/kubernetes-master/hooks/relations/container-runtime/README.md @@ -0,0 +1,45 @@ +# interface-container-runtime + +## Overview + +This interface handles communication between subordinate charms, that provide a container runtime and charms requiring a container runtime. + +## Usage + +### Provides + +The providing side of the container interface provides a place for a container runtime to connect to. + +Your charm should respond to the `endpoint.{endpoint_name}.available` state, +which indicates that there is a container runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def update_kubelet_config(containerd): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + config = endpoint.get_config() + kubelet.config['container-runtime'] = \ + config['runtime'] +``` + +### Requires + +The requiring side of the container interface requires a place for a container runtime to connect to. + +Your charm should set `{endpoint_name}.available` state, +which indicates that the container is runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def pubish_config(): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + endpoint.set_config( + socket='unix:///var/run/containerd/containerd.sock', + runtime='remote', + nvidia_enabled=False + ) +``` diff --git a/kubernetes-master/hooks/relations/container-runtime/__init__.py b/kubernetes-master/hooks/relations/container-runtime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/container-runtime/interface.yaml b/kubernetes-master/hooks/relations/container-runtime/interface.yaml new file mode 100644 index 0000000..294be1e --- /dev/null +++ b/kubernetes-master/hooks/relations/container-runtime/interface.yaml @@ -0,0 +1,4 @@ +name: container-runtime +summary: Interface for relating to container runtimes +version: 1 +maintainer: "Joe Borg " diff --git a/kubernetes-master/hooks/relations/container-runtime/provides.py b/kubernetes-master/hooks/relations/container-runtime/provides.py new file mode 100644 index 0000000..a9768a8 --- /dev/null +++ b/kubernetes-master/hooks/relations/container-runtime/provides.py @@ -0,0 +1,55 @@ +from charms.reactive import ( + Endpoint, + toggle_flag +) + + +class ContainerRuntimeProvides(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined) + + def _get_config(self, key): + """ + Get the published configuration for a given key. + + :param key: String dict key + :return: String value for given key + """ + return self.all_joined_units.received.get(key) + + def get_nvidia_enabled(self): + """ + Get the published nvidia config. + + :return: String + """ + return self._get_config(key='nvidia_enabled') + + def get_runtime(self): + """ + Get the published runtime config. + + :return: String + """ + return self._get_config(key='runtime') + + def get_socket(self): + """ + Get the published socket config. + + :return: String + """ + return self._get_config(key='socket') + + def set_config(self, sandbox_image=None): + """ + Set the configuration to be published. + + :param sandbox_image: String to optionally override the sandbox image + :return: None + """ + for relation in self.relations: + relation.to_publish.update({ + 'sandbox_image': sandbox_image + }) diff --git a/kubernetes-master/hooks/relations/container-runtime/requires.py b/kubernetes-master/hooks/relations/container-runtime/requires.py new file mode 100644 index 0000000..c461b68 --- /dev/null +++ b/kubernetes-master/hooks/relations/container-runtime/requires.py @@ -0,0 +1,61 @@ +from charms.reactive import ( + Endpoint, + clear_flag, + data_changed, + is_data_changed, + toggle_flag +) + + +class ContainerRuntimeRequires(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined) + toggle_flag(self.expand_name('endpoint.{endpoint_name}.reconfigure'), + self.is_joined and self._config_changed()) + + def _config_changed(self): + """ + Determine if our received data has changed. + + :return: Boolean + """ + # NB: this call should match whatever we're tracking in handle_remote_config + return is_data_changed('containerd.remote_config', + [self.get_sandbox_image()]) + + def handle_remote_config(self): + """ + Keep track of received data so we can know if it changes. + + :return: None + """ + clear_flag(self.expand_name('endpoint.{endpoint_name}.reconfigure')) + # Presently, we only care about one piece of remote config. Expand + # the list as needed. + data_changed('containerd.remote_config', + [self.get_sandbox_image()]) + + def get_sandbox_image(self): + """ + Get the sandbox image URI if a remote has published one. + + :return: String: remotely configured sandbox image + """ + return self.all_joined_units.received.get('sandbox_image') + + def set_config(self, socket, runtime, nvidia_enabled): + """ + Set the configuration to be published. + + :param socket: String uri to runtime socket + :param runtime: String runtime executable + :param nvidia_enabled: Boolean nvidia runtime enabled + :return: None + """ + for relation in self.relations: + relation.to_publish.update({ + 'socket': socket, + 'runtime': runtime, + 'nvidia_enabled': nvidia_enabled + }) diff --git a/kubernetes-master/hooks/relations/coordinator/peers.py b/kubernetes-master/hooks/relations/coordinator/peers.py new file mode 100644 index 0000000..f443bf6 --- /dev/null +++ b/kubernetes-master/hooks/relations/coordinator/peers.py @@ -0,0 +1,21 @@ +# Copyright 2016-2018 Canonical Ltd. +# +# This file is part of the Coordinator Layer for Juju charms. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charms import reactive + + +class CoordinatorPeer(reactive.Endpoint): + pass diff --git a/kubernetes-master/hooks/relations/etcd/.gitignore b/kubernetes-master/hooks/relations/etcd/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/kubernetes-master/hooks/relations/etcd/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/kubernetes-master/hooks/relations/etcd/README.md b/kubernetes-master/hooks/relations/etcd/README.md new file mode 100644 index 0000000..9ed51dd --- /dev/null +++ b/kubernetes-master/hooks/relations/etcd/README.md @@ -0,0 +1,89 @@ +# Overview + +This interface layer handles the communication with Etcd via the `etcd` +interface. + +# Usage + +## Requires + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.connected` The relation is established, but Etcd may not + yet have provided any connection or service information. + + * `{relation_name}.available` Etcd has provided its connection string + information, and is ready to serve as a KV store. + The provided information can be accessed via the following methods: + * `etcd.get_connection_string()` + * `etcd.get_version()` + * `{relation_name}.tls.available` Etcd has provided the connection string + information, and the tls client credentials to communicate with it. + The client credentials can be accessed via: + * `{relation_name}.get_client_credentials()` returning a dictionary of + the clinet certificate, key and CA. + * `{relation_name}.save_client_credentials(key, cert, ca)` is a convenience + method to save the client certificate, key and CA to files of your + choosing. + + +For example, a common application for this is configuring an applications +backend key/value storage, like Docker. + +```python +@when('etcd.available', 'docker.available') +def swarm_etcd_cluster_setup(etcd): + con_string = etcd.connection_string().replace('http', 'etcd') + opts = {} + opts['connection_string'] = con_string + render('docker-compose.yml', 'files/swarm/docker-compose.yml', opts) + +``` + + +## Provides + +A charm providing this interface is providing the Etcd rest api service. + +This interface layer will set the following states, as appropriate: + + * `{relation_name}.connected` One or more clients of any type have + been related. The charm should call the following methods to provide the + appropriate information to the clients: + + * `{relation_name}.set_connection_string(string, version)` + * `{relation_name}.set_client_credentials(key, cert, ca)` + +Example: + +```python +@when('db.connected') +def send_connection_details(db): + cert = leader_get('client_certificate') + key = leader_get('client_key') + ca = leader_get('certificate_authority') + # Set the key, cert, and ca on the db relation + db.set_client_credentials(key, cert, ca) + + port = hookenv.config().get('port') + # Get all the peers participating in the cluster relation. + addresses = cluster.get_peer_addresses() + connections = [] + for address in addresses: + connections.append('http://{0}:{1}'.format(address, port)) + # Set the connection string on the db relation. + db.set_connection_string(','.join(conections)) +``` + + +# Contact Information + +### Maintainer +- Charles Butler + + +# Etcd + +- [Etcd](https://coreos.com/etcd/) home page +- [Etcd bug trackers](https://github.com/coreos/etcd/issues) +- [Etcd Juju Charm](http://jujucharms.com/?text=etcd) diff --git a/kubernetes-master/hooks/relations/etcd/__init__.py b/kubernetes-master/hooks/relations/etcd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/etcd/interface.yaml b/kubernetes-master/hooks/relations/etcd/interface.yaml new file mode 100644 index 0000000..929b1d5 --- /dev/null +++ b/kubernetes-master/hooks/relations/etcd/interface.yaml @@ -0,0 +1,4 @@ +name: etcd +summary: Interface for relating to ETCD +version: 2 +maintainer: "Charles Butler " diff --git a/kubernetes-master/hooks/relations/etcd/peers.py b/kubernetes-master/hooks/relations/etcd/peers.py new file mode 100644 index 0000000..90980d1 --- /dev/null +++ b/kubernetes-master/hooks/relations/etcd/peers.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdPeer(RelationBase): + '''This class handles peer relation communication by setting states that + the reactive code can respond to. ''' + + scope = scopes.UNIT + + @hook('{peers:etcd}-relation-joined') + def peer_joined(self): + '''A new peer has joined, set the state on the unit so we can track + when they are departed. ''' + conv = self.conversation() + conv.set_state('{relation_name}.joined') + + @hook('{peers:etcd}-relation-departed') + def peers_going_away(self): + '''Trigger a state on the unit that it is leaving. We can use this + state in conjunction with the joined state to determine which unit to + unregister from the etcd cluster. ''' + conv = self.conversation() + conv.remove_state('{relation_name}.joined') + conv.set_state('{relation_name}.departing') + + def dismiss(self): + '''Remove the departing state from all other units in the conversation, + and we can resume normal operation. + ''' + for conv in self.conversations(): + conv.remove_state('{relation_name}.departing') + + def get_peers(self): + '''Return a list of names for the peers participating in this + conversation scope. ''' + peers = [] + # Iterate over all the conversations of this type. + for conversation in self.conversations(): + peers.append(conversation.scope) + return peers + + def set_db_ingress_address(self, address): + '''Set the ingress address belonging to the db relation.''' + for conversation in self.conversations(): + conversation.set_remote('db-ingress-address', address) + + def get_db_ingress_addresses(self): + '''Return a list of db ingress addresses''' + addresses = [] + # Iterate over all the conversations of this type. + for conversation in self.conversations(): + address = conversation.get_remote('db-ingress-address') + if address: + addresses.append(address) + return addresses diff --git a/kubernetes-master/hooks/relations/etcd/provides.py b/kubernetes-master/hooks/relations/etcd/provides.py new file mode 100644 index 0000000..3cfc174 --- /dev/null +++ b/kubernetes-master/hooks/relations/etcd/provides.py @@ -0,0 +1,47 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdProvider(RelationBase): + scope = scopes.GLOBAL + + @hook('{provides:etcd}-relation-{joined,changed}') + def joined_or_changed(self): + ''' Set the connected state from the provides side of the relation. ''' + self.set_state('{relation_name}.connected') + + @hook('{provides:etcd}-relation-{broken,departed}') + def broken_or_departed(self): + '''Remove connected state from the provides side of the relation. ''' + conv = self.conversation() + if len(conv.units) == 1: + conv.remove_state('{relation_name}.connected') + + def set_client_credentials(self, key, cert, ca): + ''' Set the client credentials on the global conversation for this + relation. ''' + self.set_remote('client_key', key) + self.set_remote('client_ca', ca) + self.set_remote('client_cert', cert) + + def set_connection_string(self, connection_string, version=''): + ''' Set the connection string on the global conversation for this + relation. ''' + # Note: Version added as a late-dependency for 2 => 3 migration + # If no version is specified, consumers should presume etcd 2.x + self.set_remote('connection_string', connection_string) + self.set_remote('version', version) diff --git a/kubernetes-master/hooks/relations/etcd/requires.py b/kubernetes-master/hooks/relations/etcd/requires.py new file mode 100644 index 0000000..435532f --- /dev/null +++ b/kubernetes-master/hooks/relations/etcd/requires.py @@ -0,0 +1,80 @@ +#!/usr/bin/python +# 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 + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class EtcdClient(RelationBase): + scope = scopes.GLOBAL + + @hook('{requires:etcd}-relation-{joined,changed}') + def changed(self): + ''' Indicate the relation is connected, and if the relation data is + set it is also available. ''' + self.set_state('{relation_name}.connected') + + if self.get_connection_string(): + self.set_state('{relation_name}.available') + # Get the ca, key, cert from the relation data. + cert = self.get_client_credentials() + # The tls state depends on the existance of the ca, key and cert. + if cert['client_cert'] and cert['client_key'] and cert['client_ca']: # noqa + self.set_state('{relation_name}.tls.available') + + @hook('{requires:etcd}-relation-{broken, departed}') + def broken(self): + ''' Indicate the relation is no longer available and not connected. ''' + self.remove_state('{relation_name}.available') + self.remove_state('{relation_name}.connected') + self.remove_state('{relation_name}.tls.available') + + def connection_string(self): + ''' This method is depreciated but ensures backward compatibility + @see get_connection_string(self). ''' + return self.get_connection_string() + + def get_connection_string(self): + ''' Return the connection string, if available, or None. ''' + return self.get_remote('connection_string') + + def get_version(self): + ''' Return the version of the etd protocol being used, or None. ''' + return self.get_remote('version') + + def get_client_credentials(self): + ''' Return a dict with the client certificate, ca and key to + communicate with etcd using tls. ''' + return {'client_cert': self.get_remote('client_cert'), + 'client_key': self.get_remote('client_key'), + 'client_ca': self.get_remote('client_ca')} + + def save_client_credentials(self, key, cert, ca): + ''' Save all the client certificates for etcd to local files. ''' + self._save_remote_data('client_cert', cert) + self._save_remote_data('client_key', key) + self._save_remote_data('client_ca', ca) + + def _save_remote_data(self, key, path): + ''' Save the remote data to a file indicated by path creating the + parent directory if needed.''' + value = self.get_remote(key) + if value: + parent = os.path.dirname(path) + if not os.path.isdir(parent): + os.makedirs(parent) + with open(path, 'w') as stream: + stream.write(value) diff --git a/kubernetes-master/hooks/relations/gcp-integration/.gitignore b/kubernetes-master/hooks/relations/gcp-integration/.gitignore new file mode 100644 index 0000000..5f9f2c5 --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/.gitignore @@ -0,0 +1,3 @@ +.tox +__pycache__ +*.pyc diff --git a/kubernetes-master/hooks/relations/gcp-integration/LICENSE b/kubernetes-master/hooks/relations/gcp-integration/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-master/hooks/relations/gcp-integration/README.md b/kubernetes-master/hooks/relations/gcp-integration/README.md new file mode 100644 index 0000000..42861fb --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/README.md @@ -0,0 +1,28 @@ +# Overview + +This layer encapsulates the `gcp-integration` interface communication protocol +and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:gcp-integration` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:gcp-integration'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `gcp-integration` interface protocol: + +```yaml +requires: + gcp: + interface: gcp-integration +``` + +For documentation on how to use the API for this interface, see: + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) (this will only be used by the gcp-integrator charm) diff --git a/kubernetes-master/hooks/relations/gcp-integration/__init__.py b/kubernetes-master/hooks/relations/gcp-integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/gcp-integration/copyright b/kubernetes-master/hooks/relations/gcp-integration/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/hooks/relations/gcp-integration/docs/provides.md b/kubernetes-master/hooks/relations/gcp-integration/docs/provides.md new file mode 100644 index 0000000..6f29a39 --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/docs/provides.md @@ -0,0 +1,183 @@ +

provides

+ + +This is the provides side of the interface layer, for use only by the GCP +integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for GCP integration features. + The GCP integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. + +

GCPIntegrationProvides

+ +```python +GCPIntegrationProvides(self, endpoint_name, relation_ids=None) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag +from charms import layer + +@when('endpoint.gcp.requests-pending') +def handle_requests(): + gcp = endpoint_from_flag('endpoint.gcp.requests-pending') + for request in gcp.requests: + if request.instance_labels: + layer.gcp.label_instance( + request.instance, + request.zone, + request.instance_labels) + if request.requested_load_balancer_management: + layer.gcp.enable_load_balancer_management( + request.charm, + request.instance, + request.zone, + ) + # ... + gcp.mark_completed() +``` + +

relation_ids

+ + +A list of the IDs of all established relations. + +

requests

+ + +A list of the new or updated `IntegrationRequests` that +have been made. + +

get_departed_charms

+ +```python +GCPIntegrationProvides.get_departed_charms(self) +``` + +Get a list of all charms that have had all units depart since the +last time this was called. + +

mark_completed

+ +```python +GCPIntegrationProvides.mark_completed(self) +``` + +Mark all requests as completed and remove the `requests-pending` flag. + +

IntegrationRequest

+ +```python +IntegrationRequest(self, unit) +``` + +A request for integration from a single remote unit. + +

application_name

+ + +The name of the application making the request. + +

charm

+ + +The charm name reported for this request. + +

has_credentials

+ + +Whether or not credentials have been set via `set_credentials`. + +

instance

+ + +The instance name reported for this request. + +

instance_labels

+ + +Mapping of label names to values to apply to this instance. + +

is_changed

+ + +Whether this request has changed since the last time it was +marked completed (if ever). + +

model_uuid

+ + +The UUID of the model containing the application making this request. + +

relation_id

+ + +The ID of the relation for the unit making the request. + +

requested_block_storage_management

+ + +Flag indicating whether block storage management was requested. + +

requested_dns_management

+ + +Flag indicating whether DNS management was requested. + +

requested_instance_inspection

+ + +Flag indicating whether the ability to inspect instances was requested. + +

requested_network_management

+ + +Flag indicating whether the ability to manage networking was requested. + +

requested_object_storage_access

+ + +Flag indicating whether object storage access was requested. + +

requested_object_storage_management

+ + +Flag indicating whether object storage management was requested. + +

requested_security_management

+ + +Flag indicating whether security management was requested. + +

unit_name

+ + +The name of the unit making the request. + +

zone

+ + +The zone reported for this request. + +

mark_completed

+ +```python +IntegrationRequest.mark_completed(self) +``` + +Mark this request as having been completed. + +

set_credentials

+ +```python +IntegrationRequest.set_credentials(self, credentials) +``` + +Set the credentials for this request. + diff --git a/kubernetes-master/hooks/relations/gcp-integration/docs/requires.md b/kubernetes-master/hooks/relations/gcp-integration/docs/requires.md new file mode 100644 index 0000000..36e23c2 --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/docs/requires.md @@ -0,0 +1,140 @@ +

requires

+ + +This is the requires side of the interface layer, for use in charms that +wish to request integration with GCP native features. The integration will +be provided by the GCP integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of GCP specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific GCP features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the GCP instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. + +

GCPIntegrationRequires

+ +```python +GCPIntegrationRequires(self, *args, **kwargs) +``` + +Interface to request integration access. + +Note that due to resource limits and permissions granularity, policies are +limited to being applied at the charm level. That means that, if any +permissions are requested (i.e., any of the enable methods are called), +what is granted will be the sum of those ever requested by any instance of +the charm on this cloud. + +Labels, on the other hand, will be instance specific. + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag + +@when('endpoint.gcp.joined') +def request_gcp_integration(): + gcp = endpoint_from_flag('endpoint.gcp.joined') + gcp.label_instance({ + 'tag1': 'value1', + 'tag2': None, + }) + gcp.request_load_balancer_management() + # ... + +@when('endpoint.gcp.ready') +def gcp_integration_ready(): + update_config_enable_gcp() +``` + +

instance

+ + +This unit's instance name. + +

is_ready

+ + +Whether or not the request for this instance has been completed. + +

zone

+ + +The zone this unit is in. + +

label_instance

+ +```python +GCPIntegrationRequires.label_instance(self, labels) +``` + +Request that the given labels be applied to this instance. + +__Parameters__ + +- __`labels` (dict)__: Mapping of labels names to values. + +

enable_instance_inspection

+ +```python +GCPIntegrationRequires.enable_instance_inspection(self) +``` + +Request the ability to inspect instances. + +

enable_network_management

+ +```python +GCPIntegrationRequires.enable_network_management(self) +``` + +Request the ability to manage networking. + +

enable_security_management

+ +```python +GCPIntegrationRequires.enable_security_management(self) +``` + +Request the ability to manage security (e.g., firewalls). + +

enable_block_storage_management

+ +```python +GCPIntegrationRequires.enable_block_storage_management(self) +``` + +Request the ability to manage block storage. + +

enable_dns_management

+ +```python +GCPIntegrationRequires.enable_dns_management(self) +``` + +Request the ability to manage DNS. + +

enable_object_storage_access

+ +```python +GCPIntegrationRequires.enable_object_storage_access(self) +``` + +Request the ability to access object storage. + +

enable_object_storage_management

+ +```python +GCPIntegrationRequires.enable_object_storage_management(self) +``` + +Request the ability to manage object storage. + diff --git a/kubernetes-master/hooks/relations/gcp-integration/interface.yaml b/kubernetes-master/hooks/relations/gcp-integration/interface.yaml new file mode 100644 index 0000000..9966e3f --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/interface.yaml @@ -0,0 +1,4 @@ +name: gcp-integration +summary: Interface for connecting to the GCP integrator charm. +version: 1 +maintainer: Cory Johns diff --git a/kubernetes-master/hooks/relations/gcp-integration/make_docs b/kubernetes-master/hooks/relations/gcp-integration/make_docs new file mode 100644 index 0000000..bd4e54e --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'gcp': {'interface': 'gcp-integration'}}, + 'provides': {'gcp': {'interface': 'gcp-integration'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-master/hooks/relations/gcp-integration/provides.py b/kubernetes-master/hooks/relations/gcp-integration/provides.py new file mode 100644 index 0000000..ba34b0d --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/provides.py @@ -0,0 +1,253 @@ +""" +This is the provides side of the interface layer, for use only by the GCP +integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for GCP integration features. + The GCP integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. +""" + +from operator import attrgetter + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class GCPIntegrationProvides(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + + @when('endpoint.gcp.requests-pending') + def handle_requests(): + gcp = endpoint_from_flag('endpoint.gcp.requests-pending') + for request in gcp.requests: + if request.instance_labels: + layer.gcp.label_instance( + request.instance, + request.zone, + request.instance_labels) + if request.requested_load_balancer_management: + layer.gcp.enable_load_balancer_management( + request.charm, + request.instance, + request.zone, + ) + # ... + gcp.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + toggle_flag(self.expand_name('requests-pending'), + len(self.requests) > 0) + clear_flag(self.expand_name('changed')) + + @property + def requests(self): + """ + A list of the new or updated #IntegrationRequests that + have been made. + """ + if not hasattr(self, '_requests'): + all_requests = [IntegrationRequest(unit) + for unit in self.all_joined_units] + is_changed = attrgetter('is_changed') + self._requests = list(filter(is_changed, all_requests)) + return self._requests + + @property + def relation_ids(self): + """ + A list of the IDs of all established relations. + """ + return [relation.relation_id for relation in self.relations] + + def get_departed_charms(self): + """ + Get a list of all charms that have had all units depart since the + last time this was called. + """ + joined_charms = {unit.received['charm'] + for unit in self.all_joined_units + if unit.received['charm']} + departed_charms = [unit.received['charm'] + for unit in self.all_departed_units + if unit.received['charm'] not in joined_charms] + self.all_departed_units.clear() + return departed_charms + + def mark_completed(self): + """ + Mark all requests as completed and remove the `requests-pending` flag. + """ + for request in self.requests: + request.mark_completed() + clear_flag(self.expand_name('requests-pending')) + self._requests = [] + + +class IntegrationRequest: + """ + A request for integration from a single remote unit. + """ + def __init__(self, unit): + self._unit = unit + + @property + def _to_publish(self): + return self._unit.relation.to_publish + + @property + def _completed(self): + return self._to_publish.get('completed', {}) + + @property + def _requested(self): + return self._unit.received['requested'] + + @property + def is_changed(self): + """ + Whether this request has changed since the last time it was + marked completed (if ever). + """ + if not all([self.charm, self.instance, self.zone, self._requested]): + return False + return self._completed.get(self.instance) != self._requested + + def mark_completed(self): + """ + Mark this request as having been completed. + """ + completed = self._completed + completed[self.instance] = self._requested + self._to_publish['completed'] = completed # have to explicitly update + + def set_credentials(self, credentials): + """ + Set the credentials for this request. + """ + self._unit.relation.to_publish['credentials'] = credentials + + @property + def has_credentials(self): + """ + Whether or not credentials have been set via `set_credentials`. + """ + return 'credentials' in self._unit.relation.to_publish + + @property + def relation_id(self): + """ + The ID of the relation for the unit making the request. + """ + return self._unit.relation.relation_id + + @property + def unit_name(self): + """ + The name of the unit making the request. + """ + return self._unit.unit_name + + @property + def application_name(self): + """ + The name of the application making the request. + """ + return self._unit.application_name + + @property + def charm(self): + """ + The charm name reported for this request. + """ + return self._unit.received['charm'] + + @property + def instance(self): + """ + The instance name reported for this request. + """ + return self._unit.received['instance'] + + @property + def zone(self): + """ + The zone reported for this request. + """ + return self._unit.received['zone'] + + @property + def model_uuid(self): + """ + The UUID of the model containing the application making this request. + """ + return self._unit.received['model-uuid'] + + @property + def instance_labels(self): + """ + Mapping of label names to values to apply to this instance. + """ + # uses dict() here to make a copy, just to be safe + return dict(self._unit.received.get('instance-labels', {})) + + @property + def requested_instance_inspection(self): + """ + Flag indicating whether the ability to inspect instances was requested. + """ + return bool(self._unit.received['enable-instance-inspection']) + + @property + def requested_network_management(self): + """ + Flag indicating whether the ability to manage networking was requested. + """ + return bool(self._unit.received['enable-network-management']) + + @property + def requested_security_management(self): + """ + Flag indicating whether security management was requested. + """ + return bool(self._unit.received['enable-security-management']) + + @property + def requested_block_storage_management(self): + """ + Flag indicating whether block storage management was requested. + """ + return bool(self._unit.received['enable-block-storage-management']) + + @property + def requested_dns_management(self): + """ + Flag indicating whether DNS management was requested. + """ + return bool(self._unit.received['enable-dns-management']) + + @property + def requested_object_storage_access(self): + """ + Flag indicating whether object storage access was requested. + """ + return bool(self._unit.received['enable-object-storage-access']) + + @property + def requested_object_storage_management(self): + """ + Flag indicating whether object storage management was requested. + """ + return bool(self._unit.received['enable-object-storage-management']) diff --git a/kubernetes-master/hooks/relations/gcp-integration/pydocmd.yml b/kubernetes-master/hooks/relations/gcp-integration/pydocmd.yml new file mode 100644 index 0000000..9ef5e78 --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'GCP Integration Interface' + +generate: + - requires.md: + - requires + - requires.GCPIntegrationRequires+ + - provides.md: + - provides + - provides.GCPIntegrationProvides+ + - provides.IntegrationRequest+ + +pages: + - Requires: requires.md + - Provides: provides.md + +gens_dir: docs diff --git a/kubernetes-master/hooks/relations/gcp-integration/requires.py b/kubernetes-master/hooks/relations/gcp-integration/requires.py new file mode 100644 index 0000000..bbd191f --- /dev/null +++ b/kubernetes-master/hooks/relations/gcp-integration/requires.py @@ -0,0 +1,227 @@ +""" +This is the requires side of the interface layer, for use in charms that +wish to request integration with GCP native features. The integration will +be provided by the GCP integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of GCP specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific GCP features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the GCP instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. +""" + + +import os +import random +import string +from urllib.parse import urljoin +from urllib.request import urlopen, Request + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import clear_flag, toggle_flag + + +# block size to read data from GCP metadata service +# (realistically, just needs to be bigger than ~20 chars) +READ_BLOCK_SIZE = 2048 + + +class GCPIntegrationRequires(Endpoint): + """ + Interface to request integration access. + + Note that due to resource limits and permissions granularity, policies are + limited to being applied at the charm level. That means that, if any + permissions are requested (i.e., any of the enable methods are called), + what is granted will be the sum of those ever requested by any instance of + the charm on this cloud. + + Labels, on the other hand, will be instance specific. + + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + + @when('endpoint.gcp.joined') + def request_gcp_integration(): + gcp = endpoint_from_flag('endpoint.gcp.joined') + gcp.label_instance({ + 'tag1': 'value1', + 'tag2': None, + }) + gcp.request_load_balancer_management() + # ... + + @when('endpoint.gcp.ready') + def gcp_integration_ready(): + update_config_enable_gcp() + ``` + """ + # https://cloud.google.com/compute/docs/storing-retrieving-metadata + _metadata_url = 'http://metadata.google.internal/computeMetadata/v1/' + _instance_url = urljoin(_metadata_url, 'instance/name') + _zone_url = urljoin(_metadata_url, 'instance/zone') + _metadata_headers = {'Metadata-Flavor': 'Google'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._instance = None + self._zone = None + + @property + def _received(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single GCP integration application with a + single unit. + """ + return self.relations[0].joined_units.received + + @property + def _to_publish(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single GCP integration application with a + single unit. + """ + return self.relations[0].to_publish + + @when('endpoint.{endpoint_name}.joined') + def send_instance_info(self): + self._to_publish['charm'] = hookenv.charm_name() + self._to_publish['instance'] = self.instance + self._to_publish['zone'] = self.zone + self._to_publish['model-uuid'] = os.environ['JUJU_MODEL_UUID'] + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + # My middle name is ready. No, that doesn't sound right. + # I eat ready for breakfast. + toggle_flag(self.expand_name('ready'), self.is_ready) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + @property + def instance(self): + """ + This unit's instance name. + """ + if self._instance is None: + cache_key = self.expand_name('instance') + cached = unitdata.kv().get(cache_key) + if cached: + self._instance = cached + else: + req = Request(self._instance_url, + headers=self._metadata_headers) + with urlopen(req) as fd: + instance = fd.read(READ_BLOCK_SIZE).decode('utf8').strip() + self._instance = instance + unitdata.kv().set(cache_key, self._instance) + return self._instance + + @property + def zone(self): + """ + The zone this unit is in. + """ + if self._zone is None: + cache_key = self.expand_name('zone') + cached = unitdata.kv().get(cache_key) + if cached: + self._zone = cached + else: + req = Request(self._zone_url, + headers=self._metadata_headers) + with urlopen(req) as fd: + zone = fd.read(READ_BLOCK_SIZE).decode('utf8').strip() + self._zone = zone.split('/')[-1] + unitdata.kv().set(cache_key, self._zone) + return self._zone + + @property + def is_ready(self): + """ + Whether or not the request for this instance has been completed. + """ + requested = self._to_publish['requested'] + completed = self._received.get('completed', {}).get(self.instance) + return requested and requested == completed + + @property + def credentials(self): + return self._received['credentials'] + + def _request(self, keyvals): + alphabet = string.ascii_letters + string.digits + nonce = ''.join(random.choice(alphabet) for _ in range(8)) + self._to_publish.update(keyvals) + self._to_publish['requested'] = nonce + clear_flag(self.expand_name('ready')) + + def label_instance(self, labels): + """ + Request that the given labels be applied to this instance. + + # Parameters + `labels` (dict): Mapping of labels names to values. + """ + self._request({'instance-labels': dict(labels)}) + + def enable_instance_inspection(self): + """ + Request the ability to inspect instances. + """ + self._request({'enable-instance-inspection': True}) + + def enable_network_management(self): + """ + Request the ability to manage networking. + """ + self._request({'enable-network-management': True}) + + def enable_security_management(self): + """ + Request the ability to manage security (e.g., firewalls). + """ + self._request({'enable-security-management': True}) + + def enable_block_storage_management(self): + """ + Request the ability to manage block storage. + """ + self._request({'enable-block-storage-management': True}) + + def enable_dns_management(self): + """ + Request the ability to manage DNS. + """ + self._request({'enable-dns': True}) + + def enable_object_storage_access(self): + """ + Request the ability to access object storage. + """ + self._request({'enable-object-storage-access': True}) + + def enable_object_storage_management(self): + """ + Request the ability to manage object storage. + """ + self._request({'enable-object-storage-management': True}) diff --git a/kubernetes-master/hooks/relations/grafana-dashboard/.gitignore b/kubernetes-master/hooks/relations/grafana-dashboard/.gitignore new file mode 100644 index 0000000..01a6a44 --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/.gitignore @@ -0,0 +1,3 @@ +.docs +__pycache__ +*.pyc diff --git a/kubernetes-master/hooks/relations/grafana-dashboard/LICENSE b/kubernetes-master/hooks/relations/grafana-dashboard/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-master/hooks/relations/grafana-dashboard/README.md b/kubernetes-master/hooks/relations/grafana-dashboard/README.md new file mode 100644 index 0000000..b111350 --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/README.md @@ -0,0 +1,92 @@ +# Interface grafana-dashboard + +This is a [Juju][] interface layer that enables a charm which provides +dashboards to be imported into Grafana. + +You can download existing [Grafana Dashboards][] or use the [Grafana Dashboard +Reference][] to create your own. + +# Example Usage + +First, you must define the relation endpoint in your charm's `metadata.yaml`: + +```yaml +provides: + grafana: + interface: grafana-dashboard +``` + +Next, you must ensure the interface layer is included in your `layer.yaml`: + +```yaml +includes: + - interface:grafana-dashboard +``` + +Then, in your reactive code, add the following, modifying the dashboard data as +your charm needs: + +```python +import json +from charms.reactive import endpoint_from_flag + + +@when('endpoint.grafana.joined') +def register_grafana_dashboards(): + grafana = endpoint_from_flag('endpoint.grafana.joined') + for dashboard_file in Path('files/grafana').glob('*.json'): + dashboard = json.loads(dashboard_file.read_text()) + grafana.register_dashboard(name=dashboard_file.stem, + dashboard=dashboard) +``` + + + +# Reference + +* [common.md](common.md) + * [ImportRequest](docs/common.md#importrequest) + * [egress_subnets](docs/common.md#importrequest-egress_subnets) + * [ingress_address](docs/common.md#importrequest-ingress_address) + * [is_created](docs/common.md#importrequest-is_created) + * [is_received](docs/common.md#importrequest-is_received) + * [respond](docs/common.md#importrequest-respond) + * [ImportResponse](docs/common.md#importresponse) + * [name](docs/common.md#importresponse-name) +* [provides.md](provides.md) + * [GrafanaDashboardProvides](docs/provides.md#grafanadashboardprovides) + * [all_departed_units](docs/provides.md#grafanadashboardprovides-all_departed_units) + * [all_joined_units](docs/provides.md#grafanadashboardprovides-all_joined_units) + * [all_units](docs/provides.md#grafanadashboardprovides-all_units) + * [endpoint_name](docs/provides.md#grafanadashboardprovides-endpoint_name) + * [failed_imports](docs/provides.md#grafanadashboardprovides-failed_imports) + * [is_joined](docs/provides.md#grafanadashboardprovides-is_joined) + * [joined](docs/provides.md#grafanadashboardprovides-joined) + * [manage_flags](docs/provides.md#grafanadashboardprovides-manage_flags) + * [register_dashboard](docs/provides.md#grafanadashboardprovides-register_dashboard) + * [relations](docs/provides.md#grafanadashboardprovides-relations) + * [requests](docs/provides.md#grafanadashboardprovides-requests) + * [responses](docs/provides.md#grafanadashboardprovides-responses) +* [requires.md](requires.md) + * [GrafanaDashboardRequires](docs/requires.md#grafanadashboardrequires) + * [all_departed_units](docs/requires.md#grafanadashboardrequires-all_departed_units) + * [all_joined_units](docs/requires.md#grafanadashboardrequires-all_joined_units) + * [all_requests](docs/requires.md#grafanadashboardrequires-all_requests) + * [all_units](docs/requires.md#grafanadashboardrequires-all_units) + * [endpoint_name](docs/requires.md#grafanadashboardrequires-endpoint_name) + * [is_joined](docs/requires.md#grafanadashboardrequires-is_joined) + * [joined](docs/requires.md#grafanadashboardrequires-joined) + * [manage_flags](docs/requires.md#grafanadashboardrequires-manage_flags) + * [new_requests](docs/requires.md#grafanadashboardrequires-new_requests) + * [relations](docs/requires.md#grafanadashboardrequires-relations) + + + +# Contact Information + +Maintainer: Cory Johns <Cory.Johns@canonical.com> + + +[Juju]: https://jujucharms.com +[Grafana Dashboards]: https://grafana.com/grafana/dashboards +[Grafana Dashboard Reference]: https://grafana.com/docs/reference/dashboard/ diff --git a/kubernetes-master/hooks/relations/grafana-dashboard/__init__.py b/kubernetes-master/hooks/relations/grafana-dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/grafana-dashboard/common.py b/kubernetes-master/hooks/relations/grafana-dashboard/common.py new file mode 100644 index 0000000..99db2d8 --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/common.py @@ -0,0 +1,38 @@ +from charms.reactive import BaseRequest, BaseResponse, Field + + +class ImportResponse(BaseResponse): + success = Field(description='Whether or not the import succeeded') + reason = Field(description='If failed, a description of why') + + @property + def name(self): + """ + The name given when the import was requested. + """ + return self.request.name + + +class ImportRequest(BaseRequest): + RESPONSE_CLASS = ImportResponse + + name = Field(description=""" + Name of the dashboard to import. Informational only, so that + you can tell which dashboard request this was, e.g. to check + for success or failure. + """) + + dashboard = Field(description=""" + Data structure defining the dashboard. Must be JSON + serializable. (Note: This should *not* be pre-serialized + JSON.) + """) + + def respond(self, success, reason=None): + """ + Acknowledge this request, and indicate success or failure with an + optional explanation. + """ + # wrap the base respond method to make the success field required and + # positional, as well as to provide a better doc string + super().respond(success=success, reason=reason) diff --git a/kubernetes-master/hooks/relations/grafana-dashboard/copyright b/kubernetes-master/hooks/relations/grafana-dashboard/copyright new file mode 100644 index 0000000..69768db --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2019, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/hooks/relations/grafana-dashboard/docs/common.md b/kubernetes-master/hooks/relations/grafana-dashboard/docs/common.md new file mode 100644 index 0000000..ab7de1d --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/docs/common.md @@ -0,0 +1,50 @@ +# `class ImportRequest(BaseRequest)` + +Base class for requests using the request / response pattern. + +Subclasses **must** set the ``RESPONSE_CLASS`` attribute to a subclass of +the :class:`BaseResponse` which defines the fields that the response will +use. They must also define additional attributes as :class:`Field`s. + +For example:: + + class TLSResponse(BaseResponse): + key = Field('Private key for the cert') + cert = Field('Public cert info') + + + class TLSRequest(BaseRequest): + RESPONSE_CLASS = TLSResponse + + common_name = Field('Common Name (CN) for the cert to be created') + sans = Field('List of Subject Alternative Names (SANs)') + +## `egress_subnets` + +Subnets over which network traffic to the requester will flow. + +## `ingress_address` + +Address to use if a connection to the requester is required. + +## `is_created` + +Whether this request was created by this side of the relation. + +## `is_received` + +Whether this request was received by the other side of the relation. + +## `def respond(self, success, reason=None)` + +Acknowledge this request, and indicate success or failure with an +optional explanation. + +# `class ImportResponse(BaseResponse)` + +Base class for responses using the request / response pattern. + +## `name` + +The name given when the import was requested. + diff --git a/kubernetes-master/hooks/relations/grafana-dashboard/docs/provides.md b/kubernetes-master/hooks/relations/grafana-dashboard/docs/provides.md new file mode 100644 index 0000000..cc1f3dc --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/docs/provides.md @@ -0,0 +1,120 @@ +# `class GrafanaDashboardProvides(RequesterEndpoint)` + +Base class for Endpoints that create requests in the request / response +pattern. + +Subclasses **must** set the ``REQUEST_CLASS`` attribute to a subclass +of :class:`BaseRequest` which defines the fields the request will use. + +## `all_departed_units` + +Collection of all units that were previously part of any relation on +this endpoint but which have since departed. + +This collection is persistent and mutable. The departed units will +be kept until they are explicitly removed, to allow for reasonable +cleanup of units that have left. + +Example: You need to run a command each time a unit departs the relation. + +.. code-block:: python + + @when('endpoint.{endpoint_name}.departed') + def handle_departed_unit(self): + for name, unit in self.all_departed_units.items(): + # run the command to remove `unit` from the cluster + # .. + self.all_departed_units.clear() + clear_flag(self.expand_name('departed')) + +Once a unit is departed, it will no longer show up in +:attr:`all_joined_units`. Note that units are considered departed as +soon as the departed hook is entered, which differs slightly from how +the Juju primitives behave (departing units are still returned from +``related-units`` until after the departed hook is complete). + +This collection is a :class:`KeyList`, so can be used as a mapping to +look up units by their unit name, or iterated or accessed by index. + +## `all_joined_units` + +A list view of all the units of all relations attached to this +:class:`~charms.reactive.endpoints.Endpoint`. + +This is actually a +:class:`~charms.reactive.endpoints.CombinedUnitsView`, so the units +will be in order by relation ID and then unit name, and you can access a +merged view of all the units' data as a single mapping. You should be +very careful when using the merged data collections, however, and +consider carefully what will happen when the endpoint has multiple +relations and multiple remote units on each. It is probably better to +iterate over each unit and handle its data individually. See +:class:`~charms.reactive.endpoints.CombinedUnitsView` for an +explanation of how the merged data collections work. + +Note that, because a given application might be related multiple times +on a given endpoint, units may show up in this collection more than +once. + +## `all_units` + +.. deprecated:: 0.6.1 + Use :attr:`all_joined_units` instead + +## `endpoint_name` + +Relation name of this endpoint. + +## `failed_imports` + +A list of requests that failed to import. + +## `is_joined` + +Whether this endpoint has remote applications attached to it. + +## `joined` + +.. deprecated:: 0.6.3 + Use :attr:`is_joined` instead + +## `def manage_flags(self)` + +Method that subclasses can override to perform any flag management +needed during startup. + +This will be called automatically after the framework-managed automatic +flags have been updated. + +## `def register_dashboard(self, name, dashboard)` + +Request a dashboard to be imported. + +:param name: Name of dashboard. Informational only, so that you can + tell which dashboard request this was, e.g. to check for success or + failure. +:param dashboard: Data structure defining the dashboard. Must be JSON + serializable. (Note: This should *not* be pre-serialized JSON.) + +## `relations` + +Collection of :class:`Relation` instances that are established for +this :class:`Endpoint`. + +This is a :class:`KeyList`, so it can be iterated and indexed as a list, +or you can look up relations by their ID. For example:: + + rel0 = endpoint.relations[0] + assert rel0 is endpoint.relations[rel0.relation_id] + assert all(rel is endpoint.relations[rel.relation_id] + for rel in endpoint.relations) + print(', '.join(endpoint.relations.keys())) + +## `requests` + +A list of all requests which have been submitted. + +## `responses` + +A list of all responses which have been received. + diff --git a/kubernetes-master/hooks/relations/grafana-dashboard/docs/requires.md b/kubernetes-master/hooks/relations/grafana-dashboard/docs/requires.md new file mode 100644 index 0000000..c84f1bc --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/docs/requires.md @@ -0,0 +1,109 @@ +# `class GrafanaDashboardRequires(ResponderEndpoint)` + +Base class for Endpoints that respond to requests in the request / response +pattern. + +Subclasses **must** set the ``REQUEST_CLASS`` attribute to a subclass +of :class:`BaseRequest` which defines the fields the request will use. + +## `all_departed_units` + +Collection of all units that were previously part of any relation on +this endpoint but which have since departed. + +This collection is persistent and mutable. The departed units will +be kept until they are explicitly removed, to allow for reasonable +cleanup of units that have left. + +Example: You need to run a command each time a unit departs the relation. + +.. code-block:: python + + @when('endpoint.{endpoint_name}.departed') + def handle_departed_unit(self): + for name, unit in self.all_departed_units.items(): + # run the command to remove `unit` from the cluster + # .. + self.all_departed_units.clear() + clear_flag(self.expand_name('departed')) + +Once a unit is departed, it will no longer show up in +:attr:`all_joined_units`. Note that units are considered departed as +soon as the departed hook is entered, which differs slightly from how +the Juju primitives behave (departing units are still returned from +``related-units`` until after the departed hook is complete). + +This collection is a :class:`KeyList`, so can be used as a mapping to +look up units by their unit name, or iterated or accessed by index. + +## `all_joined_units` + +A list view of all the units of all relations attached to this +:class:`~charms.reactive.endpoints.Endpoint`. + +This is actually a +:class:`~charms.reactive.endpoints.CombinedUnitsView`, so the units +will be in order by relation ID and then unit name, and you can access a +merged view of all the units' data as a single mapping. You should be +very careful when using the merged data collections, however, and +consider carefully what will happen when the endpoint has multiple +relations and multiple remote units on each. It is probably better to +iterate over each unit and handle its data individually. See +:class:`~charms.reactive.endpoints.CombinedUnitsView` for an +explanation of how the merged data collections work. + +Note that, because a given application might be related multiple times +on a given endpoint, units may show up in this collection more than +once. + +## `all_requests` + +A list of all requests, including ones which have been responded to. + +## `all_units` + +.. deprecated:: 0.6.1 + Use :attr:`all_joined_units` instead + +## `endpoint_name` + +Relation name of this endpoint. + +## `is_joined` + +Whether this endpoint has remote applications attached to it. + +## `joined` + +.. deprecated:: 0.6.3 + Use :attr:`is_joined` instead + +## `def manage_flags(self)` + +Method that subclasses can override to perform any flag management +needed during startup. + +This will be called automatically after the framework-managed automatic +flags have been updated. + +## `new_requests` + +A list of requests which have not been responded. + +Requests should be handled by the charm and then responded to by +calling ``request.respond(...)``. + +## `relations` + +Collection of :class:`Relation` instances that are established for +this :class:`Endpoint`. + +This is a :class:`KeyList`, so it can be iterated and indexed as a list, +or you can look up relations by their ID. For example:: + + rel0 = endpoint.relations[0] + assert rel0 is endpoint.relations[rel0.relation_id] + assert all(rel is endpoint.relations[rel.relation_id] + for rel in endpoint.relations) + print(', '.join(endpoint.relations.keys())) + diff --git a/kubernetes-master/hooks/relations/grafana-dashboard/interface.yaml b/kubernetes-master/hooks/relations/grafana-dashboard/interface.yaml new file mode 100644 index 0000000..0ee9ef8 --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/interface.yaml @@ -0,0 +1,6 @@ +name: grafana-dashboard +summary: Interface for importing dashboards into Grafana +version: 1 +maintainer: "Cory Johns " +exclude: + - .docs diff --git a/kubernetes-master/hooks/relations/grafana-dashboard/provides.py b/kubernetes-master/hooks/relations/grafana-dashboard/provides.py new file mode 100644 index 0000000..670ded9 --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/provides.py @@ -0,0 +1,42 @@ +from charms.reactive import ( + toggle_flag, + RequesterEndpoint, +) + +from .common import ImportRequest + + +class GrafanaDashboardProvides(RequesterEndpoint): + REQUEST_CLASS = ImportRequest + + def manage_flags(self): + super().manage_flags() + toggle_flag(self.expand_name('endpoint.{endpoint_name}.failed'), + self.is_joined and self.failed_imports) + + @property + def failed_imports(self): + """ + A list of requests that failed to import. + """ + return [response + for response in self.responses + if not response.success] + + def register_dashboard(self, name, dashboard): + """ + Request a dashboard to be imported. + + :param name: Name of dashboard. Informational only, so that you can + tell which dashboard request this was, e.g. to check for success or + failure. + :param dashboard: Data structure defining the dashboard. Must be JSON + serializable. (Note: This should *not* be pre-serialized JSON.) + """ + # we might be connected to multiple grafanas for some strange + # reason, so just send the dashboard to all of them + for relation in self.relations: + ImportRequest.create_or_update(match_fields=['name'], + relation=relation, + name=name, + dashboard=dashboard) diff --git a/kubernetes-master/hooks/relations/grafana-dashboard/requires.py b/kubernetes-master/hooks/relations/grafana-dashboard/requires.py new file mode 100644 index 0000000..de696c1 --- /dev/null +++ b/kubernetes-master/hooks/relations/grafana-dashboard/requires.py @@ -0,0 +1,15 @@ +from charms.reactive import ( + toggle_flag, + ResponderEndpoint, +) + +from .common import ImportRequest + + +class GrafanaDashboardRequires(ResponderEndpoint): + REQUEST_CLASS = ImportRequest + + def manage_flags(self): + super().manage_flags() + toggle_flag(self.expand_name('endpoint.{endpoint_name}.requests'), + self.is_joined and self.new_requests) diff --git a/kubernetes-master/hooks/relations/hacluster/.stestr.conf b/kubernetes-master/hooks/relations/hacluster/.stestr.conf new file mode 100644 index 0000000..5fcccac --- /dev/null +++ b/kubernetes-master/hooks/relations/hacluster/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./unit_tests +top_dir=./ diff --git a/kubernetes-master/hooks/relations/hacluster/README.md b/kubernetes-master/hooks/relations/hacluster/README.md new file mode 100644 index 0000000..e8147ac --- /dev/null +++ b/kubernetes-master/hooks/relations/hacluster/README.md @@ -0,0 +1,90 @@ +# Overview + +This interface handles the communication with the hacluster subordinate +charm using the `ha` interface protocol. + +# Usage + +## Requires + +The interface layer will set the following reactive states, as appropriate: + + * `{relation_name}.connected` The relation is established and ready for + the local charm to configure the hacluster subordinate charm. The + configuration of the resources to manage for the hacluster charm + can be managed via one of the following methods: + + * `manage_resources` method + * `bind_on` method + + Configuration of the managed resources within the hacluster can be + managed by passing `common.CRM` object definitions to the + `manage_resources` method. + + * `{relation_name}.available` The hacluster is up and ready. + +For example: +```python +from charms.reactive import when, when_not +from charms.reactive import set_state, remove_state + +from relations.hacluster.common import CRM + + +@when('ha.connected') +def cluster_connected(hacluster): + + resources = CRM() + resources.primitive('res_vip', 'ocf:IPAddr2', + params='ip=10.0.3.100 nic=eth0', + op='monitor interval="10s"') + resources.clone('cl_res_vip', 'res_vip') + + hacluster.bind_on(iface='eth0', mcastport=4430) + hacluster.manage_resources(resources) +``` + +Additionally, for more code clarity a custom object implements the interface +defined in common.ResourceDescriptor can be used to simplify the code for +reuse. + +For example: +```python +import ipaddress + +from relation.hacluster.common import CRM +from relation.hacluster.common import ResourceDescriptor + +class VirtualIP(ResourceDescriptor): + def __init__(self, vip, nic='eth0'): + self.vip = vip + self.nic = 'eth0' + + def configure_resource(self, crm): + ipaddr = ipaddress.ip_address(self.vip) + if isinstance(ipaddr, ipaddress.IPv4Address): + res_type = 'ocf:heartbeat:IPAddr2' + res_parms = 'ip={ip} nic={nic}'.format(ip=self.vip, + nic=self.nic) + else: + res_type = 'ocf:heartbeat:IPv6addr' + res_params = 'ipv6addr={ip} nic={nic}'.format(ip=self.vip, + nic=self.nic) + + crm.primitive('res_vip', res_type, params=res_params, + op='monitor interval="10s"') + crm.clone('cl_res_vip', 'res_vip') +``` + +Once the VirtualIP class above has been defined in charm code, it can make +the code a bit cleaner. The example above can thusly be written as: + +```python +@when('ha.connected') +def cluster_connected(hacluster): + resources = CRM() + resources.add(VirtualIP('10.0.3.100')) + + hacluster.bind_on(iface='eth0', mcastport=4430) + hacluster.manage_resources(resources) +``` diff --git a/kubernetes-master/hooks/relations/hacluster/__init__.py b/kubernetes-master/hooks/relations/hacluster/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/hacluster/copyright b/kubernetes-master/hooks/relations/hacluster/copyright new file mode 100644 index 0000000..5a49dcb --- /dev/null +++ b/kubernetes-master/hooks/relations/hacluster/copyright @@ -0,0 +1,21 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0 + +Files: * +Copyright: 2015, Canonical Ltd. +License: Apache-2.0 + +License: Apache-2.0 + 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. + . + On Debian-based systems the full text of the Apache version 2.0 license + can be found in `/usr/share/common-licenses/Apache-2.0'. diff --git a/kubernetes-master/hooks/relations/hacluster/interface.yaml b/kubernetes-master/hooks/relations/hacluster/interface.yaml new file mode 100644 index 0000000..f03f3d7 --- /dev/null +++ b/kubernetes-master/hooks/relations/hacluster/interface.yaml @@ -0,0 +1,16 @@ +name: hacluster +summary: | + Provides the hacluster interface used for configuring Corosync + and Pacemaker services. +maintainer: OpenStack Charmers +ignore: + - '.gitignore' + - '.gitreview' + - '.testr.conf' + - 'test-requirements' + - 'tox.ini' + - 'unit_tests' + - '.zuul.yaml' + - 'setup.cfg' + - 'setup.py' + - '**/ops_ha_interface.py' diff --git a/kubernetes-master/hooks/relations/hacluster/interface_hacluster/__init__.py b/kubernetes-master/hooks/relations/hacluster/interface_hacluster/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/hacluster/interface_hacluster/common.py b/kubernetes-master/hooks/relations/hacluster/interface_hacluster/common.py new file mode 100644 index 0000000..e4b13ff --- /dev/null +++ b/kubernetes-master/hooks/relations/hacluster/interface_hacluster/common.py @@ -0,0 +1,1009 @@ +#!/usr/bin/python +# 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 hashlib +import ipaddress +import json +from six import string_types + + +class ResourceManagement(): + + def data_changed(self, data_id, data, hash_type='md5'): + raise NotImplementedError + + def get_local(self, key, default=None, scope=None): + raise NotImplementedError + + def set_local(self, key=None, value=None, data=None, scope=None, **kwdata): + raise NotImplementedError + + def set_remote(self, key=None, value=None, data=None, scope=None, + **kwdata): + raise NotImplementedError + + def is_clustered(self): + """Has the hacluster charm set clustered? + + The hacluster charm sets cluster=True when it determines it is ready. + Check the relation data for clustered and force a boolean return. + + :returns: boolean + """ + clustered_values = self.get_remote_all('clustered') + if clustered_values: + # There is only ever one subordinate hacluster unit + clustered = clustered_values[0] + # Future versions of hacluster will return a bool + # Current versions return a string + if type(clustered) is bool: + return clustered + elif (clustered is not None and + (clustered.lower() == 'true' or + clustered.lower() == 'yes')): + return True + return False + + def bind_on(self, iface=None, mcastport=None): + relation_data = {} + if iface: + relation_data['corosync_bindiface'] = iface + if mcastport: + relation_data['corosync_mcastport'] = mcastport + + if relation_data and self.data_changed('hacluster-bind_on', + relation_data): + self.set_local(**relation_data) + self.set_remote(**relation_data) + + def manage_resources(self, crm): + """ + Request for the hacluster to manage the resources defined in the + crm object. + + res = CRM() + res.primitive('res_neutron_haproxy', 'lsb:haproxy', + op='monitor interval="5s"') + res.init_services('haproxy') + res.clone('cl_nova_haproxy', 'res_neutron_haproxy') + + hacluster.manage_resources(crm) + + :param crm: CRM() instance - Config object for Pacemaker resources + :returns: None + """ + relation_data = { + 'json_{}'.format(k): json.dumps(v, sort_keys=True) + for k, v in crm.items() if v + } + if self.data_changed('hacluster-manage_resources', relation_data): + self.set_local(**relation_data) + self.set_remote(**relation_data) + + def bind_resources(self, iface=None, mcastport=None): + """Inform the ha subordinate about each service it should manage. The + child class specifies the services via self.ha_resources + + :param iface: string - Network interface to bind to + :param mcastport: int - Multicast port corosync should use for cluster + management traffic + """ + if mcastport is None: + mcastport = 4440 + resources_dict = self.get_local('resources') + self.bind_on(iface=iface, mcastport=mcastport) + if resources_dict: + resources = CRM(**resources_dict) + self.manage_resources(resources) + + def delete_resource(self, resource_name): + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.add_delete_resource(resource_name) + self.set_local(resources=resources) + + def add_vip(self, name, vip, iface=None, netmask=None): + """Add a VirtualIP object for each user specified vip to self.resources + + :param name: string - Name of service + :param vip: string - Virtual IP to be managed + :param iface: string - Network interface to bind vip to + :param netmask: string - Netmask for vip + :returns: None + """ + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.add( + VirtualIP( + name, + vip, + nic=iface, + cidr=netmask,)) + + # Vip Group + group = 'grp_{}_vips'.format(name) + vip_res_group_members = [] + if resource_dict: + vip_resources = resource_dict.get('resources') + if vip_resources: + for vip_res in vip_resources: + if 'vip' in vip_res: + vip_res_group_members.append(vip_res) + resources.group(group, + *sorted(vip_res_group_members)) + + self.set_local(resources=resources) + + def remove_vip(self, name, vip, iface=None): + """Remove a virtual IP + + :param name: string - Name of service + :param vip: string - Virtual IP + :param iface: string - Network interface vip bound to + """ + if iface: + nic_name = iface + else: + nic_name = hashlib.sha1(vip.encode('UTF-8')).hexdigest()[:7] + self.delete_resource('res_{}_{}_vip'.format(name, nic_name)) + + def add_init_service(self, name, service, clone=True): + """Add a InitService object for haproxy to self.resources + + :param name: string - Name of service + :param service: string - Name service uses in init system + :returns: None + """ + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.add( + InitService(name, service, clone)) + self.set_local(resources=resources) + + def remove_init_service(self, name, service): + """Remove an init service + + :param name: string - Name of service + :param service: string - Name of service used in init system + """ + res_key = 'res_{}_{}'.format( + name.replace('-', '_'), + service.replace('-', '_')) + self.delete_resource(res_key) + + def add_systemd_service(self, name, service, clone=True): + """Add a SystemdService object to self.resources + + :param name: string - Name of service + :param service: string - Name service uses in systemd + :returns: None + """ + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.add( + SystemdService(name, service, clone)) + self.set_local(resources=resources) + + def remove_systemd_service(self, name, service): + """Remove a systemd service + + :param name: string - Name of service + :param service: string - Name of service used in systemd + """ + res_key = 'res_{}_{}'.format( + name.replace('-', '_'), + service.replace('-', '_')) + self.delete_resource(res_key) + + def add_dnsha(self, name, ip, fqdn, endpoint_type): + """Add a DNS entry to self.resources + + :param name: string - Name of service + :param ip: string - IP address dns entry should resolve to + :param fqdn: string - The DNS entry name + :param endpoint_type: string - Public, private, internal etc + :returns: None + """ + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.add( + DNSEntry(name, ip, fqdn, endpoint_type)) + + # DNS Group + group = 'grp_{}_hostnames'.format(name) + dns_res_group_members = [] + if resource_dict: + dns_resources = resource_dict.get('resources') + if dns_resources: + for dns_res in dns_resources: + if 'hostname' in dns_res: + dns_res_group_members.append(dns_res) + resources.group(group, + *sorted(dns_res_group_members)) + + self.set_local(resources=resources) + + def remove_dnsha(self, name, endpoint_type): + """Remove a DNS entry + + :param name: string - Name of service + :param endpoint_type: string - Public, private, internal etc + :returns: None + """ + res_key = 'res_{}_{}_hostname'.format( + self.service_name.replace('-', '_'), + self.endpoint_type) + self.delete_resource(res_key) + + def add_colocation(self, name, score, colo_resources, node_attribute=None): + """Add a colocation directive + + :param name: string - Name of colocation directive + :param score: string - ALWAYS, INFINITY, NEVER, NEGATIVE_INFINITY}. See + CRM.colocation for more details + :param colo_resources: List[string] - List of resource names to + colocate + :param node_attribute: Colocate resources on a set of nodes with this + attribute and not necessarily on the same node. + """ + node_config = {} + if node_attribute: + node_config = { + 'node_attribute': node_attribute} + resource_dict = self.get_local('resources') + if resource_dict: + resources = CRM(**resource_dict) + else: + resources = CRM() + resources.colocation( + name, + score, + *colo_resources, + **node_config) + self.set_local(resources=resources) + + def remove_colocation(self, name): + """Remove a colocation directive + + :param name: string - Name of colocation directive + """ + self.delete_resource(name) + + def get_remote_all(self, key, default=None): + """Return a list of all values presented by remote units for key""" + raise NotImplementedError + + +class CRM(dict): + """ + Configuration object for Pacemaker resources for the HACluster + interface. This class provides access to the supported resources + available in the 'crm configure' within the HACluster. + + See Also + -------- + More documentation is available regarding the definitions of + primitives, clones, and other pacemaker resources at the crmsh + site at http://crmsh.github.io/man + """ + + # Constants provided for ordering constraints (e.g. the kind value) + MANDATORY = "Mandatory" + OPTIONAL = "Optional" + SERIALIZE = "Serialize" + + # Constants defining weights of constraints + INFINITY = "inf" + NEG_INFINITY = "-inf" + + # Constaints aliased to their interpretations for constraints + ALWAYS = INFINITY + NEVER = NEG_INFINITY + + def __init__(self, *args, **kwargs): + self['resources'] = {} + self['delete_resources'] = [] + self['resource_params'] = {} + self['groups'] = {} + self['ms'] = {} + self['orders'] = {} + self['colocations'] = {} + self['clones'] = {} + self['locations'] = {} + self['init_services'] = [] + self['systemd_services'] = [] + super(CRM, self).__init__(*args, **kwargs) + + def primitive(self, name, agent, description=None, **kwargs): + """Configures a primitive resource within Pacemaker. + + A primitive is used to describe a resource which should be managed + by the cluster. Primitives consist of a name, the agent type, and + various configuration options to the primitive. For example: + + crm.primitive('www8', 'apache', + params='configfile=/etc/apache/www8.conf', + operations='$id-ref=apache_ops') + + will create the an apache primitive (resource) for the www8 service + hosted by the Apache HTTP server. The parameters specified can either + be provided individually (e.g. a string) or as an iterable. + + The following example shows how to specify multiple ops for a drbd + volume in a Master/Slave configuration:: + + ops = ['monitor role=Master interval=60s', + 'monitor role=Slave interval=300s'] + + crm.primitive('r0', 'ocf:linbit:drbd', + params='drbd_resource=r0', + op=ops) + + Additional arguments may be passed in as kwargs in which the key of + the kwarg is prepended to the value. + + Parameters + ---------- + name: str + the name of the primitive. + agent: str + the type of agent to use to monitor the primitive resource + (e.g. ocf:linbit:drbd). + description: str, optional, kwarg + a description about the resource + params: str or iterable, optional, kwarg + parameters which are provided to the resource agent + meta: str or iterable, optional, kwarg + metadata information for the primitive resource + utilization: str or iterable, optional, kwarg + utilization information for the primitive resource + operations: str or iterable, optional, kwarg + operations information for the primitive resource in id_spec + format (e.g. $id= or $id-ref=) + op: str or iterable, optional, kwarg + op information regarding the primitive resource. This takes the + form of ' [= = ...]' + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_primitive + """ + resources = self['resources'] + resources[name] = agent + + specs = '' + if description: + specs = specs + 'description="%s"' % description + + # Use the ordering specified in the crm manual + for key in 'params', 'meta', 'utilization', 'operations', 'op': + if key not in kwargs: + continue + specs = specs + (' %s' % self._parse(key, kwargs[key])) + + if specs: + self['resource_params'][name] = specs + + def _parse(self, prefix, data): + results = '' + if isinstance(data, string_types): + data = [data] + + first = True + for d in data: + if first: + results = results + ' ' + first = False + results = results + ('%s %s ' % (prefix, d)) + results = results.rstrip() + return results + + def clone(self, name, resource, description=None, **kwargs): + """Creates a resource which should run on all nodes. + + Parameters + ---------- + name: str + the name of the clone + resource: str + the name or id of the resource to clone + description: str, optional + text containing a description for the clone + meta: str or list of str, optional, kwarg + metadata attributes to assign to the clone + params: str or list of str, optional, kwarg + parameters to assign to the clone + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_clone + """ + clone_specs = resource + if description: + clone_specs = clone_specs + (' description="%s"' % description) + + for key in 'meta', 'params': + if key not in kwargs: + continue + value = kwargs[key] + if not value: + continue + clone_specs = clone_specs + (' %s' % self._parse(key, value)) + + self['clones'][name] = clone_specs + + def colocation(self, name, score=ALWAYS, *resources, **kwargs): + """Configures the colocation constraints of resources. + + Provides placement constraints regarding resources defined within + the cluster. Using the colocate function, resource affinity or + anti-affinity can be defined. + + For example, the following code ensures that the nova-console service + always runs where the cluster vip is running: + + crm.colocation('console_with_vip', ALWAYS, + 'nova-console', 'vip') + + The affinity or anti-affinity of resources relationships is be + expressed in the `score` parameter. A positive score indicates that + the resources should run on the same node.A score of INFINITY (or + ALWAYS) will ensure the resources are always run on the same node(s) + and a score of NEG_INFINITY (or NEVER) ensures that the resources are + never run on the same node(s). + + crm.colocation('never_apache_with_dummy', NEVER, + 'apache', 'dummy') + + Any *resources values which are provided are treated as resources which + the colocation constraint applies to. At least two resources must be + defined as part of the ordering constraint. + + The resources take the form of [:role]. If the + colocation constraint applies specifically to a role, this information + should be included int he resource supplied. + + Parameters + ---------- + id: str + id or name of the colocation constraint + score: str {ALWAYS, INFINITY, NEVER, NEGATIVE_INFINITY} or int + the score or weight of the colocation constraint. A positive value + will indicate that the resources should run on the same node. A + negative value indicates that the resources should run on separate + nodes. + resources: str or list + the list of resources which the colocation constraint applies to. + node_attribute: str, optional, kwarg + can be used to run the resources on a set of nodes, not just a + single node. + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_colocation + """ + specs = '%s: %s' % (score, ' '.join(resources)) + if 'node_attribute' in kwargs: + specs = specs + (' node-attribute=%s' % kwargs['node_attribute']) + self['colocations'][name] = specs + + def group(self, name, *resources, **kwargs): + """Creates a group of resources within Pacemaker. + + The created group includes the list of resources provided in the list + of resources supplied. For example:: + + crm.group('grp_mysql', 'res_mysql_rbd', 'res_mysql_fs', + 'res_mysql_vip', 'res_mysqld') + + will create the 'grp_mysql' resource group consisting of the + res_mysql_rbd, res_mysql_fs, res_mysql_vip, and res_mysqld resources. + + Parameters + ---------- + name: str + the name of the group resource + resources: list of str + the names or ids of resources to include within the group. + description: str, optional, kwarg + text to describe the resource + meta: str or list of str, optional, kwarg + metadata attributes to assign to the group + params: str or list of str, optional, kwarg + parameters to assign to the group + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_group + """ + specs = ' '.join(resources) + if 'description' in kwargs: + specs = specs + (' description=%s"' % kwargs['description']) + + for key in 'meta', 'params': + if key not in kwargs: + continue + value = kwargs[key] + specs = specs + (' %s' % self._parse(key, value)) + + self['groups'][name] = specs + + def remove_deleted_resources(self): + """Work through the existing resources and remove any mention of ones + which have been marked for deletion.""" + for res in self['delete_resources']: + for key in self.keys(): + if key == 'delete_resources': + continue + if isinstance(self[key], dict) and res in self[key].keys(): + del self[key][res] + elif isinstance(self[key], list) and res in self[key]: + self[key].remove(res) + elif isinstance(self[key], tuple) and res in self[key]: + self[key] = tuple(x for x in self[key] if x != res) + + def delete_resource(self, *resources): + """Specify objects/resources to be deleted from within Pacemaker. This + is not additive, the list of resources is set to exaclty what was + passed in. + + Parameters + ---------- + resources: str or list + the name or id of the specific resource to delete. + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_delete + """ + self['delete_resources'] = resources + self.remove_deleted_resources() + + def add_delete_resource(self, resource): + """Specify an object/resource to delete from within Pacemaker. It can + be called multiple times to add additional resources to the deletion + list. + + Parameters + ---------- + resources: str + the name or id of the specific resource to delete. + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_delete + """ + if resource not in self['delete_resources']: + # NOTE(fnordahl): this unpleasant piece of code is regrettably + # necessary for Python3.4 (and trusty) compability see LP: #1814218 + # and LP: #1813982 + self['delete_resources'] = tuple( + self['delete_resources'] or ()) + (resource,) + self.remove_deleted_resources() + + def init_services(self, *resources): + """Specifies that the service(s) is an init or upstart service. + + Services (resources) which are noted as upstart services are + disabled, stopped, and left to pacemaker to manage the resource. + + Parameters + ---------- + resources: str or list of str, varargs + The resources which should be noted as init services. + + Returns + ------- + None + """ + self['init_services'] = resources + + def systemd_services(self, *resources): + """Specifies that the service(s) is a systemd service. + + Services (resources) which are noted as systemd services are + disabled, stopped, and left to pacemaker to manage the resource. + + Parameters + ---------- + resources: str or list of str, varargs + The resources which should be noted as systemd services. + + Returns + ------- + None + """ + self['systemd_services'] = resources + + def ms(self, name, resource, description=None, **kwargs): + """Create a master/slave resource type. + + The following code provides an example of creating a master/slave + resource on drbd disk1:: + + crm.ms('disk1', 'drbd1', meta='notify=true globally-unique=false') + + Parameters + ---------- + name: str + the name or id of the master resource + resource: str + the name or id of the resource which now ha a master/slave + assocation tied to it. + description: str, optional + a textual description of the master resource + meta: str or list of strs, optional, kwargs + strings defining the metadata for the master/slave resource type + params: str or list of strs, optional, kwargs + parameter strings which should be passed to the master/slave + resource creation + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_ms + """ + specs = resource + if description: + specs = specs + (' description="%s"' % description) + + for key in 'meta', 'params': + if key not in kwargs: + continue + value = kwargs[key] + specs = specs + (' %s' % self._parse(key, value)) + + self['ms'][name] = specs + + def location(self, name, resource, **kwargs): + """Defines the preference of nodes for the given resource. + + The location constraitns consist of one or more rules which specify + a score to be awarded if the rules match. + + Parameters + ---------- + name: str + the name or id of the location constraint + resource: str + the name, id, resource, set, tag, or resoruce pattern defining the + set of resources which match the location placement constraint. + attributes: str or list str, optional, kwarg + attributes which should be assigned to the location constraint + rule: str or list of str, optional, kwarg + the rule(s) which define the location constraint rules when + selecting a location to run the resource. + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_location + """ + specs = resource + + # Check if there are attributes assigned to the location and if so, + # format the spec string with the attributes + if 'attributes' in kwargs: + attrs = kwargs['attributes'] + if isinstance(attrs, string_types): + attrs = [attrs] + specs = specs + (' %s' % ' '.join(attrs)) + + if 'rule' in kwargs: + rules = kwargs['rule'] + specs = specs + (' %s' % self._parse('rule', rules)) + + self['locations'][name] = specs + + def order(self, name, score=None, *resources, **kwargs): + """Configures the ordering constraints of resources. + + Provides ordering constraints to resources defined in a Pacemaker + cluster which affect the way that resources are started, stopped, + promoted, etc. Basic ordering is provided by simply specifying the + ordering name and an ordered list of the resources which the ordering + constraint applies to. + + For example, the following code ensures that the apache resource is + started after the ClusterIP is started:: + + hacluster.order('apache-after-ip', 'ClusterIP', 'apache') + + By default, the ordering constraint will specify that the ordering + constraint is mandatory. The constraint behavior can be specified + using the 'score' keyword argument, e.g.:: + + hacluster.order('apache-after-ip', score=hacluster.OPTIONAL, + 'ClusterIP', 'apache') + + Any *resources values which are provided are treated as resources which + the ordering constraint applies to. At least two resources must be + defined as part of the ordering constraint. + + The resources take the form of [:]. If the + ordering constraint applies to a specific action for the resource, + this information should be included in the resource supplied. + + Parameters + ---------- + name: str + the id or name of the order constraint + resoures: str or list of strs in varargs format + the resources the ordering constraint applies to. The ordering + of the list of resources is used to provide the ordering. + score: {MANDATORY, OPTIONAL, SERIALIZED}, optional + the score of the ordering constraint. + symmetrical: boolean, optional, kwarg + when True, then the services for the resources will be stopped in + the reverse order. The default value for this is True. + + Returns + ------- + None + + See Also + -------- + http://crmsh.github.io/man/#cmdhelp_configure_order + """ + specs = '' + if score: + specs = '%s:' % score + + specs = specs + (' %s' % ' '.join(resources)) + if 'symmetrical' in kwargs: + specs = specs + (' symmetrical=' % kwargs['symmetrical']) + + self['orders'][name] = specs + + def add(self, resource_desc): + """Adds a resource descriptor object to the CRM configuration. + + Adds a `ResourceDescriptor` object to the CRM configuration which + understands how to configure the resource itself. The + `ResourceDescriptor` object needs to know how to interact with this + CRM class in order to properly configure the pacemaker resources. + + The minimum viable resource descriptor object will implement a method + which takes a reference parameter to this CRM in order to configure + itself. + + Parameters + ---------- + resource_desC: ResourceDescriptor + an object which provides an abstraction of a monitored resource + within pacemaker. + + Returns + ------- + None + """ + method = getattr(resource_desc, 'configure_resource', None) + if not callable(method): + raise ValueError('Invalid resource_desc. The "configure_resource"' + ' function has not been defined.') + + method(self) + + +class ResourceDescriptor(object): + """ + A ResourceDescriptor provides a logical resource or concept and knows + how to configure pacemaker. + """ + + def configure_resource(self, crm): + """Configures the logical resource(s) within the CRM. + + This is the callback method which is invoked by the CRM in order + to allow this ResourceDescriptor to fully configure the logical + resource. + + For example, a Virtual IP may provide a standard abstraction and + configure the specific details under the covers. + """ + pass + + +class InitService(ResourceDescriptor): + def __init__(self, service_name, init_service_name, clone=True): + """Class for managing init resource + + :param service_name: string - Name of service + :param init_service_name: string - Name service uses in init system + :param clone: bool - clone service across all units + :returns: None + """ + self.service_name = service_name + self.init_service_name = init_service_name + self.clone = clone + + def configure_resource(self, crm): + """"Configure new init system service resource in crm + + :param crm: CRM() instance - Config object for Pacemaker resources + :returns: None + """ + res_key = 'res_{}_{}'.format( + self.service_name.replace('-', '_'), + self.init_service_name.replace('-', '_')) + res_type = 'lsb:{}'.format(self.init_service_name) + _meta = 'migration-threshold="INFINITY" failure-timeout="5s"' + crm.primitive( + res_key, res_type, op='monitor interval="5s"', meta=_meta) + crm.init_services(self.init_service_name) + if self.clone: + clone_key = 'cl_{}'.format(res_key) + crm.clone(clone_key, res_key) + + +class VirtualIP(ResourceDescriptor): + def __init__(self, service_name, vip, nic=None, cidr=None): + """Class for managing VIP resource + + :param service_name: string - Name of service + :param vip: string - Virtual IP to be managed + :param nic: string - Network interface to bind vip to + :param cidr: string - Netmask for vip + :returns: None + """ + self.service_name = service_name + self.vip = vip + self.nic = nic + self.cidr = cidr + + def configure_resource(self, crm): + """Configure new vip resource in crm + + :param crm: CRM() instance - Config object for Pacemaker resources + :returns: None + """ + if self.nic: + vip_key = 'res_{}_{}_vip'.format(self.service_name, self.nic) + else: + vip_key = 'res_{}_{}_vip'.format( + self.service_name, + hashlib.sha1(self.vip.encode('UTF-8')).hexdigest()[:7]) + ipaddr = ipaddress.ip_address(self.vip) + if isinstance(ipaddr, ipaddress.IPv4Address): + res_type = 'ocf:heartbeat:IPaddr2' + res_params = 'ip="{}"'.format(self.vip) + else: + res_type = 'ocf:heartbeat:IPv6addr' + res_params = 'ipv6addr="{}"'.format(self.vip) + vip_params = 'ipv6addr' + vip_key = 'res_{}_{}_{}_vip'.format(self.service_name, self.nic, + vip_params) + + if self.nic: + res_params = '{} nic="{}"'.format(res_params, self.nic) + if self.cidr: + res_params = '{} cidr_netmask="{}"'.format(res_params, self.cidr) + # Monitor the VIP + _op_monitor = 'monitor timeout="20s" interval="10s" depth="0"' + _meta = 'migration-threshold="INFINITY" failure-timeout="5s"' + crm.primitive( + vip_key, res_type, params=res_params, op=_op_monitor, meta=_meta) + + +class DNSEntry(ResourceDescriptor): + + def __init__(self, service_name, ip, fqdn, endpoint_type): + """Class for managing DNS entries + + :param service_name: string - Name of service + :param ip: string - IP to point DNS entry at + :param fqdn: string - DNS Entry + :param endpoint_type: string - The type of the endpoint represented by + the DNS record eg public, admin etc + :returns: None + """ + self.service_name = service_name + self.ip = ip + self.fqdn = fqdn + self.endpoint_type = endpoint_type + + def configure_resource(self, crm, res_type='ocf:maas:dns'): + """Configure new DNS resource in crm + + :param crm: CRM() instance - Config object for Pacemaker resources + :param res_type: string - Corosync Open Cluster Framework resource + agent to use for DNS HA + :returns: None + """ + res_key = 'res_{}_{}_hostname'.format( + self.service_name.replace('-', '_'), + self.endpoint_type) + res_params = '' + if self.fqdn: + res_params = '{} fqdn="{}"'.format(res_params, self.fqdn) + if self.ip: + res_params = '{} ip_address="{}"'.format(res_params, self.ip) + crm.primitive(res_key, res_type, params=res_params) + + +class SystemdService(ResourceDescriptor): + def __init__(self, service_name, systemd_service_name, clone=True): + """Class for managing systemd resource + + :param service_name: string - Name of service + :param systemd_service_name: string - Name service uses in + systemd system + :param clone: bool - clone service across all units + :returns: None + """ + self.service_name = service_name + self.systemd_service_name = systemd_service_name + self.clone = clone + + def configure_resource(self, crm): + """"Configure new systemd system service resource in crm + + :param crm: CRM() instance - Config object for Pacemaker resources + :returns: None + """ + res_key = 'res_{}_{}'.format( + self.service_name.replace('-', '_'), + self.systemd_service_name.replace('-', '_')) + res_type = 'systemd:{}'.format(self.systemd_service_name) + _meta = 'migration-threshold="INFINITY" failure-timeout="5s"' + crm.primitive( + res_key, res_type, op='monitor interval="5s"', meta=_meta) + crm.systemd_services(self.systemd_service_name) + if self.clone: + clone_key = 'cl_{}'.format(res_key) + crm.clone(clone_key, res_key) diff --git a/kubernetes-master/hooks/relations/hacluster/requires.py b/kubernetes-master/hooks/relations/hacluster/requires.py new file mode 100644 index 0000000..395a658 --- /dev/null +++ b/kubernetes-master/hooks/relations/hacluster/requires.py @@ -0,0 +1,58 @@ +#!/usr/bin/python +# 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 relations.hacluster.interface_hacluster.common as common +from charms.reactive import hook +from charms.reactive import RelationBase +from charms.reactive import scopes +from charms.reactive.helpers import data_changed as rh_data_changed +from charmhelpers.core import hookenv + + +class HAClusterRequires(RelationBase, common.ResourceManagement): + # The hacluster charm is a subordinate charm and really only works + # for a single service to the HA Cluster relation, therefore set the + # expected scope to be GLOBAL. + scope = scopes.GLOBAL + + @hook('{requires:hacluster}-relation-joined') + def joined(self): + self.set_state('{relation_name}.connected') + + @hook('{requires:hacluster}-relation-changed') + def changed(self): + if self.is_clustered(): + self.set_state('{relation_name}.available') + else: + self.remove_state('{relation_name}.available') + + @hook('{requires:hacluster}-relation-{broken,departed}') + def departed(self): + self.remove_state('{relation_name}.available') + self.remove_state('{relation_name}.connected') + + def data_changed(self, data_id, data, hash_type='md5'): + return rh_data_changed(data_id, data, hash_type) + + def get_remote_all(self, key, default=None): + """Return a list of all values presented by remote units for key""" + values = [] + for conversation in self.conversations(): + for relation_id in conversation.relation_ids: + for unit in hookenv.related_units(relation_id): + value = hookenv.relation_get(key, + unit, + relation_id) or default + if value: + values.append(value) + return list(set(values)) diff --git a/kubernetes-master/hooks/relations/hacluster/test-requirements.txt b/kubernetes-master/hooks/relations/hacluster/test-requirements.txt new file mode 100644 index 0000000..12452e5 --- /dev/null +++ b/kubernetes-master/hooks/relations/hacluster/test-requirements.txt @@ -0,0 +1,7 @@ +# Lint and unit test requirements +flake8 +stestr>=2.2.0 +charms.reactive +coverage>=3.6 +netifaces +git+https://github.com/canonical/operator.git#egg=ops diff --git a/kubernetes-master/hooks/relations/http/.gitignore b/kubernetes-master/hooks/relations/http/.gitignore new file mode 100644 index 0000000..3374ec2 --- /dev/null +++ b/kubernetes-master/hooks/relations/http/.gitignore @@ -0,0 +1,5 @@ +# Emacs save files +*~ +\#*\# +.\#* + diff --git a/kubernetes-master/hooks/relations/http/README.md b/kubernetes-master/hooks/relations/http/README.md new file mode 100644 index 0000000..3d7822a --- /dev/null +++ b/kubernetes-master/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/kubernetes-master/hooks/relations/http/__init__.py b/kubernetes-master/hooks/relations/http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/http/interface.yaml b/kubernetes-master/hooks/relations/http/interface.yaml new file mode 100644 index 0000000..54e7748 --- /dev/null +++ b/kubernetes-master/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/kubernetes-master/hooks/relations/http/provides.py b/kubernetes-master/hooks/relations/http/provides.py new file mode 100644 index 0000000..86fa9b3 --- /dev/null +++ b/kubernetes-master/hooks/relations/http/provides.py @@ -0,0 +1,67 @@ +import json + +from charmhelpers.core import hookenv +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag +from charms.reactive import Endpoint + + +class HttpProvides(Endpoint): + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + + def get_ingress_address(self, rel_id=None): + # If no rel_id is provided, we fallback to the first one + if rel_id is None: + rel_id = self.relations[0].relation_id + return hookenv.ingress_address(rel_id, hookenv.local_unit()) + + def configure(self, port, private_address=None, hostname=None): + ''' configure the address(es). private_address and hostname can + be None, a single string address/hostname, or a list of addresses + and hostnames. Note that if a list is passed, it is assumed both + private_address and hostname are either lists or None ''' + for relation in self.relations: + ingress_address = self.get_ingress_address(relation.relation_id) + if type(private_address) is list or type(hostname) is list: + # build 3 lists to zip together that are the same length + length = max(len(private_address), len(hostname)) + p = [port] * length + a = private_address + [ingress_address] *\ + (length - len(private_address)) + h = hostname + [ingress_address] * (length - len(hostname)) + zipped_list = zip(p, a, h) + # now build an array of dictionaries from that in the desired + # format for the interface + data_list = [{'hostname': h, 'port': p, 'private-address': a} + for p, a, h in zipped_list] + # for backwards compatibility, we just send a single entry + # and have an array of dictionaries in a field of that + # entry for the other entries. + data = data_list.pop(0) + data['extended_data'] = json.dumps(data_list) + + relation.to_publish_raw.update(data) + else: + relation.to_publish_raw.update({ + 'hostname': hostname or ingress_address, + 'private-address': private_address or ingress_address, + 'port': port, + }) + + def set_remote(self, **kwargs): + # NB: This method provides backwards compatibility for charms that + # called RelationBase.set_remote. Most commonly, this was done by + # charms that needed to pass reverse proxy stanzas to http proxies. + # This type of interaction with base relation classes is discouraged, + # and should be handled with logic encapsulated in appropriate + # interfaces. Eventually, this method will be deprecated in favor of + # that behavior. + for relation in self.relations: + relation.to_publish_raw.update(kwargs) diff --git a/kubernetes-master/hooks/relations/http/requires.py b/kubernetes-master/hooks/relations/http/requires.py new file mode 100644 index 0000000..17ea6b7 --- /dev/null +++ b/kubernetes-master/hooks/relations/http/requires.py @@ -0,0 +1,76 @@ +import json + +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag +from charms.reactive import Endpoint + + +class HttpRequires(Endpoint): + + @when('endpoint.{endpoint_name}.changed') + def changed(self): + if any(unit.received_raw['port'] for unit in self.all_joined_units): + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_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, + 'private-address': private_address_of_host, + 'port': port_for_host, + }, + # ... + ], + }, + # ... + ] + """ + def build_service_host(data): + private_address = data['private-address'] + host = data['hostname'] or private_address + if host and data['port']: + return (host, private_address, data['port']) + else: + return None + + services = {} + for relation in self.relations: + service_name = relation.application_name + service = services.setdefault(service_name, { + 'service_name': service_name, + 'hosts': [], + }) + host_set = set() + for unit in relation.joined_units: + data = unit.received_raw + host = build_service_host(data) + if host: + host_set.add(host) + + # if we have extended data, add it + if 'extended_data' in data: + for ed in json.loads(data['extended_data']): + host = build_service_host(ed) + if host: + host_set.add(host) + + service['hosts'] = [ + {'hostname': h, 'private-address': pa, 'port': p} + for h, pa, p in sorted(host_set) + ] + + ret = [s for s in services.values() if s['hosts']] + return ret diff --git a/kubernetes-master/hooks/relations/keystone-credentials/.gitignore b/kubernetes-master/hooks/relations/keystone-credentials/.gitignore new file mode 100644 index 0000000..172bf57 --- /dev/null +++ b/kubernetes-master/hooks/relations/keystone-credentials/.gitignore @@ -0,0 +1 @@ +.tox diff --git a/kubernetes-master/hooks/relations/keystone-credentials/.gitreview b/kubernetes-master/hooks/relations/keystone-credentials/.gitreview new file mode 100644 index 0000000..b9fc7e4 --- /dev/null +++ b/kubernetes-master/hooks/relations/keystone-credentials/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.opendev.org +port=29418 +project=openstack/charm-interface-keystone-credentials diff --git a/kubernetes-master/hooks/relations/keystone-credentials/.stestr.conf b/kubernetes-master/hooks/relations/keystone-credentials/.stestr.conf new file mode 100644 index 0000000..5fcccac --- /dev/null +++ b/kubernetes-master/hooks/relations/keystone-credentials/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./unit_tests +top_dir=./ diff --git a/kubernetes-master/hooks/relations/keystone-credentials/.zuul.yaml b/kubernetes-master/hooks/relations/keystone-credentials/.zuul.yaml new file mode 100644 index 0000000..23d3066 --- /dev/null +++ b/kubernetes-master/hooks/relations/keystone-credentials/.zuul.yaml @@ -0,0 +1,3 @@ +- project: + templates: + - python-charm-interface-jobs diff --git a/kubernetes-master/hooks/relations/keystone-credentials/__init__.py b/kubernetes-master/hooks/relations/keystone-credentials/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/keystone-credentials/copyright b/kubernetes-master/hooks/relations/keystone-credentials/copyright new file mode 100644 index 0000000..5a49dcb --- /dev/null +++ b/kubernetes-master/hooks/relations/keystone-credentials/copyright @@ -0,0 +1,21 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0 + +Files: * +Copyright: 2015, Canonical Ltd. +License: Apache-2.0 + +License: Apache-2.0 + 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. + . + On Debian-based systems the full text of the Apache version 2.0 license + can be found in `/usr/share/common-licenses/Apache-2.0'. diff --git a/kubernetes-master/hooks/relations/keystone-credentials/interface.yaml b/kubernetes-master/hooks/relations/keystone-credentials/interface.yaml new file mode 100644 index 0000000..5d99a86 --- /dev/null +++ b/kubernetes-master/hooks/relations/keystone-credentials/interface.yaml @@ -0,0 +1,16 @@ +name: keystone-credentials +summary: > + Interface for integrating with Keystone identity credentials + Charms use this relation to obtain keystone credentials + without creating a service catalog entry. Set 'username' + only on the relation and keystone will set defaults and + return authentication details. Possible relation settings: + username: Username to be created. + project: Project (tenant) name to be created. Defaults to services + project. + requested_roles: Comma delimited list of roles to be created + requested_grants: Comma delimited list of roles to be granted. + Defaults to Admin role. + domain: Keystone v3 domain the user will be created in. Defaults + to the Default domain. +maintainer: OpenStack Charmers diff --git a/kubernetes-master/hooks/relations/keystone-credentials/provides.py b/kubernetes-master/hooks/relations/keystone-credentials/provides.py new file mode 100644 index 0000000..e5a9dec --- /dev/null +++ b/kubernetes-master/hooks/relations/keystone-credentials/provides.py @@ -0,0 +1,35 @@ +# 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. + +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class KeystoneProvides(RelationBase): + scope = scopes.GLOBAL + + @hook('{provides:keystone-credentials}-relation-joined') + def joined(self): + self.set_flag('{relation_name}.connected') + + @hook('{provides:keystone-credentials}-relation-{broken,departed}') + def departed(self): + self.clear_flag('{relation_name}.connected') + + def expose_credentials(self, credentials): + """Expose Keystone credentials to related units. + + :param credentials: The Keystone credentials to be exposed. + :type credentials: dict + """ + self.set_remote(**credentials) diff --git a/kubernetes-master/hooks/relations/keystone-credentials/requires.py b/kubernetes-master/hooks/relations/keystone-credentials/requires.py new file mode 100644 index 0000000..93c7a53 --- /dev/null +++ b/kubernetes-master/hooks/relations/keystone-credentials/requires.py @@ -0,0 +1,141 @@ +#!/usr/bin/python +# 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. + +from charmhelpers.core import hookenv +from charms.reactive import RelationBase +from charms.reactive import hook +from charms.reactive import scopes + + +class KeystoneRequires(RelationBase): + scope = scopes.GLOBAL + + # These remote data fields will be automatically mapped to accessors + # with a basic documentation string provided. + + auto_accessors = ['private-address', 'credentials_host', + 'credentials_protocol', 'credentials_port', + 'credentials_project', 'credentials_username', + 'credentials_password', 'credentials_project_id', + 'credentials_project_domain_id', + 'credentials_user_domain_id', + 'credentials_project_domain_name', + 'credentials_user_domain_name', + 'api_version', 'auth_host', 'auth_protocol', 'auth_port', + 'region', 'ca_cert', 'https_keystone'] + + @hook('{requires:keystone-credentials}-relation-joined') + def joined(self): + self.set_state('{relation_name}.connected') + self.update_state() + + def update_state(self): + """Update the states of the relations based on the data that the + relation has. + + If the :meth:`base_data_complete` is False then all of the states + are removed. Otherwise, the individual states are set according to + their own data methods. + """ + base_complete = self.base_data_complete() + states = { + '{relation_name}.available': True, + '{relation_name}.available.ssl': self.ssl_data_complete(), + '{relation_name}.available.auth': self.auth_data_complete() + } + for k, v in states.items(): + if base_complete and v: + self.set_state(k) + else: + self.remove_state(k) + + @hook('{requires:keystone-credentials}-relation-changed') + def changed(self): + self.update_state() + self.set_state('{relation_name}.available.updated') + hookenv.atexit(self._clear_updated) + + @hook('{requires:keystone-credentials}-relation-{broken,departed}') + def departed(self): + self.update_state() + + def base_data_complete(self): + data = { + 'private-address': self.private_address(), + 'credentials_host': self.credentials_host(), + 'credentials_protocol': self.credentials_protocol(), + 'credentials_port': self.credentials_port(), + 'api_version': self.api_version(), + 'auth_host': self.auth_host(), + 'auth_protocol': self.auth_protocol(), + 'auth_port': self.auth_port(), + } + if all(data.values()): + return True + return False + + def auth_data_complete(self): + data = { + 'credentials_project': self.credentials_project(), + 'credentials_username': self.credentials_username(), + 'credentials_password': self.credentials_password(), + 'credentials_project_id': self.credentials_project_id(), + } + if all(data.values()): + return True + return False + + def ssl_data_complete(self): + data = { + 'https_keystone': self.https_keystone(), + 'ca_cert': self.ca_cert(), + } + for value in data.values(): + if not value or value == '__null__': + return False + return True + + def request_credentials(self, username, project=None, region=None, + requested_roles=None, requested_grants=None, + domain=None): + """ + Request credentials from Keystone + + :side effect: set requested paramaters on the identity-credentials + relation + + Required parameter + :param username: Username to be created. + + Optional parametrs + :param project: Project (tenant) name to be created. Defaults to + services project. + :param requested_roles: Comma delimited list of roles to be created + :param requested_grants: Comma delimited list of roles to be granted. + Defaults to Admin role. + :param domain: Keystone v3 domain the user will be created in. Defaults + to the Default domain. + """ + relation_info = { + 'username': username, + 'project': project, + 'requested_roles': requested_roles, + 'requested_grants': requested_grants, + 'domain': domain, + } + + self.set_local(**relation_info) + self.set_remote(**relation_info) + + def _clear_updated(self): + self.remove_state('{relation_name}.available.updated') diff --git a/kubernetes-master/hooks/relations/keystone-credentials/test-requirements.txt b/kubernetes-master/hooks/relations/keystone-credentials/test-requirements.txt new file mode 100644 index 0000000..9ea2415 --- /dev/null +++ b/kubernetes-master/hooks/relations/keystone-credentials/test-requirements.txt @@ -0,0 +1,2 @@ +flake8>=2.2.4 +stestr>=2.2.0 diff --git a/kubernetes-master/hooks/relations/kube-control/.travis.yml b/kubernetes-master/hooks/relations/kube-control/.travis.yml new file mode 100644 index 0000000..d2be8be --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-control/.travis.yml @@ -0,0 +1,9 @@ +language: python +python: + - "3.5" + - "3.6" + - "3.7" +install: + - pip install tox-travis +script: + - tox diff --git a/kubernetes-master/hooks/relations/kube-control/README.md b/kubernetes-master/hooks/relations/kube-control/README.md new file mode 100644 index 0000000..6f9ecb7 --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-control/README.md @@ -0,0 +1,171 @@ +# kube-control interface + +This interface provides communication between master and workers in a +Kubernetes cluster. + + +## Provides (kubernetes-master side) + + +### States + +* `kube-control.connected` + + Enabled when a worker has joined the relation. + +* `kube-control.gpu.available` + + Enabled when any worker has indicated that it is running in gpu mode. + +* `kube-control.departed` + + Enabled when any worker has indicated that it is leaving the cluster. + + +* `kube-control.auth.requested` + + Enabled when an authentication credential is requested. This state is + temporary and will be removed once the units authentication request has + been fulfilled. + +### Methods + +* `kube_control.set_dns(port, domain, sdn_ip)` + + Sends DNS info to the connected worker(s). + + +* `kube_control.auth_user()` + + Returns a list of the requested username and group requested for + authentication. + +* `kube_control.sign_auth_request(scope, user, kubelet_token, proxy_token, client_token)` + + Sends authentication tokens to the unit scope for the requested user + and kube-proxy services. + +* `kube_control.set_cluster_tag(cluster_tag)` + + Sends a tag used to identify resources that are part of the cluster to the + connected worker(s). + +* `kube_control.flush_departed()` + + Returns the unit departing the kube_control relationship so you can do any + post removal cleanup. Such as removing authentication tokens for the unit. + Invoking this method will also remove the `kube-control.departed` state + +* `kube_control.set_registry_location(registry_location)` + Sends the container image registry location to the connected worker(s). + +### Examples + +```python + +@when('kube-control.connected') +def send_dns(kube_control): + # send port, domain, sdn_ip to the remote side + kube_control.set_dns(53, "cluster.local", "10.1.0.10") + +@when('kube-control.gpu.available') +def on_gpu_available(kube_control): + # The remote side is gpu-enable, handle it somehow + assert kube_control.get_gpu() == True + + +@when('kube-control.departed') +@when('leadership.is_leader') +def flush_auth_for_departed(kube_control): + ''' Unit has left the cluster and needs to have its authentication + tokens removed from the token registry ''' + departing_unit = kube_control.flush_departed() + +``` + +## Requires (kubernetes-worker side) + + +### States + +* `kube-control.connected` + + Enabled when a master has joined the relation. + +* `kube-control.dns.available` + + Enabled when DNS info is available from the master. + +* `kube-control.auth.available` + + Enabled when authentication credentials are present from the master. + +* `kube-control.cluster_tag.available` + + Enabled when cluster tag is present from the master. + +* `kube-control.registry_location.available` + + Enabled when registry location is present from the master. + +### Methods + +* `kube_control.get_dns()` + + Returns a dictionary of DNS info sent by the master. The keys in the + dict are: domain, private-address, sdn-ip, port. + +* `kube_control.set_gpu(enabled=True)` + + Tell the master that we are gpu-enabled. + +* `kube_control.get_auth_credentials(user)` + + Returns a dict with the users authentication credentials. + +* `set_auth_request(kubelet, group='system:nodes')` + + Issue an authentication request against the master to receive token based + auth credentials in return. + +* `kube_control.get_cluster_tag()` + + Returns the cluster tag provided by the master. + +* `kube_control.get_registry_location()` + + Returns the container image registry location provided by the master. + +### Examples + +```python + +@when('kube-control.dns.available') +def on_dns_available(kube_control): + # Remote side has sent DNS info + dns = kube_control.get_dns() + print(context['domain']) + print(context['private-address']) + print(context['sdn-ip']) + print(context['port']) + +@when('kube-control.connected') +def send_gpu(kube_control): + # Tell the master that we're gpu-enabled + kube_control.set_gpu(True) + +@when('kube-control.auth.available') +def display_auth_tokens(kube_control): + # Remote side has sent auth info + auth = kube_control.get_auth_credentials('root') + print(auth['kubelet_token']) + print(auth['proxy_token']) + print(auth['client_token']) + +@when('kube-control.connected') +@when_not('kube-control.auth.available') +def request_auth_credentials(kube_control): + # Request an admin user with sudo level access named 'root' + kube_control.set_auth_request('root', group='system:masters') + +``` diff --git a/kubernetes-master/hooks/relations/kube-control/__init__.py b/kubernetes-master/hooks/relations/kube-control/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/kube-control/interface.yaml b/kubernetes-master/hooks/relations/kube-control/interface.yaml new file mode 100644 index 0000000..2f0b187 --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-control/interface.yaml @@ -0,0 +1,6 @@ +name: kube-control +summary: Provides master-worker communication. +version: 1 +maintainer: "Tim Van Steenburgh " +ignore: +- tests diff --git a/kubernetes-master/hooks/relations/kube-control/provides.py b/kubernetes-master/hooks/relations/kube-control/provides.py new file mode 100644 index 0000000..918ace1 --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-control/provides.py @@ -0,0 +1,160 @@ +#!/usr/local/sbin/charm-env python3 +# 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. +from charms.reactive import ( + Endpoint, + toggle_flag, + set_flag, + data_changed +) + +from charmhelpers.core import ( + hookenv, + unitdata +) + + +DB = unitdata.kv() + + +class KubeControlProvider(Endpoint): + """ + Implements the kubernetes-master side of the kube-control interface. + """ + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.connected'), + self.is_joined) + toggle_flag(self.expand_name('{endpoint_name}.gpu.available'), + self.is_joined and self._get_gpu()) + requests_data_id = self.expand_name('{endpoint_name}.requests') + requests = self.auth_user() + if data_changed(requests_data_id, requests): + set_flag(self.expand_name('{endpoint_name}.requests.changed')) + + def set_dns(self, port, domain, sdn_ip, enable_kube_dns): + """ + Send DNS info to the remote units. + + We'll need the port, domain, and sdn_ip of the dns service. If + sdn_ip is not required in your deployment, the units private-ip + is available implicitly. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'port': port, + 'domain': domain, + 'sdn-ip': sdn_ip, + 'enable-kube-dns': enable_kube_dns, + }) + + def auth_user(self): + """ + Return the kubelet_user value on the wire from the requestors. + """ + requests = [] + + for unit in self.all_joined_units: + requests.append( + (unit.unit_name, + {'user': unit.received_raw.get('kubelet_user'), + 'group': unit.received_raw.get('auth_group')}) + ) + + requests.sort() + return requests + + def sign_auth_request(self, scope, user, kubelet_token, proxy_token, + client_token): + """ + Send authorization tokens to the requesting unit. + """ + cred = { + 'scope': scope, + 'kubelet_token': kubelet_token, + 'proxy_token': proxy_token, + 'client_token': client_token + } + + if not DB.get('creds'): + DB.set('creds', {}) + + all_creds = DB.get('creds') + all_creds[user] = cred + DB.set('creds', all_creds) + + for relation in self.relations: + relation.to_publish.update({ + 'creds': all_creds + }) + + def clear_creds(self): + """ + Clear creds from the relation. This is used by non-leader units to stop + advertising creds so that the leader can assume full control of them. + """ + DB.unset('creds') + for relation in self.relations: + relation.to_publish_raw['creds'] = '' + + def _get_gpu(self): + """ + Return True if any remote worker is gpu-enabled. + """ + for unit in self.all_joined_units: + if unit.received_raw.get('gpu') == 'True': + hookenv.log('Unit {} has gpu enabled'.format(unit)) + return True + + return False + + def set_cluster_tag(self, cluster_tag): + """ + Send the cluster tag to the remote units. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'cluster-tag': cluster_tag + }) + + def set_registry_location(self, registry_location): + """ + Send the registry location to the remote units. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'registry-location': registry_location + }) + + def set_cohort_keys(self, cohort_keys): + """ + Send the cohort snapshot keys. + """ + for relation in self.relations: + relation.to_publish['cohort-keys'] = cohort_keys + + def set_default_cni(self, default_cni): + """ + Send the default CNI. The default_cni value should be a string + containing the name of a related CNI application to use as the + default CNI. For example: "flannel" or "calico". If no default has + been chosen then "" can be sent instead. + """ + for relation in self.relations: + relation.to_publish['default-cni'] = default_cni + + def set_api_endpoints(self, endpoints): + """ + Send the list of API endpoint URLs to which workers should connect. + """ + endpoints = sorted(endpoints) + for relation in self.relations: + relation.to_publish['api-endpoints'] = endpoints diff --git a/kubernetes-master/hooks/relations/kube-control/requires.py b/kubernetes-master/hooks/relations/kube-control/requires.py new file mode 100644 index 0000000..a0c3b0d --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-control/requires.py @@ -0,0 +1,161 @@ +#!/usr/local/sbin/charm-env python3 +# 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. + +from charms.reactive import ( + Endpoint, + toggle_flag, +) + +from charmhelpers.core.hookenv import log + + +class KubeControlRequirer(Endpoint): + """ + Implements the kubernetes-worker side of the kube-control interface. + """ + def manage_flags(self): + """ + Set states corresponding to the data we have. + """ + toggle_flag( + self.expand_name('{endpoint_name}.connected'), + self.is_joined) + toggle_flag( + self.expand_name('{endpoint_name}.dns.available'), + self.is_joined and self.dns_ready()) + toggle_flag( + self.expand_name('{endpoint_name}.auth.available'), + self.is_joined and self._has_auth_credentials()) + toggle_flag( + self.expand_name('{endpoint_name}.cluster_tag.available'), + self.is_joined and self.get_cluster_tag()) + toggle_flag( + self.expand_name('{endpoint_name}.registry_location.available'), + self.is_joined and self.get_registry_location()) + toggle_flag( + self.expand_name('{endpoint_name}.cohort_keys.available'), + self.is_joined and self.cohort_keys) + toggle_flag( + self.expand_name('{endpoint_name}.default_cni.available'), + self.is_joined and self.get_default_cni() is not None) + toggle_flag( + self.expand_name('{endpoint_name}.api_endpoints.available'), + self.is_joined and self.get_api_endpoints()) + + def get_auth_credentials(self, user): + """ + Return the authentication credentials. + """ + rx = {} + for unit in self.all_joined_units: + rx.update(unit.received.get('creds', {})) + if not rx: + return None + + if user in rx: + return { + 'user': user, + 'kubelet_token': rx[user]['kubelet_token'], + 'proxy_token': rx[user]['proxy_token'], + 'client_token': rx[user]['client_token'] + } + else: + return None + + def get_dns(self): + """ + Return DNS info provided by the master. + """ + rx = self.all_joined_units.received_raw + + return { + 'port': rx.get('port'), + 'domain': rx.get('domain'), + 'sdn-ip': rx.get('sdn-ip'), + 'enable-kube-dns': rx.get('enable-kube-dns'), + } + + def dns_ready(self): + """ + Return True if we have all DNS info from the master. + """ + keys = ['port', 'domain', 'sdn-ip', 'enable-kube-dns'] + dns_info = self.get_dns() + return (set(dns_info.keys()) == set(keys) and + dns_info['enable-kube-dns'] is not None) + + def set_auth_request(self, kubelet, group='system:nodes'): + """ + Tell the master that we are requesting auth, and to use this + hostname for the kubelet system account. + + Param groups - Determines the level of eleveted privleges of the + requested user. Can be overridden to request sudo level access on the + cluster via changing to system:masters. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'kubelet_user': kubelet, + 'auth_group': group + }) + + def set_gpu(self, enabled=True): + """ + Tell the master that we're gpu-enabled (or not). + """ + log('Setting gpu={} on kube-control relation'.format(enabled)) + for relation in self.relations: + relation.to_publish_raw.update({ + 'gpu': enabled + }) + + def _has_auth_credentials(self): + """ + Predicate method to signal we have authentication credentials. + """ + if self.all_joined_units.received_raw.get('creds'): + return True + + def get_cluster_tag(self): + """ + Tag for identifying resources that are part of the cluster. + """ + return self.all_joined_units.received_raw.get('cluster-tag') + + def get_registry_location(self): + """ + URL for container image registry. + """ + return self.all_joined_units.received_raw.get('registry-location') + + @property + def cohort_keys(self): + """ + The cohort snapshot keys sent by the masters. + """ + return self.all_joined_units.received['cohort-keys'] + + def get_default_cni(self): + """ + Default CNI network to use. + """ + return self.all_joined_units.received['default-cni'] + + def get_api_endpoints(self): + """ + Returns a list of API endpoint URLs. + """ + endpoints = set() + for unit in self.all_joined_units: + endpoints.update(unit.received['api-endpoints'] or []) + return sorted(endpoints) diff --git a/kubernetes-master/hooks/relations/kube-dns/README.md b/kubernetes-master/hooks/relations/kube-dns/README.md new file mode 100644 index 0000000..15ce8bb --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-dns/README.md @@ -0,0 +1,21 @@ +# Kube-DNS + +This interface allows a DNS provider, such as CoreDNS, to provide name +resolution for a Kubernetes cluster. + +(Note: this interface was previously used by the Kubernetes Master charm to +communicate the DNS provider info to the Kubernetes Worker charm, but that +usage was folded into the `kube-control` interface.) + + +# Provides + +The provider should look for the `{endpoint_name}.connected` flag and call +the `set_dns_info` method with the `domain`, `sdn_ip`, and `port` info (note: +these must be provided as keyword arguments). + +# Requires + +The requirer should look for the `{endpoint_name}.available` flag and call the +`details` method, which will return a dictionary with the `domain`, `sdn-ip`, +and `port` keys. diff --git a/kubernetes-master/hooks/relations/kube-dns/__init__.py b/kubernetes-master/hooks/relations/kube-dns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/kube-dns/interface.yaml b/kubernetes-master/hooks/relations/kube-dns/interface.yaml new file mode 100644 index 0000000..2de32b0 --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-dns/interface.yaml @@ -0,0 +1,4 @@ +name: kube-dns +summary: provides the kubernetes dns settings +version: 1 +maintainer: "Charles Butler " diff --git a/kubernetes-master/hooks/relations/kube-dns/provides.py b/kubernetes-master/hooks/relations/kube-dns/provides.py new file mode 100644 index 0000000..a7199c3 --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-dns/provides.py @@ -0,0 +1,29 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import Endpoint, toggle_flag + + +class KubeDNSProvider(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.connected'), + self.is_joined) + + def set_dns_info(self, *, domain, sdn_ip, port): + '''Set the domain, sdn_ip, and port of the DNS provider.''' + for relation in self.relations: + relation.to_publish_raw.update({ + 'domain': domain, + 'sdn-ip': sdn_ip, + 'port': port, + }) diff --git a/kubernetes-master/hooks/relations/kube-dns/requires.py b/kubernetes-master/hooks/relations/kube-dns/requires.py new file mode 100644 index 0000000..9595c4a --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-dns/requires.py @@ -0,0 +1,36 @@ +#!/usr/bin/python +# 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. + +from charms.reactive import Endpoint, toggle_flag + + +class KubeDNSRequireer(Endpoint): + def manage_flags(self): + '''Set flags according to whether we have DNS provider details.''' + toggle_flag(self.expand_name('{endpoint_name}.available'), + self.has_info()) + + def details(self): + '''Return the DNS provider details.''' + return { + 'domain': self._get_value('domain'), + 'sdn-ip': self._get_value('sdn-ip'), + 'port': self._get_value('port'), + } + + def has_info(self): + ''' Determine if we have all needed info''' + return all(self.details().values()) + + def _get_value(self, key): + return self.all_joined_units.received_raw.get(key) diff --git a/kubernetes-master/hooks/relations/kube-masters/README.md b/kubernetes-master/hooks/relations/kube-masters/README.md new file mode 100644 index 0000000..43cfa90 --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-masters/README.md @@ -0,0 +1,45 @@ +# kube-masters interface + +This interface provides communication amongst kubernetes-masters in a cluster. + +## States + +* `kube-masters.connected` + + Enabled when any kubernetes-master unit has joined the relation. + +* `kube-masters.cohorts.ready` + + Enabled when all peers have snap cohort data. + +### Methods and Properties + +* `kube-masters.set_cohort_keys(cohort_keys)` + + Set a dictionary of cohort keys created by the snap layer. + +* `kube-masters.cohort_keys` + + Dictionary of all cohort keys sent by peers. + +### Examples + +```python + +@when('kube-masters.connected') +def agree_on_cohorts(): + kube_masters = endpoint_from_flag('kube-masters.connected') + cohort_keys = create_cohorts_for_my_snaps() + kube_masters.set_cohort_keys(cohort_keys) + +@when('kube-masters.cohorts.ready', + 'kube-control.connected') +def send_cohorts_to_workers(): + kube_masters = endpoint_from_flag('kube-masters.cohorts.ready') + cohort_keys = kube_masters.cohort_keys + + kube_control = endpoint_from_flag('kube-control.connected') + # The following set method is defined in interface-kube-control + kube_control.set_cohort_keys(cohort_keys) + +``` diff --git a/kubernetes-master/hooks/relations/kube-masters/__init__.py b/kubernetes-master/hooks/relations/kube-masters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/kube-masters/interface.yaml b/kubernetes-master/hooks/relations/kube-masters/interface.yaml new file mode 100644 index 0000000..bd346d9 --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-masters/interface.yaml @@ -0,0 +1,4 @@ +name: kube-masters +summary: Provides master peer communication. +version: 1 +maintainer: "Kevin W. Monroe " diff --git a/kubernetes-master/hooks/relations/kube-masters/peers.py b/kubernetes-master/hooks/relations/kube-masters/peers.py new file mode 100644 index 0000000..0e3021e --- /dev/null +++ b/kubernetes-master/hooks/relations/kube-masters/peers.py @@ -0,0 +1,54 @@ +#!/usr/local/sbin/charm-env python3 +# 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. + +from charms.reactive import ( + Endpoint, + toggle_flag, +) + +from charmhelpers.core.hookenv import log + + +class KubeMastersPeer(Endpoint): + """ + Implements peering for kubernetes-master units. + """ + def manage_flags(self): + """ + Set states corresponding to the data we have. + """ + toggle_flag( + self.expand_name('{endpoint_name}.connected'), + self.is_joined) + toggle_flag( + self.expand_name('{endpoint_name}.cohorts.ready'), + self.is_joined and self._peers_have_cohorts()) + + def _peers_have_cohorts(self): + """ + Return True if all peers have cohort keys. + """ + for unit in self.all_joined_units: + if not unit.received.get('cohort-keys'): + log('Unit {} does not yet have cohort-keys'.format(unit)) + return False + + log('All units have cohort-keys') + return True + + def set_cohort_keys(self, cohort_keys): + """ + Send the cohort snapshot keys. + """ + for relation in self.relations: + relation.to_publish['cohort-keys'] = cohort_keys diff --git a/kubernetes-master/hooks/relations/kubernetes-cni/.github/workflows/tests.yaml b/kubernetes-master/hooks/relations/kubernetes-cni/.github/workflows/tests.yaml new file mode 100644 index 0000000..9801450 --- /dev/null +++ b/kubernetes-master/hooks/relations/kubernetes-cni/.github/workflows/tests.yaml @@ -0,0 +1,24 @@ +name: Test Suite for K8s Service Interface + +on: + - pull_request + +jobs: + lint-and-unit-tests: + name: Lint & Unit tests + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Tox + run: pip install tox + - name: Run lint & unit tests + run: tox + diff --git a/kubernetes-master/hooks/relations/kubernetes-cni/.gitignore b/kubernetes-master/hooks/relations/kubernetes-cni/.gitignore new file mode 100644 index 0000000..8d150f3 --- /dev/null +++ b/kubernetes-master/hooks/relations/kubernetes-cni/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.tox +__pycache__ +*.pyc diff --git a/kubernetes-master/hooks/relations/kubernetes-cni/README.md b/kubernetes-master/hooks/relations/kubernetes-cni/README.md new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/kubernetes-cni/__init__.py b/kubernetes-master/hooks/relations/kubernetes-cni/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/kubernetes-cni/interface.yaml b/kubernetes-master/hooks/relations/kubernetes-cni/interface.yaml new file mode 100644 index 0000000..7e3c123 --- /dev/null +++ b/kubernetes-master/hooks/relations/kubernetes-cni/interface.yaml @@ -0,0 +1,6 @@ +name: kubernetes-cni +summary: Interface for relating various CNI implementations +version: 0 +maintainer: "George Kraft " +ignore: +- tests diff --git a/kubernetes-master/hooks/relations/kubernetes-cni/provides.py b/kubernetes-master/hooks/relations/kubernetes-cni/provides.py new file mode 100644 index 0000000..9095c19 --- /dev/null +++ b/kubernetes-master/hooks/relations/kubernetes-cni/provides.py @@ -0,0 +1,89 @@ +#!/usr/bin/python + +from charmhelpers.core import hookenv +from charmhelpers.core.host import file_hash +from charms.layer.kubernetes_common import kubeclientconfig_path +from charms.reactive import Endpoint +from charms.reactive import toggle_flag, is_flag_set, clear_flag, set_flag + + +class CNIPluginProvider(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name("{endpoint_name}.connected"), self.is_joined) + toggle_flag( + self.expand_name("{endpoint_name}.available"), self.config_available() + ) + if is_flag_set(self.expand_name("endpoint.{endpoint_name}.changed")): + clear_flag(self.expand_name("{endpoint_name}.configured")) + clear_flag(self.expand_name("endpoint.{endpoint_name}.changed")) + + def set_config(self, is_master): + """Relays a dict of kubernetes configuration information.""" + for relation in self.relations: + relation.to_publish_raw.update({"is_master": is_master}) + set_flag(self.expand_name("{endpoint_name}.configured")) + + def config_available(self): + """Ensures all config from the CNI plugin is available.""" + goal_state = hookenv.goal_state() + related_apps = [ + app + for app in goal_state.get("relations", {}).get(self.endpoint_name, "") + if "/" not in app + ] + if not related_apps: + return False + configs = self.get_configs() + return all( + "cidr" in config and "cni-conf-file" in config + for config in [configs.get(related_app, {}) for related_app in related_apps] + ) + + def get_config(self, default=None): + """Get CNI config for one related application. + + If default is specified, and there is a related application with a + matching name, then that application is chosen. Otherwise, the + application is chosen alphabetically. + + Whichever application is chosen, that application's CNI config is + returned. + """ + configs = self.get_configs() + if not configs: + return {} + elif default and default not in configs: + msg = "relation not found for default CNI %s, ignoring" % default + hookenv.log(msg, level="WARN") + return self.get_config() + elif default: + return configs.get(default, {}) + else: + return configs.get(sorted(configs)[0], {}) + + def get_configs(self): + """Get CNI configs for all related applications. + + This returns a mapping of application names to CNI configs. Here's an + example return value: + { + 'flannel': { + 'cidr': '10.1.0.0/16', + 'cni-conf-file': '10-flannel.conflist' + }, + 'calico': { + 'cidr': '192.168.0.0/16', + 'cni-conf-file': '10-calico.conflist' + } + } + """ + return { + relation.application_name: relation.joined_units.received_raw + for relation in self.relations + if relation.application_name + } + + def notify_kubeconfig_changed(self): + kubeconfig_hash = file_hash(kubeclientconfig_path) + for relation in self.relations: + relation.to_publish_raw.update({"kubeconfig-hash": kubeconfig_hash}) diff --git a/kubernetes-master/hooks/relations/kubernetes-cni/requires.py b/kubernetes-master/hooks/relations/kubernetes-cni/requires.py new file mode 100644 index 0000000..2067826 --- /dev/null +++ b/kubernetes-master/hooks/relations/kubernetes-cni/requires.py @@ -0,0 +1,54 @@ +#!/usr/bin/python + +from charmhelpers.core import unitdata +from charms.reactive import Endpoint +from charms.reactive import when_any, when_not +from charms.reactive import set_state, remove_state + +db = unitdata.kv() + + +class CNIPluginClient(Endpoint): + def manage_flags(self): + kubeconfig_hash = self.get_config().get("kubeconfig-hash") + kubeconfig_hash_key = self.expand_name("{endpoint_name}.kubeconfig-hash") + if kubeconfig_hash: + set_state(self.expand_name("{endpoint_name}.kubeconfig.available")) + if kubeconfig_hash != db.get(kubeconfig_hash_key): + set_state(self.expand_name("{endpoint_name}.kubeconfig.changed")) + db.set(kubeconfig_hash_key, kubeconfig_hash) + + @when_any("endpoint.{endpoint_name}.joined", "endpoint.{endpoint_name}.changed") + def changed(self): + """Indicate the relation is connected, and if the relation data is + set it is also available.""" + set_state(self.expand_name("{endpoint_name}.connected")) + config = self.get_config() + if config["is_master"] == "True": + set_state(self.expand_name("{endpoint_name}.is-master")) + set_state(self.expand_name("{endpoint_name}.configured")) + elif config["is_master"] == "False": + set_state(self.expand_name("{endpoint_name}.is-worker")) + set_state(self.expand_name("{endpoint_name}.configured")) + else: + remove_state(self.expand_name("{endpoint_name}.configured")) + remove_state(self.expand_name("endpoint.{endpoint_name}.changed")) + + @when_not("endpoint.{endpoint_name}.joined") + def broken(self): + """Indicate the relation is no longer available and not connected.""" + remove_state(self.expand_name("{endpoint_name}.connected")) + remove_state(self.expand_name("{endpoint_name}.is-master")) + remove_state(self.expand_name("{endpoint_name}.is-worker")) + remove_state(self.expand_name("{endpoint_name}.configured")) + + def get_config(self): + """Get the kubernetes configuration information.""" + return self.all_joined_units.received_raw + + def set_config(self, cidr, cni_conf_file): + """Sets the CNI configuration information.""" + for relation in self.relations: + relation.to_publish_raw.update( + {"cidr": cidr, "cni-conf-file": cni_conf_file} + ) diff --git a/kubernetes-master/hooks/relations/nrpe-external-master/README.md b/kubernetes-master/hooks/relations/nrpe-external-master/README.md new file mode 100644 index 0000000..e33deb8 --- /dev/null +++ b/kubernetes-master/hooks/relations/nrpe-external-master/README.md @@ -0,0 +1,66 @@ +# nrpe-external-master interface + +Use this interface to register nagios checks in your charm layers. + +## Purpose + +This interface is designed to interoperate with the +[nrpe-external-master](https://jujucharms.com/nrpe-external-master) subordinate charm. + +## How to use in your layers + +The event handler for `nrpe-external-master.available` is called with an object +through which you can register your own custom nagios checks, when a relation +is established with `nrpe-external-master:nrpe-external-master`. + +This object provides a method, + +_add_check_(args, name=_check_name_, description=_description_, context=_context_, unit=_unit_) + +which is called to register a nagios plugin check for your service. + +All arguments are required. + +*args* is a list of nagios plugin command line arguments, starting with the path to the plugin executable. + +*name* is the name of the check registered in nagios + +*description* is some text that describes what the check is for and what it does + +*context* is the nagios context name, something that identifies your application + +*unit* is `hookenv.local_unit()` + +The nrpe subordinate installs `check_http`, so you can use it like this: + +``` +@when('nrpe-external-master.available') +def setup_nagios(nagios): + config = hookenv.config() + unit_name = hookenv.local_unit() + nagios.add_check(['/usr/lib/nagios/plugins/check_http', + '-I', '127.0.0.1', '-p', str(config['port']), + '-e', " 200 OK", '-u', '/publickey'], + name="check_http", + description="Verify my awesome service is responding", + context=config["nagios_context"], + unit=unit_name, + ) +``` +If your `nagios.add_check` defines a custom plugin, you will also need to restart the `nagios-nrpe-server` service. + +Consult the nagios documentation for more information on [how to write your own +plugins](https://assets.nagios.com/downloads/nagioscore/docs/nagioscore/4/en/pluginapi.html) +or [find one](https://www.nagios.org/projects/nagios-plugins/) that does what you need. + +## Example deployment + +``` +$ juju deploy your-awesome-charm +$ juju deploy nrpe-external-master --config site-nagios.yaml +$ juju add-relation your-awesome-charm nrpe-external-master +``` + +where `site-nagios.yaml` has the necessary configuration settings for the +subordinate to connect to nagios. + diff --git a/kubernetes-master/hooks/relations/nrpe-external-master/__init__.py b/kubernetes-master/hooks/relations/nrpe-external-master/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/nrpe-external-master/interface.yaml b/kubernetes-master/hooks/relations/nrpe-external-master/interface.yaml new file mode 100644 index 0000000..859a423 --- /dev/null +++ b/kubernetes-master/hooks/relations/nrpe-external-master/interface.yaml @@ -0,0 +1,3 @@ +name: nrpe-external-master +summary: Nagios interface +version: 1 diff --git a/kubernetes-master/hooks/relations/nrpe-external-master/provides.py b/kubernetes-master/hooks/relations/nrpe-external-master/provides.py new file mode 100644 index 0000000..b6c7f0d --- /dev/null +++ b/kubernetes-master/hooks/relations/nrpe-external-master/provides.py @@ -0,0 +1,91 @@ +import datetime +import os + +from charmhelpers.core import hookenv + +from charms.reactive import hook +from charms.reactive import RelationBase +from charms.reactive import scopes + + +class NrpeExternalMasterProvides(RelationBase): + scope = scopes.GLOBAL + + @hook('{provides:nrpe-external-master}-relation-{joined,changed}') + def changed_nrpe(self): + self.set_state('{relation_name}.available') + + @hook('{provides:nrpe-external-master}-relation-{broken,departed}') + def broken_nrpe(self): + self.remove_state('{relation_name}.available') + + def add_check(self, args, name=None, description=None, context=None, + servicegroups=None, unit=None): + nagios_files = self.get_local('nagios.check.files', []) + + if not unit: + unit = hookenv.local_unit() + unit = unit.replace('/', '-') + context = self.get_remote('nagios_host_context', context) + host_name = self.get_remote('nagios_hostname', + '%s-%s' % (context, unit)) + + check_tmpl = """ +#--------------------------------------------------- +# This file is Juju managed +#--------------------------------------------------- +command[%(check_name)s]=%(check_args)s +""" + service_tmpl = """ +#--------------------------------------------------- +# This file is Juju managed +#--------------------------------------------------- +define service { + use active-service + host_name %(host_name)s + service_description %(description)s + check_command check_nrpe!%(check_name)s + servicegroups %(servicegroups)s +} +""" + check_filename = "/etc/nagios/nrpe.d/check_%s.cfg" % (name) + with open(check_filename, "w") as fh: + fh.write(check_tmpl % { + 'check_args': ' '.join(args), + 'check_name': name, + }) + nagios_files.append(check_filename) + + service_filename = "/var/lib/nagios/export/service__%s_%s.cfg" % ( + unit, name) + with open(service_filename, "w") as fh: + fh.write(service_tmpl % { + 'servicegroups': servicegroups or context, + 'context': context, + 'description': description, + 'check_name': name, + 'host_name': host_name, + 'unit_name': unit, + }) + nagios_files.append(service_filename) + + self.set_local('nagios.check.files', nagios_files) + + def removed(self): + files = self.get_local('nagios.check.files', []) + for f in files: + try: + os.unlink(f) + except Exception as e: + hookenv.log("failed to remove %s: %s" % (f, e)) + self.set_local('nagios.check.files', []) + self.remove_state('{relation_name}.removed') + + def added(self): + self.updated() + + def updated(self): + relation_info = { + 'timestamp': datetime.datetime.now().isoformat(), + } + self.set_remote(**relation_info) diff --git a/kubernetes-master/hooks/relations/nrpe-external-master/requires.py b/kubernetes-master/hooks/relations/nrpe-external-master/requires.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/openstack-integration/.gitignore b/kubernetes-master/hooks/relations/openstack-integration/.gitignore new file mode 100644 index 0000000..5f9f2c5 --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/.gitignore @@ -0,0 +1,3 @@ +.tox +__pycache__ +*.pyc diff --git a/kubernetes-master/hooks/relations/openstack-integration/LICENSE b/kubernetes-master/hooks/relations/openstack-integration/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-master/hooks/relations/openstack-integration/README.md b/kubernetes-master/hooks/relations/openstack-integration/README.md new file mode 100644 index 0000000..ae021c2 --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/README.md @@ -0,0 +1,28 @@ +# Overview + +This layer encapsulates the `openstack-integration` interface communciation +protocol and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:openstack-integration` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:openstack-integration'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `openstack-integration` interface protocol: + +```yaml +requires: + openstack: + interface: openstack-integration +``` + +For documentation on how to use the API for this interface, see: + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) (this will only be used by the openstack-integrator charm) diff --git a/kubernetes-master/hooks/relations/openstack-integration/__init__.py b/kubernetes-master/hooks/relations/openstack-integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/openstack-integration/copyright b/kubernetes-master/hooks/relations/openstack-integration/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/hooks/relations/openstack-integration/docs/provides.md b/kubernetes-master/hooks/relations/openstack-integration/docs/provides.md new file mode 100644 index 0000000..ee17ac6 --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/docs/provides.md @@ -0,0 +1,108 @@ +

provides

+ + +This is the provides side of the interface layer, for use only by the +OpenStack integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for OpenStack integration + features. The OpenStack integration charm should then iterate over each + request, perform whatever actions are necessary to satisfy those requests, + and then mark them as complete. + +

OpenStackIntegrationProvides

+ +```python +OpenStackIntegrationProvides(endpoint_name, relation_ids=None) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag +from charms import layer + +@when('endpoint.openstack.requests-pending') +def handle_requests(): + openstack = endpoint_from_flag('endpoint.openstack.requests-pending') + for request in openstack.requests: + request.set_credentials(layer.openstack.get_user_credentials()) + openstack.mark_completed() +``` + +

all_requests

+ + +A list of all of the [`IntegrationRequests`](#provides.OpenStackIntegrationProvides.all_requests.IntegrationRequests) that have been made. + +

new_requests

+ + +A list of the new or updated [`IntegrationRequests`](#provides.OpenStackIntegrationProvides.new_requests.IntegrationRequests) that have been made. + +

mark_completed

+ +```python +OpenStackIntegrationProvides.mark_completed() +``` + +Mark all requests as completed and remove the `requests-pending` flag. + +

IntegrationRequest

+ +```python +IntegrationRequest(unit) +``` + +A request for integration from a single remote unit. + +

has_credentials

+ + +Whether or not credentials have been set via `set_credentials`. + +

is_changed

+ + +Whether this request has changed since the last time it was +marked completed (if ever). + +

set_credentials

+ +```python +IntegrationRequest.set_credentials(auth_url, + region, + username, + password, + user_domain_name, + project_domain_name, + project_name, + endpoint_tls_ca, + version=None) +``` + +Set the credentials for this request. + +

set_lbaas_config

+ +```python +IntegrationRequest.set_lbaas_config(subnet_id, + floating_network_id, + lb_method, + manage_security_groups, + has_octavia=None) +``` + +Set the load-balancer-as-a-service config for this request. + +

set_block_storage_config

+ +```python +IntegrationRequest.set_block_storage_config(bs_version, trust_device_path, + ignore_volume_az) +``` + +Set the block storage config for this request. + diff --git a/kubernetes-master/hooks/relations/openstack-integration/docs/requires.md b/kubernetes-master/hooks/relations/openstack-integration/docs/requires.md new file mode 100644 index 0000000..510e292 --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/docs/requires.md @@ -0,0 +1,160 @@ +

requires

+ + +This is the requires side of the interface layer, for use in charms that wish +to request integration with OpenStack native features. The integration will be +provided by the OpenStack integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of OpenStack +specific API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific OpenStack features. This flag is automatically removed + if the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the OpenStack instance on which the charm is + running. This flag is automatically removed if new integration features are + requested. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready.changed`** This flag is set if the data + changes after the ready flag was set. This flag should be removed by the + charm once handled. + +

OpenStackIntegrationRequires

+ +```python +OpenStackIntegrationRequires(endpoint_name, relation_ids=None) +``` + +Interface to request integration access. + +Note that due to resource limits and permissions granularity, policies are +limited to being applied at the charm level. That means that, if any +permissions are requested (i.e., any of the enable methods are called), +what is granted will be the sum of those ever requested by any instance of +the charm on this cloud. + +Labels, on the other hand, will be instance specific. + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag + +@when('endpoint.openstack.ready') +def openstack_integration_ready(): + openstack = endpoint_from_flag('endpoint.openstack.ready') + update_config_enable_openstack(openstack) +``` + +

auth_url

+ + +The authentication endpoint URL. + +

bs_version

+ + +What block storage API version to use, `auto` if autodetection is +desired, or `None` to use the default. + +

endpoint_tls_ca

+ + +Optional base64-encoded CA certificate for the authentication endpoint, +or None. + +

floating_network_id

+ + +Optional floating network ID, or None. + +

has_octavia

+ + +Whether the underlying OpenStack supports Octavia instead of +Neutron-based LBaaS. + +Will either be True, False, or None if it could not be determined for +some reason (typically due to connecting to an older integrator charm). + +

ignore_volume_az

+ + +Whether to ignore availability zones when attaching Cinder volumes. + +Will be `True`, `False`, or `None`. + +

is_changed

+ + +Whether or not the request for this instance has changed. + +

is_ready

+ + +Whether or not the request for this instance has been completed. + +

lb_method

+ + +Optional load-balancer method, or None. + +

manage_security_groups

+ + +Whether or not the Load Balancer should automatically manage security +group rules. + +Will be `True` or `False`. + +

password

+ + +The password. + +

project_domain_name

+ + +The project domain name. + +

project_name

+ + +The project name, also known as the tenant ID. + +

region

+ + +The region name. + +

subnet_id

+ + +Optional subnet ID to work in, or None. + +

trust_device_path

+ + +Whether to trust the block device name provided by Ceph. + +Will be `True`, `False`, or `None`. + +

user_domain_name

+ + +The user domain name. + +

username

+ + +The username. + +

version

+ + +Optional version number for the APIs or None. + diff --git a/kubernetes-master/hooks/relations/openstack-integration/interface.yaml b/kubernetes-master/hooks/relations/openstack-integration/interface.yaml new file mode 100644 index 0000000..a94fed4 --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/interface.yaml @@ -0,0 +1,4 @@ +name: openstack-integration +summary: Interface for connecting to the OpenStack integrator charm. +version: 1 +maintainer: Cory Johns diff --git a/kubernetes-master/hooks/relations/openstack-integration/make_docs b/kubernetes-master/hooks/relations/openstack-integration/make_docs new file mode 100644 index 0000000..a09c66f --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'openstack': {'interface': 'openstack'}}, + 'provides': {'openstack': {'interface': 'openstack'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-master/hooks/relations/openstack-integration/provides.py b/kubernetes-master/hooks/relations/openstack-integration/provides.py new file mode 100644 index 0000000..7aa8146 --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/provides.py @@ -0,0 +1,152 @@ +""" +This is the provides side of the interface layer, for use only by the +OpenStack integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for OpenStack integration + features. The OpenStack integration charm should then iterate over each + request, perform whatever actions are necessary to satisfy those requests, + and then mark them as complete. +""" + +from operator import attrgetter + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class OpenStackIntegrationProvides(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + + @when('endpoint.openstack.requests-pending') + def handle_requests(): + openstack = endpoint_from_flag('endpoint.openstack.requests-pending') + for request in openstack.requests: + request.set_credentials(layer.openstack.get_user_credentials()) + openstack.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + toggle_flag(self.expand_name('requests-pending'), + len(self.all_requests) > 0) + clear_flag(self.expand_name('changed')) + + @property + def all_requests(self): + """ + A list of all of the #IntegrationRequests that have been made. + """ + if not hasattr(self, '_all_requests'): + self._all_requests = [IntegrationRequest(unit) + for unit in self.all_joined_units] + return self._all_requests + + @property + def new_requests(self): + """ + A list of the new or updated #IntegrationRequests that have been made. + """ + is_changed = attrgetter('is_changed') + return list(filter(is_changed, self.all_requests)) + + def mark_completed(self): + """ + Mark all requests as completed and remove the `requests-pending` flag. + """ + clear_flag(self.expand_name('requests-pending')) + + +class IntegrationRequest: + """ + A request for integration from a single remote unit. + """ + def __init__(self, unit): + self._unit = unit + + @property + def _to_publish(self): + return self._unit.relation.to_publish + + @property + def is_changed(self): + """ + Whether this request has changed since the last time it was + marked completed (if ever). + """ + return not self.has_credentials + + @property + def unit_name(self): + return self._unit.unit_name + + def set_credentials(self, + auth_url, + region, + username, + password, + user_domain_name, + project_domain_name, + project_name, + endpoint_tls_ca, + version=None): + """ + Set the credentials for this request. + """ + self._unit.relation.to_publish.update({ + 'auth_url': auth_url, + 'region': region, + 'username': username, + 'password': password, + 'user_domain_name': user_domain_name, + 'project_domain_name': project_domain_name, + 'project_name': project_name, + 'endpoint_tls_ca': endpoint_tls_ca, + 'version': version, + }) + + def set_lbaas_config(self, + subnet_id, + floating_network_id, + lb_method, + manage_security_groups, + has_octavia=None): + """ + Set the load-balancer-as-a-service config for this request. + """ + self._unit.relation.to_publish.update({ + 'subnet_id': subnet_id, + 'floating_network_id': floating_network_id, + 'lb_method': lb_method, + 'manage_security_groups': manage_security_groups, + 'has_octavia': has_octavia, + }) + + def set_block_storage_config(self, + bs_version, + trust_device_path, + ignore_volume_az): + """ + Set the block storage config for this request. + """ + self._unit.relation.to_publish.update({ + 'bs_version': bs_version, + 'trust_device_path': trust_device_path, + 'ignore_volume_az': ignore_volume_az, + }) + + @property + def has_credentials(self): + """ + Whether or not credentials have been set via `set_credentials`. + """ + return 'credentials' in self._unit.relation.to_publish diff --git a/kubernetes-master/hooks/relations/openstack-integration/pydocmd.yml b/kubernetes-master/hooks/relations/openstack-integration/pydocmd.yml new file mode 100644 index 0000000..aa0a286 --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'OpenStack Integration Interface' + +generate: + - requires.md: + - requires + - requires.OpenStackIntegrationRequires+ + - provides.md: + - provides + - provides.OpenStackIntegrationProvides+ + - provides.IntegrationRequest+ + +pages: + - Requires: requires.md + - Provides: provides.md + +gens_dir: docs diff --git a/kubernetes-master/hooks/relations/openstack-integration/requires.py b/kubernetes-master/hooks/relations/openstack-integration/requires.py new file mode 100644 index 0000000..420f767 --- /dev/null +++ b/kubernetes-master/hooks/relations/openstack-integration/requires.py @@ -0,0 +1,254 @@ +""" +This is the requires side of the interface layer, for use in charms that wish +to request integration with OpenStack native features. The integration will be +provided by the OpenStack integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of OpenStack +specific API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific OpenStack features. This flag is automatically removed + if the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the OpenStack instance on which the charm is + running. This flag is automatically removed if new integration features are + requested. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready.changed`** This flag is set if the data + changes after the ready flag was set. This flag should be removed by the + charm once handled. +""" + + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag, is_flag_set +from charms.reactive import data_changed + + +class OpenStackIntegrationRequires(Endpoint): + """ + Interface to request integration access. + + Note that due to resource limits and permissions granularity, policies are + limited to being applied at the charm level. That means that, if any + permissions are requested (i.e., any of the enable methods are called), + what is granted will be the sum of those ever requested by any instance of + the charm on this cloud. + + Labels, on the other hand, will be instance specific. + + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + + @when('endpoint.openstack.ready') + def openstack_integration_ready(): + openstack = endpoint_from_flag('endpoint.openstack.ready') + update_config_enable_openstack(openstack) + ``` + """ + + @property + def _received(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single OpenStack integration application with a + single unit. + """ + return self.relations[0].joined_units.received + + @property + def _to_publish(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single OpenStack integration application with a + single unit. + """ + return self.relations[0].to_publish + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + # My middle name is ready. No, that doesn't sound right. + # I eat ready for breakfast. + was_ready = is_flag_set(self.expand_name('ready')) + toggle_flag(self.expand_name('ready'), self.is_ready) + if self.is_ready and was_ready and self.is_changed: + set_flag(self.expand_name('ready.changed')) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + @property + def is_ready(self): + """ + Whether or not the request for this instance has been completed. + """ + # Although more information can be passed, such as LBaaS access + # the minimum needed to be considered ready is defined here + return all(field is not None for field in [ + self.auth_url, + self.username, + self.password, + self.user_domain_name, + self.project_domain_name, + self.project_name, + ]) + + @property + def is_changed(self): + """ + Whether or not the request for this instance has changed. + """ + return data_changed(self.expand_name('all-data'), [ + self.auth_url, + self.region, + self.username, + self.password, + self.user_domain_name, + self.project_domain_name, + self.project_name, + self.endpoint_tls_ca, + self.subnet_id, + self.floating_network_id, + self.lb_method, + self.manage_security_groups, + ]) + + @property + def auth_url(self): + """ + The authentication endpoint URL. + """ + return self._received['auth_url'] + + @property + def region(self): + """ + The region name. + """ + return self._received['region'] + + @property + def username(self): + """ + The username. + """ + return self._received['username'] + + @property + def password(self): + """ + The password. + """ + return self._received['password'] + + @property + def user_domain_name(self): + """ + The user domain name. + """ + return self._received['user_domain_name'] + + @property + def project_domain_name(self): + """ + The project domain name. + """ + return self._received['project_domain_name'] + + @property + def project_name(self): + """ + The project name, also known as the tenant ID. + """ + return self._received['project_name'] + + @property + def endpoint_tls_ca(self): + """ + Optional base64-encoded CA certificate for the authentication endpoint, + or None. + """ + return self._received['endpoint_tls_ca'] or None + + @property + def version(self): + """ + Optional version number for the APIs or None. + """ + return self._received['version'] or None + + @property + def subnet_id(self): + """ + Optional subnet ID to work in, or None. + """ + return self._received['subnet_id'] + + @property + def floating_network_id(self): + """ + Optional floating network ID, or None. + """ + return self._received['floating_network_id'] + + @property + def lb_method(self): + """ + Optional load-balancer method, or None. + """ + return self._received['lb_method'] + + @property + def manage_security_groups(self): + """ + Whether or not the Load Balancer should automatically manage security + group rules. + + Will be `True` or `False`. + """ + return self._received['manage_security_groups'] or False + + @property + def bs_version(self): + """ + What block storage API version to use, `auto` if autodetection is + desired, or `None` to use the default. + """ + return self._received['bs_version'] + + @property + def trust_device_path(self): + """ + Whether to trust the block device name provided by Ceph. + + Will be `True`, `False`, or `None`. + """ + return self._received['trust_device_path'] + + @property + def ignore_volume_az(self): + """ + Whether to ignore availability zones when attaching Cinder volumes. + + Will be `True`, `False`, or `None`. + """ + return self._received['ignore_volume_az'] + + @property + def has_octavia(self): + """ + Whether the underlying OpenStack supports Octavia instead of + Neutron-based LBaaS. + + Will either be True, False, or None if it could not be determined for + some reason (typically due to connecting to an older integrator charm). + """ + return self._received['has_octavia'] diff --git a/kubernetes-master/hooks/relations/prometheus-manual/.gitignore b/kubernetes-master/hooks/relations/prometheus-manual/.gitignore new file mode 100644 index 0000000..01a6a44 --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/.gitignore @@ -0,0 +1,3 @@ +.docs +__pycache__ +*.pyc diff --git a/kubernetes-master/hooks/relations/prometheus-manual/LICENSE b/kubernetes-master/hooks/relations/prometheus-manual/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-master/hooks/relations/prometheus-manual/README.md b/kubernetes-master/hooks/relations/prometheus-manual/README.md new file mode 100644 index 0000000..4ff5c83 --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/README.md @@ -0,0 +1,113 @@ +# Interface prometheus-manual + +This is a [Juju][] interface layer that enables a charm which provides manual +or raw metric scraper job configuration stanzas for Prometheus 2. + +The format for the job configuration data can be found in the [Prometheus +Configuration Docs][]. The job configuration will be included as an item +under `scrape_configs` largely unchanged, except for two things: + +* To ensure uniqueness, the provided job name will have a UUID appended to it. +* Because the CA cert must be written to disk separately from the config, any + `tls_config` sections will have their `ca_file` field values replaced with + the path to the file where the provided `ca_cert` data is written. + +# Example Usage + +First, you must define the relation endpoint in your charm's `metadata.yaml`: + +```yaml +provides: + prometheus: + interface: prometheus-manual +``` + +Next, you must ensure the interface layer is included in your `layer.yaml`: + +```yaml +includes: + - interface:prometheus-manual +``` + +Then, in your reactive code, add the following, modifying the job data as +your charm needs: + +```python +from charms.reactive import endpoint_from_flag + + +@when('endpoint.prometheus.joined', + 'tls.ca.available') +def register_prometheus_jobs(): + prometheus = endpoint_from_flag('endpoint.prometheus.joined') + tls = endpoint_from_flag('tls.ca.available') + prometheus.register_job(job_name='kubernetes-apiservers', + ca_cert=tls.root_ca_cert, + job_data={ + 'kubernetes_sd_configs': [{'role': 'endpoints'}], + 'scheme': 'https', + 'tls_config': {'ca_file': '__ca_file__'}, # placeholder for saved filename + 'bearer_token': get_token('system:prometheus'), + }) + prometheus.register_job(job_name='kubernetes-nodes', + ca_cert=tls.root_ca_cert, + job_data={ + 'kubernetes_sd_configs': [{'role': 'node'}], + 'scheme': 'https', + 'tls_config': {'ca_file': '__ca_file__'}, # placeholder for saved filename + 'bearer_token': get_token('system:prometheus'), + }) +``` + + + +# Reference + +* [common.md](common.md) + * [JobRequest](docs/common.md#jobrequest) + * [egress_subnets](docs/common.md#jobrequest-egress_subnets) + * [fromkeys](docs/common.md#jobrequest-fromkeys) + * [ingress_address](docs/common.md#jobrequest-ingress_address) + * [is_created](docs/common.md#jobrequest-is_created) + * [is_received](docs/common.md#jobrequest-is_received) + * [respond](docs/common.md#jobrequest-respond) + * [to_json](docs/common.md#jobrequest-to_json) + * [JobResponse](docs/common.md#jobresponse) + * [fromkeys](docs/common.md#jobresponse-fromkeys) +* [provides.md](provides.md) + * [PrometheusManualProvides](docs/provides.md#prometheusmanualprovides) + * [all_departed_units](docs/provides.md#prometheusmanualprovides-all_departed_units) + * [all_joined_units](docs/provides.md#prometheusmanualprovides-all_joined_units) + * [all_units](docs/provides.md#prometheusmanualprovides-all_units) + * [endpoint_name](docs/provides.md#prometheusmanualprovides-endpoint_name) + * [is_joined](docs/provides.md#prometheusmanualprovides-is_joined) + * [joined](docs/provides.md#prometheusmanualprovides-joined) + * [manage_flags](docs/provides.md#prometheusmanualprovides-manage_flags) + * [register_job](docs/provides.md#prometheusmanualprovides-register_job) + * [relations](docs/provides.md#prometheusmanualprovides-relations) + * [requests](docs/provides.md#prometheusmanualprovides-requests) + * [responses](docs/provides.md#prometheusmanualprovides-responses) +* [requires.md](requires.md) + * [PrometheusManualRequires](docs/requires.md#prometheusmanualrequires) + * [all_departed_units](docs/requires.md#prometheusmanualrequires-all_departed_units) + * [all_joined_units](docs/requires.md#prometheusmanualrequires-all_joined_units) + * [all_requests](docs/requires.md#prometheusmanualrequires-all_requests) + * [all_units](docs/requires.md#prometheusmanualrequires-all_units) + * [endpoint_name](docs/requires.md#prometheusmanualrequires-endpoint_name) + * [is_joined](docs/requires.md#prometheusmanualrequires-is_joined) + * [jobs](docs/requires.md#prometheusmanualrequires-jobs) + * [joined](docs/requires.md#prometheusmanualrequires-joined) + * [manage_flags](docs/requires.md#prometheusmanualrequires-manage_flags) + * [new_jobs](docs/requires.md#prometheusmanualrequires-new_jobs) + * [new_requests](docs/requires.md#prometheusmanualrequires-new_requests) + * [relations](docs/requires.md#prometheusmanualrequires-relations) + + + +# Contact Information + +Maintainer: Cory Johns <Cory.Johns@canonical.com> + + +[Juju]: https://jujucharms.com +[Prometheus Configuration Docs]: https://prometheus.io/docs/prometheus/latest/configuration/configuration/ diff --git a/kubernetes-master/hooks/relations/prometheus-manual/__init__.py b/kubernetes-master/hooks/relations/prometheus-manual/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/prometheus-manual/common.py b/kubernetes-master/hooks/relations/prometheus-manual/common.py new file mode 100644 index 0000000..530f965 --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/common.py @@ -0,0 +1,57 @@ +import json +from copy import deepcopy + +from charms.reactive import BaseRequest, BaseResponse, Field + + +class JobResponse(BaseResponse): + success = Field('Whether or not the registration succeeded') + reason = Field('If failed, a description of why') + + +class JobRequest(BaseRequest): + RESPONSE_CLASS = JobResponse + + job_name = Field('Desired name for the job. To ensure uniqueness, the ' + 'the request ID will be appended to the final job name.') + + job_data = Field('Config data for the job.') + + ca_cert = Field('Cert data for the CA used to validate connections.') + + def to_json(self, ca_file=None): + """ + Render the job request to JSON string which can be included directly + into Prometheus config. + + Keys will be sorted in the rendering to ensure a stable ordering for + comparisons to detect changes. + + If `ca_file` is given, it will be used to replace the value of any + `ca_file` fields in the job. The charm should ensure that the + request's `ca_cert` data is writen to that path prior to calling this + method. + """ + job_data = deepcopy(self.job_data) # make a copy we can modify + job_data['job_name'] = '{}-{}'.format(self.job_name, self.request_id) + + if ca_file: + for key, value in job_data.items(): + # update the cert path at the job level + if key == 'tls_config': + value['ca_file'] = str(ca_file) + + # update the cert path at the SD config level + if key.endswith('_sd_configs'): + for sd_config in value: + if 'ca_file' in sd_config.get('tls_config', {}): + sd_config['tls_config']['ca_file'] = str(ca_file) + + return json.dumps(job_data, sort_keys=True) + + def respond(self, success, reason=None): + """ + Acknowledge this request, and indicate success or failure with an + optional explanation. + """ + super().respond(success=success, reason=reason) diff --git a/kubernetes-master/hooks/relations/prometheus-manual/copyright b/kubernetes-master/hooks/relations/prometheus-manual/copyright new file mode 100644 index 0000000..69768db --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2019, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/hooks/relations/prometheus-manual/docs/common.md b/kubernetes-master/hooks/relations/prometheus-manual/docs/common.md new file mode 100644 index 0000000..a97d54b --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/docs/common.md @@ -0,0 +1,62 @@ +# `class JobRequest(BaseRequest)` + +Base class for requests using the request / response pattern. + +Subclasses **must** set the ``RESPONSE_CLASS`` attribute to a subclass of +the :class:`BaseResponse` which defines the fields that the response will +use. They must also define additional attributes as :class:`Field`s. + +For example:: + + class TLSResponse(BaseResponse): + key = Field('Private key for the cert') + cert = Field('Public cert info') + + + class TLSRequest(BaseRequest): + RESPONSE_CLASS = TLSResponse + + common_name = Field('Common Name (CN) for the cert to be created') + sans = Field('List of Subject Alternative Names (SANs)') + +## `egress_subnets` + +Subnets over which network traffic to the requester will flow. + +## `None` + +Returns a new dict with keys from iterable and values equal to value. + +## `ingress_address` + +Address to use if a connection to the requester is required. + +## `is_created` + +Whether this request was created by this side of the relation. + +## `is_received` + +Whether this request was received by the other side of the relation. + +## `def respond(self, success, reason=None)` + +Acknowledge this request, and indicate success or failure with an +optional explanation. + +## `def to_json(self)` + +Render the job request to JSON string which can be included directly +into Prometheus config. + +Keys will be sorted in the rendering to ensure a stable ordering for +comparisons to detect changes. + +# `class JobResponse(BaseResponse)` + +Base class for responses using the request / response pattern. + +## `None` + +Returns a new dict with keys from iterable and values equal to value. + diff --git a/kubernetes-master/hooks/relations/prometheus-manual/docs/provides.md b/kubernetes-master/hooks/relations/prometheus-manual/docs/provides.md new file mode 100644 index 0000000..439b4e5 --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/docs/provides.md @@ -0,0 +1,119 @@ +# `class PrometheusManualProvides(RequesterEndpoint)` + +Base class for Endpoints that create requests in the request / response +pattern. + +Subclasses **must** set the ``REQUEST_CLASS`` attribute to a subclass +of :class:`BaseRequest` which defines the fields the request will use. + +## `all_departed_units` + +Collection of all units that were previously part of any relation on +this endpoint but which have since departed. + +This collection is persistent and mutable. The departed units will +be kept until they are explicitly removed, to allow for reasonable +cleanup of units that have left. + +Example: You need to run a command each time a unit departs the relation. + +.. code-block:: python + + @when('endpoint.{endpoint_name}.departed') + def handle_departed_unit(self): + for name, unit in self.all_departed_units.items(): + # run the command to remove `unit` from the cluster + # .. + self.all_departed_units.clear() + clear_flag(self.expand_name('departed')) + +Once a unit is departed, it will no longer show up in +:attr:`all_joined_units`. Note that units are considered departed as +soon as the departed hook is entered, which differs slightly from how +the Juju primitives behave (departing units are still returned from +``related-units`` until after the departed hook is complete). + +This collection is a :class:`KeyList`, so can be used as a mapping to +look up units by their unit name, or iterated or accessed by index. + +## `all_joined_units` + +A list view of all the units of all relations attached to this +:class:`~charms.reactive.endpoints.Endpoint`. + +This is actually a +:class:`~charms.reactive.endpoints.CombinedUnitsView`, so the units +will be in order by relation ID and then unit name, and you can access a +merged view of all the units' data as a single mapping. You should be +very careful when using the merged data collections, however, and +consider carefully what will happen when the endpoint has multiple +relations and multiple remote units on each. It is probably better to +iterate over each unit and handle its data individually. See +:class:`~charms.reactive.endpoints.CombinedUnitsView` for an +explanation of how the merged data collections work. + +Note that, because a given application might be related multiple times +on a given endpoint, units may show up in this collection more than +once. + +## `all_units` + +.. deprecated:: 0.6.1 + Use :attr:`all_joined_units` instead + +## `endpoint_name` + +Relation name of this endpoint. + +## `is_joined` + +Whether this endpoint has remote applications attached to it. + +## `joined` + +.. deprecated:: 0.6.3 + Use :attr:`is_joined` instead + +## `def manage_flags(self)` + +Method that subclasses can override to perform any flag management +needed during startup. + +This will be called automatically after the framework-managed automatic +flags have been updated. + +## `def register_job(self, job_name, job_data, ca_cert=None)` + +Register a manual job. + +The job data should be the (unserialized) data defining the job. + +To ensure uniqueness, a UUID will be added to the job name, and it will +be injected into the job data. + +If a CA cert is given, the value of any ca_file field in the job data +will be replaced with a filename after the CA cert data is written, so +a placeholder value should be used. + +## `relations` + +Collection of :class:`Relation` instances that are established for +this :class:`Endpoint`. + +This is a :class:`KeyList`, so it can be iterated and indexed as a list, +or you can look up relations by their ID. For example:: + + rel0 = endpoint.relations[0] + assert rel0 is endpoint.relations[rel0.relation_id] + assert all(rel is endpoint.relations[rel.relation_id] + for rel in endpoint.relations) + print(', '.join(endpoint.relations.keys())) + +## `requests` + +A list of all requests which have been submitted. + +## `responses` + +A list of all responses which have been received. + diff --git a/kubernetes-master/hooks/relations/prometheus-manual/docs/requires.md b/kubernetes-master/hooks/relations/prometheus-manual/docs/requires.md new file mode 100644 index 0000000..31a7e8f --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/docs/requires.md @@ -0,0 +1,117 @@ +# `class PrometheusManualRequires(ResponderEndpoint)` + +Base class for Endpoints that respond to requests in the request / response +pattern. + +Subclasses **must** set the ``REQUEST_CLASS`` attribute to a subclass +of :class:`BaseRequest` which defines the fields the request will use. + +## `all_departed_units` + +Collection of all units that were previously part of any relation on +this endpoint but which have since departed. + +This collection is persistent and mutable. The departed units will +be kept until they are explicitly removed, to allow for reasonable +cleanup of units that have left. + +Example: You need to run a command each time a unit departs the relation. + +.. code-block:: python + + @when('endpoint.{endpoint_name}.departed') + def handle_departed_unit(self): + for name, unit in self.all_departed_units.items(): + # run the command to remove `unit` from the cluster + # .. + self.all_departed_units.clear() + clear_flag(self.expand_name('departed')) + +Once a unit is departed, it will no longer show up in +:attr:`all_joined_units`. Note that units are considered departed as +soon as the departed hook is entered, which differs slightly from how +the Juju primitives behave (departing units are still returned from +``related-units`` until after the departed hook is complete). + +This collection is a :class:`KeyList`, so can be used as a mapping to +look up units by their unit name, or iterated or accessed by index. + +## `all_joined_units` + +A list view of all the units of all relations attached to this +:class:`~charms.reactive.endpoints.Endpoint`. + +This is actually a +:class:`~charms.reactive.endpoints.CombinedUnitsView`, so the units +will be in order by relation ID and then unit name, and you can access a +merged view of all the units' data as a single mapping. You should be +very careful when using the merged data collections, however, and +consider carefully what will happen when the endpoint has multiple +relations and multiple remote units on each. It is probably better to +iterate over each unit and handle its data individually. See +:class:`~charms.reactive.endpoints.CombinedUnitsView` for an +explanation of how the merged data collections work. + +Note that, because a given application might be related multiple times +on a given endpoint, units may show up in this collection more than +once. + +## `all_requests` + +A list of all requests, including ones which have been responded to. + +## `all_units` + +.. deprecated:: 0.6.1 + Use :attr:`all_joined_units` instead + +## `endpoint_name` + +Relation name of this endpoint. + +## `is_joined` + +Whether this endpoint has remote applications attached to it. + +## `jobs` + +Return a list of all jobs to be registered. + +## `joined` + +.. deprecated:: 0.6.3 + Use :attr:`is_joined` instead + +## `def manage_flags(self)` + +Method that subclasses can override to perform any flag management +needed during startup. + +This will be called automatically after the framework-managed automatic +flags have been updated. + +## `new_jobs` + +Return a list of new jobs to be registered. + +## `new_requests` + +A list of requests which have not been responded. + +Requests should be handled by the charm and then responded to by +calling ``request.respond(...)``. + +## `relations` + +Collection of :class:`Relation` instances that are established for +this :class:`Endpoint`. + +This is a :class:`KeyList`, so it can be iterated and indexed as a list, +or you can look up relations by their ID. For example:: + + rel0 = endpoint.relations[0] + assert rel0 is endpoint.relations[rel0.relation_id] + assert all(rel is endpoint.relations[rel.relation_id] + for rel in endpoint.relations) + print(', '.join(endpoint.relations.keys())) + diff --git a/kubernetes-master/hooks/relations/prometheus-manual/interface.yaml b/kubernetes-master/hooks/relations/prometheus-manual/interface.yaml new file mode 100644 index 0000000..5c324c6 --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/interface.yaml @@ -0,0 +1,6 @@ +name: prometheus-manual +summary: Interface for registering manual job definitions with Prometheus +version: 1 +maintainer: "Cory Johns " +exclude: + - .docs diff --git a/kubernetes-master/hooks/relations/prometheus-manual/provides.py b/kubernetes-master/hooks/relations/prometheus-manual/provides.py new file mode 100644 index 0000000..884629c --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/provides.py @@ -0,0 +1,41 @@ +from charms.reactive import ( + toggle_flag, + RequesterEndpoint, +) + +from .common import JobRequest + + +class PrometheusManualProvides(RequesterEndpoint): + REQUEST_CLASS = JobRequest + + def manage_flags(self): + super().manage_flags() + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined and self.requests) + + def register_job(self, job_name, job_data, ca_cert=None, relation=None): + """ + Register a manual job. + + The job data should be the (unserialized) data defining the job. + + To ensure uniqueness, a UUID will be added to the job name, and it will + be injected into the job data. + + If a CA cert is given, the value of any ca_file field in the job data + will be replaced with a filename after the CA cert data is written, so + a placeholder value should be used. + + If a specific relation is not given, the job will be registered with + every related Prometheus. + """ + # we might be connected to multiple prometheuses for some strange + # reason, so just send the job to all of them + relations = [relation] if relation is not None else self.relations + for relation in relations: + JobRequest.create_or_update(match_fields=['job_name'], + relation=relation, + job_name=job_name, + job_data=job_data, + ca_cert=ca_cert) diff --git a/kubernetes-master/hooks/relations/prometheus-manual/requires.py b/kubernetes-master/hooks/relations/prometheus-manual/requires.py new file mode 100644 index 0000000..a8d1acb --- /dev/null +++ b/kubernetes-master/hooks/relations/prometheus-manual/requires.py @@ -0,0 +1,31 @@ +from charms.reactive import ( + toggle_flag, + ResponderEndpoint, +) + +from .common import JobRequest + + +class PrometheusManualRequires(ResponderEndpoint): + REQUEST_CLASS = JobRequest + + def manage_flags(self): + super().manage_flags() + toggle_flag(self.expand_name('endpoint.{endpoint_name}.has_jobs'), + self.is_joined and self.jobs) + toggle_flag(self.expand_name('endpoint.{endpoint_name}.new_jobs'), + self.is_joined and self.new_jobs) + + @property + def jobs(self): + """ + Return a list of all jobs to be registered. + """ + return self.all_requests + + @property + def new_jobs(self): + """ + Return a list of new jobs to be registered. + """ + return self.new_requests diff --git a/kubernetes-master/hooks/relations/public-address/README.md b/kubernetes-master/hooks/relations/public-address/README.md new file mode 100644 index 0000000..06be3ae --- /dev/null +++ b/kubernetes-master/hooks/relations/public-address/README.md @@ -0,0 +1,59 @@ +# Overview + +This interface layer implements a public address protocol useful for load +balancers and their subordinates. The load balancers (providers) set their +own public address and port, which is then available to the subordinates +(requirers). + +# Usage + +## Provides + +By providing the `public-address` interface, your charm is providing an HTTP +server that can load-balance for another HTTP based service. + +Your charm need only provide the address and port on which it is serving its +content, as soon as the `{relation_name}.available` state is set: + +```python +from charmhelpers.core import hookenv +@when('website.available') +def configure_website(website): + website.set_address_port(hookenv.unit_get('public-address'), hookenv.config('port')) +``` + +## Requires + +By requiring the `public-address` interface, your charm is consuming one or +more HTTP servers, 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 `get_addresses_ports()` method returns a list of available addresses and +ports. + +The return value is a list of dicts of the following form: + +```python +[ + { + 'public-address': address_of_host, + 'port': port_for_host, + }, + # ... +] +``` + +A trivial example of handling this interface would be: + +```python +from charmhelpers.core import hookenv +@when('loadbalancer.available') +def update_reverse_proxy_config(loadbalancer): + hosts = loadbalancer.get_addresses_ports() + for host in hosts: + hookenv.log('The loadbalancer for this unit is {}:{}'.format( + host['public-address'], + host['port'])) +``` diff --git a/kubernetes-master/hooks/relations/public-address/__init__.py b/kubernetes-master/hooks/relations/public-address/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/public-address/interface.yaml b/kubernetes-master/hooks/relations/public-address/interface.yaml new file mode 100644 index 0000000..c9849e4 --- /dev/null +++ b/kubernetes-master/hooks/relations/public-address/interface.yaml @@ -0,0 +1,4 @@ +name: public-address +summary: A basic interface to provide the public address for load balancers. +version: 1 +repo: https://githb.com/juju-solutions/interface-public-address.git diff --git a/kubernetes-master/hooks/relations/public-address/provides.py b/kubernetes-master/hooks/relations/public-address/provides.py new file mode 100644 index 0000000..09b9915 --- /dev/null +++ b/kubernetes-master/hooks/relations/public-address/provides.py @@ -0,0 +1,60 @@ +import json + +from charms.reactive import toggle_flag +from charms.reactive import Endpoint + + +class PublicAdddressProvides(Endpoint): + + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.available'), + self.is_joined) + + def set_address_port(self, address, port, relation=None): + if relation is None: + # no relation specified, so send the same data to everyone + relations = self.relations + else: + # specific relation given, so only send the data to that one + relations = [relation] + if type(address) is list: + # build 2 lists to zip together that are the same length + length = len(address) + p = [port] * length + combined = zip(address, p) + clients = [{'public-address': a, 'port': p} + for a, p in combined] + # for backwards compatibility, we just send a single entry + # and have an array of dictionaries in a field of that + # entry for the other entries. + first = clients.pop(0) + first['extended_data'] = json.dumps(clients) + for relation in relations: + relation.to_publish_raw.update(first) + else: + for relation in relations: + relation.to_publish_raw.update({'public-address': address, + 'port': port}) + + @property + def requests(self): + return [Request(rel) for rel in self.relations] + + +class Request: + def __init__(self, rel): + self.rel = rel + + @property + def application_name(self): + return self.rel.application_name + + @property + def members(self): + return [(u.received_raw.get('ingress-address', + u.received_raw['private-address']), + u.received_raw.get('port', '6443')) + for u in self.rel.joined_units] + + def set_address_port(self, address, port): + self.rel.endpoint.set_address_port(address, port, self.rel) diff --git a/kubernetes-master/hooks/relations/public-address/requires.py b/kubernetes-master/hooks/relations/public-address/requires.py new file mode 100644 index 0000000..467d129 --- /dev/null +++ b/kubernetes-master/hooks/relations/public-address/requires.py @@ -0,0 +1,44 @@ +import json + +from charms.reactive import toggle_flag, Endpoint + + +class PublicAddressRequires(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.available'), + len(self.get_addresses_ports()) > 0) + + def set_backend_port(self, port): + """ + Set the port that the backend service is listening on. + + Defaults to 6443 if not set. + """ + for rel in self.relations: + rel.to_publish_raw['port'] = str(port) + + def get_addresses_ports(self): + '''Returns a list of available HTTP providers and their associated + public addresses and ports. + + The return value is a list of dicts of the following form:: + [ + { + 'public-address': address_for_frontend, + 'port': port_for_frontend, + }, + # ... + ] + ''' + hosts = set() + for relation in self.relations: + for unit in relation.joined_units: + data = unit.received_raw + hosts.add((data['public-address'], data['port'])) + if 'extended_data' in data: + for ed in json.loads(data['extended_data']): + hosts.add((ed['public-address'], ed['port'])) + + return [{'public-address': pa, 'port': p} + for pa, p in sorted(host for host in hosts + if None not in host)] diff --git a/kubernetes-master/hooks/relations/tls-certificates/.gitignore b/kubernetes-master/hooks/relations/tls-certificates/.gitignore new file mode 100644 index 0000000..93813bc --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/.gitignore @@ -0,0 +1,4 @@ +.tox +__pycache__ +*.pyc +_build diff --git a/kubernetes-master/hooks/relations/tls-certificates/README.md b/kubernetes-master/hooks/relations/tls-certificates/README.md new file mode 100644 index 0000000..733da6d --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/README.md @@ -0,0 +1,90 @@ +# Interface tls-certificates + +This is a [Juju][] interface layer that enables a charm which requires TLS +certificates to relate to a charm which can provide them, such as [Vault][] or +[EasyRSA][] + +To get started please read the [Introduction to PKI][] which defines some PKI +terms, concepts and processes used in this document. + +# Example Usage + +Let's say you have a charm which needs a server certificate for a service it +provides to other charms and a client certificate for a database it consumes +from another charm. The charm provides its own service on the `clients` +relation endpoint, and it consumes the database on the `db` relation endpoint. + +First, you must define the relation endpoint in your charm's `metadata.yaml`: + +```yaml +requires: + cert-provider: + interface: tls-certificates +``` + +Next, you must ensure the interface layer is included in your `layer.yaml`: + +```yaml +includes: + - interface:tls-certificates +``` + +Then, in your reactive code, add the following, changing `update_certs` to +handle the certificates however your charm needs: + +```python +from charmhelpers.core import hookenv, host +from charms.reactive import endpoint_from_flag + + +@when('cert-provider.ca.changed') +def install_root_ca_cert(): + cert_provider = endpoint_from_flag('cert-provider.ca.available') + host.install_ca_cert(cert_provider.root_ca_cert) + clear_flag('cert-provider.ca.changed') + + +@when('cert-provider.available') +def request_certificates(): + cert_provider = endpoint_from_flag('cert-provider.available') + + # get ingress info + ingress_for_clients = hookenv.network_get('clients')['ingress-addresses'] + ingress_for_db = hookenv.network_get('db')['ingress-addresses'] + + # use first ingress address as primary and any additional as SANs + server_cn, server_sans = ingress_for_clients[0], ingress_for_clients[:1] + client_cn, client_sans = ingress_for_db[0], ingress_for_db[:1] + + # request a single server and single client cert; note that multiple certs + # of either type can be requested as long as they have unique common names + cert_provider.request_server_cert(server_cn, server_sans) + cert_provider.request_client_cert(client_cn, client_sans) + + +@when('cert-provider.certs.changed') +def update_certs(): + cert_provider = endpoint_from_flag('cert-provider.available') + server_cert = cert_provider.server_certs[0] # only requested one + myserver.update_server_cert(server_cert.cert, server_cert.key) + + client_cert = cert_provider.client_certs[0] # only requested one + myclient.update_client_cert(client_cert.cert, client_cert.key) + clear_flag('cert-provider.certs.changed') +``` + + +# Reference + + * [Requires](docs/requires.md) + * [Provides](docs/provides.md) + +# Contact Information + +Maintainer: Cory Johns <Cory.Johns@canonical.com> + + +[Juju]: https://jujucharms.com +[Vault]: https://jujucharms.com/u/openstack-charmers/vault +[EasyRSA]: https://jujucharms.com/u/containers/easyrsa +[Introduction to PKI]: https://github.com/OpenVPN/easy-rsa/blob/master/doc/Intro-To-PKI.md diff --git a/kubernetes-master/hooks/relations/tls-certificates/__init__.py b/kubernetes-master/hooks/relations/tls-certificates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/tls-certificates/docs/common.md b/kubernetes-master/hooks/relations/tls-certificates/docs/common.md new file mode 100644 index 0000000..25d0e08 --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/docs/common.md @@ -0,0 +1,51 @@ +

CertificateRequest

+ +```python +CertificateRequest(self, unit, cert_type, cert_name, common_name, sans) +``` + +

application_name

+ +Name of the application which the request came from. + +:returns: Name of application +:rtype: str + +

cert

+ + +The cert published for this request, if any. + +

cert_type

+ + +Type of certificate, 'server' or 'client', being requested. + +

resolve_unit_name

+ +```python +CertificateRequest.resolve_unit_name(unit) +``` +Return name of unit associated with this request. + +unit_name should be provided in the relation data to ensure +compatability with cross-model relations. If the unit name +is absent then fall back to unit_name attribute of the +unit associated with this request. + +:param unit: Unit to extract name from +:type unit: charms.reactive.endpoints.RelatedUnit +:returns: Name of unit +:rtype: str + +

Certificate

+ +```python +Certificate(self, cert_type, common_name, cert, key) +``` + +Represents a created certificate and key. + +The ``cert_type``, ``common_name``, ``cert``, and ``key`` values can +be accessed either as properties or as the contents of the dict. + diff --git a/kubernetes-master/hooks/relations/tls-certificates/docs/provides.md b/kubernetes-master/hooks/relations/tls-certificates/docs/provides.md new file mode 100644 index 0000000..c213546 --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/docs/provides.md @@ -0,0 +1,212 @@ +

provides

+ + +

TlsProvides

+ +```python +TlsProvides(self, endpoint_name, relation_ids=None) +``` + +The provider's side of the interface protocol. + +The following flags may be set: + + * `{endpoint_name}.available` + Whenever any clients are joined. + + * `{endpoint_name}.certs.requested` + When there are new certificate requests of any kind to be processed. + The requests can be accessed via [new_requests][]. + + * `{endpoint_name}.server.certs.requested` + When there are new server certificate requests to be processed. + The requests can be accessed via [new_server_requests][]. + + * `{endpoint_name}.client.certs.requested` + When there are new client certificate requests to be processed. + The requests can be accessed via [new_client_requests][]. + +[Certificate]: common.md#tls_certificates_common.Certificate +[CertificateRequest]: common.md#tls_certificates_common.CertificateRequest +[all_requests]: provides.md#provides.TlsProvides.all_requests +[new_requests]: provides.md#provides.TlsProvides.new_requests +[new_server_requests]: provides.md#provides.TlsProvides.new_server_requests +[new_client_requests]: provides.md#provides.TlsProvides.new_client_requests + +

all_published_certs

+ + +List of all [Certificate][] instances that this provider has published +for all related applications. + +

all_requests

+ + +List of all requests that have been made. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('certs.regen', + 'tls.certs.available') +def regen_all_certs(): + tls = endpoint_from_flag('tls.certs.available') + for request in tls.all_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_application_requests

+ + +Filtered view of [new_requests][] that only includes application cert +requests. + +Each will be an instance of [ApplicationCertificateRequest][]. + +Example usage: + +```python +@when('tls.application.certs.requested') +def gen_application_certs(): + tls = endpoint_from_flag('tls.application.certs.requested') + for request in tls.new_application_requests: + cert, key = generate_application_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_client_requests

+ + +Filtered view of [new_requests][] that only includes client cert +requests. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('tls.client.certs.requested') +def gen_client_certs(): + tls = endpoint_from_flag('tls.client.certs.requested') + for request in tls.new_client_requests: + cert, key = generate_client_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_requests

+ + +Filtered view of [all_requests][] that only includes requests that +haven't been handled. + +Each will be an instance of [CertificateRequest][]. + +This collection can also be further filtered by request type using +[new_server_requests][] or [new_client_requests][]. + +Example usage: + +```python +@when('tls.certs.requested') +def gen_certs(): + tls = endpoint_from_flag('tls.certs.requested') + for request in tls.new_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_server_requests

+ + +Filtered view of [new_requests][] that only includes server cert +requests. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('tls.server.certs.requested') +def gen_server_certs(): + tls = endpoint_from_flag('tls.server.certs.requested') + for request in tls.new_server_requests: + cert, key = generate_server_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

set_ca

+ +```python +TlsProvides.set_ca(certificate_authority) +``` + +Publish the CA to all related applications. + +

set_chain

+ +```python +TlsProvides.set_chain(chain) +``` + +Publish the chain of trust to all related applications. + +

set_client_cert

+ +```python +TlsProvides.set_client_cert(cert, key) +``` + +Deprecated. This is only for backwards compatibility. + +Publish a globally shared client cert and key. + +

set_server_cert

+ +```python +TlsProvides.set_server_cert(scope, cert, key) +``` + +Deprecated. Use one of the [new_requests][] collections and +`request.set_cert()` instead. + +Set the server cert and key for the request identified by `scope`. + +

set_server_multicerts

+ +```python +TlsProvides.set_server_multicerts(scope) +``` + +Deprecated. Done automatically. + +

add_server_cert

+ +```python +TlsProvides.add_server_cert(scope, cn, cert, key) +``` + +Deprecated. Use `request.set_cert()` instead. + +

get_server_requests

+ +```python +TlsProvides.get_server_requests() +``` + +Deprecated. Use the [new_requests][] or [server_requests][] +collections instead. + +One provider can have many requests to generate server certificates. +Return a map of all server request objects indexed by a unique +identifier. + diff --git a/kubernetes-master/hooks/relations/tls-certificates/docs/requires.md b/kubernetes-master/hooks/relations/tls-certificates/docs/requires.md new file mode 100644 index 0000000..fdec902 --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/docs/requires.md @@ -0,0 +1,207 @@ +

requires

+ + +

TlsRequires

+ +```python +TlsRequires(self, endpoint_name, relation_ids=None) +``` + +The client's side of the interface protocol. + +The following flags may be set: + + * `{endpoint_name}.available` + Whenever the relation is joined. + + * `{endpoint_name}.ca.available` + When the root CA information is available via the [root_ca_cert][] and + [root_ca_chain][] properties. + + * `{endpoint_name}.ca.changed` + When the root CA information has changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.certs.available` + When the requested server or client certs are available. + + * `{endpoint_name}.certs.changed` + When the requested server or client certs have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.server.certs.available` + When the server certificates requested by [request_server_cert][] are + available via the [server_certs][] collection. + + * `{endpoint_name}.server.certs.changed` + When the requested server certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.client.certs.available` + When the client certificates requested by [request_client_cert][] are + available via the [client_certs][] collection. + + * `{endpoint_name}.client.certs.changed` + When the requested client certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + +The following flags have been deprecated: + + * `{endpoint_name}.server.cert.available` + * `{endpoint_name}.client.cert.available` + * `{endpoint_name}.batch.cert.available` + +[Certificate]: common.md#tls_certificates_common.Certificate +[CertificateRequest]: common.md#tls_certificates_common.CertificateRequest +[root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert +[root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain +[request_server_cert]: requires.md#requires.TlsRequires.request_server_cert +[request_client_cert]: requires.md#requires.TlsRequires.request_client_cert +[server_certs]: requires.md#requires.TlsRequires.server_certs +[server_certs_map]: requires.md#requires.TlsRequires.server_certs_map +[client_certs]: requires.md#requires.TlsRequires.server_certs + +

application_certs

+ + +List of [Certificate][] instances for all available application certs. + +

client_certs

+ + +List of [Certificate][] instances for all available client certs. + +

client_certs_map

+ + +Mapping of client [Certificate][] instances by their `common_name`. + +

root_ca_cert

+ + +Root CA certificate. + +

root_ca_chain

+ + +The chain of trust for the root CA. + +

server_certs

+ + +List of [Certificate][] instances for all available server certs. + +

server_certs_map

+ + +Mapping of server [Certificate][] instances by their `common_name`. + +

get_ca

+ +```python +TlsRequires.get_ca() +``` + +Return the root CA certificate. + +Same as [root_ca_cert][]. + +

get_chain

+ +```python +TlsRequires.get_chain() +``` + +Return the chain of trust for the root CA. + +Same as [root_ca_chain][]. + +

get_client_cert

+ +```python +TlsRequires.get_client_cert() +``` + +Deprecated. Use [request_client_cert][] and the [client_certs][] +collection instead. + +Return a globally shared client certificate and key. + +

get_server_cert

+ +```python +TlsRequires.get_server_cert() +``` + +Deprecated. Use the [server_certs][] collection instead. + +Return the cert and key of the first server certificate requested. + +

get_batch_requests

+ +```python +TlsRequires.get_batch_requests() +``` + +Deprecated. Use [server_certs_map][] instead. + +Mapping of server [Certificate][] instances by their `common_name`. + +

request_server_cert

+ +```python +TlsRequires.request_server_cert(cn, sans=None, cert_name=None) +``` + +Request a server certificate and key be generated for the given +common name (`cn`) and optional list of alternative names (`sans`). + +The `cert_name` is deprecated and not needed. + +This can be called multiple times to request more than one server +certificate, although the common names must be unique. If called +again with the same common name, it will be ignored. + +

add_request_server_cert

+ +```python +TlsRequires.add_request_server_cert(cn, sans) +``` + +Deprecated. Use [request_server_cert][] instead. + +

request_server_certs

+ +```python +TlsRequires.request_server_certs() +``` + +Deprecated. Just use [request_server_cert][]; this does nothing. + +

request_client_cert

+ +```python +TlsRequires.request_client_cert(cn, sans) +``` + +Request a client certificate and key be generated for the given +common name (`cn`) and list of alternative names (`sans`). + +This can be called multiple times to request more than one client +certificate, although the common names must be unique. If called +again with the same common name, it will be ignored. + +

request_application_cert

+ +```python +TlsRequires.request_application_cert(cn, sans) +``` + +Request an application certificate and key be generated for the given +common name (`cn`) and list of alternative names (`sans` ) of this +unit and all peer units. All units will share a single certificates. + diff --git a/kubernetes-master/hooks/relations/tls-certificates/interface.yaml b/kubernetes-master/hooks/relations/tls-certificates/interface.yaml new file mode 100644 index 0000000..beec53b --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/interface.yaml @@ -0,0 +1,6 @@ +name: tls-certificates +summary: | + A Transport Layer Security (TLS) charm layer that uses requires and provides + to exchange certifcates. +version: 1 +repo: https://github.com/juju-solutions/interface-tls-certificates diff --git a/kubernetes-master/hooks/relations/tls-certificates/make_docs b/kubernetes-master/hooks/relations/tls-certificates/make_docs new file mode 100644 index 0000000..2f2274a --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/make_docs @@ -0,0 +1,23 @@ +#!.tox/py3/bin/python + +import sys +import importlib +from pathlib import Path +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'cert': {'interface': 'tls-certificates'}}, + 'provides': {'cert': {'interface': 'tls-certificates'}}, + } + sys.path.append('..') + sys.modules[''] = importlib.import_module(Path.cwd().name) + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-master/hooks/relations/tls-certificates/provides.py b/kubernetes-master/hooks/relations/tls-certificates/provides.py new file mode 100644 index 0000000..0262baa --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/provides.py @@ -0,0 +1,301 @@ +if not __package__: + # fix relative imports when building docs + import sys + __package__ = sys.modules[''].__name__ + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag + +from .tls_certificates_common import ( + ApplicationCertificateRequest, + CertificateRequest +) + + +class TlsProvides(Endpoint): + """ + The provider's side of the interface protocol. + + The following flags may be set: + + * `{endpoint_name}.available` + Whenever any clients are joined. + + * `{endpoint_name}.certs.requested` + When there are new certificate requests of any kind to be processed. + The requests can be accessed via [new_requests][]. + + * `{endpoint_name}.server.certs.requested` + When there are new server certificate requests to be processed. + The requests can be accessed via [new_server_requests][]. + + * `{endpoint_name}.client.certs.requested` + When there are new client certificate requests to be processed. + The requests can be accessed via [new_client_requests][]. + + [Certificate]: common.md#tls_certificates_common.Certificate + [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest + [all_requests]: provides.md#provides.TlsProvides.all_requests + [new_requests]: provides.md#provides.TlsProvides.new_requests + [new_server_requests]: provides.md#provides.TlsProvides.new_server_requests + [new_client_requests]: provides.md#provides.TlsProvides.new_client_requests + """ + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('{endpoint_name}.available')) + toggle_flag(self.expand_name('{endpoint_name}.certs.requested'), + self.new_requests) + toggle_flag(self.expand_name('{endpoint_name}.server.certs.requested'), + self.new_server_requests) + toggle_flag(self.expand_name('{endpoint_name}.client.certs.requested'), + self.new_client_requests) + toggle_flag( + self.expand_name('{endpoint_name}.application.certs.requested'), + self.new_application_requests) + # For backwards compatibility, set the old "cert" flags as well + toggle_flag(self.expand_name('{endpoint_name}.server.cert.requested'), + self.new_server_requests) + toggle_flag(self.expand_name('{endpoint_name}.client.cert.requested'), + self.new_client_requests) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + clear_flag(self.expand_name('{endpoint_name}.certs.requested')) + clear_flag(self.expand_name('{endpoint_name}.server.certs.requested')) + clear_flag(self.expand_name('{endpoint_name}.client.certs.requested')) + clear_flag( + self.expand_name('{endpoint_name}.application.certs.requested')) + + def set_ca(self, certificate_authority): + """ + Publish the CA to all related applications. + """ + for relation in self.relations: + # All the clients get the same CA, so send it to them. + relation.to_publish_raw['ca'] = certificate_authority + + def set_chain(self, chain): + """ + Publish the chain of trust to all related applications. + """ + for relation in self.relations: + # All the clients get the same chain, so send it to them. + relation.to_publish_raw['chain'] = chain + + def set_client_cert(self, cert, key): + """ + Deprecated. This is only for backwards compatibility. + + Publish a globally shared client cert and key. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'client.cert': cert, + 'client.key': key, + }) + + def set_server_cert(self, scope, cert, key): + """ + Deprecated. Use one of the [new_requests][] collections and + `request.set_cert()` instead. + + Set the server cert and key for the request identified by `scope`. + """ + request = self.get_server_requests()[scope] + request.set_cert(cert, key) + + def set_server_multicerts(self, scope): + """ + Deprecated. Done automatically. + """ + pass + + def add_server_cert(self, scope, cn, cert, key): + ''' + Deprecated. Use `request.set_cert()` instead. + ''' + self.set_server_cert(scope, cert, key) + + def get_server_requests(self): + """ + Deprecated. Use the [new_requests][] or [server_requests][] + collections instead. + + One provider can have many requests to generate server certificates. + Return a map of all server request objects indexed by a unique + identifier. + """ + return {req._key: req for req in self.new_server_requests} + + @property + def all_requests(self): + """ + List of all requests that have been made. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('certs.regen', + 'tls.certs.available') + def regen_all_certs(): + tls = endpoint_from_flag('tls.certs.available') + for request in tls.all_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + requests = [] + for unit in self.all_joined_units: + # handle older single server cert request + if unit.received_raw['common_name']: + requests.append(CertificateRequest( + unit, + 'server', + unit.received_raw['certificate_name'], + unit.received_raw['common_name'], + unit.received['sans'], + )) + + # handle mutli server cert requests + reqs = unit.received['cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(CertificateRequest( + unit, + 'server', + common_name, + common_name, + req['sans'], + )) + + # handle client cert requests + reqs = unit.received['client_cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(CertificateRequest( + unit, + 'client', + common_name, + common_name, + req['sans'], + )) + # handle application cert requests + reqs = unit.received['application_cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(ApplicationCertificateRequest( + unit, + 'application', + common_name, + common_name, + req['sans'] + )) + return requests + + @property + def new_requests(self): + """ + Filtered view of [all_requests][] that only includes requests that + haven't been handled. + + Each will be an instance of [CertificateRequest][]. + + This collection can also be further filtered by request type using + [new_server_requests][] or [new_client_requests][]. + + Example usage: + + ```python + @when('tls.certs.requested') + def gen_certs(): + tls = endpoint_from_flag('tls.certs.requested') + for request in tls.new_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.all_requests if not req.is_handled] + + @property + def new_server_requests(self): + """ + Filtered view of [new_requests][] that only includes server cert + requests. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('tls.server.certs.requested') + def gen_server_certs(): + tls = endpoint_from_flag('tls.server.certs.requested') + for request in tls.new_server_requests: + cert, key = generate_server_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.new_requests if req.cert_type == 'server'] + + @property + def new_client_requests(self): + """ + Filtered view of [new_requests][] that only includes client cert + requests. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('tls.client.certs.requested') + def gen_client_certs(): + tls = endpoint_from_flag('tls.client.certs.requested') + for request in tls.new_client_requests: + cert, key = generate_client_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.new_requests if req.cert_type == 'client'] + + @property + def new_application_requests(self): + """ + Filtered view of [new_requests][] that only includes application cert + requests. + + Each will be an instance of [ApplicationCertificateRequest][]. + + Example usage: + + ```python + @when('tls.application.certs.requested') + def gen_application_certs(): + tls = endpoint_from_flag('tls.application.certs.requested') + for request in tls.new_application_requests: + cert, key = generate_application_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + + :returns: List of certificate requests. + :rtype: [CertificateRequest, ] + """ + return [req for req in self.new_requests + if req.cert_type == 'application'] + + @property + def all_published_certs(self): + """ + List of all [Certificate][] instances that this provider has published + for all related applications. + """ + return [req.cert for req in self.all_requests if req.cert] diff --git a/kubernetes-master/hooks/relations/tls-certificates/pydocmd.yml b/kubernetes-master/hooks/relations/tls-certificates/pydocmd.yml new file mode 100644 index 0000000..c568913 --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/pydocmd.yml @@ -0,0 +1,19 @@ +site_name: 'TLS Certificates Interface' + +generate: + - requires.md: + - requires + - requires.TlsRequires+ + - provides.md: + - provides + - provides.TlsProvides+ + - common.md: + - tls_certificates_common.CertificateRequest+ + - tls_certificates_common.Certificate+ + +pages: + - Requires: requires.md + - Provides: provides.md + - Common: common.md + +gens_dir: docs diff --git a/kubernetes-master/hooks/relations/tls-certificates/requires.py b/kubernetes-master/hooks/relations/tls-certificates/requires.py new file mode 100644 index 0000000..951f953 --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/requires.py @@ -0,0 +1,342 @@ +if not __package__: + # fix relative imports when building docs + import sys + __package__ = sys.modules[''].__name__ + +import uuid + +from charmhelpers.core import hookenv + +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag +from charms.reactive import Endpoint +from charms.reactive import data_changed + +from .tls_certificates_common import Certificate + + +class TlsRequires(Endpoint): + """ + The client's side of the interface protocol. + + The following flags may be set: + + * `{endpoint_name}.available` + Whenever the relation is joined. + + * `{endpoint_name}.ca.available` + When the root CA information is available via the [root_ca_cert][] and + [root_ca_chain][] properties. + + * `{endpoint_name}.ca.changed` + When the root CA information has changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.certs.available` + When the requested server or client certs are available. + + * `{endpoint_name}.certs.changed` + When the requested server or client certs have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.server.certs.available` + When the server certificates requested by [request_server_cert][] are + available via the [server_certs][] collection. + + * `{endpoint_name}.server.certs.changed` + When the requested server certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.client.certs.available` + When the client certificates requested by [request_client_cert][] are + available via the [client_certs][] collection. + + * `{endpoint_name}.client.certs.changed` + When the requested client certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + The following flags have been deprecated: + + * `{endpoint_name}.server.cert.available` + * `{endpoint_name}.client.cert.available` + * `{endpoint_name}.batch.cert.available` + + [Certificate]: common.md#tls_certificates_common.Certificate + [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest + [root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert + [root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain + [request_server_cert]: requires.md#requires.TlsRequires.request_server_cert + [request_client_cert]: requires.md#requires.TlsRequires.request_client_cert + [server_certs]: requires.md#requires.TlsRequires.server_certs + [server_certs_map]: requires.md#requires.TlsRequires.server_certs_map + [client_certs]: requires.md#requires.TlsRequires.server_certs + """ + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + self.relations[0].to_publish_raw['unit_name'] = self._unit_name + prefix = self.expand_name('{endpoint_name}.') + ca_available = self.root_ca_cert + ca_changed = ca_available and data_changed(prefix + 'ca', + self.root_ca_cert) + server_available = self.server_certs + server_changed = server_available and data_changed(prefix + 'servers', + self.server_certs) + client_available = self.client_certs + client_changed = client_available and data_changed(prefix + 'clients', + self.client_certs) + certs_available = server_available or client_available + certs_changed = server_changed or client_changed + + set_flag(prefix + 'available') + toggle_flag(prefix + 'ca.available', ca_available) + toggle_flag(prefix + 'ca.changed', ca_changed) + toggle_flag(prefix + 'server.certs.available', server_available) + toggle_flag(prefix + 'server.certs.changed', server_changed) + toggle_flag(prefix + 'client.certs.available', client_available) + toggle_flag(prefix + 'client.certs.changed', client_changed) + toggle_flag(prefix + 'certs.available', certs_available) + toggle_flag(prefix + 'certs.changed', certs_changed) + # deprecated + toggle_flag(prefix + 'server.cert.available', self.server_certs) + toggle_flag(prefix + 'client.cert.available', self.get_client_cert()) + toggle_flag(prefix + 'batch.cert.available', self.server_certs) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + prefix = self.expand_name('{endpoint_name}.') + clear_flag(prefix + 'available') + clear_flag(prefix + 'ca.available') + clear_flag(prefix + 'ca.changed') + clear_flag(prefix + 'server.certs.available') + clear_flag(prefix + 'server.certs.changed') + clear_flag(prefix + 'client.certs.available') + clear_flag(prefix + 'client.certs.changed') + clear_flag(prefix + 'certs.available') + clear_flag(prefix + 'certs.changed') + # deprecated + clear_flag(prefix + 'server.cert.available') + clear_flag(prefix + 'client.cert.available') + clear_flag(prefix + 'batch.cert.available') + + @property + def _unit_name(self): + return hookenv.local_unit().replace('/', '_') + + @property + def root_ca_cert(self): + """ + Root CA certificate. + """ + # only the leader of the provider should set the CA, or all units + # had better agree + return self.all_joined_units.received_raw['ca'] + + def get_ca(self): + """ + Return the root CA certificate. + + Same as [root_ca_cert][]. + """ + return self.root_ca_cert + + @property + def root_ca_chain(self): + """ + The chain of trust for the root CA. + """ + # only the leader of the provider should set the CA, or all units + # had better agree + return self.all_joined_units.received_raw['chain'] + + def get_chain(self): + """ + Return the chain of trust for the root CA. + + Same as [root_ca_chain][]. + """ + return self.root_ca_chain + + def get_client_cert(self): + """ + Deprecated. Use [request_client_cert][] and the [client_certs][] + collection instead. + + Return a globally shared client certificate and key. + """ + data = self.all_joined_units.received_raw + return (data['client.cert'], data['client.key']) + + def get_server_cert(self): + """ + Deprecated. Use the [server_certs][] collection instead. + + Return the cert and key of the first server certificate requested. + """ + if not self.server_certs: + return (None, None) + cert = self.server_certs[0] + return (cert.cert, cert.key) + + @property + def server_certs(self): + """ + List of [Certificate][] instances for all available server certs. + """ + certs = [] + raw_data = self.all_joined_units.received_raw + json_data = self.all_joined_units.received + + # for backwards compatibility, the first cert goes in its own fields + if self.relations: + common_name = self.relations[0].to_publish_raw['common_name'] + cert = raw_data['{}.server.cert'.format(self._unit_name)] + key = raw_data['{}.server.key'.format(self._unit_name)] + if cert and key: + certs.append(Certificate('server', + common_name, + cert, + key)) + + # subsequent requests go in the collection + field = '{}.processed_requests'.format(self._unit_name) + certs_data = json_data[field] or {} + certs.extend(Certificate('server', + common_name, + cert['cert'], + cert['key']) + for common_name, cert in certs_data.items()) + return certs + + @property + def application_certs(self): + """ + List containg the application Certificate cert. + + :returns: A list containing one certificate + :rtype: [Certificate()] + """ + certs = [] + json_data = self.all_joined_units.received + field = '{}.processed_application_requests'.format(self._unit_name) + certs_data = json_data[field] or {} + app_cert_data = certs_data.get('app_data') + if app_cert_data: + certs = [Certificate( + 'server', + 'app_data', + app_cert_data['cert'], + app_cert_data['key'])] + return certs + + @property + def server_certs_map(self): + """ + Mapping of server [Certificate][] instances by their `common_name`. + """ + return {cert.common_name: cert for cert in self.server_certs} + + def get_batch_requests(self): + """ + Deprecated. Use [server_certs_map][] instead. + + Mapping of server [Certificate][] instances by their `common_name`. + """ + return self.server_certs_map + + @property + def client_certs(self): + """ + List of [Certificate][] instances for all available client certs. + """ + field = '{}.processed_client_requests'.format(self._unit_name) + certs_data = self.all_joined_units.received[field] or {} + return [Certificate('client', + common_name, + cert['cert'], + cert['key']) + for common_name, cert in certs_data.items()] + + @property + def client_certs_map(self): + """ + Mapping of client [Certificate][] instances by their `common_name`. + """ + return {cert.common_name: cert for cert in self.client_certs} + + def request_server_cert(self, cn, sans=None, cert_name=None): + """ + Request a server certificate and key be generated for the given + common name (`cn`) and optional list of alternative names (`sans`). + + The `cert_name` is deprecated and not needed. + + This can be called multiple times to request more than one server + certificate, although the common names must be unique. If called + again with the same common name, it will be ignored. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + to_publish_raw = self.relations[0].to_publish_raw + if to_publish_raw['common_name'] in (None, '', cn): + # for backwards compatibility, first request goes in its own fields + to_publish_raw['common_name'] = cn + to_publish_json['sans'] = sans or [] + cert_name = to_publish_raw.get('certificate_name') or cert_name + if cert_name is None: + cert_name = str(uuid.uuid4()) + to_publish_raw['certificate_name'] = cert_name + else: + # subsequent requests go in the collection + requests = to_publish_json.get('cert_requests', {}) + requests[cn] = {'sans': sans or []} + to_publish_json['cert_requests'] = requests + + def add_request_server_cert(self, cn, sans): + """ + Deprecated. Use [request_server_cert][] instead. + """ + self.request_server_cert(cn, sans) + + def request_server_certs(self): + """ + Deprecated. Just use [request_server_cert][]; this does nothing. + """ + pass + + def request_client_cert(self, cn, sans): + """ + Request a client certificate and key be generated for the given + common name (`cn`) and list of alternative names (`sans`). + + This can be called multiple times to request more than one client + certificate, although the common names must be unique. If called + again with the same common name, it will be ignored. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + requests = to_publish_json.get('client_cert_requests', {}) + requests[cn] = {'sans': sans} + to_publish_json['client_cert_requests'] = requests + + def request_application_cert(self, cn, sans): + """ + Request an application certificate and key be generated for the given + common name (`cn`) and list of alternative names (`sans` ) of this + unit and all peer units. All units will share a single certificates. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + requests = to_publish_json.get('application_cert_requests', {}) + requests[cn] = {'sans': sans} + to_publish_json['application_cert_requests'] = requests diff --git a/kubernetes-master/hooks/relations/tls-certificates/tls_certificates_common.py b/kubernetes-master/hooks/relations/tls-certificates/tls_certificates_common.py new file mode 100644 index 0000000..99a2f8c --- /dev/null +++ b/kubernetes-master/hooks/relations/tls-certificates/tls_certificates_common.py @@ -0,0 +1,302 @@ +from charms.reactive import clear_flag, is_data_changed, data_changed + + +class CertificateRequest(dict): + def __init__(self, unit, cert_type, cert_name, common_name, sans): + self._unit = unit + self._cert_type = cert_type + super().__init__({ + 'certificate_name': cert_name, + 'common_name': common_name, + 'sans': sans, + }) + + @property + def _key(self): + return '.'.join((self._unit.relation.relation_id, + self.unit_name, + self.common_name)) + + def resolve_unit_name(self, unit): + """Return name of unit associated with this request. + + unit_name should be provided in the relation data to ensure + compatability with cross-model relations. If the unit name + is absent then fall back to unit_name attribute of the + unit associated with this request. + + :param unit: Unit to extract name from + :type unit: charms.reactive.endpoints.RelatedUnit + :returns: Name of unit + :rtype: str + """ + unit_name = unit.received_raw['unit_name'] + if not unit_name: + unit_name = unit.unit_name + return unit_name + + @property + def unit_name(self): + """Name of this unit. + + :returns: Name of unit + :rtype: str + """ + return self.resolve_unit_name(unit=self._unit).replace('/', '_') + + @property + def application_name(self): + """Name of the application which the request came from. + + :returns: Name of application + :rtype: str + """ + return self.resolve_unit_name(unit=self._unit).split('/')[0] + + @property + def cert_type(self): + """ + Type of certificate, 'server' or 'client', being requested. + """ + return self._cert_type + + @property + def cert_name(self): + return self['certificate_name'] + + @property + def common_name(self): + return self['common_name'] + + @property + def sans(self): + return self['sans'] + + @property + def _publish_key(self): + if self.cert_type == 'server': + return '{}.processed_requests'.format(self.unit_name) + elif self.cert_type == 'client': + return '{}.processed_client_requests'.format(self.unit_name) + raise ValueError('Unknown cert_type: {}'.format(self.cert_type)) + + @property + def _server_cert_key(self): + return '{}.server.cert'.format(self.unit_name) + + @property + def _server_key_key(self): + return '{}.server.key'.format(self.unit_name) + + @property + def _is_top_level_server_cert(self): + return (self.cert_type == 'server' and + self.common_name == self._unit.received_raw['common_name']) + + @property + def cert(self): + """ + The cert published for this request, if any. + """ + cert, key = None, None + if self._is_top_level_server_cert: + tpr = self._unit.relation.to_publish_raw + cert = tpr[self._server_cert_key] + key = tpr[self._server_key_key] + else: + tp = self._unit.relation.to_publish + certs_data = tp.get(self._publish_key, {}) + cert_data = certs_data.get(self.common_name, {}) + cert = cert_data.get('cert') + key = cert_data.get('key') + if cert and key: + return Certificate(self.cert_type, self.common_name, cert, key) + return None + + @property + def is_handled(self): + has_cert = self.cert is not None + same_sans = not is_data_changed(self._key, + sorted(set(self.sans or []))) + return has_cert and same_sans + + def set_cert(self, cert, key): + rel = self._unit.relation + if self._is_top_level_server_cert: + # backwards compatibility; if this is the cert that was requested + # as a single server cert, set it in the response as the single + # server cert + rel.to_publish_raw.update({ + self._server_cert_key: cert, + self._server_key_key: key, + }) + else: + data = rel.to_publish.get(self._publish_key, {}) + data[self.common_name] = { + 'cert': cert, + 'key': key, + } + rel.to_publish[self._publish_key] = data + if not rel.endpoint.new_server_requests: + clear_flag(rel.endpoint.expand_name('{endpoint_name}.server' + '.cert.requested')) + if not rel.endpoint.new_requests: + clear_flag(rel.endpoint.expand_name('{endpoint_name}.' + 'certs.requested')) + data_changed(self._key, sorted(set(self.sans or []))) + + +class ApplicationCertificateRequest(CertificateRequest): + """ + A request for an application consistent certificate. + + This is a request for a certificate that works for all units of an + application. All sans and cns are added together to produce one + certificate and the same certificate and key are sent to all the + units of an application. Only one ApplicationCertificateRequest + is needed per application. + """ + + @property + def _key(self): + """Key to identify this cert. + + :returns: cert key + :rtype: str + """ + return '{}.{}'.format(self._unit.relation.relation_id, 'app_cert') + + @property + def cert(self): + """ + The cert published for this request, if any. + + :returns: Certificate + :rtype: Certificate or None + """ + cert, key = None, None + tp = self._unit.relation.to_publish + certs_data = tp.get(self._publish_key, {}) + cert_data = certs_data.get('app_data', {}) + cert = cert_data.get('cert') + key = cert_data.get('key') + if cert and key: + return Certificate(self.cert_type, self.common_name, cert, key) + return None + + @property + def is_handled(self): + """Whether the certificate has been handled. + + :returns: If the cert has been handled + :rtype: bool + """ + has_cert = self.cert is not None + same_sans = not is_data_changed(self._key, + sorted(set(self.sans or []))) + return has_cert and same_sans + + @property + def sans(self): + """Generate a list of all sans from all units of application + + Examine all units of the application and compile a list of + all sans. CNs are treated as addition san entries. + + :returns: List of sans + :rtype: List[str] + """ + _sans = [] + for unit in self._unit.relation.units: + reqs = unit.received['application_cert_requests'] or {} + for cn, req in reqs.items(): + _sans.append(cn) + _sans.extend(req['sans']) + return sorted(list(set(_sans))) + + @property + def _request_key(self): + """Key used to request cert + + :returns: Key used to request cert + :rtype: str + """ + return 'application_cert_requests' + + def derive_publish_key(self, unit=None): + """Derive the application cert publish key for a unit. + + :param unit: Unit to extract name from + :type unit: charms.reactive.endpoints.RelatedUnit + :returns: publish key + :rtype: str + """ + if not unit: + unit = self._unit + unit_name = self.resolve_unit_name(unit).replace('/', '_') + return '{}.processed_application_requests'.format(unit_name) + + @property + def _publish_key(self): + """Key used to publish cert + + :returns: Key used to publish cert + :rtype: str + """ + return self.derive_publish_key(unit=self._unit) + + def set_cert(self, cert, key): + """Send the cert and key to all units of the application + + :param cert: TLS Certificate + :type cert: str + :param key: TLS Private Key + :type cert: str + """ + rel = self._unit.relation + for unit in self._unit.relation.units: + pub_key = self.derive_publish_key(unit=unit) + data = rel.to_publish.get( + pub_key, + {}) + data['app_data'] = { + 'cert': cert, + 'key': key, + } + rel.to_publish[pub_key] = data + if not rel.endpoint.new_application_requests: + clear_flag(rel.endpoint.expand_name( + '{endpoint_name}.application.certs.requested')) + data_changed(self._key, sorted(set(self.sans or []))) + + +class Certificate(dict): + """ + Represents a created certificate and key. + + The ``cert_type``, ``common_name``, ``cert``, and ``key`` values can + be accessed either as properties or as the contents of the dict. + """ + def __init__(self, cert_type, common_name, cert, key): + super().__init__({ + 'cert_type': cert_type, + 'common_name': common_name, + 'cert': cert, + 'key': key, + }) + + @property + def cert_type(self): + return self['cert_type'] + + @property + def common_name(self): + return self['common_name'] + + @property + def cert(self): + return self['cert'] + + @property + def key(self): + return self['key'] diff --git a/kubernetes-master/hooks/relations/vault-kv/.gitignore b/kubernetes-master/hooks/relations/vault-kv/.gitignore new file mode 100644 index 0000000..9dd3eb8 --- /dev/null +++ b/kubernetes-master/hooks/relations/vault-kv/.gitignore @@ -0,0 +1,2 @@ +.tox +.testrepository diff --git a/kubernetes-master/hooks/relations/vault-kv/README.md b/kubernetes-master/hooks/relations/vault-kv/README.md new file mode 100644 index 0000000..f09c312 --- /dev/null +++ b/kubernetes-master/hooks/relations/vault-kv/README.md @@ -0,0 +1,52 @@ +# Overview + +This interface handles the communication with the vault charm using the +vault-kv interface type. + +Vault will enable simple KV based secrets backends with AppRole based +authentication and policies to allow consuming charms to store and retrieve +secrets in Vault. + +Access to the backend will be limited to the network address binding of +of the relation endpoint name and ownership of a secret\_id which the +consuming application must retrieve using a one-shot token out-of-band +from Juju. + +# Usage + +## Requires + +The interface layer will set the following reactive states, as appropriate: + + * `{relation_name}.connected` The relation is established and ready for + the local charm to make a request for access to a secrets backend using + the `request_secret_backend` method. + + * `{relation_name}.available` When vault has created the backend and an + associated AppRole to allow the local charm to store and retrieve secrets + in vault - the `vault_url` and `unit_role_id` properties will be set. + + For example: + +```python +from charms.reactive.flags import endpoint_from_flag + + @when('secrets-storage.connected') + def ss_connected(): + secrets = endpoint_from_flag('secrets-storage.connected') + secrets.request_secret_backend('charm-vaultlocker', isolated=True) + + + @when('secrets-storage.available') + def ss_ready_for_use(): + secrets = endpoint_from_flag('secrets-storage.connected') + configure_my_local_service( + vault_url=secrets.vault_url, + role_id=secrets.unit_role_id, + secret_id=vault.get_response(secrets.unit_token), + backend='charm-vaultlocker', + ) + ``` + + Note that the backend name must be prefixed with 'charm-' otherwise the vault + charm will skip creation of the secrets backend and associated access. diff --git a/kubernetes-master/hooks/relations/vault-kv/__init__.py b/kubernetes-master/hooks/relations/vault-kv/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/vault-kv/copyright b/kubernetes-master/hooks/relations/vault-kv/copyright new file mode 100644 index 0000000..32a8f52 --- /dev/null +++ b/kubernetes-master/hooks/relations/vault-kv/copyright @@ -0,0 +1,21 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0 + +Files: * +Copyright: 2018, Canonical Ltd. +License: Apache-2.0 + +License: Apache-2.0 + 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. + . + On Debian-based systems the full text of the Apache version 2.0 license + can be found in `/usr/share/common-licenses/Apache-2.0'. diff --git a/kubernetes-master/hooks/relations/vault-kv/interface.yaml b/kubernetes-master/hooks/relations/vault-kv/interface.yaml new file mode 100644 index 0000000..b03cb19 --- /dev/null +++ b/kubernetes-master/hooks/relations/vault-kv/interface.yaml @@ -0,0 +1,4 @@ +name: vault-kv +summary: Vault simple Key/Value secret storage interface +version: 1 +maintainer: "James Page " diff --git a/kubernetes-master/hooks/relations/vault-kv/provides.py b/kubernetes-master/hooks/relations/vault-kv/provides.py new file mode 100644 index 0000000..8039448 --- /dev/null +++ b/kubernetes-master/hooks/relations/vault-kv/provides.py @@ -0,0 +1,94 @@ +# 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. + +from charms.reactive import is_flag_set, toggle_flag, clear_flag +from charms.reactive import Endpoint +from charmhelpers import core as ch_core + + +class VaultKVProvides(Endpoint): + def manage_flags(self): + any_fields_changed = False + for field in ('access_address', + 'secret_backend', + 'hostname', + 'isolated'): + flag = self.expand_name('endpoint.{endpoint_name}.' + 'changed.{}').format(field) + any_fields_changed = any_fields_changed or is_flag_set(flag) + clear_flag(flag) + toggle_flag(self.expand_name('{endpoint_name}.connected'), + self.is_joined) + toggle_flag(self.expand_name('endpoint.{endpoint_name}.new-request'), + any_fields_changed) + + def publish_url(self, vault_url, remote_binding=None): + """ Publish URL for Vault to all Relations + + :param vault_url: api url used by remote client to speak to vault. + :param remote_binding: Deprecated + """ + if remote_binding: + ch_core.hookenv.log( + "Use of remote_binding in publish_url is deprecated. " + "See LP Bug #1895185", "WARNING") + for relation in self.relations: + relation.to_publish['vault_url'] = vault_url + + def publish_ca(self, vault_ca): + """ Publish SSL CA for Vault to all Relations """ + for relation in self.relations: + relation.to_publish['vault_ca'] = vault_ca + + def get_remote_unit_name(self, unit): + """Get the remote units name. + + :param unit: Unit to get name for. + :type name: Unit + :returns: Unit name + :rtype: str + """ + return unit.received.get('unit_name') or unit.unit_name + + def set_role_id(self, unit, role_id, token): + """ Set the AppRole ID and token for out-of-band Secret ID retrieval + for a specific remote unit """ + # for cmr we will need to the other end to provide their unit name + # expicitly. + unit_name = self.get_remote_unit_name(unit) + unit.relation.to_publish['{}_role_id'.format(unit_name)] = role_id + unit.relation.to_publish['{}_token'.format(unit_name)] = token + + def requests(self): + """ Retrieve full set of setup requests from all remote units """ + requests = [] + for relation in self.relations: + for unit in relation.units: + access_address = unit.received['access_address'] + ingress_address = unit.received['ingress-address'] + secret_backend = unit.received['secret_backend'] + hostname = unit.received['hostname'] + isolated = unit.received['isolated'] + unit_name = self.get_remote_unit_name(unit) + if not (secret_backend and access_address and + hostname and isolated is not None): + continue + requests.append({ + 'unit': unit, + 'unit_name': unit_name, + 'access_address': access_address, + 'ingress_address': ingress_address, + 'secret_backend': secret_backend, + 'hostname': hostname, + 'isolated': isolated, + }) + return requests diff --git a/kubernetes-master/hooks/relations/vault-kv/requires.py b/kubernetes-master/hooks/relations/vault-kv/requires.py new file mode 100644 index 0000000..550d8ef --- /dev/null +++ b/kubernetes-master/hooks/relations/vault-kv/requires.py @@ -0,0 +1,108 @@ +# 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 base64 +import socket + +from charmhelpers.core import hookenv + +from charms.reactive import toggle_flag +from charms.reactive import Endpoint + + +class VaultKVRequires(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.connected'), + self.is_joined) + toggle_flag(self.expand_name('{endpoint_name}.available'), + all([self.is_joined, + self.unit_role_id, + self.unit_token, + self.vault_url])) + + @property + def endpoint_address(self): + """ Determine the local endpoint network address """ + try: + return hookenv.network_get_primary_address( + self.expand_name('{endpoint_name}') + ) + except NotImplementedError: + return hookenv.unit_private_ip() + + def request_secret_backend(self, name, isolated=True): + """Request creation and access to a secret backend + + :param name: name of secret backend to create/access + :type name: str + :param isolated: enforce isolation in backend between units + :type isolated: bool""" + for relation in self.relations: + relation.to_publish['secret_backend'] = name + relation.to_publish['access_address'] = self.endpoint_address + relation.to_publish['hostname'] = socket.gethostname() + relation.to_publish['isolated'] = isolated + relation.to_publish['unit_name'] = hookenv.local_unit() + + @property + def unit_role_id(self): + """Retrieve the AppRole ID for this application unit or None + + :returns role_id: AppRole ID for unit + :rtype role_id: str""" + role_key = '{}_role_id'.format(hookenv.local_unit()) + return self.all_joined_units.received.get(role_key) + + @property + def unit_token(self): + """Retrieve the one-shot token for secret_id retrieval for + this application unit or None + + :returns token: Vault one-shot toekn for secret_id response + :rtype token: str""" + token_key = '{}_token'.format(hookenv.local_unit()) + return self.all_joined_units.received.get(token_key) + + @property + def all_unit_tokens(self): + """Retrieve the one-shot token(s) for secret_id retrieval for + all application units or empty list. + + :returns token: Vault one-shot token for secret_id response + :rtype token: str""" + token_key = '{}_token'.format(hookenv.local_unit()) + tokens = set() + for relation in self.relations: + for unit in relation.units: + token = unit.received.get(token_key) + if token: + tokens.add(token) + + return list(tokens) + + @property + def vault_url(self): + """Retrieve the URL to access Vault + + :returns vault_url: URL to access vault + :rtype vault_url: str""" + return self.all_joined_units.received.get('vault_url') + + @property + def vault_ca(self): + """Retrieve the CA published by Vault + + :returns vault_ca: Vault CA Certificate data + :rtype vault_ca: str""" + encoded_ca = self.all_joined_units.received.get('vault_ca') + if encoded_ca: + return base64.b64decode(encoded_ca) diff --git a/kubernetes-master/hooks/relations/vault-kv/test-requirements.txt b/kubernetes-master/hooks/relations/vault-kv/test-requirements.txt new file mode 100644 index 0000000..db5ef38 --- /dev/null +++ b/kubernetes-master/hooks/relations/vault-kv/test-requirements.txt @@ -0,0 +1,2 @@ +flake8>=2.2.4 +os-testr>=0.4.1 diff --git a/kubernetes-master/hooks/relations/vsphere-integration/.gitignore b/kubernetes-master/hooks/relations/vsphere-integration/.gitignore new file mode 100644 index 0000000..5f9f2c5 --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/.gitignore @@ -0,0 +1,3 @@ +.tox +__pycache__ +*.pyc diff --git a/kubernetes-master/hooks/relations/vsphere-integration/LICENSE b/kubernetes-master/hooks/relations/vsphere-integration/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-master/hooks/relations/vsphere-integration/README.md b/kubernetes-master/hooks/relations/vsphere-integration/README.md new file mode 100644 index 0000000..28ff438 --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/README.md @@ -0,0 +1,28 @@ +# Overview + +This layer encapsulates the `vsphere-integration` interface communication +protocol and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:vsphere-integration` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:vsphere-integration'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `vsphere-integration` interface protocol: + +```yaml +requires: + vsphere: + interface: vsphere-integration +``` + +For documentation on how to use the API for this interface, see: + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) (this will only be used by the vsphere-integrator charm) diff --git a/kubernetes-master/hooks/relations/vsphere-integration/__init__.py b/kubernetes-master/hooks/relations/vsphere-integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/hooks/relations/vsphere-integration/copyright b/kubernetes-master/hooks/relations/vsphere-integration/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-master/hooks/relations/vsphere-integration/docs/provides.md b/kubernetes-master/hooks/relations/vsphere-integration/docs/provides.md new file mode 100644 index 0000000..796b7e6 --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/docs/provides.md @@ -0,0 +1,74 @@ +

provides

+ + +This is the provides side of the interface layer, for use only by the +vSphere integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for vSphere integration + features. The vSphere integration charm should then iterate over each + request, perform whatever actions are necessary to satisfy those requests, + and then mark them as complete. + +

VsphereIntegrationProvides

+ +```python +VsphereIntegrationProvides(self, endpoint_name, relation_ids=None) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag +from charms import layer + +@when('endpoint.vsphere.requests-pending') +def handle_requests(): + vsphere = endpoint_from_flag('endpoint.vsphere.requests-pending') + for request in vsphere.requests: + request.set_credentials(layer.vsphere.get_user_credentials()) + vsphere.mark_completed() +``` + +

requests

+ + +A list of the new or updated `IntegrationRequests` that +have been made. + +

mark_completed

+ +```python +VsphereIntegrationProvides.mark_completed(self) +``` + +Mark all requests as completed and remove the `requests-pending` flag. + +

IntegrationRequest

+ +```python +IntegrationRequest(self, unit) +``` + +A request for integration from a single remote unit. + +

has_credentials

+ + +Whether or not credentials have been set via `set_credentials`. + +

is_changed

+ + +Whether this request has changed since the last time it was +marked completed (if ever). + +

set_credentials

+ +```python +IntegrationRequest.set_credentials(self, vsphere_ip, user, password, datacenter, datastore) +``` + +Set the credentials for this request. diff --git a/kubernetes-master/hooks/relations/vsphere-integration/docs/requires.md b/kubernetes-master/hooks/relations/vsphere-integration/docs/requires.md new file mode 100644 index 0000000..0ce10a9 --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/docs/requires.md @@ -0,0 +1,56 @@ +

requires

+ + +This is the requires side of the interface layer, for use in charms that wish +to request integration with vSphere native features. The integration will be +provided by the vSphere integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of vSphere +specific API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific vSphere features. This flag is automatically removed + if the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the vSphere instance on which the charm is + running. This flag is automatically removed if new integration features are + requested. It should not be removed by the charm. + +

VsphereIntegrationRequires

+ +```python +VsphereIntegrationRequires(self, endpoint_name, relation_ids=None) +``` + +Interface to request integration access. + +Note that due to resource limits and permissions granularity, policies are +limited to being applied at the charm level. That means that, if any +permissions are requested (i.e., any of the enable methods are called), +what is granted will be the sum of those ever requested by any instance of +the charm on this cloud. + +Labels, on the other hand, will be instance specific. + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag + +@when('endpoint.vsphere.ready') +def vsphere_integration_ready(): + vsphere = endpoint_from_flag('endpoint.vsphere.joined') + update_config_enable_vsphere(vsphere.vsphere_ip, + vsphere.user, + vsphere.password, + vsphere.datacenter, + vsphere.datastore) +``` + +

is_ready

+ + +Whether or not the request for this instance has been completed. diff --git a/kubernetes-master/hooks/relations/vsphere-integration/interface.yaml b/kubernetes-master/hooks/relations/vsphere-integration/interface.yaml new file mode 100644 index 0000000..c4c0c07 --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/interface.yaml @@ -0,0 +1,4 @@ +name: vsphere-integration +summary: Interface for connecting to the VMware vSphere integrator charm. +version: 1 +maintainer: Kevin Monroe diff --git a/kubernetes-master/hooks/relations/vsphere-integration/make_docs b/kubernetes-master/hooks/relations/vsphere-integration/make_docs new file mode 100644 index 0000000..04cf35b --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'vsphere': {'interface': 'vsphere'}}, + 'provides': {'vsphere': {'interface': 'vsphere'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-master/hooks/relations/vsphere-integration/provides.py b/kubernetes-master/hooks/relations/vsphere-integration/provides.py new file mode 100644 index 0000000..c3db1d8 --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/provides.py @@ -0,0 +1,132 @@ +""" +This is the provides side of the interface layer, for use only by the +vSphere integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for vSphere integration + features. The vSphere integration charm should then iterate over each + request, perform whatever actions are necessary to satisfy those requests, + and then mark them as complete. +""" + +from operator import attrgetter + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class VsphereIntegrationProvides(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + + @when('endpoint.vsphere.requests-pending') + def handle_requests(): + vsphere = endpoint_from_flag('endpoint.vsphere.requests-pending') + for request in vsphere.requests: + request.set_credentials(layer.vsphere.get_vsphere_credentials()) + request.set_config(layer.vsphere.get_vsphere_config()) + vsphere.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + toggle_flag(self.expand_name('requests-pending'), + len(self.new_requests) > 0) + clear_flag(self.expand_name('changed')) + + @property + def all_requests(self): + """ + A list of all the #IntegrationRequests that have been made. + """ + return [IntegrationRequest(unit) for unit in self.all_joined_units] + + @property + def new_requests(self): + """ + A list of the new or updated #IntegrationRequests that have been made. + """ + is_changed = attrgetter('is_changed') + return list(filter(is_changed, self.all_requests)) + + def mark_completed(self): + """ + Remove the `requests-pending` flag. + """ + clear_flag(self.expand_name('requests-pending')) + + +class IntegrationRequest: + """ + A request for integration from a single remote unit. + """ + def __init__(self, unit): + self._unit = unit + + @property + def _to_publish(self): + return self._unit.relation.to_publish + + @property + def has_credentials(self): + """ + Whether or not `set_credentials` has been called. + """ + return {'vsphere_ip', 'user', + 'password', 'datacenter'}.issubset(self._to_publish) + + @property + def has_config(self): + """ + Whether or not `set_config` has been called. + """ + return {'datastore', 'folder', + 'respool_path'}.issubset(self._to_publish) + + @property + def is_changed(self): + """ + Whether this request has changed since the last time it was + marked completed (if ever). + """ + return not (self.has_credentials and self.has_config) + + @property + def unit_name(self): + return self._unit.unit_name + + def set_credentials(self, + vsphere_ip, + user, + password, + datacenter): + """ + Set the vsphere credentials for this request. + """ + self._to_publish.update({ + 'vsphere_ip': vsphere_ip, + 'user': user, + 'password': password, + 'datacenter': datacenter, + }) + + def set_config(self, + datastore, + folder, + respool_path): + """ + Set the non-credential vsphere config for this request. + """ + self._to_publish.update({ + 'datastore': datastore, + 'folder': folder, + 'respool_path': respool_path, + }) diff --git a/kubernetes-master/hooks/relations/vsphere-integration/pydocmd.yml b/kubernetes-master/hooks/relations/vsphere-integration/pydocmd.yml new file mode 100644 index 0000000..e1d5d4a --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'VMware vSphere Integration Interface' + +generate: + - requires.md: + - requires + - requires.VsphereIntegrationRequires+ + - provides.md: + - provides + - provides.VsphereIntegrationProvides+ + - provides.IntegrationRequest+ + +pages: + - Requires: requires.md + - Provides: provides.md + +gens_dir: docs diff --git a/kubernetes-master/hooks/relations/vsphere-integration/requires.py b/kubernetes-master/hooks/relations/vsphere-integration/requires.py new file mode 100644 index 0000000..d8b9cdb --- /dev/null +++ b/kubernetes-master/hooks/relations/vsphere-integration/requires.py @@ -0,0 +1,141 @@ +""" +This is the requires side of the interface layer, for use in charms that wish +to request integration with vSphere native features. The integration will be +provided by the vSphere integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of vSphere +specific API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific vSphere features. This flag is automatically removed + if the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the vSphere instance on which the charm is + running. This flag is automatically removed if new integration features are + requested. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready.changed`** This flag is set if the data + changes after the ready flag was set. This flag should be removed by the + charm once handled. +""" + + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import clear_flag, is_flag_set, set_flag, toggle_flag +from charms.reactive import data_changed + + +class VsphereIntegrationRequires(Endpoint): + """ + Interface to request integration access. + + Note that due to resource limits and permissions granularity, policies are + limited to being applied at the charm level. That means that, if any + permissions are requested (i.e., any of the enable methods are called), + what is granted will be the sum of those ever requested by any instance of + the charm on this cloud. + + Labels, on the other hand, will be instance specific. + + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + + @when('endpoint.vsphere.ready') + def vsphere_integration_ready(): + vsphere = endpoint_from_flag('endpoint.vsphere.joined') + update_config_enable_vsphere(vsphere.vsphere_ip, + vsphere.user, + vsphere.password, + vsphere.datacenter, + vsphere.datastore, + vsphere.folder, + vsphere.respool_path) + ``` + """ + + @property + def _received(self): + """ + Helper to streamline access to received data. + """ + return self.all_joined_units.received + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + """ + Manage flags to signal when the endpoint is ready as well as noting + if changes have been made since it became ready. + """ + was_ready = is_flag_set(self.expand_name('ready')) + toggle_flag(self.expand_name('ready'), self.is_ready) + if self.is_ready and was_ready and self.is_changed: + set_flag(self.expand_name('ready.changed')) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + @property + def is_ready(self): + """ + Whether or not the request for this instance has been completed. + """ + return all(field is not None for field in [ + self.vsphere_ip, + self.user, + self.password, + self.datacenter, + self.datastore, + self.folder, + self.respool_path, + ]) + + @property + def is_changed(self): + """ + Whether or not the request for this instance has changed. + """ + return data_changed(self.expand_name('all-data'), [ + self.vsphere_ip, + self.user, + self.password, + self.datacenter, + self.datastore, + self.folder, + self.respool_path, + ]) + + @property + def vsphere_ip(self): + return self._received['vsphere_ip'] + + @property + def user(self): + return self._received['user'] + + @property + def password(self): + return self._received['password'] + + @property + def datacenter(self): + return self._received['datacenter'] + + @property + def datastore(self): + return self._received['datastore'] + + @property + def folder(self): + return self._received['folder'] + + @property + def respool_path(self): + return self._received['respool_path'] diff --git a/kubernetes-master/hooks/start b/kubernetes-master/hooks/start new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/start @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/stop b/kubernetes-master/hooks/stop new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/stop @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/update-status b/kubernetes-master/hooks/update-status new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/update-status @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/upgrade-charm b/kubernetes-master/hooks/upgrade-charm new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/upgrade-charm @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/vault-kv-relation-broken b/kubernetes-master/hooks/vault-kv-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/vault-kv-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/vault-kv-relation-changed b/kubernetes-master/hooks/vault-kv-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/vault-kv-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/vault-kv-relation-created b/kubernetes-master/hooks/vault-kv-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/vault-kv-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/vault-kv-relation-departed b/kubernetes-master/hooks/vault-kv-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/vault-kv-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/vault-kv-relation-joined b/kubernetes-master/hooks/vault-kv-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/vault-kv-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/vsphere-relation-broken b/kubernetes-master/hooks/vsphere-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/vsphere-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/vsphere-relation-changed b/kubernetes-master/hooks/vsphere-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/vsphere-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/vsphere-relation-created b/kubernetes-master/hooks/vsphere-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/vsphere-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/vsphere-relation-departed b/kubernetes-master/hooks/vsphere-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/vsphere-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/hooks/vsphere-relation-joined b/kubernetes-master/hooks/vsphere-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-master/hooks/vsphere-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-master/icon.svg b/kubernetes-master/icon.svg new file mode 100644 index 0000000..0ab453f --- /dev/null +++ b/kubernetes-master/icon.svg @@ -0,0 +1,362 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/kubernetes-master/layer.yaml b/kubernetes-master/layer.yaml new file mode 100644 index 0000000..1d6db2a --- /dev/null +++ b/kubernetes-master/layer.yaml @@ -0,0 +1,97 @@ +"includes": +- "layer:options" +- "layer:basic" +- "layer:debug" +- "interface:tls-certificates" +- "interface:nrpe-external-master" +- "layer:cis-benchmark" +- "layer:coordinator" +- "layer:kubernetes-common" +- "interface:container-runtime" +- "interface:vault-kv" +- "layer:status" +- "layer:apt" +- "layer:vault-kv" +- "interface:hacluster" +- "layer:snap" +- "layer:tls-client" +- "layer:leadership" +- "layer:metrics" +- "layer:nagios" +- "layer:cdk-service-kicker" +- "layer:kubernetes-master-worker-base" +- "layer:vaultlocker" +- "layer:hacluster" +- "interface:ceph-admin" +- "interface:ceph-client" +- "interface:etcd" +- "interface:http" +- "interface:kubernetes-cni" +- "interface:kube-dns" +- "interface:kube-control" +- "interface:kube-masters" +- "interface:public-address" +- "interface:aws-integration" +- "interface:gcp-integration" +- "interface:openstack-integration" +- "interface:vsphere-integration" +- "interface:azure-integration" +- "interface:keystone-credentials" +- "interface:prometheus-manual" +- "interface:grafana-dashboard" +- "interface:aws-iam" +"exclude": [".travis.yml", "tests", "tox.ini", "test-requirements.txt", "unit_tests", + ".tox", "__pycache__", "Makefile", "conftest.py"] +"options": + "coordinator": + # Absolute path to the charmhelpers.coordinator.BaseCoordinator to use. + "class": "charms.coordinator.SimpleCoordinator" + # Layer log level (debug, info, warning, error, critical) + "log_level": "info" + "basic": + "packages": + - "socat" + "python_packages": [] + "use_venv": !!bool "true" + "include_system_packages": !!bool "false" + "tls-client": + "ca_certificate_path": "/root/cdk/ca.crt" + "server_certificate_path": "" + "server_key_path": "" + "client_certificate_path": "" + "client_key_path": "" + "cdk-service-kicker": + "services": + - "snap.kube-apiserver.daemon" + - "snap.kube-controller-manager.daemon" + - "snap.kube-scheduler.daemon" + - "snap.kube-proxy.daemon" + "hacluster": + "binding_address": "kube-api-endpoint" + "snap": {} + "debug": {} + "leadership": {} + "nagios": {} + "cis-benchmark": {} + "kubernetes-common": {} + "kubernetes-master-worker-base": {} + "vault-kv": {} + "status": + "patch-hookenv": !!bool "true" + "apt": + "packages": [] + "version_package": "" + "full_version": !!bool "false" + "keys": [] + "vaultlocker": {} + "kubernetes-master": {} +"repo": "https://github.com/kubernetes/kubernetes.git" +"proof": + "storage": + - "name": "vaultlocker-encrypt" + "type": "Boolean" + "missing": !!bool "false" + - "name": "vaultlocker-mountbase" + "type": "String" + "missing": "" +"is": "kubernetes-master" diff --git a/kubernetes-master/lib/charms/apt.py b/kubernetes-master/lib/charms/apt.py new file mode 100644 index 0000000..14508c4 --- /dev/null +++ b/kubernetes-master/lib/charms/apt.py @@ -0,0 +1,209 @@ +# Copyright 2015-2020 Canonical Ltd. +# +# This file is part of the Apt layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +charms.reactive helpers for dealing with deb packages. + +Add apt package sources using add_source(). Queue deb packages for +installation with install(). Configure and work with your software +once the apt.installed.{packagename} flag is set. +''' +import itertools +import re +import subprocess + +from charmhelpers import fetch +from charmhelpers.core import hookenv, unitdata +from charms import layer, reactive +from charms.layer import status +from charms.reactive import flags + + +__all__ = ['add_source', 'update', 'queue_install', 'install_queued', 'installed', 'purge', 'ensure_package_status'] + + +def add_source(source, key=None): + '''Add an apt source. + + Sets the apt.needs_update flag. + + A source may be either a line that can be added directly to + sources.list(5), or in the form ppa:/ for adding + Personal Package Archives, or a distribution component to enable. + + The package signing key should be an ASCII armoured GPG key. While + GPG key ids are also supported, the retrieval mechanism is insecure. + There is no need to specify the package signing key for PPAs or for + the main Ubuntu archives. + ''' + # Maybe we should remember which sources have been added already + # so we don't waste time re-adding them. Is this time significant? + fetch.add_source(source, key) + reactive.set_flag('apt.needs_update') + + +def queue_install(packages, options=None): + """Queue one or more deb packages for install. + + The `apt.installed.{name}` flag is set once the package is installed. + + If a package has already been installed it will not be reinstalled. + + If a package has already been queued it will not be requeued, and + the install options will not be changed. + + Sets the apt.queued_installs flag. + """ + if isinstance(packages, str): + packages = [packages] + # Filter installed packages. + store = unitdata.kv() + queued_packages = store.getrange('apt.install_queue.', strip=True) + packages = { + package: options + for package in packages + if not (package in queued_packages or reactive.is_flag_set('apt.installed.' + package)) + } + if packages: + unitdata.kv().update(packages, prefix='apt.install_queue.') + reactive.set_flag('apt.queued_installs') + + +def installed(): + '''Return the set of deb packages completed install''' + return set(flag.split('.', 2)[2] for flag in flags.get_flags() if flag.startswith('apt.installed.')) + + +def purge(packages): + """Purge one or more deb packages from the system""" + fetch.apt_purge(packages, fatal=True) + store = unitdata.kv() + store.unsetrange(packages, prefix='apt.install_queue.') + for package in packages: + reactive.clear_flag('apt.installed.{}'.format(package)) + + +def update(): + """Update the apt cache. + + Removes the apt.needs_update flag. + """ + status.maintenance('Updating apt cache') + fetch.apt_update(fatal=True) # Friends don't let friends set fatal=False + reactive.clear_flag('apt.needs_update') + + +def install_queued(): + '''Installs queued deb packages. + + Removes the apt.queued_installs flag and sets the apt.installed flag. + + On failure, sets the unit's workload status to 'blocked' and returns + False. Package installs remain queued. + + On success, sets the apt.installed.{packagename} flag for each + installed package and returns True. + ''' + store = unitdata.kv() + queue = sorted((options, package) for package, options in store.getrange('apt.install_queue.', strip=True).items()) + + installed = set() + for options, batch in itertools.groupby(queue, lambda x: x[0]): + packages = [b[1] for b in batch] + try: + status.maintenance('Installing {}'.format(','.join(packages))) + fetch.apt_install(packages, options, fatal=True) + store.unsetrange(packages, prefix='apt.install_queue.') + installed.update(packages) + except subprocess.CalledProcessError: + status.blocked('Unable to install packages {}'.format(','.join(packages))) + return False # Without setting reactive flag. + + for package in installed: + reactive.set_flag('apt.installed.{}'.format(package)) + reactive.clear_flag('apt.queued_installs') + + reset_application_version() + + return True + + +def get_package_version(package, full_version=False): + '''Return the version of an installed package. + + If `full_version` is True, returns the full Debian package version. + Otherwise, returns the shorter 'upstream' version number. + ''' + # Don't use fetch.get_upstream_version, as it depends on python-apt + # and not available if the basic layer's use_site_packages option is off. + cmd = ['dpkg-query', '--show', r'--showformat=${Version}\n', package] + full = subprocess.check_output(cmd, universal_newlines=True).strip() + if not full_version: + # Attempt to strip off Debian style metadata from the end of the + # version number. + m = re.search(r'^([\d.a-z]+)', full, re.I) + if m is not None: + return m.group(1) + return full + + +def reset_application_version(): + '''Set the Juju application version, per settings in layer.yaml''' + # Reset the application version. We call this after installing + # packages to initialize the version. We also call this every + # hook, incase the version has changed (eg. Landscape upgraded + # the package). + opts = layer.options().get('apt', {}) + pkg = opts.get('version_package') + if pkg and pkg in installed(): + ver = get_package_version(pkg, opts.get('full_version', False)) + hookenv.application_version_set(ver) + + +def ensure_package_status(): + '''Hold or unhold packages per the package_status configuration option. + + All packages installed using this module and handlers are affected. + + An mechanism may be added in the future to override this for a + subset of installed packages. + ''' + packages = installed() + if not packages: + return + config = hookenv.config() + package_status = config.get('package_status') or '' + changed = reactive.data_changed('apt.package_status', (package_status, sorted(packages))) + if changed: + if package_status == 'hold': + hookenv.log('Holding packages {}'.format(','.join(packages))) + fetch.apt_hold(packages) + else: + hookenv.log('Unholding packages {}'.format(','.join(packages))) + fetch.apt_unhold(packages) + reactive.clear_flag('apt.needs_hold') + + +def status_set(state, message): + '''DEPRECATED, set the unit's workload status. + + Set state == None to keep the same state and just change the message. + ''' + if state is None: + state = hookenv.status_get()[0] + if state not in ('active', 'waiting', 'blocked'): + state = 'maintenance' # Guess + status.status_set(state, message) diff --git a/kubernetes-master/lib/charms/coordinator.py b/kubernetes-master/lib/charms/coordinator.py new file mode 100644 index 0000000..b954b92 --- /dev/null +++ b/kubernetes-master/lib/charms/coordinator.py @@ -0,0 +1,144 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Coordinator Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import importlib + +from charmhelpers.coordinator import BaseCoordinator +from charmhelpers.core import hookenv +from charms import reactive +import charms.layer + + +__all__ = ['coordinator', 'acquire'] + + +def acquire(lock): + """ + Sets either the coordinator.granted.{lockname} or + coordinator.requested.{lockname} state. + + Returns True if the lock could be immediately granted. + + If locks cannot be granted immediately, they will be granted + in a future hook and the coordinator.granted.{lockname} state set. + """ + global coordinator + if coordinator.acquire(lock): + s = 'coordinator.granted.{}'.format(lock) + if not reactive.is_state(s): + log('Granted {} lock'.format(lock), hookenv.DEBUG) + reactive.set_state('coordinator.granted.{}'.format(lock)) + return True + else: + log('Requested {} lock'.format(lock), hookenv.DEBUG) + reactive.set_state('coordinator.requested.{}'.format(lock)) + return False + + +options = charms.layer.options('coordinator') + + +def log(msg, level=hookenv.INFO): + lmap = {hookenv.DEBUG: 1, + hookenv.INFO: 2, + hookenv.WARNING: 3, + hookenv.ERROR: 4, + hookenv.CRITICAL: 5} + if lmap[level] >= lmap[options.get('log_level', 'DEBUG').upper()]: + hookenv.log('Coordinator: {}'.format(msg), level) + + +class SimpleCoordinator(BaseCoordinator): + '''A simple BaseCoordinator that is suitable for almost all cases. + + Only one unit at a time will be granted locks. All requests by that + unit will be granted. So only one unit may run tasks guarded by a lock, + and the lock name is irrelevant. + ''' + def default_grant(self, lock, unit, granted, queue): + '''Grant locks to only one unit at a time, regardless of the lock name. + + This lets us keep separate locks like join and restart, + while ensuring the operations do not occur on different nodes + at the same time. + ''' + existing_grants = {k: v for k, v in self.grants.items() if v} + + # Return True if this unit has already been granted any lock. + if existing_grants.get(unit): + self.msg('Granting {} to {} (existing grants)'.format(lock, unit), + hookenv.INFO) + return True + + # Return False if another unit has been granted any lock. + if existing_grants: + self.msg('Not granting {} to {} (locks held by {})' + ''.format(lock, unit, ','.join(existing_grants.keys())), + hookenv.INFO) + return False + + # Otherwise, return True if the unit is first in the queue for + # this named lock. + if queue[0] == unit: + self.msg('Granting {} to {} (first in queue)' + ''.format(lock, unit), hookenv.INFO) + return True + else: + self.msg('Not granting {} to {} (not first in queue)' + ''.format(lock, unit), hookenv.INFO) + return False + + def msg(self, msg, level=hookenv.DEBUG): + '''Emit a message.''' + log(msg, level) + + def _save_state(self): + # If the leader aquired a lock, and now released it, + # there may be outstanding requests in the queue from other + # units. We need to grant them now, as we have no guarantee + # of another hook running on the leader for some time (until + # update-status). + self.handle() + super(SimpleCoordinator, self)._save_state() + + +def _instantiate(): + default_name = 'charms.coordinator.SimpleCoordinator' + full_name = options.get('class', default_name) + components = full_name.split('.') + module = '.'.join(components[:-1]) + name = components[-1] + + if not module: + module = 'charms.coordinator' + + class_ = getattr(importlib.import_module(module), name) + + assert issubclass(class_, BaseCoordinator), \ + '{} is not a BaseCoordinator subclass'.format(full_name) + + try: + # The Coordinator layer defines its own peer relation, as it + # can't piggy back on an existing peer relation that may not + # exist. + return class_(peer_relation_name='coordinator') + finally: + log('Using {} coordinator'.format(full_name), hookenv.DEBUG) + + +# Instantiate the BaseCoordinator singleton, which installs +# its charmhelpers.core.atstart() hooks. +coordinator = _instantiate() diff --git a/kubernetes-master/lib/charms/layer/__init__.py b/kubernetes-master/lib/charms/layer/__init__.py new file mode 100644 index 0000000..a8e0c64 --- /dev/null +++ b/kubernetes-master/lib/charms/layer/__init__.py @@ -0,0 +1,60 @@ +import sys +from importlib import import_module +from pathlib import Path + + +def import_layer_libs(): + """ + Ensure that all layer libraries are imported. + + This makes it possible to do the following: + + from charms import layer + + layer.foo.do_foo_thing() + + Note: This function must be called after bootstrap. + """ + for module_file in Path('lib/charms/layer').glob('*'): + module_name = module_file.stem + if module_name in ('__init__', 'basic', 'execd') or not ( + module_file.suffix == '.py' or module_file.is_dir() + ): + continue + import_module('charms.layer.{}'.format(module_name)) + + +# Terrible hack to support the old terrible interface. +# Try to get people to call layer.options.get() instead so +# that we can remove this garbage. +# Cribbed from https://stackoverfLow.com/a/48100440/4941864 +class OptionsBackwardsCompatibilityHack(sys.modules[__name__].__class__): + def __call__(self, section=None, layer_file=None): + if layer_file is None: + return self.get(section=section) + else: + return self.get(section=section, + layer_file=Path(layer_file)) + + +def patch_options_interface(): + from charms.layer import options + if sys.version_info.minor >= 5: + options.__class__ = OptionsBackwardsCompatibilityHack + else: + # Py 3.4 doesn't support changing the __class__, so we have to do it + # another way. The last line is needed because we already have a + # reference that doesn't get updated with sys.modules. + name = options.__name__ + hack = OptionsBackwardsCompatibilityHack(name) + hack.get = options.get + sys.modules[name] = hack + sys.modules[__name__].options = hack + + +try: + patch_options_interface() +except ImportError: + # This may fail if pyyaml hasn't been installed yet. But in that + # case, the bootstrap logic will try it again once it has. + pass diff --git a/kubernetes-master/lib/charms/layer/basic.py b/kubernetes-master/lib/charms/layer/basic.py new file mode 100644 index 0000000..bbdd074 --- /dev/null +++ b/kubernetes-master/lib/charms/layer/basic.py @@ -0,0 +1,501 @@ +import os +import sys +import re +import shutil +from distutils.version import LooseVersion +from pkg_resources import Requirement +from glob import glob +from subprocess import check_call, check_output, CalledProcessError +from time import sleep + +from charms import layer +from charms.layer.execd import execd_preinstall + + +def _get_subprocess_env(): + env = os.environ.copy() + env['LANG'] = env.get('LANG', 'C.UTF-8') + return env + + +def get_series(): + """ + Return series for a few known OS:es. + Tested as of 2019 november: + * centos6, centos7, rhel6. + * bionic + """ + series = "" + + # Looking for content in /etc/os-release + # works for ubuntu + some centos + if os.path.isfile('/etc/os-release'): + d = {} + with open('/etc/os-release', 'r') as rel: + for l in rel: + if not re.match(r'^\s*$', l): + k, v = l.split('=') + d[k.strip()] = v.strip().replace('"', '') + series = "{ID}{VERSION_ID}".format(**d) + + # Looking for content in /etc/redhat-release + # works for redhat enterprise systems + elif os.path.isfile('/etc/redhat-release'): + with open('/etc/redhat-release', 'r') as redhatlsb: + # CentOS Linux release 7.7.1908 (Core) + line = redhatlsb.readline() + release = int(line.split("release")[1].split()[0][0]) + series = "centos" + str(release) + + # Looking for content in /etc/lsb-release + # works for ubuntu + elif os.path.isfile('/etc/lsb-release'): + d = {} + with open('/etc/lsb-release', 'r') as lsb: + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + series = d['DISTRIB_CODENAME'] + + # This is what happens if we cant figure out the OS. + else: + series = "unknown" + return series + + +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 + + series = get_series() + + # OMG?! is build-essentials needed? + ubuntu_packages = ['python3-pip', + 'python3-setuptools', + 'python3-yaml', + 'python3-dev', + 'python3-wheel', + 'build-essential'] + + # I'm not going to "yum group info "Development Tools" + # omitting above madness + centos_packages = ['python3-pip', + 'python3-setuptools', + 'python3-devel', + 'python3-wheel'] + + packages_needed = [] + if 'centos' in series: + packages_needed = centos_packages + else: + packages_needed = ubuntu_packages + + 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') + hook_name = os.path.basename(sys.argv[0]) + is_bootstrapped = os.path.exists('wheelhouse/.bootstrapped') + is_charm_upgrade = hook_name == 'upgrade-charm' + is_series_upgrade = hook_name == 'post-series-upgrade' + is_post_upgrade = os.path.exists('wheelhouse/.upgraded') + is_upgrade = (not is_post_upgrade and + (is_charm_upgrade or is_series_upgrade)) + if is_bootstrapped and not is_upgrade: + # older subordinates might have downgraded charm-env, so we should + # restore it if necessary + install_or_update_charm_env() + activate_venv() + # the .upgrade file prevents us from getting stuck in a loop + # when re-execing to activate the venv; at this point, we've + # activated the venv, so it's safe to clear it + if is_post_upgrade: + os.unlink('wheelhouse/.upgraded') + return + if os.path.exists(venv): + try: + # focal installs or upgrades prior to PR 160 could leave the venv + # in a broken state which would prevent subsequent charm upgrades + _load_installed_versions(vpip) + except CalledProcessError: + is_broken_venv = True + else: + is_broken_venv = False + if is_upgrade or is_broken_venv: + # All upgrades should do a full clear of the venv, rather than + # just updating it, to bring in updates to Python itself + shutil.rmtree(venv) + if is_upgrade: + if os.path.exists('wheelhouse/.bootstrapped'): + os.unlink('wheelhouse/.bootstrapped') + # bootstrap wheelhouse + if os.path.exists('wheelhouse'): + pre_eoan = series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty', + 'ubuntu16.04', 'xenial', + 'ubuntu18.04', 'bionic') + pydistutils_lines = [ + "[easy_install]\n", + "find_links = file://{}/wheelhouse/\n".format(charm_dir), + "no_index=True\n", + "index_url=\n", # deliberately nothing here; disables it. + ] + if pre_eoan: + pydistutils_lines.append("allow_hosts = ''\n") + 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(pydistutils_lines) + if 'centos' in series: + yum_install(packages_needed) + else: + apt_install(packages_needed) + from charms.layer import options + cfg = options.get('basic') + # include packages defined in layer.yaml + if 'centos' in series: + yum_install(cfg.get('packages', [])) + else: + 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 = get_series() + if series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty'): + apt_install(['python-virtualenv']) + elif 'centos' in series: + yum_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, env=_get_subprocess_env()) + 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') + pre_install_pkgs = ['pip', 'setuptools', 'setuptools-scm'] + # we bundle these packages to work around bugs in older versions (such + # as https://github.com/pypa/pip/issues/56), but if the system already + # provided a newer version, downgrading it can cause other problems + _update_if_newer(pip, pre_install_pkgs) + # install the rest of the wheelhouse deps (extract the pkg names into + # a set so that we can ignore the pre-install packages and let pip + # choose the best version in case there are multiple from layer + # conflicts) + _versions = _load_wheelhouse_versions() + _pkgs = _versions.keys() - set(pre_install_pkgs) + # add back the versions such that each package in pkgs is + # ==. + # This ensures that pip 20.3.4+ will install the packages from the + # wheelhouse without (erroneously) flagging an error. + pkgs = _add_back_versions(_pkgs, _versions) + reinstall_flag = '--force-reinstall' + if not cfg.get('use_venv', True) and pre_eoan: + reinstall_flag = '--ignore-installed' + check_call([pip, 'install', '-U', reinstall_flag, '--no-index', + '--no-cache-dir', '-f', 'wheelhouse'] + list(pkgs), + env=_get_subprocess_env()) + # re-enable installation from pypi + os.remove('/root/.pydistutils.cfg') + + # install pyyaml for centos7, since, unlike the ubuntu image, the + # default image for centos doesn't include pyyaml; see the discussion: + # https://discourse.jujucharms.com/t/charms-for-centos-lets-begin + if 'centos' in series: + check_call([pip, 'install', '-U', 'pyyaml'], + env=_get_subprocess_env()) + + # install python packages from layer options + if cfg.get('python_packages'): + check_call([pip, 'install', '-U'] + cfg.get('python_packages'), + env=_get_subprocess_env()) + 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') + # setup wrappers to ensure envs are used for scripts + install_or_update_charm_env() + for wrapper in ('charms.reactive', 'charms.reactive.sh', + 'chlp', 'layer_option'): + src = os.path.join('/usr/local/sbin', 'charm-env') + dst = os.path.join('/usr/local/sbin', wrapper) + if not os.path.exists(dst): + os.symlink(src, dst) + if cfg.get('use_venv'): + shutil.copy2('bin/layer_option', vbin) + else: + shutil.copy2('bin/layer_option', '/usr/local/bin/') + # re-link the charm copy to the wrapper in case charms + # call bin/layer_option directly (as was the old pattern) + os.remove('bin/layer_option') + os.symlink('/usr/local/sbin/layer_option', 'bin/layer_option') + # flag us as having already bootstrapped so we don't do it again + open('wheelhouse/.bootstrapped', 'w').close() + if is_upgrade: + # flag us as having already upgraded so we don't do it again + open('wheelhouse/.upgraded', '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 _load_installed_versions(pip): + pip_freeze = check_output([pip, 'freeze']).decode('utf8') + versions = {} + for pkg_ver in pip_freeze.splitlines(): + try: + req = Requirement.parse(pkg_ver) + except ValueError: + continue + versions.update({ + req.project_name: LooseVersion(ver) + for op, ver in req.specs if op == '==' + }) + return versions + + +def _load_wheelhouse_versions(): + versions = {} + for wheel in glob('wheelhouse/*'): + pkg, ver = os.path.basename(wheel).rsplit('-', 1) + # nb: LooseVersion ignores the file extension + versions[pkg.replace('_', '-')] = LooseVersion(ver) + return versions + + +def _add_back_versions(pkgs, versions): + """Add back the version strings to each of the packages. + + The versions are LooseVersion() from _load_wheelhouse_versions(). This + function strips the ".zip" or ".tar.gz" from the end of the version string + and adds it back to the package in the form of == + + If a package name is not a key in the versions dictionary, then it is + returned in the list unchanged. + + :param pkgs: A list of package names + :type pkgs: List[str] + :param versions: A map of package to LooseVersion + :type versions: Dict[str, LooseVersion] + :returns: A list of (maybe) versioned packages + :rtype: List[str] + """ + def _strip_ext(s): + """Strip an extension (if it exists) from the string + + :param s: the string to strip an extension off if it exists + :type s: str + :returns: string without an extension of .zip or .tar.gz + :rtype: str + """ + for ending in [".zip", ".tar.gz"]: + if s.endswith(ending): + return s[:-len(ending)] + return s + + def _maybe_add_version(pkg): + """Maybe add back the version number to a package if it exists. + + Adds the version number, if the package exists in the lexically + captured `versions` dictionary, in the form ==. Strips + the extension if it exists. + + :param pkg: the package name to (maybe) add the version number to. + :type pkg: str + """ + try: + return "{}=={}".format(pkg, _strip_ext(str(versions[pkg]))) + except KeyError: + pass + return pkg + + return [_maybe_add_version(pkg) for pkg in pkgs] + + +def _update_if_newer(pip, pkgs): + installed = _load_installed_versions(pip) + wheelhouse = _load_wheelhouse_versions() + for pkg in pkgs: + if pkg not in installed or wheelhouse[pkg] > installed[pkg]: + check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', + pkg], env=_get_subprocess_env()) + + +def install_or_update_charm_env(): + # On Trusty python3-pkg-resources is not installed + try: + from pkg_resources import parse_version + except ImportError: + apt_install(['python3-pkg-resources']) + from pkg_resources import parse_version + + try: + installed_version = parse_version( + check_output(['/usr/local/sbin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + installed_version = parse_version('0.0.0') + try: + bundled_version = parse_version( + check_output(['bin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + bundled_version = parse_version('0.0.0') + if installed_version < bundled_version: + shutil.copy2('bin/charm-env', '/usr/local/sbin/') + + +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. + """ + from charms.layer import options + venv = os.path.abspath('../.venv') + vbin = os.path.join(venv, 'bin') + vpy = os.path.join(vbin, 'python') + use_venv = options.get('basic', 'use_venv') + if use_venv and '.venv' not in sys.executable: + # activate the venv + os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) + reload_interpreter(vpy) + layer.patch_options_interface() + layer.import_layer_libs() + + +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 = _get_subprocess_env() + + 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 + try: + # sometimes apt-get update needs to be run + check_call(['apt-get', 'update'], env=env) + except CalledProcessError: + # sometimes it's a dpkg lock issue + pass + sleep(5) + else: + break + + +def yum_install(packages): + """ Installs packages with yum. + This function largely mimics the apt_install function for consistency. + """ + if packages: + env = os.environ.copy() + cmd = ['yum', '-y', 'install'] + for attempt in range(3): + try: + check_call(cmd + packages, env=env) + except CalledProcessError: + if attempt == 2: + raise + try: + check_call(['yum', 'update'], env=env) + except CalledProcessError: + pass + sleep(5) + else: + break + else: + pass + + +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]) + + +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/kubernetes-master/lib/charms/layer/execd.py b/kubernetes-master/lib/charms/layer/execd.py new file mode 100644 index 0000000..438d9a1 --- /dev/null +++ b/kubernetes-master/lib/charms/layer/execd.py @@ -0,0 +1,114 @@ +# 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 . + +# This module may only import from the Python standard library. +import os +import sys +import subprocess +import time + +''' +execd/preinstall + +Read the layer-basic docs for more info on how to use this feature. +https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#exec-d-support +''' + + +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<>> `get_version('kubelet') + (1, 6, 0) + + """ + cmd = "{} --version".format(bin_name).split() + version_string = subprocess.check_output(cmd).decode("utf-8") + return tuple(int(q) for q in re.findall("[0-9]+", version_string)[:3]) + + +def retry(times, delay_secs): + """Decorator for retrying a method call. + + Args: + times: How many times should we retry before giving up + delay_secs: Delay in secs + + Returns: A callable that would return the last call outcome + """ + + def retry_decorator(func): + """Decorator to wrap the function provided. + + Args: + func: Provided function should return either True od False + + Returns: A callable that would return the last call outcome + + """ + + def _wrapped(*args, **kwargs): + res = func(*args, **kwargs) + attempt = 0 + while not res and attempt < times: + sleep(delay_secs) + res = func(*args, **kwargs) + if res: + break + attempt += 1 + return res + + return _wrapped + + return retry_decorator + + +def calculate_resource_checksum(resource): + """Calculate a checksum for a resource""" + md5 = hashlib.md5() + path = hookenv.resource_get(resource) + if path: + with open(path, "rb") as f: + data = f.read() + md5.update(data) + return md5.hexdigest() + + +def get_resource_checksum_db_key(checksum_prefix, resource): + """Convert a resource name to a resource checksum database key.""" + return checksum_prefix + resource + + +def migrate_resource_checksums(checksum_prefix, snap_resources): + """Migrate resource checksums from the old schema to the new one""" + for resource in snap_resources: + new_key = get_resource_checksum_db_key(checksum_prefix, resource) + if not db.get(new_key): + path = hookenv.resource_get(resource) + if path: + # old key from charms.reactive.helpers.any_file_changed + old_key = "reactive.files_changed." + path + old_checksum = db.get(old_key) + db.set(new_key, old_checksum) + else: + # No resource is attached. Previously, this meant no checksum + # would be calculated and stored. But now we calculate it as if + # it is a 0-byte resource, so let's go ahead and do that. + zero_checksum = hashlib.md5().hexdigest() + db.set(new_key, zero_checksum) + + +def check_resources_for_upgrade_needed(checksum_prefix, snap_resources): + hookenv.status_set("maintenance", "Checking resources") + for resource in snap_resources: + key = get_resource_checksum_db_key(checksum_prefix, resource) + old_checksum = db.get(key) + new_checksum = calculate_resource_checksum(resource) + if new_checksum != old_checksum: + return True + return False + + +def calculate_and_store_resource_checksums(checksum_prefix, snap_resources): + for resource in snap_resources: + key = get_resource_checksum_db_key(checksum_prefix, resource) + checksum = calculate_resource_checksum(resource) + db.set(key, checksum) + + +def get_ingress_address(endpoint_name, ignore_addresses=None): + try: + network_info = hookenv.network_get(endpoint_name) + except NotImplementedError: + network_info = {} + + if not network_info or "ingress-addresses" not in network_info: + # if they don't have ingress-addresses they are running a juju that + # doesn't support spaces, so just return the private address + return hookenv.unit_get("private-address") + + addresses = network_info["ingress-addresses"] + + if ignore_addresses: + hookenv.log("ingress-addresses before filtering: {}".format(addresses)) + iter_filter = filter(lambda item: item not in ignore_addresses, addresses) + addresses = list(iter_filter) + hookenv.log("ingress-addresses after filtering: {}".format(addresses)) + + # Need to prefer non-fan IP addresses due to various issues, e.g. + # https://bugs.launchpad.net/charm-gcp-integrator/+bug/1822997 + # Fan typically likes to use IPs in the 240.0.0.0/4 block, so we'll + # prioritize those last. Not technically correct, but good enough. + try: + sort_key = lambda a: int(a.partition(".")[0]) >= 240 # noqa: E731 + addresses = sorted(addresses, key=sort_key) + except Exception: + hookenv.log(traceback.format_exc()) + + return addresses[0] + + +def get_ingress_address6(endpoint_name): + try: + network_info = hookenv.network_get(endpoint_name) + except NotImplementedError: + network_info = {} + + if not network_info or "ingress-addresses" not in network_info: + return None + + addresses = network_info["ingress-addresses"] + + for addr in addresses: + ip_addr = ipaddress.ip_interface(addr).ip + if ip_addr.version == 6: + return str(ip_addr) + else: + return None + + +def service_restart(service_name): + hookenv.status_set("maintenance", "Restarting {0} service".format(service_name)) + host.service_restart(service_name) + + +def service_start(service_name): + hookenv.log("Starting {0} service.".format(service_name)) + host.service_stop(service_name) + + +def service_stop(service_name): + hookenv.log("Stopping {0} service.".format(service_name)) + host.service_stop(service_name) + + +def arch(): + """Return the package architecture as a string. Raise an exception if the + architecture is not supported by kubernetes.""" + # Get the package architecture for this system. + architecture = check_output(["dpkg", "--print-architecture"]).rstrip() + # Convert the binary result into a string. + architecture = architecture.decode("utf-8") + return architecture + + +def get_service_ip(service, namespace="kube-system", errors_fatal=True): + try: + output = kubectl( + "get", "service", "--namespace", namespace, service, "--output", "json" + ) + except CalledProcessError: + if errors_fatal: + raise + else: + return None + else: + svc = json.loads(output.decode()) + return svc["spec"]["clusterIP"] + + +def kubectl(*args): + """Run a kubectl cli command with a config file. Returns stdout and throws + an error if the command fails.""" + command = ["kubectl", "--kubeconfig=" + kubeclientconfig_path] + list(args) + hookenv.log("Executing {}".format(command)) + return check_output(command) + + +def kubectl_success(*args): + """Runs kubectl with the given args. Returns True if successful, False if + not.""" + try: + kubectl(*args) + return True + except CalledProcessError: + return False + + +def kubectl_manifest(operation, manifest): + """Wrap the kubectl creation command when using filepath resources + :param operation - one of get, create, delete, replace + :param manifest - filepath to the manifest + """ + # Deletions are a special case + if operation == "delete": + # Ensure we immediately remove requested resources with --now + return kubectl_success(operation, "-f", manifest, "--now") + else: + # Guard against an error re-creating the same manifest multiple times + if operation == "create": + # If we already have the definition, its probably safe to assume + # creation was true. + if kubectl_success("get", "-f", manifest): + hookenv.log("Skipping definition for {}".format(manifest)) + return True + # Execute the requested command that did not match any of the special + # cases above + return kubectl_success(operation, "-f", manifest) + + +def get_node_name(): + kubelet_extra_args = parse_extra_args("kubelet-extra-args") + cloud_provider = kubelet_extra_args.get("cloud-provider", "") + if is_state("endpoint.aws.ready"): + cloud_provider = "aws" + elif is_state("endpoint.gcp.ready"): + cloud_provider = "gce" + elif is_state("endpoint.openstack.ready"): + cloud_provider = "openstack" + elif is_state("endpoint.vsphere.ready"): + cloud_provider = "vsphere" + elif is_state("endpoint.azure.ready"): + cloud_provider = "azure" + if cloud_provider == "aws": + return getfqdn().lower() + else: + return gethostname().lower() + + +def create_kubeconfig( + kubeconfig, + server, + ca, + key=None, + certificate=None, + user="ubuntu", + context="juju-context", + cluster="juju-cluster", + password=None, + token=None, + keystone=False, + aws_iam_cluster_id=None, +): + """Create a configuration for Kubernetes based on path using the supplied + arguments for values of the Kubernetes server, CA, key, certificate, user + context and cluster.""" + if not key and not certificate and not password and not token: + raise ValueError("Missing authentication mechanism.") + elif key and not certificate: + raise ValueError("Missing certificate.") + elif not key and certificate: + raise ValueError("Missing key.") + elif token and password: + # token and password are mutually exclusive. Error early if both are + # present. The developer has requested an impossible situation. + # see: kubectl config set-credentials --help + raise ValueError("Token and Password are mutually exclusive.") + + old_kubeconfig = Path(kubeconfig) + new_kubeconfig = Path(str(kubeconfig) + ".new") + + # Create the config file with the address of the master server. + cmd = ( + "kubectl config --kubeconfig={0} set-cluster {1} " + "--server={2} --certificate-authority={3} --embed-certs=true" + ) + check_call(split(cmd.format(new_kubeconfig, cluster, server, ca))) + # Delete old users + cmd = "kubectl config --kubeconfig={0} unset users" + check_call(split(cmd.format(new_kubeconfig))) + # Create the credentials using the client flags. + cmd = "kubectl config --kubeconfig={0} " "set-credentials {1} ".format( + new_kubeconfig, user + ) + + if key and certificate: + cmd = ( + "{0} --client-key={1} --client-certificate={2} " + "--embed-certs=true".format(cmd, key, certificate) + ) + if password: + cmd = "{0} --username={1} --password={2}".format(cmd, user, password) + # This is mutually exclusive from password. They will not work together. + if token: + cmd = "{0} --token={1}".format(cmd, token) + check_call(split(cmd)) + # Create a default context with the cluster. + cmd = "kubectl config --kubeconfig={0} set-context {1} " "--cluster={2} --user={3}" + check_call(split(cmd.format(new_kubeconfig, context, cluster, user))) + # Make the config use this new context. + cmd = "kubectl config --kubeconfig={0} use-context {1}" + check_call(split(cmd.format(new_kubeconfig, context))) + if keystone: + # create keystone user + cmd = "kubectl config --kubeconfig={0} " "set-credentials keystone-user".format( + new_kubeconfig + ) + check_call(split(cmd)) + # create keystone context + cmd = ( + "kubectl config --kubeconfig={0} " + "set-context --cluster={1} " + "--user=keystone-user keystone".format(new_kubeconfig, cluster) + ) + check_call(split(cmd)) + # use keystone context + cmd = "kubectl config --kubeconfig={0} " "use-context keystone".format( + new_kubeconfig + ) + check_call(split(cmd)) + # manually add exec command until kubectl can do it for us + with open(new_kubeconfig, "r") as f: + content = f.read() + content = content.replace( + """- name: keystone-user + user: {}""", + """- name: keystone-user + user: + exec: + command: "/snap/bin/client-keystone-auth" + apiVersion: "client.authentication.k8s.io/v1beta1" +""", + ) + with open(new_kubeconfig, "w") as f: + f.write(content) + if aws_iam_cluster_id: + # create aws-iam context + cmd = ( + "kubectl config --kubeconfig={0} " + "set-context --cluster={1} " + "--user=aws-iam-user aws-iam-authenticator" + ) + check_call(split(cmd.format(new_kubeconfig, cluster))) + + # append a user for aws-iam + cmd = ( + "kubectl --kubeconfig={0} config set-credentials " + "aws-iam-user --exec-command=aws-iam-authenticator " + '--exec-arg="token" --exec-arg="-i" --exec-arg="{1}" ' + '--exec-arg="-r" --exec-arg="<>" ' + "--exec-api-version=client.authentication.k8s.io/v1alpha1" + ) + check_call(split(cmd.format(new_kubeconfig, aws_iam_cluster_id))) + + # not going to use aws-iam context by default since we don't have + # the desired arn. This will make the config not usable if copied. + + # cmd = 'kubectl config --kubeconfig={0} ' \ + # 'use-context aws-iam-authenticator'.format(new_kubeconfig) + # check_call(split(cmd)) + if old_kubeconfig.exists(): + changed = new_kubeconfig.read_text() != old_kubeconfig.read_text() + else: + changed = True + if changed: + new_kubeconfig.rename(old_kubeconfig) + + +def parse_extra_args(config_key): + elements = hookenv.config().get(config_key, "").split() + args = {} + + for element in elements: + if "=" in element: + key, _, value = element.partition("=") + args[key] = value + else: + args[element] = "true" + + return args + + +def configure_kubernetes_service(key, service, base_args, extra_args_key): + db = unitdata.kv() + + prev_args_key = key + service + prev_snap_args = db.get(prev_args_key) or {} + + extra_args = parse_extra_args(extra_args_key) + + args = {} + args.update(base_args) + args.update(extra_args) + + # CIS benchmark action may inject kv config to pass failing tests. Merge + # these after the func args as they should take precedence. + cis_args_key = "cis-" + service + cis_args = db.get(cis_args_key) or {} + args.update(cis_args) + + # Remove any args with 'None' values (all k8s args are 'k=v') and + # construct an arg string for use by 'snap set'. + args = {k: v for k, v in args.items() if v is not None} + args = ['--%s="%s"' % arg for arg in args.items()] + args = " ".join(args) + + snap_opts = {} + for arg in prev_snap_args: + # remove previous args by setting to null + snap_opts[arg] = "null" + snap_opts["args"] = args + snap_opts = ["%s=%s" % opt for opt in snap_opts.items()] + + cmd = ["snap", "set", service] + snap_opts + check_call(cmd) + + # Now that we've started doing snap configuration through the "args" + # option, we should never need to clear previous args again. + db.set(prev_args_key, {}) + + +def _snap_common_path(component): + return Path("/var/snap/{}/common".format(component)) + + +def cloud_config_path(component): + return _snap_common_path(component) / "cloud-config.conf" + + +def _gcp_creds_path(component): + return _snap_common_path(component) / "gcp-creds.json" + + +def _daemon_env_path(component): + return _snap_common_path(component) / "environment" + + +def _cloud_endpoint_ca_path(component): + return _snap_common_path(component) / "cloud-endpoint-ca.crt" + + +def encryption_config_path(): + apiserver_snap_common_path = _snap_common_path("kube-apiserver") + encryption_conf_dir = apiserver_snap_common_path / "encryption" + return encryption_conf_dir / "encryption_config.yaml" + + +def write_gcp_snap_config(component): + # gcp requires additional credentials setup + gcp = endpoint_from_flag("endpoint.gcp.ready") + creds_path = _gcp_creds_path(component) + with creds_path.open("w") as fp: + os.fchmod(fp.fileno(), 0o600) + fp.write(gcp.credentials) + + # create a cloud-config file that sets token-url to nil to make the + # services use the creds env var instead of the metadata server, as + # well as making the cluster multizone + comp_cloud_config_path = cloud_config_path(component) + comp_cloud_config_path.write_text( + "[Global]\n" "token-url = nil\n" "multizone = true\n" + ) + + daemon_env_path = _daemon_env_path(component) + if daemon_env_path.exists(): + daemon_env = daemon_env_path.read_text() + if not daemon_env.endswith("\n"): + daemon_env += "\n" + else: + daemon_env = "" + if gcp_creds_env_key not in daemon_env: + daemon_env += "{}={}\n".format(gcp_creds_env_key, creds_path) + daemon_env_path.parent.mkdir(parents=True, exist_ok=True) + daemon_env_path.write_text(daemon_env) + + +def generate_openstack_cloud_config(): + # openstack requires additional credentials setup + openstack = endpoint_from_flag("endpoint.openstack.ready") + + lines = [ + "[Global]", + "auth-url = {}".format(openstack.auth_url), + "region = {}".format(openstack.region), + "username = {}".format(openstack.username), + "password = {}".format(openstack.password), + "tenant-name = {}".format(openstack.project_name), + "domain-name = {}".format(openstack.user_domain_name), + "tenant-domain-name = {}".format(openstack.project_domain_name), + ] + if openstack.endpoint_tls_ca: + lines.append("ca-file = /etc/config/endpoint-ca.cert") + + lines.extend( + [ + "", + "[LoadBalancer]", + ] + ) + + if openstack.has_octavia in (True, None): + # Newer integrator charm will detect whether underlying OpenStack has + # Octavia enabled so we can set this intelligently. If we're still + # related to an older integrator, though, default to assuming Octavia + # is available. + lines.append("use-octavia = true") + else: + lines.append("use-octavia = false") + lines.append("lb-provider = haproxy") + if openstack.subnet_id: + lines.append("subnet-id = {}".format(openstack.subnet_id)) + if openstack.floating_network_id: + lines.append("floating-network-id = {}".format(openstack.floating_network_id)) + if openstack.lb_method: + lines.append("lb-method = {}".format(openstack.lb_method)) + if openstack.manage_security_groups: + lines.append( + "manage-security-groups = {}".format(openstack.manage_security_groups) + ) + if any( + [openstack.bs_version, openstack.trust_device_path, openstack.ignore_volume_az] + ): + lines.append("") + lines.append("[BlockStorage]") + if openstack.bs_version is not None: + lines.append("bs-version = {}".format(openstack.bs_version)) + if openstack.trust_device_path is not None: + lines.append("trust-device-path = {}".format(openstack.trust_device_path)) + if openstack.ignore_volume_az is not None: + lines.append("ignore-volume-az = {}".format(openstack.ignore_volume_az)) + return "\n".join(lines) + "\n" + + +def write_azure_snap_config(component): + azure = endpoint_from_flag("endpoint.azure.ready") + comp_cloud_config_path = cloud_config_path(component) + comp_cloud_config_path.write_text( + json.dumps( + { + "useInstanceMetadata": True, + "useManagedIdentityExtension": azure.managed_identity, + "subscriptionId": azure.subscription_id, + "resourceGroup": azure.resource_group, + "location": azure.resource_group_location, + "vnetName": azure.vnet_name, + "vnetResourceGroup": azure.vnet_resource_group, + "subnetName": azure.subnet_name, + "securityGroupName": azure.security_group_name, + "loadBalancerSku": "standard", + "securityGroupResourceGroup": azure.security_group_resource_group, + "aadClientId": azure.aad_client_id, + "aadClientSecret": azure.aad_client_secret, + "tenantId": azure.tenant_id, + } + ) + ) + + +def configure_kube_proxy( + configure_prefix, api_servers, cluster_cidr, bind_address=None +): + kube_proxy_opts = {} + kube_proxy_opts["cluster-cidr"] = cluster_cidr + kube_proxy_opts["kubeconfig"] = kubeproxyconfig_path + kube_proxy_opts["logtostderr"] = "true" + kube_proxy_opts["v"] = "0" + num_apis = len(api_servers) + kube_proxy_opts["master"] = api_servers[get_unit_number() % num_apis] + kube_proxy_opts["hostname-override"] = get_node_name() + if bind_address: + kube_proxy_opts["bind-address"] = bind_address + elif is_ipv6(cluster_cidr): + kube_proxy_opts["bind-address"] = "::" + + if host.is_container(): + kube_proxy_opts["conntrack-max-per-core"] = "0" + + if is_dual_stack(cluster_cidr): + kube_proxy_opts["feature-gates"] = "IPv6DualStack=true" + + configure_kubernetes_service( + configure_prefix, "kube-proxy", kube_proxy_opts, "proxy-extra-args" + ) + + +def get_unit_number(): + return int(hookenv.local_unit().split("/")[1]) + + +def cluster_cidr(): + """Return the cluster CIDR provided by the CNI""" + cni = endpoint_from_flag("cni.available") + if not cni: + return None + config = hookenv.config() + if "default-cni" in config: + # master + default_cni = config["default-cni"] + else: + # worker + kube_control = endpoint_from_flag("kube-control.dns.available") + if not kube_control: + return None + default_cni = kube_control.get_default_cni() + return cni.get_config(default=default_cni)["cidr"] + + +def is_dual_stack(cidrs): + """Detect IPv4/IPv6 dual stack from CIDRs""" + return {net.version for net in get_networks(cidrs)} == {4, 6} + + +def is_ipv4(cidrs): + """Detect IPv6 from CIDRs""" + return get_ipv4_network(cidrs) is not None + + +def is_ipv6(cidrs): + """Detect IPv6 from CIDRs""" + return get_ipv6_network(cidrs) is not None + + +def is_ipv6_preferred(cidrs): + """Detect if IPv6 is preffered from CIDRs""" + return get_networks(cidrs)[0].version == 6 + + +def get_networks(cidrs): + """Convert a comma-separated list of CIDRs to a list of networks.""" + if not cidrs: + return [] + return [ipaddress.ip_interface(cidr).network for cidr in cidrs.split(",")] + + +def get_ipv4_network(cidrs): + """Get the IPv4 network from the given CIDRs or None""" + return {net.version: net for net in get_networks(cidrs)}.get(4) + + +def get_ipv6_network(cidrs): + """Get the IPv6 network from the given CIDRs or None""" + return {net.version: net for net in get_networks(cidrs)}.get(6) + + +def enable_ipv6_forwarding(): + """Enable net.ipv6.conf.all.forwarding in sysctl if it is not already.""" + check_call(["sysctl", "net.ipv6.conf.all.forwarding=1"]) + + +def get_bind_addrs(ipv4=True, ipv6=True): + """Get all global-scoped addresses that we might bind to.""" + try: + output = check_output(["ip", "-br", "addr", "show", "scope", "global"]) + except CalledProcessError: + # stderr will have any details, and go to the log + hookenv.log("Unable to determine global addresses", hookenv.ERROR) + return [] + + ignore_interfaces = ("lxdbr", "flannel", "cni", "virbr", "docker") + accept_versions = set() + if ipv4: + accept_versions.add(4) + if ipv6: + accept_versions.add(6) + + addrs = [] + for line in output.decode("utf8").splitlines(): + intf, state, *intf_addrs = line.split() + if state != "UP" or any( + intf.startswith(prefix) for prefix in ignore_interfaces + ): + continue + for addr in intf_addrs: + ip_addr = ipaddress.ip_interface(addr).ip + if ip_addr.version in accept_versions: + addrs.append(str(ip_addr)) + return addrs + + +class InvalidVMwareHost(Exception): + pass + + +def _get_vmware_uuid(): + serial_id_file = "/sys/class/dmi/id/product_serial" + # The serial id from VMWare VMs comes in following format: + # VMware-42 28 13 f5 d4 20 71 61-5d b0 7b 96 44 0c cf 54 + try: + with open(serial_id_file, "r") as f: + serial_string = f.read().strip() + if "VMware-" not in serial_string: + hookenv.log( + "Unable to find VMware ID in " + "product_serial: {}".format(serial_string) + ) + raise InvalidVMwareHost + serial_string = ( + serial_string.split("VMware-")[1].replace(" ", "").replace("-", "") + ) + uuid = "%s-%s-%s-%s-%s" % ( + serial_string[0:8], + serial_string[8:12], + serial_string[12:16], + serial_string[16:20], + serial_string[20:32], + ) + except IOError as err: + hookenv.log("Unable to read UUID from sysfs: {}".format(err)) + uuid = "UNKNOWN" + + return uuid + + +def token_generator(length=32): + """Generate a random token for use in account tokens. + + param: length - the length of the token to generate + """ + alpha = string.ascii_letters + string.digits + token = "".join(random.SystemRandom().choice(alpha) for _ in range(length)) + return token + + +def get_secret_names(): + """Return a dict of 'username: secret_id' for Charmed Kubernetes users.""" + try: + output = kubectl( + "get", + "secrets", + "-n", + AUTH_SECRET_NS, + "--field-selector", + "type={}".format(AUTH_SECRET_TYPE), + "-o", + "json", + ).decode("UTF-8") + except (CalledProcessError, FileNotFoundError): + # The api server may not be up, or we may be trying to run kubelet before + # the snap is installed. Send back an empty dict. + hookenv.log("Unable to get existing secrets", level=hookenv.WARNING) + return {} + + secrets = json.loads(output) + secret_names = {} + if "items" in secrets: + for secret in secrets["items"]: + try: + secret_id = secret["metadata"]["name"] + username_b64 = secret["data"]["username"].encode("UTF-8") + except (KeyError, TypeError): + # CK secrets will have populated 'data', but not all secrets do + continue + secret_names[b64decode(username_b64).decode("UTF-8")] = secret_id + return secret_names + + +def generate_rfc1123(length=10): + """Generate a random string compliant with RFC 1123. + + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names + + param: length - the length of the string to generate + """ + length = 253 if length > 253 else length + valid_chars = string.ascii_lowercase + string.digits + rand_str = "".join(random.SystemRandom().choice(valid_chars) for _ in range(length)) + return rand_str + + +def create_secret(token, username, user, groups=None): + secrets = get_secret_names() + if username in secrets: + # Use existing secret ID if one exists for our username + secret_id = secrets[username] + else: + # secret IDs must be unique and rfc1123 compliant + sani_name = re.sub("[^0-9a-z.-]+", "-", user.lower()) + secret_id = "auth-{}-{}".format(sani_name, generate_rfc1123(10)) + + # The authenticator expects tokens to be in the form user::token + token_delim = "::" + if token_delim not in token: + token = "{}::{}".format(user, token) + + context = { + "type": AUTH_SECRET_TYPE, + "secret_name": secret_id, + "secret_namespace": AUTH_SECRET_NS, + "user": b64encode(user.encode("UTF-8")).decode("utf-8"), + "username": b64encode(username.encode("UTF-8")).decode("utf-8"), + "password": b64encode(token.encode("UTF-8")).decode("utf-8"), + "groups": b64encode(groups.encode("UTF-8")).decode("utf-8") if groups else "", + } + with tempfile.NamedTemporaryFile() as tmp_manifest: + render("cdk.auth-webhook-secret.yaml", tmp_manifest.name, context=context) + + if kubectl_manifest("apply", tmp_manifest.name): + hookenv.log("Created secret for {}".format(username)) + return True + else: + hookenv.log("WARN: Unable to create secret for {}".format(username)) + return False + + +def get_secret_password(username): + """Get the password for the given user from the secret that CK created.""" + try: + output = kubectl( + "get", + "secrets", + "-n", + AUTH_SECRET_NS, + "--field-selector", + "type={}".format(AUTH_SECRET_TYPE), + "-o", + "json", + ).decode("UTF-8") + except CalledProcessError: + # NB: apiserver probably isn't up. This can happen on boostrap or upgrade + # while trying to build kubeconfig files. If we need the 'admin' token during + # this time, pull it directly out of the kubeconfig file if possible. + token = None + if username == "admin": + admin_kubeconfig = Path("/root/.kube/config") + if admin_kubeconfig.exists(): + data = yaml.safe_load(admin_kubeconfig.read_text()) + try: + token = data["users"][0]["user"]["token"] + except (KeyError, IndexError, TypeError): + pass + return token + except FileNotFoundError: + # New deployments may ask for a token before the kubectl snap is installed. + # Give them nothing! + return None + + secrets = json.loads(output) + if "items" in secrets: + for secret in secrets["items"]: + try: + data_b64 = secret["data"] + password_b64 = data_b64["password"].encode("UTF-8") + username_b64 = data_b64["username"].encode("UTF-8") + except (KeyError, TypeError): + # CK authn secrets will have populated 'data', but not all secrets do + continue + + password = b64decode(password_b64).decode("UTF-8") + secret_user = b64decode(username_b64).decode("UTF-8") + if username == secret_user: + return password + return None diff --git a/kubernetes-master/lib/charms/layer/kubernetes_master.py b/kubernetes-master/lib/charms/layer/kubernetes_master.py new file mode 100644 index 0000000..9b72c34 --- /dev/null +++ b/kubernetes-master/lib/charms/layer/kubernetes_master.py @@ -0,0 +1,479 @@ +import csv +import json +import random +import socket +import string +from pathlib import Path +import ipaddress +from subprocess import check_output, CalledProcessError, TimeoutExpired +from time import sleep +from yaml import safe_load + +from charmhelpers.core import host +from charmhelpers.core import hookenv +from charmhelpers.core.templating import render +from charmhelpers.core import unitdata +from charmhelpers.fetch import apt_install +from charms.reactive import endpoint_from_flag, endpoint_from_name, is_flag_set +from charms.layer import kubernetes_common +from charms.layer.kubernetes_common import AUTH_SECRET_NS, create_secret + + +AUTH_BACKUP_EXT = "pre-secrets" +AUTH_BASIC_FILE = "/root/cdk/basic_auth.csv" +AUTH_TOKENS_FILE = "/root/cdk/known_tokens.csv" +EXTERNAL_API_PORT = 443 +STANDARD_API_PORT = 6443 +CEPH_CONF_DIR = Path("/etc/ceph") +CEPH_CONF = CEPH_CONF_DIR / "ceph.conf" +CEPH_KEYRING = CEPH_CONF_DIR / "ceph.client.admin.keyring" + +db = unitdata.kv() + + +def get_endpoints_from_config(): + """ + Return a list of any manually configured API endpoints. + """ + ha_connected = is_flag_set("ha.connected") + forced_lb_ips = hookenv.config("loadbalancer-ips").split() + vips = hookenv.config("ha-cluster-vip").split() + dns_record = hookenv.config("ha-cluster-dns") + if forced_lb_ips: + # if the user gave us IPs for the load balancer, assume + # they know what they are talking about and use that + # instead of our information. + return [(address, STANDARD_API_PORT) for address in forced_lb_ips] + elif ha_connected and vips: + return [(vip, STANDARD_API_PORT) for vip in vips] + elif ha_connected and dns_record: + return [(dns_record, STANDARD_API_PORT)] + else: + return [] + + +def get_local_api_endpoint(): + """ + Return the local address & port for self-access. + + Returns a list with a single tuple to match the other functions below. + """ + return [("127.0.0.1", STANDARD_API_PORT)] + + +def get_internal_api_endpoints(relation=None): + """ + Determine the best API endpoints for an internal client to connect to. + + If a relation is given, it will try to take that into account. + + May return an empty list if an endpoint is expected but not yet available. + """ + try: + goal_state = hookenv.goal_state() + except NotImplementedError: + goal_state = {} + goal_state.setdefault("relations", {}) + + # Config takes precedence. + endpoints_from_config = get_endpoints_from_config() + if endpoints_from_config: + return endpoints_from_config + + # If the internal LB relation is attached, use that or nothing. If it's + # not attached but the external LB relation is, use that or nothing. + for lb_type in ("internal", "external"): + lb_endpoint = "loadbalancer-" + lb_type + request_name = "api-server-" + lb_type + api_port = EXTERNAL_API_PORT if lb_type == "external" else STANDARD_API_PORT + if lb_endpoint in goal_state["relations"]: + lb_provider = endpoint_from_name(lb_endpoint) + lb_response = lb_provider.get_response(request_name) + if not lb_response or lb_response.error: + return [] + return [(lb_response.address, api_port)] + + # Support the older loadbalancer relation (public-address interface). + if "loadbalancer" in goal_state["relations"]: + loadbalancer = endpoint_from_name("loadbalancer") + lb_addresses = loadbalancer.get_addresses_ports() + return [(host.get("public-address"), host.get("port")) for host in lb_addresses] + + # No LBs of any kind, so fall back to ingress-address. + if not relation: + kube_control = endpoint_from_name("kube-control") + if not kube_control.relations: + return [] + relation = kube_control.relations[0] + ingress_address = hookenv.ingress_address( + relation.relation_id, hookenv.local_unit() + ) + return [(ingress_address, STANDARD_API_PORT)] + + +def get_external_api_endpoints(): + """ + Determine the best API endpoints for an external client to connect to. + + May return an empty list if an endpoint is expected but not yet available. + """ + try: + goal_state = hookenv.goal_state() + except NotImplementedError: + goal_state = {} + goal_state.setdefault("relations", {}) + + # Config takes precedence. + endpoints_from_config = get_endpoints_from_config() + if endpoints_from_config: + return endpoints_from_config + + # If the external LB relation is attached, use that or nothing. If it's + # not attached but the internal LB relation is, use that or nothing. + for lb_type in ("external", "internal"): + lb_endpoint = "loadbalancer-" + lb_type + lb_name = "api-server-" + lb_type + api_port = EXTERNAL_API_PORT if lb_type == "external" else STANDARD_API_PORT + if lb_endpoint in goal_state["relations"]: + lb_provider = endpoint_from_name(lb_endpoint) + lb_response = lb_provider.get_response(lb_name) + if not lb_response or lb_response.error: + return [] + return [(lb_response.address, api_port)] + + # Support the older loadbalancer relation (public-address interface). + if "loadbalancer" in goal_state["relations"]: + loadbalancer = endpoint_from_name("loadbalancer") + lb_addresses = loadbalancer.get_addresses_ports() + return [(host.get("public-address"), host.get("port")) for host in lb_addresses] + + # No LBs of any kind, so fall back to public-address. + return [(hookenv.unit_public_ip(), STANDARD_API_PORT)] + + +def get_api_urls(endpoints): + """ + Convert a list of API server endpoints to URLs. + """ + return ["https://{0}:{1}".format(*endpoint) for endpoint in endpoints] + + +def get_api_url(endpoints): + """ + Choose an API endpoint from the list and build a URL from it. + """ + if not endpoints: + return None + urls = get_api_urls(endpoints) + return urls[kubernetes_common.get_unit_number() % len(urls)] + + +def install_ceph_common(): + """Install ceph-common tools. + + :return: None + """ + ceph_admin = endpoint_from_flag("ceph-storage.available") + + ceph_context = { + "mon_hosts": ceph_admin.mon_hosts(), + "fsid": ceph_admin.fsid(), + "auth_supported": ceph_admin.auth(), + "use_syslog": "true", + "ceph_public_network": "", + "ceph_cluster_network": "", + "loglevel": 1, + "hostname": socket.gethostname(), + } + # Install the ceph common utilities. + apt_install(["ceph-common"], fatal=True) + + CEPH_CONF_DIR.mkdir(exist_ok=True, parents=True) + # Render the ceph configuration from the ceph conf template. + render("ceph.conf", str(CEPH_CONF), ceph_context) + + # The key can rotate independently of other ceph config, so validate it. + try: + with open(str(CEPH_KEYRING), "w") as key_file: + key_file.write("[client.admin]\n\tkey = {}\n".format(ceph_admin.key())) + except IOError as err: + hookenv.log("IOError writing admin.keyring: {}".format(err)) + + +def query_cephfs_enabled(): + install_ceph_common() + try: + out = check_output( + ["ceph", "mds", "versions", "-c", str(CEPH_CONF)], timeout=60 + ) + return bool(json.loads(out.decode())) + except CalledProcessError: + hookenv.log("Unable to determine if CephFS is enabled", "ERROR") + return False + except TimeoutExpired: + hookenv.log("Timeout attempting to determine if CephFS is enabled", "ERROR") + return False + + +def get_cephfs_fsname(): + install_ceph_common() + try: + data = json.loads(check_output(["ceph", "fs", "ls", "-f", "json"], timeout=60)) + except TimeoutExpired: + hookenv.log("Timeout attempting to determine fsname", "ERROR") + return None + for fs in data: + if "ceph-fs_data" in fs["data_pools"]: + return fs["name"] + + +def deprecate_auth_file(auth_file): + """ + In 1.19+, file-based authentication was deprecated in favor of webhook + auth. Write out generic files that inform the user of this. + """ + csv_file = Path(auth_file) + csv_file.parent.mkdir(exist_ok=True) + + csv_backup = Path("{}.{}".format(csv_file, AUTH_BACKUP_EXT)) + if csv_file.exists() and not csv_backup.exists(): + csv_file.rename(csv_backup) + with csv_file.open("w") as f: + f.write("# File-based authentication was removed in Charmed Kubernetes 1.19\n") + + +def migrate_auth_file(filename): + """Create secrets or known tokens depending on what file is being migrated.""" + with open(str(filename), "r") as f: + rows = list(csv.reader(f)) + + for row in rows: + try: + if row[0].startswith("#"): + continue + else: + if filename == AUTH_BASIC_FILE: + create_known_token(*row) + elif filename == AUTH_TOKENS_FILE: + create_secret(*row) + else: + # log and return if we don't recognize the auth file + hookenv.log("Unknown auth file: {}".format(filename)) + return False + except IndexError: + pass + deprecate_auth_file(filename) + return True + + +def token_generator(length=32): + """Generate a random token for use in account tokens. + + param: length - the length of the token to generate + """ + alpha = string.ascii_letters + string.digits + token = "".join(random.SystemRandom().choice(alpha) for _ in range(length)) + return token + + +def create_known_token(token, username, user, groups=None): + known_tokens = Path(AUTH_TOKENS_FILE) + known_tokens.parent.mkdir(exist_ok=True) + csv_fields = ["token", "username", "user", "groups"] + + try: + with known_tokens.open("r") as f: + tokens_by_user = {r["user"]: r for r in csv.DictReader(f, csv_fields)} + except FileNotFoundError: + tokens_by_user = {} + tokens_by_username = {r["username"]: r for r in tokens_by_user.values()} + + if user in tokens_by_user: + record = tokens_by_user[user] + elif username in tokens_by_username: + record = tokens_by_username[username] + else: + record = tokens_by_user[user] = {} + record.update( + { + "token": token, + "username": username, + "user": user, + "groups": groups, + } + ) + + if not record["groups"]: + del record["groups"] + + with known_tokens.open("w") as f: + csv.DictWriter(f, csv_fields, lineterminator="\n").writerows( + tokens_by_user.values() + ) + + +def delete_secret(secret_id): + """Delete a given secret id.""" + # If this fails, it's most likely because we're trying to delete a secret + # that doesn't exist. Let the caller decide if failure is a problem. + return kubernetes_common.kubectl_success( + "delete", "secret", "-n", AUTH_SECRET_NS, secret_id + ) + + +def get_csv_password(csv_fname, user): + """Get the password for the given user within the csv file provided.""" + root_cdk = "/root/cdk" + tokens_fname = Path(root_cdk) / csv_fname + if not tokens_fname.is_file(): + return None + with tokens_fname.open("r") as stream: + for line in stream: + record = line.split(",") + try: + if record[1] == user: + return record[0] + except IndexError: + # probably a blank line or comment; move on + continue + return None + + +try: + ipaddress.IPv4Network.subnet_of +except AttributeError: + # Returns True if a is subnet of b + # This method is copied from cpython as it is available only from + # python 3.7 + # https://github.com/python/cpython/blob/3.7/Lib/ipaddress.py#L1000 + def _is_subnet_of(a, b): + try: + # Always false if one is v4 and the other is v6. + if a._version != b._version: + raise TypeError("{} and {} are not of the same version".format(a, b)) + return ( + b.network_address <= a.network_address + and b.broadcast_address >= a.broadcast_address + ) + except AttributeError: + raise TypeError( + "Unable to test subnet containment " "between {} and {}".format(a, b) + ) + + ipaddress.IPv4Network.subnet_of = _is_subnet_of + ipaddress.IPv6Network.subnet_of = _is_subnet_of + + +def is_service_cidr_expansion(): + service_cidr_from_db = db.get("kubernetes-master.service-cidr") + service_cidr_from_config = hookenv.config("service-cidr") + if not service_cidr_from_db: + return False + + # Do not consider as expansion if both old and new service cidr are same + if service_cidr_from_db == service_cidr_from_config: + return False + + current_networks = kubernetes_common.get_networks(service_cidr_from_db) + new_networks = kubernetes_common.get_networks(service_cidr_from_config) + if len(current_networks) != len(new_networks) or not all( + cur.subnet_of(new) for cur, new in zip(current_networks, new_networks) + ): + hookenv.log("WARN: New k8s service cidr not superset of old one") + return False + + return True + + +def service_cidr(): + """Return the charm's service-cidr config""" + frozen_cidr = db.get("kubernetes-master.service-cidr") + return frozen_cidr or hookenv.config("service-cidr") + + +def freeze_service_cidr(): + """Freeze the service CIDR. Once the apiserver has started, we can no + longer safely change this value.""" + frozen_service_cidr = db.get("kubernetes-master.service-cidr") + if not frozen_service_cidr or is_service_cidr_expansion(): + db.set("kubernetes-master.service-cidr", hookenv.config("service-cidr")) + + +def get_preferred_service_network(service_cidrs): + """Get the network preferred for cluster service, preferring IPv4""" + net_ipv4 = kubernetes_common.get_ipv4_network(service_cidrs) + net_ipv6 = kubernetes_common.get_ipv6_network(service_cidrs) + return net_ipv4 or net_ipv6 + + +def get_dns_ip(): + return kubernetes_common.get_service_ip("kube-dns", namespace="kube-system") + + +def get_kubernetes_service_ips(): + """Get the IP address(es) for the kubernetes service based on the cidr.""" + return [ + next(network.hosts()).exploded + for network in kubernetes_common.get_networks(service_cidr()) + ] + + +def get_snap_revs(snaps): + """Get a dict of snap revisions for a given list of snaps.""" + channel = hookenv.config("channel") + rev_info = {} + for s in sorted(snaps): + try: + # valid info should looke like: + # ... + # channels: + # latest/stable: 1.18.8 2020-08-27 (1595) 22MB classic + # latest/candidate: 1.18.8 2020-08-27 (1595) 22MB classic + # ... + info = check_output(["snap", "info", s]).decode("utf8", errors="ignore") + except CalledProcessError: + # If 'snap info' fails for whatever reason, just empty the info + info = "" + snap_rev = None + yaml_data = safe_load(info) + if yaml_data and "channels" in yaml_data: + try: + # valid data should look like: + # ['1.18.8', '2020-08-27', '(1604)', '21MB', 'classic'] + d = yaml_data["channels"][channel].split() + snap_rev = d[2].strip("()") + except (KeyError, IndexError): + hookenv.log( + "Could not determine revision for snap: {}".format(s), + level=hookenv.WARNING, + ) + rev_info[s] = snap_rev + return rev_info + + +def check_service(service, attempts=6, delay=10): + """Check if a given service is up, giving it a bit of time to come up if needed. + + Returns True if the service is running, False if not, or raises a ValueError if + the service is unknown. Will automatically handle translating master component + names (e.g., kube-apiserver) to service names (snap.kube-apiserver.daemon). + """ + for pattern in ("{}", "snap.{}", "snap.{}.daemon", "snap.kube-{}.daemon"): + if host.service("is-enabled", pattern.format(service)): + service = pattern.format(service) + break + else: + raise ValueError("Unknown service: {}".format(service)) + # Give each service up to a minute to become active; this is especially + # needed now that controller-mgr/scheduler/proxy need the apiserver + # to validate their token against a k8s secret. + attempt = 0 + while attempt < attempts: + hookenv.log( + "Checking if {} is active ({} / {})".format(service, attempt, attempts) + ) + if host.service_running(service): + return True + sleep(delay) + attempt += 1 + return False diff --git a/kubernetes-master/lib/charms/layer/nagios.py b/kubernetes-master/lib/charms/layer/nagios.py new file mode 100644 index 0000000..f6ad998 --- /dev/null +++ b/kubernetes-master/lib/charms/layer/nagios.py @@ -0,0 +1,60 @@ +from pathlib import Path + +NAGIOS_PLUGINS_DIR = '/usr/lib/nagios/plugins' + + +def install_nagios_plugin_from_text(text, plugin_name): + """ Install a nagios plugin. + + Args: + text: Plugin source code (str) + plugin_name: Name of the plugin in nagios + + Returns: Full path to installed plugin + """ + dest_path = Path(NAGIOS_PLUGINS_DIR) / plugin_name + if dest_path.exists(): + # we could complain here, test the files are the same contents, or + # just bail. Idempotency is a big deal in Juju, so I'd like to be + # ok with being called with the same file multiple times, but we + # certainly want to catch the case where multiple layers are using + # the same filename for their nagios checks. + dest = dest_path.read_text() + if dest == text: + # same file + return dest_path + # different file contents! + # maybe someone changed options or something so we need to write + # it again + + dest_path.write_text(text) + dest_path.chmod(0o755) + + return dest_path + + +def install_nagios_plugin_from_file(source_file_path, plugin_name): + """ Install a nagios plugin. + + Args: + source_file_path: Path to plugin source file + plugin_name: Name of the plugin in nagios + + Returns: Full path to installed plugin + """ + + return install_nagios_plugin_from_text(Path(source_file_path).read_text(), + plugin_name) + + +def remove_nagios_plugin(plugin_name): + """ Remove a nagios plugin. + + Args: + plugin_name: Name of the plugin in nagios + + Returns: None + """ + dest_path = Path(NAGIOS_PLUGINS_DIR) / plugin_name + if dest_path.exists(): + dest_path.unlink() diff --git a/kubernetes-master/lib/charms/layer/options.py b/kubernetes-master/lib/charms/layer/options.py new file mode 100644 index 0000000..d3f273f --- /dev/null +++ b/kubernetes-master/lib/charms/layer/options.py @@ -0,0 +1,26 @@ +import os +from pathlib import Path + +import yaml + + +_CHARM_PATH = Path(os.environ.get('JUJU_CHARM_DIR', '.')) +_DEFAULT_FILE = _CHARM_PATH / 'layer.yaml' +_CACHE = {} + + +def get(section=None, option=None, layer_file=_DEFAULT_FILE): + if option and not section: + raise ValueError('Cannot specify option without section') + + layer_file = (_CHARM_PATH / layer_file).resolve() + if layer_file not in _CACHE: + with layer_file.open() as fp: + _CACHE[layer_file] = yaml.safe_load(fp.read()) + + data = _CACHE[layer_file].get('options', {}) + if section: + data = data.get(section, {}) + if option: + data = data.get(option) + return data diff --git a/kubernetes-master/lib/charms/layer/snap.py b/kubernetes-master/lib/charms/layer/snap.py new file mode 100644 index 0000000..06cc4b1 --- /dev/null +++ b/kubernetes-master/lib/charms/layer/snap.py @@ -0,0 +1,466 @@ +# Copyright 2016-2019 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 + +import tenacity +import yaml + +from charmhelpers.core import hookenv +from charms import layer +from charms import reactive +from charms.reactive.helpers import any_file_changed, data_changed +from datetime import datetime, timedelta + + +def get_installed_flag(snapname): + return "snap.installed.{}".format(snapname) + + +def get_refresh_available_flag(snapname): + return "snap.refresh-available.{}".format(snapname) + + +def get_local_flag(snapname): + return "snap.local.{}".format(snapname) + + +def get_disabled_flag(snapname): + return "snap.disabled.{}".format(snapname) + + +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} flag. + + If the snap.installed.{snapname} flag is already set then the refresh() + function is called. + """ + installed_flag = get_installed_flag(snapname) + local_flag = get_local_flag(snapname) + if reactive.is_flag_set(installed_flag): + 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) + reactive.set_flag(local_flag) + else: + _install_store(snapname, **kw) + reactive.set_flag(installed_flag) + + # Installing any snap will first ensure that 'core' is installed. Set an + # appropriate flag for consumers that want to get/set core options. + core_installed = get_installed_flag("core") + if not reactive.is_flag_set(core_installed): + reactive.set_flag(core_installed) + + +def is_installed(snapname): + return reactive.is_flag_set(get_installed_flag(snapname)) + + +def is_local(snapname): + return reactive.is_flag_set(get_local_flag(snapname)) + + +def get_installed_snaps(): + """Return a list of snaps which are installed by this layer.""" + flag_prefix = "snap.installed." + return [flag[len(flag_prefix) :] for flag in reactive.get_flags() if flag.startswith(flag_prefix)] + + +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. + local_flag = get_local_flag(snapname) + if hookenv.has_juju_version("2.0"): + res_path = _resource_get(snapname) + if res_path is False: + _refresh_store(snapname, **kw) + reactive.clear_flag(local_flag) + else: + _install_local(res_path, **kw) + reactive.set_flag(local_flag) + else: + _refresh_store(snapname, **kw) + reactive.clear_flag(local_flag) + + +def remove(snapname): + hookenv.log("Removing snap {}".format(snapname)) + subprocess.check_call(["snap", "remove", snapname]) + reactive.clear_flag(get_installed_flag(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]) + + +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 disable(snapname): + """Disables a snap in the system + + Sets the snap.disabled.{snapname} flag + + This method doesn't affect any snap flag if requested snap does not + exist + """ + hookenv.log("Disabling {} snap".format(snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot disable {} snap because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + + subprocess.check_call(["snap", "disable", snapname]) + reactive.set_flag(get_disabled_flag(snapname)) + + +def enable(snapname): + """Enables a snap in the system + + Clears the snap.disabled.{snapname} flag + + This method doesn't affect any snap flag if requeted snap does not + exist + """ + hookenv.log("Enabling {} snap".format(snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot enable {} snap because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + + subprocess.check_call(["snap", "enable", snapname]) + reactive.clear_flag(get_disabled_flag(snapname)) + + +def restart(snapname): + """Restarts a snap in the system + + This method doesn't affect any snap flag if requested snap does not + exist + """ + hookenv.log("Restarting {} snap".format(snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot restart {} snap because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + + subprocess.check_call(["snap", "restart", snapname]) + + +def set(snapname, key, value): + """Changes configuration options in a snap + + This method will fail if snapname is not an installed snap + """ + hookenv.log("Set config {}={} for snap {}".format(key, value, snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot set {} snap config because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + + subprocess.check_call(["snap", "set", snapname, "{}={}".format(key, value)]) + + +def set_refresh_timer(timer=""): + """Set the system refresh.timer option (snapd 2.31+) + + This method sets how often snapd will refresh installed snaps. Call with + an empty timer string to use the system default (currently 4x per day). + Use 'max' to schedule refreshes as far into the future as possible + (currently 1 month). Also accepts custom timer strings as defined in the + refresh.timer section here: + https://forum.snapcraft.io/t/system-options/87 + + This method does not validate custom strings and will lead to a + CalledProcessError if an invalid string is given. + + :param: timer: empty string (default), 'max', or custom string + """ + if timer == "max": + # A month from yesterday is the farthest we should delay to safely stay + # under the 1 month max. Translate that to a valid refresh.timer value. + # Examples: + # - Today is Friday the 13th, set the refresh timer to + # 'thu2' (Thursday the 12th is the 2nd thursday of the month). + # - Today is Tuesday the 1st, set the refresh timer to + # 'mon5' (Monday the [28..31] is the 5th monday of the month). + yesterday = datetime.now() - timedelta(1) + dow = yesterday.strftime("%a").lower() + # increment after int division because we want occurrence 1-5, not 0-4. + occurrence = yesterday.day // 7 + 1 + timer = "{}{}".format(dow, occurrence) + + # NB: 'system' became synonymous with 'core' in 2.32.5, but we use 'core' + # here to ensure max compatibility. + set(snapname="core", key="refresh.timer", value=timer) + subprocess.check_call(["systemctl", "restart", "snapd.service"]) + + +def get(snapname, key): + """Gets configuration options for a snap + + This method returns the stripped output from the snap get command. + This method will fail if snapname is not an installed snap. + """ + hookenv.log("Get config {} for snap {}".format(key, snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot get {} snap config because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + + return subprocess.check_output(["snap", "get", snapname, key]).strip() + + +def get_installed_version(snapname): + """Gets the installed version of a snapname. + This function will fail if snapname is not an installed snap. + """ + cmd = ["snap", "info", snapname] + hookenv.log("Get installed key for snap {}".format(snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot get {} snap installed version because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + return subprocess.check_output(cmd).decode("utf-8", errors="replace").partition("installed:")[-1].split()[0] + + +def get_installed_channel(snapname): + """Gets the tracking (channel) of a snapname. + This function will fail if snapname is not an installed snap. + """ + cmd = ["snap", "info", snapname] + hookenv.log("Get channel for snap {}".format(snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot get snap tracking (channel) because it is not installed", + hookenv.WARNING, + ) + return + try: + return subprocess.check_output(cmd).decode("utf-8", errors="replace").partition("tracking:")[-1].split()[0] + except Exception as e: + # If it fails to get the channel information(ex. installed via resource), return nothing. + hookenv.log( + "Cannot get snap tracking (channel): {}".format(e), + hookenv.WARNING, + ) + return + + +def _snap_args( + channel="stable", + devmode=False, + jailmode=False, + dangerous=False, + force_dangerous=False, + connect=None, + classic=False, + revision=None, +): + 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) + + +def _install_store(snapname, **kw): + """Install snap from store + + :param snapname: Name of snap to install + :type snapname: str + :param kw: Keyword arguments to pass on to ``snap install`` + :type kw: Dict[str, str] + :raises: subprocess.CalledProcessError + """ + cmd = ["snap", "install"] + cmd.extend(_snap_args(**kw)) + cmd.append(snapname) + hookenv.log("Installing {} from store".format(snapname)) + + # Use tenacity decorator for Trusty support (See LP Bug #1934163) + @tenacity.retry( + wait=tenacity.wait_fixed(10), # seconds + stop=tenacity.stop_after_attempt(3), + reraise=True, + ) + def _run_install(): + try: + out = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + hookenv.log( + 'Installation successful cmd="{}" output="{}"'.format(cmd, out), + level=hookenv.DEBUG, + ) + reactive.clear_flag(get_local_flag(snapname)) + except subprocess.CalledProcessError as cp: + hookenv.log( + 'Installation failed cmd="{}" returncode={} output="{}"'.format(cmd, cp.returncode, cp.output), + level=hookenv.ERROR, + ) + raise + + _run_install() + + +def _refresh_store(snapname, **kw): + if not data_changed("snap.opts.{}".format(snapname), kw): + return + + # --amend allows us to refresh from a local resource + cmd = ["snap", "refresh", "--amend"] + cmd.extend(_snap_args(**kw)) + cmd.append(snapname) + hookenv.log("Refreshing {} from store".format(snapname)) + out = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + print(out) + + +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 + + +def get_available_refreshes(): + """Return a list of snaps which have refreshes available.""" + try: + out = subprocess.check_output(["snap", "refresh", "--list"]).decode("utf8") + except subprocess.CalledProcessError: + # If snap refresh fails for whatever reason, we should just return no + # refreshes available - LP:1869630. + return [] + + if out == "All snaps up to date.": + return [] + else: + return [line.split()[0] for line in out.splitlines()[1:]] + + +def is_refresh_available(snapname): + """Check whether a new revision is available for the given snap.""" + return reactive.is_flag_set(get_refresh_available_flag(snapname)) + + +def _check_refresh_available(snapname): + return snapname in get_available_refreshes() + + +def create_cohort_snapshot(snapname): + """Create a new cohort key for the given snap. + + Cohort keys represent a snapshot of the revision of a snap at the time + the key was created. These keys can then be used on any machine to lock + the revision of the snap until a new cohort is joined (or the key expires, + after 90 days). This is used to maintain consistency of the revision of + the snap across units or applications, and to manage the refresh of the + snap in a controlled manner. + + Returns a cohort key. + """ + out = subprocess.check_output(["snap", "create-cohort", snapname]) + data = yaml.safe_load(out.decode("utf8")) + return data["cohorts"][snapname]["cohort-key"] + + +def join_cohort_snapshot(snapname, cohort_key): + """Refresh the snap into the given cohort. + + If the snap was previously in a cohort, this will update the revision + to that of the new cohort snapshot. Note that this does not change the + channel that the snap is in, only the revision within that channel. + """ + if is_local(snapname): + # joining a cohort can override a locally installed snap + hookenv.log("Skipping joining cohort for local snap: " "{}".format(snapname)) + return + subprocess.check_output(["snap", "refresh", snapname, "--cohort", cohort_key]) + # even though we just refreshed to the latest in the cohort, it's + # slightly possible that there's a newer rev available beyond the cohort + reactive.toggle_flag(get_refresh_available_flag(snapname), _check_refresh_available(snapname)) diff --git a/kubernetes-master/lib/charms/layer/status.py b/kubernetes-master/lib/charms/layer/status.py new file mode 100644 index 0000000..95b2997 --- /dev/null +++ b/kubernetes-master/lib/charms/layer/status.py @@ -0,0 +1,189 @@ +import inspect +import errno +import subprocess +import yaml +from enum import Enum +from functools import wraps +from pathlib import Path + +from charmhelpers.core import hookenv +from charms import layer + + +_orig_call = subprocess.call +_statuses = {'_initialized': False, + '_finalized': False} + + +class WorkloadState(Enum): + """ + Enum of the valid workload states. + + Valid options are: + + * `WorkloadState.MAINTENANCE` + * `WorkloadState.BLOCKED` + * `WorkloadState.WAITING` + * `WorkloadState.ACTIVE` + """ + # note: order here determines precedence of state + MAINTENANCE = 'maintenance' + BLOCKED = 'blocked' + WAITING = 'waiting' + ACTIVE = 'active' + + +def maintenance(message): + """ + Set the status to the `MAINTENANCE` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.MAINTENANCE, message) + + +def maint(message): + """ + Shorthand alias for + [maintenance](status.md#charms.layer.status.maintenance). + + # Parameters + `message` (str): Message to convey to the operator. + """ + maintenance(message) + + +def blocked(message): + """ + Set the status to the `BLOCKED` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.BLOCKED, message) + + +def waiting(message): + """ + Set the status to the `WAITING` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.WAITING, message) + + +def active(message): + """ + Set the status to the `ACTIVE` state with the given operator message. + + # Parameters + `message` (str): Message to convey to the operator. + """ + status_set(WorkloadState.ACTIVE, message) + + +def status_set(workload_state, message): + """ + Set the status to the given workload state with a message. + + # Parameters + `workload_state` (WorkloadState or str): State of the workload. Should be + a [WorkloadState](status.md#charms.layer.status.WorkloadState) enum + member, or the string value of one of those members. + `message` (str): Message to convey to the operator. + """ + if not isinstance(workload_state, WorkloadState): + workload_state = WorkloadState(workload_state) + if workload_state is WorkloadState.MAINTENANCE: + _status_set_immediate(workload_state, message) + return + layer = _find_calling_layer() + _statuses.setdefault(workload_state, []).append((layer, message)) + if not _statuses['_initialized'] or _statuses['_finalized']: + # We either aren't initialized, so the finalizer may never be run, + # or the finalizer has already run, so it won't run again. In either + # case, we need to manually invoke it to ensure the status gets set. + _finalize() + + +def _find_calling_layer(): + for frame in inspect.stack(): + # switch to .filename when trusty (Python 3.4) is EOL + fn = Path(frame[1]) + if fn.parent.stem not in ('reactive', 'layer', 'charms'): + continue + layer_name = fn.stem + if layer_name == 'status': + continue # skip our own frames + return layer_name + return None + + +def _initialize(): + if not _statuses['_initialized']: + if layer.options.get('status', 'patch-hookenv'): + _patch_hookenv() + hookenv.atexit(_finalize) + _statuses['_initialized'] = True + + +def _finalize(): + if _statuses['_initialized']: + # If we haven't been initialized, we can't truly be finalized. + # This makes things more efficient if an action sets a status + # but subsequently starts the reactive bus. + _statuses['_finalized'] = True + charm_name = hookenv.charm_name() + charm_dir = Path(hookenv.charm_dir()) + with charm_dir.joinpath('layer.yaml').open() as fp: + includes = yaml.safe_load(fp.read()).get('includes', []) + layer_order = includes + [charm_name] + + for workload_state in WorkloadState: + if workload_state not in _statuses: + continue + if not _statuses[workload_state]: + continue + + def _get_key(record): + layer_name, message = record + if layer_name in layer_order: + return layer_order.index(layer_name) + else: + return 0 + + sorted_statuses = sorted(_statuses[workload_state], key=_get_key) + layer_name, message = sorted_statuses[-1] + _status_set_immediate(workload_state, message) + break + + +def _status_set_immediate(workload_state, message): + workload_state = workload_state.value + try: + hookenv.log('status-set: {}: {}'.format(workload_state, message), + hookenv.INFO) + ret = _orig_call(['status-set', workload_state, message]) + if ret == 0: + return + except OSError as e: + # ignore status-set not available on older controllers + if e.errno != errno.ENOENT: + raise + + +def _patch_hookenv(): + # we can't patch hookenv.status_set directly because other layers may have + # already imported it into their namespace, so we have to patch sp.call + subprocess.call = _patched_call + + +@wraps(_orig_call) +def _patched_call(cmd, *args, **kwargs): + if not isinstance(cmd, list) or cmd[0] != 'status-set': + return _orig_call(cmd, *args, **kwargs) + _, workload_state, message = cmd + status_set(workload_state, message) + return 0 # make hookenv.status_set not emit spurious failure logs diff --git a/kubernetes-master/lib/charms/layer/tls_client.py b/kubernetes-master/lib/charms/layer/tls_client.py new file mode 100644 index 0000000..b2980dc --- /dev/null +++ b/kubernetes-master/lib/charms/layer/tls_client.py @@ -0,0 +1,61 @@ +# Copyright 2016-2017 Canonical Ltd. +# +# This file is part of the tls-client 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. + +from charmhelpers.core.hookenv import log +from charmhelpers.core import unitdata + +from charms.reactive import remove_state +from charms.reactive import endpoint_from_flag + + +def reset_certificate_write_flag(cert_type): + """ + Reset the certificate written flag so notification will work on the next + write cert_type must be 'server', 'client', or 'ca' to indicate type of + certificate + """ + if cert_type not in ['server', 'client', 'ca']: + log('Unknown certificate type!') + else: + remove_state('tls_client.{0}.certificate.written'.format(cert_type)) + + +def request_server_cert(common_name, sans=None, crt_path=None, key_path=None): + tls = endpoint_from_flag('certificates.available') + tls.request_server_cert(common_name, sans) + if not crt_path and not key_path: + return + kv = unitdata.kv() + cert_paths = kv.get('layer.tls-client.cert-paths', {}) + cert_paths.setdefault('server', {})[common_name] = { + 'crt': str(crt_path), + 'key': str(key_path), + } + kv.set('layer.tls-client.cert-paths', cert_paths) + + +def request_client_cert(common_name, sans=None, crt_path=None, key_path=None): + tls = endpoint_from_flag('certificates.available') + tls.request_client_cert(common_name, sans) + if not crt_path and not key_path: + return + kv = unitdata.kv() + cert_paths = kv.get('layer.tls-client.cert-paths', {}) + cert_paths.setdefault('client', {})[common_name] = { + 'crt': str(crt_path), + 'key': str(key_path), + } + kv.set('layer.tls-client.cert-paths', cert_paths) diff --git a/kubernetes-master/lib/charms/layer/vault_kv.py b/kubernetes-master/lib/charms/layer/vault_kv.py new file mode 100644 index 0000000..fc30c19 --- /dev/null +++ b/kubernetes-master/lib/charms/layer/vault_kv.py @@ -0,0 +1,266 @@ +import json +from hashlib import md5 + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata +from charmhelpers.contrib.openstack.vaultlocker import retrieve_secret_id +from charms.reactive import data_changed +from charms.reactive import endpoint_from_flag +from charms.reactive import set_flag, clear_flag, get_flags + +import requests +import hvac + + +def log(msg, *args, **kwargs): + hookenv.log('vault-kv.log: {}'.format(msg.format(*args, **kwargs)), + level=hookenv.DEBUG) + + +class VaultNotReady(Exception): + """ + Exception indicating that Vault was accessed before it was ready. + """ + pass + + +class _Singleton(type): + # metaclass to make a class a singleton + def __call__(cls, *args, **kwargs): + if not isinstance(getattr(cls, '_singleton_instance', None), cls): + cls._singleton_instance = super().__call__(*args, **kwargs) + return cls._singleton_instance + + +class _VaultBaseKV(dict, metaclass=_Singleton): + _path = None # set by subclasses + + def __init__(self): + response = self._client.read(self._path) + data = response['data'] if response else {} + super().__init__(data) + + @property + def _client(self): + """ + Get an authenticated hvac.Client. + + The authentication token for the client is only valid for 60 seconds, + after which a new client will need to be authenticated. + """ + try: + log('Logging {cls} in to {vault_url}', + cls=type(self).__name__, + vault_url=self._config['vault_url']) + client = hvac.Client(url=self._config['vault_url']) + client.auth_approle(self._config['role_id'], + self._config['secret_id']) + return client + except (requests.exceptions.ConnectionError, + hvac.exceptions.VaultDown, + hvac.exceptions.VaultNotInitialized, + hvac.exceptions.BadGateway) as e: + raise VaultNotReady() from e + + @property + def _config(self): + _VaultBaseKV._config = get_vault_config() + return _VaultBaseKV._config + + def __setitem__(self, key, value): + log('Writing data to vault') + self._client.write(self._path, **{key: value}) + super().__setitem__(key, value) + + def set(self, key, value): + # alias in case a KV-like interface is preferred + self[key] = value + + +class VaultUnitKV(_VaultBaseKV): + """ + A simplified interface for storing data in Vault, with the data scoped to + the current unit. + + Keys must be strings, but data can be structured as long as it is + JSON-serializable. + + This class can be used as a dict, or you can use `self.get` and `self.set` + for a more KV-like interface. When values are set, via either style, they + are immediately persisted to Vault. Values are also cached in memory. + + Note: This class is a singleton. + """ + def __init__(self): + unit_num = hookenv.local_unit().split('/')[1] + self._path = '{}/kv/unit/{}'.format(self._config['secret_backend'], + unit_num) + super().__init__() + + +class VaultAppKV(_VaultBaseKV): + """ + A simplified interface for storing data in Vault, with data shared by every + unit of the application. + + Keys must be strings, but data can be structured as long as it is + JSON-serializable. + + This class can be used as a dict, or you can use `self.get` and `self.set` + for a more KV-like interface. When values are set, via either style, they + are immediately persisted to Vault. Values are also cached in memory. + + Note: This is intended to be used as a secure replacement for leadership + data. Therefore, only the leader should set data here. This is not + enforced, but data changed by non-leaders will not trigger hooks on other + units, so they may not be notified of changes in a timely fashion. + + Note: This class is a singleton. + """ + def __init__(self): + self._path = '{}/kv/app'.format(self._config['secret_backend']) + self._hash_path = '{}/kv/app-hashes/{}'.format( + self._config['secret_backend'], + hookenv.local_unit().split('/')[1]) + super().__init__() + self._load_hashes() + + def _load_hashes(self): + log('Reading hashes from {}', self._hash_path) + response = self._client.read(self._hash_path) + self._old_hashes = response['data'] if response else {} + self._new_hashes = {} + for key in self.keys(): + self._rehash(key) + + def _rehash(self, key): + serialized = json.dumps(self[key], sort_keys=True).encode('utf8') + self._new_hashes[key] = md5(serialized).hexdigest() + + def __setitem__(self, key, value): + super().__setitem__(key, value) + self._rehash(key) + self._manage_flags(key) + + def _manage_flags(self, key): + flag_any_changed = 'layer.vault-kv.app-kv.changed' + flag_key_changed = 'layer.vault-kv.app-kv.changed.{}'.format(key) + flag_key_set = 'layer.vault-kv.app-kv.set.{}'.format(key) + if self.is_changed(key): + # clear then set flag to ensure triggers are run even if the main + # flag was never cleared + clear_flag(flag_any_changed) + set_flag(flag_any_changed) + clear_flag(flag_key_changed) + set_flag(flag_key_changed) + if self.get(key) is not None: + set_flag(flag_key_set) + else: + clear_flag(flag_key_set) + + @classmethod + def _clear_all_flags(cls): + for flag in get_flags(): + if flag.startswith('layer.vault-kv.app-kv.'): + clear_flag(flag) + + def is_changed(self, key): + """ + Determine if the value for the given key has changed. + + In order to detect changes, hashes of the values are also stored + in Vault. These hashes are updated automatically at exit via + `self.update_hashes()`. + """ + return self._new_hashes.get(key) != self._old_hashes.get(key) + + def any_changed(self): + """ + Determine if any data has changed. + + In order to detect changes, hashes of the values are also stored + in Vault. These hashes are updated automatically at exit via + `self.update_hashes()`. + """ + all_keys = self._new_hashes.keys() | self._old_hashes.keys() + return any(self.is_changed(key) for key in all_keys) + + def update_hashes(self): + """ + Update the hashes in Vault, thus marking all fields as unchanged. + + This is done automatically at exit. + """ + log('Writing hashes to {}', self._hash_path) + self._client.write(self._hash_path, **self._new_hashes) + self._old_hashes.clear() + self._old_hashes.update(self._new_hashes) + + +def get_vault_config(): + """ + Get the config data needed for this application to access Vault. + + This is only needed if you're using another application, such as + VaultLocker, using the secrets backend provided by this layer. + + Returns a dictionary containing the following keys: + + * vault_url + * secret_backend + * role_id + * secret_id + + Note: This data is cached in [UnitData][] so anything with access to that + could access Vault as this application. + + If any of this data changes (such as the secret_id being rotated), this + layer will set the `layer.vault-kv.config.changed` flag. + + If this is called before the Vault relation is available, it will raise + `VaultNotReady`. + + [UnitData]: https://charm-helpers.readthedocs.io/en/latest/api/charmhelpers.core.unitdata.html + """ # noqa + vault = endpoint_from_flag('vault-kv.available') + if not (vault and vault.vault_url and vault.unit_role_id and + vault.unit_token): + raise VaultNotReady() + vault_config = { + 'vault_url': vault.vault_url, + 'secret_backend': _get_secret_backend(), + 'role_id': vault.unit_role_id, + 'secret_id': _get_secret_id(vault), + } + return vault_config + + +def _get_secret_backend(): + app_name = hookenv.application_name() + return 'charm-{}'.format(app_name) + + +def _get_secret_id(vault): + token = vault.unit_token + if data_changed('layer.vault-kv.token', token): + log('Changed unit_token, getting new secret_id') + # token is one-shot, but if it changes it might mean that we're + # being told to rotate the secret ID, or we might not have fetched + # one yet + vault_url = vault.vault_url + try: + secret_id = retrieve_secret_id(vault_url, token) + except (requests.exceptions.ConnectionError, + hvac.exceptions.VaultDown, + hvac.exceptions.VaultNotInitialized, + hvac.exceptions.BadGateway) as e: + raise VaultNotReady() from e + unitdata.kv().set('layer.vault-kv.secret_id', secret_id) + # have to flush immediately because if we don't and hit some error + # elsewhere, it could get us into a state where we have forgotten the + # secret ID and can't retrieve it again because we've already used the + # token + unitdata.kv().flush() + else: + secret_id = unitdata.kv().get('layer.vault-kv.secret_id') + return secret_id diff --git a/kubernetes-master/lib/charms/layer/vaultlocker.py b/kubernetes-master/lib/charms/layer/vaultlocker.py new file mode 100644 index 0000000..235224c --- /dev/null +++ b/kubernetes-master/lib/charms/layer/vaultlocker.py @@ -0,0 +1,170 @@ +import json +from pathlib import Path +from subprocess import check_call, check_output, CalledProcessError +from uuid import uuid4 + +from charms.reactive import set_flag +from charmhelpers.core import hookenv +from charmhelpers.core import host +from charmhelpers.core import unitdata +from charmhelpers.contrib.openstack.vaultlocker import ( # noqa + retrieve_secret_id, + write_vaultlocker_conf, +) +from charmhelpers.contrib.storage.linux.utils import ( + is_block_device, + is_device_mounted, + mkfs_xfs, +) + + +LOOP_ENVS = Path('/etc/vaultlocker/loop-envs') + + +class VaultLockerError(Exception): + """ + Wrapper for exceptions raised when configuring VaultLocker. + """ + def __init__(self, msg, *args, **kwargs): + super().__init__(msg.format(*args, **kwargs)) + + +def encrypt_storage(storage_name, mountbase=None): + """ + Set up encryption for the given Juju storage entry, and optionally create + and mount XFS filesystems on the encrypted storage entry location(s). + + Note that the storage entry **must** be defined with ``type: block``. + + If ``mountbase`` is not given, the location(s) will not be formatted or + mounted. When interacting with or mounting the location(s) manually, the + name returned by :func:`decrypted_device` called on the storage entry's + location should be used in place of the raw location. + + If the storage is defined as ``multiple``, the individual locations + will be mounted at ``{mountbase}/{storage_name}/{num}`` where ``{num}`` + is based on the storage ID. Otherwise, the storage will mounted at + ``{mountbase}/{storage_name}``. + """ + metadata = hookenv.metadata() + storage_metadata = metadata['storage'][storage_name] + if storage_metadata['type'] != 'block': + raise VaultLockerError('Cannot encrypt non-block storage: {}', + storage_name) + multiple = 'multiple' in storage_metadata + for storage_id in hookenv.storage_list(): + if not storage_id.startswith(storage_name + '/'): + continue + storage_location = hookenv.storage_get('location', storage_id) + if mountbase and multiple: + mountpoint = Path(mountbase) / storage_id + elif mountbase: + mountpoint = Path(mountbase) / storage_name + else: + mountpoint = None + encrypt_device(storage_location, mountpoint) + set_flag('layer.vaultlocker.{}.ready'.format(storage_id)) + set_flag('layer.vaultlocker.{}.ready'.format(storage_name)) + + +def encrypt_device(device, mountpoint=None, uuid=None): + """ + Set up encryption for the given block device, and optionally create and + mount an XFS filesystem on the encrypted device. + + If ``mountpoint`` is not given, the device will not be formatted or + mounted. When interacting with or mounting the device manually, the + name returned by :func:`decrypted_device` called on the device name + should be used in place of the raw device name. + """ + if not is_block_device(device): + raise VaultLockerError('Cannot encrypt non-block device: {}', device) + if is_device_mounted(device): + raise VaultLockerError('Cannot encrypt mounted device: {}', device) + hookenv.log('Encrypting device: {}'.format(device)) + if uuid is None: + uuid = str(uuid4()) + try: + check_call(['vaultlocker', 'encrypt', '--uuid', uuid, device]) + unitdata.kv().set('layer.vaultlocker.uuids.{}'.format(device), uuid) + if mountpoint: + mapped_device = decrypted_device(device) + hookenv.log('Creating filesystem on {} ({})'.format(mapped_device, + device)) + # If this fails, it's probalby due to the size of the loopback + # backing file that is defined by the `dd`. + mkfs_xfs(mapped_device) + Path(mountpoint).mkdir(mode=0o755, parents=True, exist_ok=True) + hookenv.log('Mounting filesystem for {} ({}) at {}' + ''.format(mapped_device, device, mountpoint)) + host.mount(mapped_device, mountpoint, filesystem='xfs') + host.fstab_add(mapped_device, mountpoint, 'xfs', ','.join([ + "defaults", + "nofail", + "x-systemd.requires=vaultlocker-decrypt@{uuid}.service".format( + uuid=uuid, + ), + "comment=vaultlocker", + ])) + except (CalledProcessError, OSError) as e: + raise VaultLockerError('Error configuring VaultLocker') from e + + +def decrypted_device(device): + """ + Returns the mapped device name for the decrypted version of the encrypted + device. + + This mapped device name is what should be used for mounting the device. + """ + uuid = unitdata.kv().get('layer.vaultlocker.uuids.{}'.format(device)) + if not uuid: + return None + return '/dev/mapper/crypt-{uuid}'.format(uuid=uuid) + + +def create_encrypted_loop_mount(mount_path, block_size='1M', block_count=20, + backing_file=None): + """ + Creates a persistent loop device, encrypts it, formats it as XFS, and + mounts it at the given `mount_path`. + + A backing file will be created under `/var/lib/vaultlocker/backing_files`, + in a UUID named file, according to `block_size` and `block_count` + parameters, which map to `bs` and `count` of the `dd` command. Note that + the backing file must be a bit over 16M to allow for the XFS file system + plus some additional metadata needed for the encryption. It is not + recommended to go below the default of 20M (20 blocks, 1M each). + + The `backing_file` parameter can be used to change the location where the + backing file is created. + """ + uuid = str(uuid4()) + if backing_file is None: + backing_file = Path('/var/lib/vaultlocker/backing_files') / uuid + backing_file.parent.mkdir(parents=True, exist_ok=True) + else: + backing_file = Path(backing_file) + if backing_file.exists(): + raise VaultLockerError('Backing file already exists: {}', + backing_file) + + try: + # ensure loop devices are enabled + check_call(['modprobe', 'loop']) + # create the backing file filled with random data + check_call(['dd', 'if=/dev/urandom', 'of={}'.format(backing_file), + 'bs=8M', 'count=4']) + # claim an unused loop device + output = check_output(['losetup', '--show', '-f', str(backing_file)]) + device_name = output.decode('utf8').strip() + # encrypt the new loop device + encrypt_device(device_name, str(mount_path), uuid) + # setup the service to ensure loop device is restored after reboot + (LOOP_ENVS / uuid).write_text(''.join([ + 'BACK_FILE={}\n'.format(backing_file), + ])) + check_call(['systemctl', 'enable', + 'vaultlocker-loop@{}.service'.format(uuid)]) + except (CalledProcessError, OSError) as e: + raise VaultLockerError('Error configuring VaultLocker') from e diff --git a/kubernetes-master/lib/charms/leadership.py b/kubernetes-master/lib/charms/leadership.py new file mode 100644 index 0000000..d2a95fa --- /dev/null +++ b/kubernetes-master/lib/charms/leadership.py @@ -0,0 +1,68 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Leadership Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.reactive import not_unless + + +__all__ = ['leader_get', 'leader_set'] + + +@not_unless('leadership.is_leader') +def leader_set(*args, **kw): + '''Change leadership settings, per charmhelpers.core.hookenv.leader_set. + + Settings may either be passed in as a single dictionary, or using + keyword arguments. All values must be strings. + + The leadership.set.{key} reactive state will be set while the + leadership hook environment setting remains set. + + Changed leadership settings will set the leadership.changed.{key} + and leadership.changed states. These states will remain set until + the following hook. + + These state changes take effect immediately on the leader, and + in future hooks run on non-leaders. In this way both leaders and + non-leaders can share handlers, waiting on these states. + ''' + if args: + if len(args) > 1: + raise TypeError('leader_set() takes 1 positional argument but ' + '{} were given'.format(len(args))) + else: + settings = dict(args[0]) + else: + settings = {} + settings.update(kw) + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + + for key, value in settings.items(): + if value != previous.get(key): + reactive.set_state('leadership.changed.{}'.format(key)) + reactive.set_state('leadership.changed') + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + hookenv.leader_set(settings) + unitdata.kv().update(settings, prefix='leadership.settings.') + + +def leader_get(attribute=None): + '''Return leadership settings, per charmhelpers.core.hookenv.leader_get.''' + return hookenv.leader_get(attribute) diff --git a/kubernetes-master/lib/debug_script.py b/kubernetes-master/lib/debug_script.py new file mode 100644 index 0000000..e156924 --- /dev/null +++ b/kubernetes-master/lib/debug_script.py @@ -0,0 +1,8 @@ +import os + +dir = os.environ["DEBUG_SCRIPT_DIR"] + + +def open_file(path, *args, **kwargs): + """ Open a file within the debug script dir """ + return open(os.path.join(dir, path), *args, **kwargs) diff --git a/kubernetes-master/lxd-profile.yaml b/kubernetes-master/lxd-profile.yaml new file mode 100644 index 0000000..6b4babc --- /dev/null +++ b/kubernetes-master/lxd-profile.yaml @@ -0,0 +1,16 @@ +name: juju-default-k8s-deployment-0 +config: + linux.kernel_modules: ip_tables,ip6_tables,netlink_diag,nf_nat,overlay + raw.lxc: | + lxc.apparmor.profile=unconfined + lxc.mount.auto=proc:rw sys:rw + lxc.cgroup.devices.allow=a + lxc.cap.drop= + security.nesting: true + security.privileged: true +description: "" +devices: + aadisable: + path: /dev/kmsg + source: /dev/kmsg + type: unix-char diff --git a/kubernetes-master/make_docs b/kubernetes-master/make_docs new file mode 100644 index 0000000..dcd4c1f --- /dev/null +++ b/kubernetes-master/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import os +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + sys.path.insert(0, 'lib') + sys.path.insert(1, 'reactive') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') + if os.path.exists('.unit-state.db'): + os.remove('.unit-state.db') diff --git a/kubernetes-master/metadata.yaml b/kubernetes-master/metadata.yaml new file mode 100644 index 0000000..5fa8e5f --- /dev/null +++ b/kubernetes-master/metadata.yaml @@ -0,0 +1,149 @@ +"name": "kubernetes-master" +"summary": "The Kubernetes control plane." +"maintainers": +- "Tim Van Steenburgh " +- "George Kraft " +- "Rye Terrell " +- "Konstantinos Tsakalozos " +- "Charles Butler " +- "Matthew Bruzek " +- "Mike Wilson " +- "Joe Borg " +"description": | + Kubernetes is an open-source platform for deploying, scaling, and operations + of application containers across a cluster of hosts. Kubernetes is portable + in that it works with public, private, and hybrid clouds. Extensible through + a pluggable infrastructure. Self healing in that it will automatically + restart and place containers on healthy nodes if a node ever goes away. +"tags": +- "misc" +- "infrastructure" +- "kubernetes" +- "master" +"series": +- "focal" +- "bionic" +- "xenial" +"requires": + "certificates": + "interface": "tls-certificates" + "vault-kv": + "interface": "vault-kv" + "ha": + "interface": "hacluster" + "etcd": + "interface": "etcd" + "loadbalancer": + # Use of this relation is strongly discouraged in favor of the more + # explicit loadbalancer-internal / loadbalancer-external relations. + "interface": "public-address" + "ceph-storage": + "interface": "ceph-admin" + "ceph-client": + "interface": "ceph-client" + "aws": + "interface": "aws-integration" + "gcp": + "interface": "gcp-integration" + "openstack": + "interface": "openstack-integration" + "vsphere": + "interface": "vsphere-integration" + "azure": + "interface": "azure-integration" + "keystone-credentials": + "interface": "keystone-credentials" + "dns-provider": + "interface": "kube-dns" + "loadbalancer-internal": + # Indicates that the LB should not be public and should use internal + # networks if available. Intended for control plane and other internal use. + "interface": "loadbalancer" + "loadbalancer-external": + # Indicates that the LB should be public facing. Intended for clients which + # must reach the API server via external networks. + "interface": "loadbalancer" +"provides": + "nrpe-external-master": + "interface": "nrpe-external-master" + "scope": "container" + "container-runtime": + "interface": "container-runtime" + "scope": "container" + "kube-api-endpoint": + # Use of this relation is strongly discouraged as the API endpoints will be + # provided via the kube-control relation. However, it can be used to + # override those endpoints if you need to inject a reverse proxy between + # the master and workers using a charm which only supports the old MITM + # style relations. Note, though, that since this reverse proxy will not be + # visible to the master, it will not be used in any of the client or + # component kube config files. + "interface": "http" + "kube-control": + "interface": "kube-control" + "cni": + "interface": "kubernetes-cni" + "scope": "container" + "prometheus": + "interface": "prometheus-manual" + "grafana": + "interface": "grafana-dashboard" + "aws-iam": + "interface": "aws-iam" + "scope": "container" +"peers": + "coordinator": + "interface": "coordinator" + "kube-masters": + "interface": "kube-masters" +"resources": + "core": + "type": "file" + "filename": "core.snap" + "description": | + core snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. + "kubectl": + "type": "file" + "filename": "kubectl.snap" + "description": | + kubectl snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. + "kube-apiserver": + "type": "file" + "filename": "kube-apiserver.snap" + "description": | + kube-apiserver snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. + "kube-controller-manager": + "type": "file" + "filename": "kube-controller-manager.snap" + "description": | + kube-controller-manager snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. + "kube-scheduler": + "type": "file" + "filename": "kube-scheduler.snap" + "description": | + kube-scheduler snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. + "cdk-addons": + "type": "file" + "filename": "cdk-addons.snap" + "description": | + CDK addons snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. + "kube-proxy": + "type": "file" + "filename": "kube-proxy.snap" + "description": | + kube-proxy snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. +"subordinate": !!bool "false" diff --git a/kubernetes-master/metrics.yaml b/kubernetes-master/metrics.yaml new file mode 100644 index 0000000..0d422ff --- /dev/null +++ b/kubernetes-master/metrics.yaml @@ -0,0 +1,38 @@ +metrics: + juju-units: {} + pods: + type: gauge + description: number of pods + command: /snap/bin/kubectl --kubeconfig /root/.kube/config get po --all-namespaces | tail -n+2 | wc -l + services: + type: gauge + description: number of services + command: /snap/bin/kubectl --kubeconfig /root/.kube/config get svc --all-namespaces | tail -n+2 | wc -l + replicasets: + type: gauge + description: number of replicasets + command: /snap/bin/kubectl --kubeconfig /root/.kube/config get rs --all-namespaces | tail -n+2 | wc -l + replicationcontrollers: + type: gauge + description: number of replicationcontrollers + command: /snap/bin/kubectl --kubeconfig /root/.kube/config get rc --all-namespaces | tail -n+2 | wc -l + nodes: + type: gauge + description: number of kubernetes nodes + command: /snap/bin/kubectl --kubeconfig /root/.kube/config get nodes | tail -n+2 | wc -l + nodes-gpu: + type: gauge + description: number of gpu-enabled kubernetes nodes + command: /snap/bin/kubectl --kubeconfig /root/.kube/config get nodes -l gpu=true -o name | wc -l + persistentvolume: + type: gauge + description: number of pv + command: /snap/bin/kubectl --kubeconfig /root/.kube/config get pv | tail -n+2 | wc -l + persistentvolumeclaims: + type: gauge + description: number of claims + command: /snap/bin/kubectl --kubeconfig /root/.kube/config get pvc --all-namespaces | tail -n+2 | wc -l + serviceaccounts: + type: gauge + description: number of sa + command: /snap/bin/kubectl --kubeconfig /root/.kube/config get sa --all-namespaces | tail -n+2 | wc -l diff --git a/kubernetes-master/pydocmd.yml b/kubernetes-master/pydocmd.yml new file mode 100644 index 0000000..7b3a610 --- /dev/null +++ b/kubernetes-master/pydocmd.yml @@ -0,0 +1,10 @@ +site_name: 'VaultLocker Block Device Encryption Layer' + +generate: + - vaultlocker.md: + - charms.layer.vaultlocker+ + +pages: + - VaultLocker Block Device Encryption Layer: vaultlocker.md + +gens_dir: docs diff --git a/kubernetes-master/pyproject.toml b/kubernetes-master/pyproject.toml new file mode 100644 index 0000000..db0dcd0 --- /dev/null +++ b/kubernetes-master/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length=120 +target-version=['py35'] diff --git a/kubernetes-master/reactive/__init__.py b/kubernetes-master/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-master/reactive/apt.py b/kubernetes-master/reactive/apt.py new file mode 100644 index 0000000..8832296 --- /dev/null +++ b/kubernetes-master/reactive/apt.py @@ -0,0 +1,158 @@ +# Copyright 2015-2020 Canonical Ltd. +# +# This file is part of the Apt layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' +charms.reactive helpers for dealing with deb packages. + +Add apt package sources using add_source(). Queue deb packages for +installation with install(). Configure and work with your software +once the apt.installed.{packagename} flag is set. +''' +import os.path +import subprocess +import re + +from charmhelpers import fetch +from charmhelpers.core import hookenv +from charmhelpers.core.hookenv import DEBUG, ERROR, WARNING +from charms import layer +from charms.layer import status +from charms import reactive +from charms.reactive import when, when_not + +import charms.apt + + +@when('apt.needs_update') +def update(): + charms.apt.update() + + +@when('apt.queued_installs') +@when_not('apt.needs_update') +def install_queued(): + charms.apt.install_queued() + + +@when_not('apt.queued_installs') +def ensure_package_status(): + charms.apt.ensure_package_status() + + +def filter_installed_packages(packages): + # Don't use fetch.filter_installed_packages, as it depends on python-apt + # and not available if the basic layer's use_site_packages option is off + cmd = ['dpkg-query', '--show', r'--showformat=${Package}\n'] + installed = set(subprocess.check_output(cmd, universal_newlines=True).split()) + + # list of packages that are not installed + not_installed = set(packages) - installed + + # now we want to check for any regex in the installation of the packages + not_installed_iterable = not_installed.copy() + for pkg in not_installed_iterable: + # grab the pattern that we want to match against the packages + p = re.compile(pkg) + for pkg2 in installed: + matched = p.search(pkg2) + if matched: + not_installed.remove(pkg) + break + + return not_installed + + +def clear_removed_package_flags(): + """On hook startup, clear install flags for removed packages.""" + removed = filter_installed_packages(charms.apt.installed()) + if removed: + hookenv.log('{} missing packages ({})'.format(len(removed), ','.join(removed)), WARNING) + for package in removed: + reactive.clear_flag('apt.installed.{}'.format(package)) + + +def add_implicit_signing_keys(): + """Add keys specified in layer.yaml + + The charm can ship trusted keys, avoiding the need to specify + them in config.yaml. We need to add them before we attempt + to add any custom sources, or apt will block under Bionic + if we attempt to add a source before the key becomes trusted. + """ + opts = layer.options() + if 'apt' not in opts or 'keys' not in opts['apt']: + return + keys = opts['apt']['keys'] + for p in keys: + full_p = os.path.join(hookenv.charm_dir(), p) + if os.path.exists(full_p): + hookenv.log("Adding key {}".format(p), DEBUG) + subprocess.check_call( + ['apt-key', 'add', full_p], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + else: + hookenv.log('Key {!r} does not exist'.format(full_p), ERROR) + + +def configure_sources(): + """Add user specified package sources from the service configuration. + + See charmhelpers.fetch.configure_sources for details. + """ + config = hookenv.config() + + # We don't have enums, so we need to validate this ourselves. + package_status = config.get('package_status') or '' + if package_status not in ('hold', 'install'): + status.blocked('Unknown package_status {}'.format(package_status)) + # Die before further hooks are run. This isn't very nice, but + # there is no other way to inform the operator that they have + # invalid configuration. + raise SystemExit(0) + + sources = config.get('install_sources') or '' + keys = config.get('install_keys') or '' + if reactive.helpers.data_changed('apt.configure_sources', (sources, keys)): + fetch.configure_sources(update=False, sources_var='install_sources', keys_var='install_keys') + reactive.set_flag('apt.needs_update') + + # Clumsy 'config.get() or' per Bug #1641362 + extra_packages = sorted((config.get('extra_packages') or '').split()) + if extra_packages: + charms.apt.queue_install(extra_packages) + + +def queue_layer_packages(): + """Add packages listed in build-time layer options.""" + # Both basic and apt layer. basic layer will have already installed + # its defined packages, but rescheduling it here gets the apt layer + # flag set and they will pinned as any other apt layer installed + # package. + opts = layer.options() + for section in ['basic', 'apt']: + if section in opts and 'packages' in opts[section]: + charms.apt.queue_install(opts[section]['packages']) + + +hookenv.atstart(hookenv.log, 'Initializing Apt Layer') +hookenv.atstart(clear_removed_package_flags) +hookenv.atstart(add_implicit_signing_keys) +hookenv.atstart(configure_sources) +hookenv.atstart(queue_layer_packages) +hookenv.atstart(charms.apt.reset_application_version) diff --git a/kubernetes-master/reactive/cdk_service_kicker.py b/kubernetes-master/reactive/cdk_service_kicker.py new file mode 100644 index 0000000..f7fd33a --- /dev/null +++ b/kubernetes-master/reactive/cdk_service_kicker.py @@ -0,0 +1,32 @@ +import os +import subprocess +from charms import layer +from charms.reactive import hook, when_not, remove_state, set_state +from charmhelpers.core.templating import render + + +@hook('upgrade-charm') +def upgrade_charm(): + remove_state('cdk-service-kicker.installed') + + +@when_not('cdk-service-kicker.installed') +def install_cdk_service_kicker(): + ''' Installs the cdk-service-kicker service. Workaround for + https://github.com/juju-solutions/bundle-canonical-kubernetes/issues/357 + ''' + source = 'cdk-service-kicker' + dest = '/usr/bin/cdk-service-kicker' + services = layer.options('cdk-service-kicker').get('services') + context = {'services': ' '.join(services)} + render(source, dest, context) + os.chmod('/usr/bin/cdk-service-kicker', 0o775) + + source = 'cdk-service-kicker.service' + dest = '/etc/systemd/system/cdk-service-kicker.service' + context = {} + render(source, dest, context) + command = ['systemctl', 'enable', 'cdk-service-kicker'] + subprocess.check_call(command) + + set_state('cdk-service-kicker.installed') diff --git a/kubernetes-master/reactive/coordinator.py b/kubernetes-master/reactive/coordinator.py new file mode 100644 index 0000000..474a95d --- /dev/null +++ b/kubernetes-master/reactive/coordinator.py @@ -0,0 +1,71 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Coordinator Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charms.coordinator import coordinator, log +import charms.reactive + + +def initialize_coordinator_state(): + ''' + The coordinator.granted.{lockname} state will be set and the + coordinator.requested.{lockname} state removed for every lock + granted to the currently running hook. + + The coordinator.requested.{lockname} state will remain set for locks + not yet granted + ''' + log('Initializing coordinator layer') + + requested = set(coordinator.requests.get(hookenv.local_unit(), {}).keys()) + previously_requested = set(state.split('.', 2)[2] + for state in charms.reactive.bus.get_states() + if state.startswith('coordinator.requested.')) + + granted = set(coordinator.grants.get(hookenv.local_unit(), {}).keys()) + previously_granted = set(state.split('.', 2)[2] + for state in charms.reactive.bus.get_states() + if state.startswith('coordinator.granted.')) + + # Set reactive state for requested locks. + for lock in requested: + log('Requested {} lock'.format(lock), hookenv.DEBUG) + charms.reactive.set_state('coordinator.requested.{}'.format(lock)) + + # Set reactive state for locks that have been granted. + for lock in granted: + log('Granted {} lock'.format(lock), hookenv.DEBUG) + charms.reactive.set_state('coordinator.granted.{}'.format(lock)) + + # Remove reactive state for locks that have been released. + for lock in (previously_granted - granted): + log('Dropped {} lock'.format(lock), hookenv.DEBUG) + charms.reactive.remove_state('coordinator.granted.{}'.format(lock)) + + # Remove requested state for locks no longer requested and not granted. + for lock in (previously_requested - requested - granted): + log('Request for {} lock was dropped'.format(lock), hookenv.DEBUG) + charms.reactive.remove_state('coordinator.requested.{}'.format(lock)) + + +# 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(charms.reactive, '_coordinator_registered'): + hookenv.atstart(initialize_coordinator_state) + charms.reactive._coordinator_registered = True diff --git a/kubernetes-master/reactive/hacluster.py b/kubernetes-master/reactive/hacluster.py new file mode 100644 index 0000000..f921f76 --- /dev/null +++ b/kubernetes-master/reactive/hacluster.py @@ -0,0 +1,110 @@ +from charms import layer + +from charms.reactive import hook +from charms.reactive import when, when_not, clear_flag, set_flag, is_flag_set +from charms.reactive import endpoint_from_flag + +from charms.layer.kubernetes_common import get_ingress_address + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +db = unitdata.kv() + + +@hook('upgrade-charm') +def do_upgrade(): + # bump the services from upstart to systemd. :-/ + hacluster = endpoint_from_flag('ha.connected') + if not hacluster: + return + + if not is_flag_set('layer-hacluster.upgraded-systemd'): + services = db.get('layer-hacluster.services', {'current_services': {}, + 'desired_services': {}, + 'deleted_services': {}}) + for name, service in services['current_services'].items(): + hookenv.log("changing service {} to systemd service".format(name)) + hacluster.remove_init_service(name, service) + hacluster.add_systemd_service(name, service) + + # change any pending lsb entries to systemd + for name, service in services['desired_services'].items(): + msg = "changing pending service {} to systemd service" + hookenv.log(msg.format(name)) + hacluster.remove_init_service(name, service) + hacluster.add_systemd_service(name, service) + + clear_flag('layer-hacluster.configured') + set_flag('layer-hacluster.upgraded-systemd') + + +@when('ha.connected', 'layer.hacluster.services_configured') +@when_not('layer-hacluster.configured') +def configure_hacluster(): + """Configure HA resources in corosync""" + hacluster = endpoint_from_flag('ha.connected') + vips = hookenv.config('ha-cluster-vip').split() + dns_record = hookenv.config('ha-cluster-dns') + if vips and dns_record: + set_flag('layer-hacluster.dns_vip.invalid') + msg = "Unsupported configuration. " \ + "ha-cluster-vip and ha-cluster-dns cannot both be set", + hookenv.log(msg) + return + else: + clear_flag('layer-hacluster.dns_vip.invalid') + if vips: + for vip in vips: + hacluster.add_vip(hookenv.application_name(), vip) + elif dns_record: + layer_options = layer.options('hacluster') + binding_address = layer_options.get('binding_address') + ip = get_ingress_address(binding_address) + hacluster.add_dnsha(hookenv.application_name(), ip, dns_record, + 'public') + + services = db.get('layer-hacluster.services', {'current_services': {}, + 'desired_services': {}, + 'deleted_services': {}}) + for name, service in services['deleted_services'].items(): + hacluster.remove_systemd_service(name, service) + for name, service in services['desired_services'].items(): + hacluster.add_systemd_service(name, service) + services['current_services'][name] = service + + services['deleted_services'] = {} + services['desired_services'] = {} + + hacluster.bind_resources() + set_flag('layer-hacluster.configured') + + +@when('config.changed.ha-cluster-vip', + 'ha.connected') +def update_vips(): + hacluster = endpoint_from_flag('ha.connected') + config = hookenv.config() + original_vips = set(config.previous('ha-cluster-vip').split()) + new_vips = set(config['ha-cluster-vip'].split()) + old_vips = original_vips - new_vips + + for vip in old_vips: + hacluster.remove_vip(hookenv.application_name(), vip) + + clear_flag('layer-hacluster.configured') + + +@when('config.changed.ha-cluster-dns', + 'ha.connected') +def update_dns(): + hacluster = endpoint_from_flag('ha.connected') + config = hookenv.config() + original_dns = set(config.previous('ha-cluster-dns').split()) + new_dns = set(config['ha-cluster-dns'].split()) + old_dns = original_dns - new_dns + + for dns in old_dns: + hacluster.remove_dnsha(hookenv.application_name, 'public') + + clear_flag('layer-hacluster.configured') diff --git a/kubernetes-master/reactive/kubernetes_master.py b/kubernetes-master/reactive/kubernetes_master.py new file mode 100644 index 0000000..c916cca --- /dev/null +++ b/kubernetes-master/reactive/kubernetes_master.py @@ -0,0 +1,3518 @@ +#!/usr/local/sbin/charm-env python3 + +# Copyright 2015 The Kubernetes Authors. +# +# 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 base64 +import os +import re +import socket +import json +import traceback +import yaml + +from itertools import filterfalse +from shutil import move, copyfile +from pathlib import Path +from subprocess import check_call +from subprocess import check_output +from subprocess import CalledProcessError +from urllib.request import Request, urlopen + +import charms.coordinator +from charms.layer import snap +from charms.leadership import leader_get, leader_set +from charms.reactive import hook +from charms.reactive import remove_state, clear_flag +from charms.reactive import set_state, set_flag +from charms.reactive import is_state, is_flag_set, get_unset_flags +from charms.reactive import endpoint_from_flag, endpoint_from_name +from charms.reactive import when, when_any, when_not, when_none +from charms.reactive import register_trigger +from charms.reactive import data_changed, any_file_changed + +from charms.layer import tls_client +from charms.layer import vaultlocker +from charms.layer import vault_kv + +from charmhelpers.core import hookenv +from charmhelpers.core import host +from charmhelpers.core import unitdata +from charmhelpers.core.host import restart_on_change +from charmhelpers.core.host import service_pause, service_resume, service_stop +from charmhelpers.core.templating import render +from charmhelpers.contrib.charmsupport import nrpe + +from charms.layer import kubernetes_master +from charms.layer import kubernetes_common + +from charms.layer.hacluster import add_service_to_hacluster +from charms.layer.hacluster import remove_service_from_hacluster +from charms.layer.kubernetes_common import kubeclientconfig_path +from charms.layer.kubernetes_common import migrate_resource_checksums +from charms.layer.kubernetes_common import check_resources_for_upgrade_needed +from charms.layer.kubernetes_common import ( + calculate_and_store_resource_checksums, +) # noqa +from charms.layer.kubernetes_common import arch +from charms.layer.kubernetes_common import service_restart +from charms.layer.kubernetes_common import get_ingress_address +from charms.layer.kubernetes_common import get_ingress_address6 +from charms.layer.kubernetes_common import create_kubeconfig +from charms.layer.kubernetes_common import get_service_ip +from charms.layer.kubernetes_common import configure_kubernetes_service +from charms.layer.kubernetes_common import cloud_config_path +from charms.layer.kubernetes_common import encryption_config_path +from charms.layer.kubernetes_common import write_gcp_snap_config +from charms.layer.kubernetes_common import generate_openstack_cloud_config +from charms.layer.kubernetes_common import write_azure_snap_config +from charms.layer.kubernetes_common import configure_kube_proxy +from charms.layer.kubernetes_common import kubeproxyconfig_path +from charms.layer.kubernetes_common import get_version +from charms.layer.kubernetes_common import retry +from charms.layer.kubernetes_common import ca_crt_path +from charms.layer.kubernetes_common import server_crt_path +from charms.layer.kubernetes_common import server_key_path +from charms.layer.kubernetes_common import client_crt_path +from charms.layer.kubernetes_common import client_key_path +from charms.layer.kubernetes_common import kubectl, kubectl_manifest, kubectl_success +from charms.layer.kubernetes_common import _get_vmware_uuid + +from charms.layer.nagios import install_nagios_plugin_from_file +from charms.layer.nagios import remove_nagios_plugin + + +# Override the default nagios shortname regex to allow periods, which we +# need because our bin names contain them (e.g. 'snap.foo.daemon'). The +# default regex in charmhelpers doesn't allow periods, but nagios itself does. +nrpe.Check.shortname_re = r"[\.A-Za-z0-9-_]+$" + +snap_resources = [ + "kubectl", + "kube-apiserver", + "kube-controller-manager", + "kube-scheduler", + "cdk-addons", + "kube-proxy", +] + +master_services = [ + "kube-apiserver", + "kube-controller-manager", + "kube-scheduler", + "kube-proxy", +] + +cohort_snaps = snap_resources + ["kubelet"] + + +os.environ["PATH"] += os.pathsep + os.path.join(os.sep, "snap", "bin") +db = unitdata.kv() +checksum_prefix = "kubernetes-master.resource-checksums." +configure_prefix = "kubernetes-master.prev_args." +keystone_root = "/root/cdk/keystone" +keystone_policy_path = os.path.join(keystone_root, "keystone-policy.yaml") +kubecontrollermanagerconfig_path = "/root/cdk/kubecontrollermanagerconfig" +kubeschedulerconfig_path = "/root/cdk/kubeschedulerconfig" +cdk_addons_kubectl_config_path = "/root/cdk/cdk_addons_kubectl_config" +aws_iam_webhook = "/root/cdk/aws-iam-webhook.yaml" +auth_webhook_root = "/root/cdk/auth-webhook" +auth_webhook_conf = os.path.join(auth_webhook_root, "auth-webhook-conf.yaml") +auth_webhook_exe = os.path.join(auth_webhook_root, "auth-webhook.py") +auth_webhook_svc = "/etc/systemd/system/cdk.master.auth-webhook.service" + +register_trigger( + when="endpoint.aws.ready", set_flag="kubernetes-master.aws.changed" # when set +) +register_trigger( + when_not="endpoint.aws.ready", # when cleared + set_flag="kubernetes-master.aws.changed", +) +register_trigger( + when="endpoint.azure.ready", set_flag="kubernetes-master.azure.changed" # when set +) +register_trigger( + when_not="endpoint.azure.ready", # when cleared + set_flag="kubernetes-master.azure.changed", +) +register_trigger( + when="endpoint.gcp.ready", set_flag="kubernetes-master.gcp.changed" # when set +) +register_trigger( + when_not="endpoint.gcp.ready", # when cleared + set_flag="kubernetes-master.gcp.changed", +) +register_trigger( + when="kubernetes-master.ceph.configured", set_flag="cdk-addons.reconfigure" +) +register_trigger( + when_not="kubernetes-master.ceph.configured", set_flag="cdk-addons.reconfigure" +) +register_trigger( + when="keystone-credentials.available", set_flag="cdk-addons.reconfigure" +) +register_trigger( + when_not="keystone-credentials.available", set_flag="cdk-addons.reconfigure" +) +register_trigger( + when="kubernetes-master.aws.changed", set_flag="cdk-addons.reconfigure" +) +register_trigger( + when="kubernetes-master.azure.changed", set_flag="cdk-addons.reconfigure" +) +register_trigger( + when="kubernetes-master.gcp.changed", set_flag="cdk-addons.reconfigure" +) +register_trigger( + when="kubernetes-master.openstack.changed", set_flag="cdk-addons.reconfigure" +) +register_trigger( + when_not="cni.available", clear_flag="kubernetes-master.components.started" +) +register_trigger( + when="kube-control.requests.changed", clear_flag="authentication.setup" +) +register_trigger( + when_not="kubernetes-master.apiserver.configured", + clear_flag="kubernetes-master.apiserver.running", +) + + +def set_upgrade_needed(forced=False): + set_state("kubernetes-master.upgrade-needed") + config = hookenv.config() + previous_channel = config.previous("channel") + require_manual = config.get("require-manual-upgrade") + hookenv.log("set upgrade needed") + if previous_channel is None or not require_manual or forced: + hookenv.log("forcing upgrade") + set_state("kubernetes-master.upgrade-specified") + + +@when("config.changed.channel") +def channel_changed(): + set_upgrade_needed() + + +def maybe_install_kube_proxy(): + if not snap.is_installed("kube-proxy"): + channel = hookenv.config("channel") + hookenv.status_set("maintenance", "Installing kube-proxy snap") + snap.install("kube-proxy", channel=channel, classic=True) + calculate_and_store_resource_checksums(checksum_prefix, snap_resources) + + +@hook("install") +def fresh_install(): + # fresh installs should always send the unique cluster tag to cdk-addons + set_state("kubernetes-master.cdk-addons.unique-cluster-tag") + + +@hook("upgrade-charm") +def check_for_upgrade_needed(): + """An upgrade charm event was triggered by Juju, react to that here.""" + hookenv.status_set("maintenance", "Checking resources") + is_leader = is_state("leadership.is_leader") + + # migrate to new flags + if is_state("kubernetes-master.restarted-for-cloud"): + remove_state("kubernetes-master.restarted-for-cloud") + set_state("kubernetes-master.cloud.ready") + if is_state("kubernetes-master.cloud-request-sent"): + # minor change, just for consistency + remove_state("kubernetes-master.cloud-request-sent") + set_state("kubernetes-master.cloud.request-sent") + + # ceph-storage.configured flag no longer exists + remove_state("ceph-storage.configured") + + # reconfigure ceph. we need this in case we're reverting from ceph-csi back + # to old ceph on Kubernetes 1.10 or 1.11 + remove_state("kubernetes-master.ceph.configured") + + maybe_install_kube_proxy() + update_certificates() + switch_auth_mode(forced=True) + + # File-based auth is gone in 1.19; ensure any entries in basic_auth.csv are + # added to known_tokens.csv, and any known_tokens entries are created as secrets. + if not is_flag_set("kubernetes-master.basic-auth.migrated"): + if kubernetes_master.migrate_auth_file(kubernetes_master.AUTH_BASIC_FILE): + set_flag("kubernetes-master.basic-auth.migrated") + else: + hookenv.log( + "Unable to migrate {} to {}".format( + kubernetes_master.AUTH_BASIC_FILE, + kubernetes_master.AUTH_TOKENS_FILE, + ) + ) + if not is_flag_set("kubernetes-master.token-auth.migrated"): + register_auth_webhook() + add_rbac_roles() + if kubernetes_master.migrate_auth_file(kubernetes_master.AUTH_TOKENS_FILE): + set_flag("kubernetes-master.token-auth.migrated") + else: + hookenv.log( + "Unable to migrate {} to Kubernetes secrets".format( + kubernetes_master.AUTH_TOKENS_FILE + ) + ) + set_state("reconfigure.authentication.setup") + remove_state("authentication.setup") + + if not db.get("snap.resources.fingerprint.initialised"): + # We are here on an upgrade from non-rolling master + # Since this upgrade might also include resource updates eg + # juju upgrade-charm kubernetes-master --resource kube-any=my.snap + # we take no risk and forcibly upgrade the snaps. + # Forcibly means we do not prompt the user to call the upgrade action. + set_upgrade_needed(forced=True) + + migrate_resource_checksums(checksum_prefix, snap_resources) + if check_resources_for_upgrade_needed(checksum_prefix, snap_resources): + set_upgrade_needed() + + # Set the auto storage backend to etcd2. + auto_storage_backend = leader_get("auto_storage_backend") + if not auto_storage_backend and is_leader: + leader_set(auto_storage_backend="etcd2") + + if is_leader and not leader_get("auto_dns_provider"): + was_kube_dns = hookenv.config().previous("enable-kube-dns") + if was_kube_dns is True: + leader_set(auto_dns_provider="kube-dns") + elif was_kube_dns is False: + leader_set(auto_dns_provider="none") + + if is_flag_set("nrpe-external-master.available"): + update_nrpe_config() + + remove_state("kubernetes-master.system-monitoring-rbac-role.applied") + + +@hook("pre-series-upgrade") +def pre_series_upgrade(): + """Stop the kubernetes master services""" + for service in master_services: + service_pause("snap.%s.daemon" % service) + + +@hook("post-series-upgrade") +def post_series_upgrade(): + for service in master_services: + service_resume("snap.%s.daemon" % service) + # set ourselves up to restart + remove_state("kubernetes-master.components.started") + + +@hook("leader-elected") +def leader_elected(): + clear_flag("authentication.setup") + + +def add_rbac_roles(): + """Update the known_tokens file with proper groups. + + DEPRECATED: Once known_tokens are migrated, group data will be stored in K8s + secrets. Do not use this function after migrating to authn with secrets. + """ + if is_flag_set("kubernetes-master.token-auth.migrated"): + hookenv.log("Known tokens have migrated to secrets. Skipping group changes") + return + tokens_fname = "/root/cdk/known_tokens.csv" + tokens_backup_fname = "/root/cdk/known_tokens.csv.backup" + move(tokens_fname, tokens_backup_fname) + with open(tokens_fname, "w") as ftokens: + with open(tokens_backup_fname, "r") as stream: + for line in stream: + if line.startswith("#"): + continue + record = line.strip().split(",") + try: + # valid line looks like: token,username,user,groups + if record[2] == "admin" and len(record) == 3: + towrite = '{0},{1},{2},"{3}"\n'.format( + record[0], record[1], record[2], "system:masters" + ) + ftokens.write(towrite) + continue + if record[2] == "kube_proxy": + towrite = "{0},{1},{2}\n".format( + record[0], "system:kube-proxy", "kube-proxy" + ) + ftokens.write(towrite) + continue + if record[2] == "kube_controller_manager": + towrite = "{0},{1},{2}\n".format( + record[0], + "system:kube-controller-manager", + "kube-controller-manager", + ) + ftokens.write(towrite) + continue + if record[2] == "kubelet" and record[1] == "kubelet": + continue + except IndexError: + msg = "Skipping invalid line from {}: {}".format( + tokens_backup_fname, line + ) + hookenv.log(msg, level=hookenv.DEBUG) + continue + else: + ftokens.write("{}".format(line)) + + +@when("kubernetes-master.upgrade-specified") +def do_upgrade(): + install_snaps() + remove_state("kubernetes-master.upgrade-needed") + remove_state("kubernetes-master.upgrade-specified") + + +def install_snaps(): + channel = hookenv.config("channel") + hookenv.status_set("maintenance", "Installing core snap") + snap.install("core") + hookenv.status_set("maintenance", "Installing kubectl snap") + snap.install("kubectl", channel=channel, classic=True) + hookenv.status_set("maintenance", "Installing kube-apiserver snap") + snap.install("kube-apiserver", channel=channel) + hookenv.status_set("maintenance", "Installing kube-controller-manager snap") + snap.install("kube-controller-manager", channel=channel) + hookenv.status_set("maintenance", "Installing kube-scheduler snap") + snap.install("kube-scheduler", channel=channel) + hookenv.status_set("maintenance", "Installing cdk-addons snap") + snap.install("cdk-addons", channel=channel) + hookenv.status_set("maintenance", "Installing kube-proxy snap") + snap.install("kube-proxy", channel=channel, classic=True) + calculate_and_store_resource_checksums(checksum_prefix, snap_resources) + db.set("snap.resources.fingerprint.initialised", True) + set_state("kubernetes-master.snaps.installed") + remove_state("kubernetes-master.components.started") + + +@when("kubernetes-master.snaps.installed", "leadership.is_leader") +@when_not("leadership.set.cohort_keys") +def create_or_update_cohort_keys(): + cohort_keys = {} + for snapname in cohort_snaps: + cohort_key = snap.create_cohort_snapshot(snapname) + cohort_keys[snapname] = cohort_key + leader_set(cohort_keys=json.dumps(cohort_keys)) + hookenv.log("Snap cohort keys have been created.", level=hookenv.INFO) + + # Prime revision info so we can detect changes later + cohort_revs = kubernetes_master.get_snap_revs(cohort_snaps) + data_changed("leader-cohort-revs", cohort_revs) + hookenv.log( + "Tracking cohort revisions: {}".format(cohort_revs), level=hookenv.DEBUG + ) + + +@when( + "kubernetes-master.snaps.installed", + "leadership.is_leader", + "leadership.set.cohort_keys", +) +def check_cohort_updates(): + cohort_revs = kubernetes_master.get_snap_revs(cohort_snaps) + if cohort_revs and data_changed("leader-cohort-revs", cohort_revs): + leader_set(cohort_keys=None) + hookenv.log("Snap cohort revisions have changed.", level=hookenv.INFO) + + +@when("kubernetes-master.snaps.installed", "leadership.set.cohort_keys") +@when_none("coordinator.granted.cohort", "coordinator.requested.cohort") +def safely_join_cohort(): + """Coordinate the rollout of snap refreshes. + + When cohort keys change, grab a lock so that only 1 unit in the + application joins the new cohort at a time. This allows us to roll out + snap refreshes without risking all units going down at once. + """ + cohort_keys = leader_get("cohort_keys") + # NB: initial data-changed is always true + if data_changed("leader-cohorts", cohort_keys): + clear_flag("kubernetes-master.cohorts.joined") + clear_flag("kubernetes-master.cohorts.sent") + charms.coordinator.acquire("cohort") + + +@when( + "kubernetes-master.snaps.installed", + "leadership.set.cohort_keys", + "coordinator.granted.cohort", +) +@when_not("kubernetes-master.cohorts.joined") +def join_or_update_cohorts(): + """Join or update a cohort snapshot. + + All units of this application (leader and followers) need to refresh their + installed snaps to the current cohort snapshot. + """ + cohort_keys = json.loads(leader_get("cohort_keys")) + for snapname in cohort_snaps: + cohort_key = cohort_keys[snapname] + if snap.is_installed(snapname): # we also manage workers' cohorts + hookenv.status_set("maintenance", "Joining snap cohort.") + snap.join_cohort_snapshot(snapname, cohort_key) + set_flag("kubernetes-master.cohorts.joined") + hookenv.log("{} has joined the snap cohort".format(hookenv.local_unit())) + + +@when( + "kubernetes-master.snaps.installed", + "leadership.set.cohort_keys", + "kubernetes-master.cohorts.joined", + "kube-control.connected", +) +@when_not("kubernetes-master.cohorts.sent") +def send_cohorts(): + """Send cohort information to workers. + + If we have peers, wait until all peers are updated before sending. + Otherwise, we're a single unit k8s-master and can fire when connected. + """ + cohort_keys = json.loads(leader_get("cohort_keys")) + kube_control = endpoint_from_flag("kube-control.connected") + kube_masters = endpoint_from_flag("kube-masters.connected") + + # If we have peers, tell them we've joined the cohort. This is needed so + # we don't tell workers about cohorts until all masters are in-sync. + goal_peers = len(list(hookenv.expected_peer_units())) + if goal_peers > 0: + if kube_masters: + # tell peers about the cohort keys + kube_masters.set_cohort_keys(cohort_keys) + else: + msg = "Waiting for {} peers before setting the cohort.".format(goal_peers) + hookenv.log(msg, level=hookenv.DEBUG) + return + + if is_flag_set("kube-masters.cohorts.ready"): + # tell workers about the cohort keys + kube_control.set_cohort_keys(cohort_keys) + hookenv.log( + "{} (peer) sent cohort keys to workers".format(hookenv.local_unit()) + ) + else: + msg = "Waiting for k8s-masters to agree on cohorts." + hookenv.log(msg, level=hookenv.DEBUG) + return + else: + # tell workers about the cohort keys + kube_control.set_cohort_keys(cohort_keys) + hookenv.log( + "{} (single) sent cohort keys to workers".format(hookenv.local_unit()) + ) + + set_flag("kubernetes-master.cohorts.sent") + + +@when("etcd.available") +@when("config.changed.enable-metrics") +def enable_metric_changed(): + """ + Trigger an api server update. + + :return: None + """ + clear_flag("kubernetes-master.apiserver.configured") + + if is_state("leadership.is_leader"): + configure_cdk_addons() + + +@when("config.changed.client_password", "leadership.is_leader") +def password_changed(): + """Handle password change by reconfiguring authentication.""" + remove_state("authentication.setup") + + +@when("config.changed.storage-backend") +def storage_backend_changed(): + remove_state("kubernetes-master.components.started") + + +@when("cni.connected") +@when_not("cni.configured") +def configure_cni(cni): + """Set master configuration on the CNI relation. This lets the CNI + subordinate know that we're the master so it can respond accordingly.""" + cni.set_config(is_master=True) + + +@when("leadership.is_leader") +@when_not("authentication.setup") +def setup_leader_authentication(): + """ + Setup service accounts and tokens for the cluster. + + As of 1.19 charms, this will also propogate a generic basic_auth.csv, which is + merged into known_tokens.csv, which are migrated to secrets during upgrade-charm. + """ + basic_auth = "/root/cdk/basic_auth.csv" + known_tokens = "/root/cdk/known_tokens.csv" + service_key = "/root/cdk/serviceaccount.key" + os.makedirs("/root/cdk", exist_ok=True) + + hookenv.status_set("maintenance", "Rendering authentication templates.") + + keys = [basic_auth, known_tokens, service_key] + # Try first to fetch data from an old leadership broadcast. + if not get_keys_from_leader(keys) or is_state("reconfigure.authentication.setup"): + kubernetes_master.deprecate_auth_file(basic_auth) + set_flag("kubernetes-master.basic-auth.migrated") + + kubernetes_master.deprecate_auth_file(known_tokens) + set_flag("kubernetes-master.token-auth.migrated") + + # Generate the default service account token key + if not os.path.isfile(service_key): + cmd = ["openssl", "genrsa", "-out", service_key, "2048"] + check_call(cmd) + remove_state("reconfigure.authentication.setup") + + # Write the admin token every time we setup authn to ensure we honor a + # configured password. + client_pass = hookenv.config("client_password") or get_token("admin") + setup_tokens(client_pass, "admin", "admin", "system:masters") + + create_tokens_and_sign_auth_requests() + + # send auth files to followers via leadership data + leader_data = {} + for f in [basic_auth, known_tokens, service_key]: + try: + with open(f, "r") as fp: + leader_data[f] = fp.read() + except FileNotFoundError: + pass + + # this is slightly opaque, but we are sending file contents under its file + # path as a key. + # eg: + # {'/root/cdk/serviceaccount.key': 'RSA:2471731...'} + leader_set(leader_data) + + remove_state("kubernetes-master.components.started") + remove_state("kube-control.requests.changed") + set_state("authentication.setup") + + +@when_not("leadership.is_leader") +def setup_non_leader_authentication(): + basic_auth = "/root/cdk/basic_auth.csv" + known_tokens = "/root/cdk/known_tokens.csv" + service_key = "/root/cdk/serviceaccount.key" + + # Starting with 1.19, we don't use csv auth files; handle changing secrets. + secrets = { + "admin": get_token("admin"), + "kube-controller-manager": get_token("system:kube-controller-manager"), + "kube-proxy": get_token("system:kube-proxy"), + "kube-scheduler": get_token("system:kube-scheduler"), + } + if data_changed("secrets-data", secrets): + set_flag("kubernetes-master.token-auth.migrated") + build_kubeconfig() + remove_state("kubernetes-master.components.started") + + keys = [basic_auth, known_tokens, service_key] + # Pre-secrets, the source of truth for non-leaders is the leader. + # Therefore we overwrite_local with whatever the leader has. + if not get_keys_from_leader(keys, overwrite_local=True): + # the keys were not retrieved. Non-leaders have to retry. + return + + if any_file_changed(keys): + remove_state("kubernetes-master.components.started") + + # Clear stale creds from the kube-control relation so that the leader can + # assume full control of them. + kube_control = endpoint_from_flag("kube-control.connected") + if kube_control: + kube_control.clear_creds() + + remove_state("kube-control.requests.changed") + set_state("authentication.setup") + + +def get_keys_from_leader(keys, overwrite_local=False): + """ + Gets the broadcasted keys from the leader and stores them in + the corresponding files. + + Args: + keys: list of keys. Keys are actually files on the FS. + + Returns: True if all key were fetched, False if not. + + """ + # This races with other codepaths, and seems to require being created first + # This block may be extracted later, but for now seems to work as intended + os.makedirs("/root/cdk", exist_ok=True) + + for k in keys: + # If the path does not exist, assume we need it + if not os.path.exists(k) or overwrite_local: + # Fetch data from leadership broadcast + contents = leader_get(k) + # Default to logging the warning and wait for leader data to be set + if contents is None: + hookenv.log("Missing content for file {}".format(k)) + return False + # Write out the file and move on to the next item + with open(k, "w+") as fp: + fp.write(contents) + fp.write("\n") + + return True + + +@when("kubernetes-master.snaps.installed") +def set_app_version(): + """Declare the application version to juju""" + version = check_output(["kube-apiserver", "--version"]) + hookenv.application_version_set(version.split(b" v")[-1].rstrip()) + + +@hookenv.atstart +def check_vault_pending(): + try: + goal_state = hookenv.goal_state() + except NotImplementedError: + goal_state = {} + vault_kv_goal = "vault-kv" in goal_state.get("relations", {}) + vault_kv_connected = is_state("vault-kv.connected") + vault_kv_related = vault_kv_goal or vault_kv_connected + vault_kv_ready = is_state("layer.vault-kv.ready") + if vault_kv_related and not vault_kv_ready: + set_flag("kubernetes-master.vault-kv.pending") + else: + clear_flag("kubernetes-master.vault-kv.pending") + + +@hookenv.atexit +def set_final_status(): + """Set the final status of the charm as we leave hook execution""" + try: + goal_state = hookenv.goal_state() + except NotImplementedError: + goal_state = {} + + if is_flag_set("upgrade.series.in-progress"): + hookenv.status_set("blocked", "Series upgrade in progress") + return + + if not is_flag_set("certificates.available"): + if "certificates" in goal_state.get("relations", {}): + hookenv.status_set("waiting", "Waiting for certificates authority.") + else: + hookenv.status_set("blocked", "Missing relation to certificate authority.") + return + + if is_flag_set("kubernetes-master.secure-storage.failed"): + hookenv.status_set( + "blocked", + "Failed to configure encryption; " + "secrets are unencrypted or inaccessible", + ) + return + elif is_flag_set("kubernetes-master.secure-storage.created"): + if not encryption_config_path().exists(): + hookenv.status_set( + "blocked", "VaultLocker containing encryption config unavailable" + ) + return + + vsphere_joined = is_state("endpoint.vsphere.joined") + azure_joined = is_state("endpoint.azure.joined") + cloud_blocked = is_state("kubernetes-master.cloud.blocked") + if vsphere_joined and cloud_blocked: + hookenv.status_set( + "blocked", "vSphere integration requires K8s 1.12 or greater" + ) + return + if azure_joined and cloud_blocked: + hookenv.status_set("blocked", "Azure integration requires K8s 1.11 or greater") + return + + if is_state("kubernetes-master.cloud.pending"): + hookenv.status_set("waiting", "Waiting for cloud integration") + return + + if "kube-api-endpoint" in goal_state.get("relations", {}): + if not is_state("kube-api-endpoint.available"): + hookenv.status_set("waiting", "Waiting for kube-api-endpoint relation") + return + + for lb_endpoint in ("loadbalancer-internal", "loadbalancer-external"): + if lb_endpoint in goal_state.get("relations", {}): + lb_provider = endpoint_from_name(lb_endpoint) + if not lb_provider.has_response: + hookenv.status_set("waiting", "Waiting for " + lb_endpoint) + return + + if not is_state("kube-control.connected"): + if "kube-control" in goal_state.get("relations", {}): + status = "waiting" + else: + status = "blocked" + hookenv.status_set(status, "Waiting for workers.") + return + + ks = endpoint_from_flag("keystone-credentials.available") + if ks and ks.api_version() == "2": + msg = "Keystone auth v2 detected. v3 is required." + hookenv.status_set("blocked", msg) + return + + upgrade_needed = is_state("kubernetes-master.upgrade-needed") + upgrade_specified = is_state("kubernetes-master.upgrade-specified") + if upgrade_needed and not upgrade_specified: + msg = "Needs manual upgrade, run the upgrade action" + hookenv.status_set("blocked", msg) + return + + try: + get_dns_provider() + except InvalidDnsProvider as e: + if e.value == "core-dns": + msg = "dns-provider=core-dns requires k8s 1.14+" + else: + msg = "dns-provider=%s is invalid" % e.value + hookenv.status_set("blocked", msg) + return + + if is_state("kubernetes-master.vault-kv.pending"): + hookenv.status_set( + "waiting", "Waiting for encryption info from Vault to secure secrets" + ) + return + + if is_state("kubernetes-master.had-service-cidr-expanded"): + hookenv.status_set( + "waiting", "Waiting to retry updates for service-cidr expansion" + ) + return + + if not is_state("etcd.available"): + if "etcd" in goal_state.get("relations", {}): + status = "waiting" + else: + status = "blocked" + hookenv.status_set(status, "Waiting for etcd") + return + + if not is_state("cni.available"): + if "cni" in goal_state.get("relations", {}): + status = "waiting" + else: + status = "blocked" + hookenv.status_set(status, "Waiting for CNI plugins to become available") + return + + if not is_state("tls_client.certs.saved"): + hookenv.status_set("waiting", "Waiting for certificates") + return + + if not is_flag_set("kubernetes-master.auth-webhook-service.started"): + hookenv.status_set("waiting", "Waiting for auth-webhook service to start") + return + + if not is_flag_set("kubernetes-master.apiserver.configured"): + hookenv.status_set("waiting", "Waiting for API server to be configured") + return + + if not is_flag_set("kubernetes-master.apiserver.running"): + hookenv.status_set("waiting", "Waiting for API server to start") + return + + authentication_setup = is_state("authentication.setup") + if not authentication_setup: + hookenv.status_set("waiting", "Waiting on crypto keys.") + return + + if not is_flag_set("kubernetes-master.auth-webhook-tokens.setup"): + hookenv.status_set("waiting", "Waiting for auth-webhook tokens") + return + + if is_state("kubernetes-master.components.started"): + # All services should be up and running at this point. Double-check... + failing_services = master_services_down() + if len(failing_services) != 0: + msg = "Stopped services: {}".format(",".join(failing_services)) + hookenv.status_set("blocked", msg) + return + else: + # if we don't have components starting, we're waiting for that and + # shouldn't fall through to Kubernetes master running. + hookenv.status_set("maintenance", "Waiting for master components to start") + return + + # Note that after this point, kubernetes-master.components.started is + # always True. + + is_leader = is_state("leadership.is_leader") + addons_configured = is_state("cdk-addons.configured") + if is_leader and not addons_configured: + hookenv.status_set("waiting", "Waiting to retry addon deployment") + return + + if is_leader and not is_state( + "kubernetes-master.system-monitoring-rbac-role.applied" + ): + msg = "Waiting to retry applying system:monitoring RBAC role" + hookenv.status_set("waiting", msg) + return + + try: + unready = get_kube_system_pods_not_running() + except FailedToGetPodStatus: + hookenv.status_set("waiting", "Waiting for kube-system pods to start") + return + + if unready: + plural = "s" if len(unready) > 1 else "" + msg = "Waiting for {} kube-system pod{} to start" + msg = msg.format(len(unready), plural) + hookenv.status_set("waiting", msg) + return + + service_cidr = kubernetes_master.service_cidr() + if hookenv.config("service-cidr") != service_cidr: + msg = "WARN: cannot change service-cidr, still using " + service_cidr + hookenv.status_set("active", msg) + return + + gpu_available = is_state("kube-control.gpu.available") + gpu_enabled = is_state("kubernetes-master.gpu.enabled") + if gpu_available and not gpu_enabled: + msg = 'GPUs available. Set allow-privileged="auto" to enable.' + hookenv.status_set("active", msg) + return + + if ( + is_state("ceph-storage.available") + and is_state("ceph-client.connected") + and is_state("kubernetes-master.privileged") + and not is_state("kubernetes-master.ceph.configured") + ): + + ceph_admin = endpoint_from_flag("ceph-storage.available") + + if get_version("kube-apiserver") >= (1, 12) and not ceph_admin.key(): + hookenv.status_set("waiting", "Waiting for Ceph to provide a key.") + return + + if is_leader and ks and is_flag_set("kubernetes-master.keystone-policy-error"): + hookenv.status_set("blocked", "Invalid keystone policy file.") + return + + if ( + is_leader + and ks + and not is_flag_set("kubernetes-master.keystone-policy-handled") + ): + hookenv.status_set("waiting", "Waiting to apply keystone policy file.") + return + + hookenv.status_set("active", "Kubernetes master running.") + + +def master_services_down(): + """Ensure master services are up and running. + + Return: list of failing services""" + return list(filterfalse(kubernetes_master.check_service, master_services)) + + +def add_systemd_file_limit(): + directory = "/etc/systemd/system/snap.kube-apiserver.daemon.service.d" + if not os.path.isdir(directory): + os.makedirs(directory) + + file_name = "file-limit.conf" + path = os.path.join(directory, file_name) + if not os.path.isfile(path): + with open(path, "w") as f: + f.write("[Service]\n") + f.write("LimitNOFILE=65535") + + +def add_systemd_restart_always(): + template = "templates/service-always-restart.systemd-latest.conf" + + try: + # Get the systemd version + cmd = ["systemd", "--version"] + output = check_output(cmd).decode("UTF-8") + line = output.splitlines()[0] + words = line.split() + assert words[0] == "systemd" + systemd_version = int(words[1]) + + # Check for old version (for xenial support) + if systemd_version < 230: + template = "templates/service-always-restart.systemd-229.conf" + except Exception: + traceback.print_exc() + hookenv.log( + "Failed to detect systemd version, using latest template", level="ERROR" + ) + + for service in master_services: + dest_dir = "/etc/systemd/system/snap.{}.daemon.service.d".format(service) + os.makedirs(dest_dir, exist_ok=True) + copyfile(template, "{}/always-restart.conf".format(dest_dir)) + + +def add_systemd_file_watcher(): + """Setup systemd file-watcher service. + + This service watches these files for changes: + + /root/cdk/known_tokens.csv + /root/cdk/serviceaccount.key + + If a file is changed, the service uses juju-run to invoke a script in a + hook context on this unit. If this unit is the leader, the script will + call leader-set to distribute the contents of these files to the + non-leaders so they can sync their local copies to match. + + """ + render( + "cdk.master.leader.file-watcher.sh", + "/usr/local/sbin/cdk.master.leader.file-watcher.sh", + {}, + perms=0o777, + ) + render( + "cdk.master.leader.file-watcher.service", + "/etc/systemd/system/cdk.master.leader.file-watcher.service", + {"unit": hookenv.local_unit()}, + perms=0o644, + ) + render( + "cdk.master.leader.file-watcher.path", + "/etc/systemd/system/cdk.master.leader.file-watcher.path", + {}, + perms=0o644, + ) + service_resume("cdk.master.leader.file-watcher.path") + + +@when("etcd.available", "tls_client.certs.saved") +@restart_on_change( + { + auth_webhook_conf: ["cdk.master.auth-webhook"], + auth_webhook_exe: ["cdk.master.auth-webhook"], + auth_webhook_svc: ["cdk.master.auth-webhook"], + } +) +def register_auth_webhook(): + """Render auth webhook templates and start the related service.""" + os.makedirs(auth_webhook_root, exist_ok=True) + config = hookenv.config() + + # For 'api_ver', match the api version of the authentication.k8s.io TokenReview + # that k8s-apiserver will be sending: + # https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18 + context = { + "api_ver": "v1beta1", + "charm_dir": hookenv.charm_dir(), + "host": get_ingress_address( + "kube-api-endpoint", ignore_addresses=[hookenv.config("ha-cluster-vip")] + ), + "pidfile": "auth-webhook.pid", + "port": 5000, + "root_dir": auth_webhook_root, + } + + context["aws_iam_endpoint"] = None + if endpoint_from_flag("endpoint.aws-iam.ready"): + aws_webhook = Path(aws_iam_webhook) + if aws_webhook.exists(): + aws_yaml = yaml.safe_load(aws_webhook.read_text()) + try: + context["aws_iam_endpoint"] = aws_yaml["clusters"][0]["cluster"][ + "server" + ] + except (KeyError, TypeError): + hookenv.log( + "Unable to find server in AWS IAM webhook: {}".format(aws_yaml) + ) + pass + + context["keystone_endpoint"] = None + if endpoint_from_flag("keystone-credentials.available"): + ks_webhook = Path(keystone_root) / "webhook.yaml" + if ks_webhook.exists(): + ks_yaml = yaml.safe_load(ks_webhook.read_text()) + try: + context["keystone_endpoint"] = ks_yaml["clusters"][0]["cluster"][ + "server" + ] + except (KeyError, TypeError): + hookenv.log( + "Unable to find server in Keystone webhook: {}".format(ks_yaml) + ) + pass + + context["custom_authn_endpoint"] = None + custom_authn = config.get("authn-webhook-endpoint") + if custom_authn: + context["custom_authn_endpoint"] = custom_authn + + render("cdk.master.auth-webhook-conf.yaml", auth_webhook_conf, context) + render("cdk.master.auth-webhook.py", auth_webhook_exe, context) + render( + "cdk.master.auth-webhook.logrotate", "/etc/logrotate.d/auth-webhook", context + ) + + # Set the number of gunicorn workers based on our core count. (2*cores)+1 is + # recommended: https://docs.gunicorn.org/en/stable/design.html#how-many-workers + try: + cores = int(check_output(["nproc"]).decode("utf-8").strip()) + except CalledProcessError: + # Our default architecture is 2-cores for k8s-master units + cores = 2 + else: + # Put an upper bound on cores; more than 12ish workers is overkill + cores = 6 if cores > 6 else cores + context["num_workers"] = cores * 2 + 1 + render("cdk.master.auth-webhook.service", auth_webhook_svc, context) + if any_file_changed([auth_webhook_svc]): + # if the service file has changed (or is new), + # we have to inform systemd about it + check_call(["systemctl", "daemon-reload"]) + if not is_flag_set("kubernetes-master.auth-webhook-service.started"): + if service_resume("cdk.master.auth-webhook"): + set_flag("kubernetes-master.auth-webhook-service.started") + clear_flag("kubernetes-master.apiserver.configured") + else: + hookenv.status_set( + "maintenance", "Waiting for cdk.master.auth-webhook to start." + ) + hookenv.log("cdk.master.auth-webhook failed to start; will retry") + + +@when( + "kubernetes-master.apiserver.running", + "kubernetes-master.auth-webhook-service.started", + "authentication.setup", +) +@when_not("kubernetes-master.auth-webhook-tokens.setup") +def setup_auth_webhook_tokens(): + """Reconfigure authentication to setup auth-webhook tokens. + + If authentication has been setup with a non-auth-webhook configuration, + convert it to use auth-webhook tokens instead. Alternatively, if the + auth-webhook setup failed, this will also ensure that it is retried. + """ + # Even if the apiserver is configured, it may not be fully started. Only + # proceed if we can get secrets. + if not kubectl_success("get", "secrets"): + hookenv.log("Secrets are not yet available; will retry") + return + if create_tokens_and_sign_auth_requests(): + # Force setup_leader_authentication to be re-run. + remove_state("authentication.setup") + + +@when( + "etcd.available", + "tls_client.certs.saved", + "authentication.setup", + "leadership.set.auto_storage_backend", + "leadership.set.cluster_tag", + "cni.available", +) +@when_not( + "kubernetes-master.components.started", + "kubernetes-master.cloud.pending", + "kubernetes-master.cloud.blocked", + "kubernetes-master.vault-kv.pending", + "tls_client.certs.changed", + "tls_client.ca.written", + "upgrade.series.in-progress", +) +def start_master(): + """Run the Kubernetes master components.""" + hookenv.status_set("maintenance", "Configuring the Kubernetes master services.") + + if not is_state("kubernetes-master.vault-kv.pending") and not is_state( + "kubernetes-master.secure-storage.created" + ): + encryption_config_path().parent.mkdir(parents=True, exist_ok=True) + host.write_file( + path=str(encryption_config_path()), + perms=0o600, + content=yaml.safe_dump( + { + "kind": "EncryptionConfig", + "apiVersion": "v1", + "resources": [ + {"resources": ["secrets"], "providers": [{"identity": {}}]} + ], + } + ), + ) + + kubernetes_master.freeze_service_cidr() + + etcd = endpoint_from_flag("etcd.available") + if not etcd.get_connection_string(): + # etcd is not returning a connection string. This happens when + # the master unit disconnects from etcd and is ready to terminate. + # No point in trying to start master services and fail. Just return. + return + + # TODO: Make sure below relation is handled on change + # https://github.com/kubernetes/kubernetes/issues/43461 + handle_etcd_relation(etcd) + + # Set up additional systemd services + add_systemd_restart_always() + add_systemd_file_limit() + add_systemd_file_watcher() + add_systemd_iptables_patch() + check_call(["systemctl", "daemon-reload"]) + + # Add CLI options to all components + clear_flag("kubernetes-master.apiserver.configured") + configure_controller_manager() + configure_scheduler() + + # kube-proxy + cluster_cidr = kubernetes_common.cluster_cidr() + if kubernetes_common.is_ipv6(cluster_cidr): + kubernetes_common.enable_ipv6_forwarding() + + local_address = get_ingress_address("kube-api-endpoint") + local_server = "https://{0}:{1}".format(local_address, 6443) + + configure_kube_proxy(configure_prefix, [local_server], cluster_cidr) + service_restart("snap.kube-proxy.daemon") + + set_state("kubernetes-master.components.started") + hookenv.open_port(6443) + + +@when("config.changed.proxy-extra-args") +def proxy_args_changed(): + clear_flag("kubernetes-master.components.started") + clear_flag("config.changed.proxy-extra-args") + + +@when("tls_client.certs.changed") +def certs_changed(): + clear_flag("kubernetes-master.components.started") + clear_flag("tls_client.certs.changed") + + +@when("tls_client.ca.written") +def ca_written(): + clear_flag("kubernetes-master.components.started") + if is_state("leadership.is_leader"): + if leader_get("kubernetes-master-addons-ca-in-use"): + leader_set({"kubernetes-master-addons-restart-for-ca": True}) + clear_flag("tls_client.ca.written") + + +@when("etcd.available") +def etcd_data_change(etcd): + """Etcd scale events block master reconfiguration due to the + kubernetes-master.components.started state. We need a way to + handle these events consistently only when the number of etcd + units has actually changed""" + + # key off of the connection string + connection_string = etcd.get_connection_string() + + # If the connection string changes, remove the started state to trigger + # handling of the master components + if data_changed("etcd-connect", connection_string): + remove_state("kubernetes-master.components.started") + + # If the cert info changes, remove the started state to trigger + # handling of the master components + if data_changed("etcd-certs", etcd.get_client_credentials()): + clear_flag("kubernetes-master.components.started") + + # We are the leader and the auto_storage_backend is not set meaning + # this is the first time we connect to etcd. + auto_storage_backend = leader_get("auto_storage_backend") + is_leader = is_state("leadership.is_leader") + if is_leader and not auto_storage_backend: + if etcd.get_version().startswith("3."): + leader_set(auto_storage_backend="etcd3") + else: + leader_set(auto_storage_backend="etcd2") + + +@when("kube-control.connected") +@when("cdk-addons.configured") +def send_cluster_dns_detail(kube_control): + """Send cluster DNS info""" + dns_provider = endpoint_from_flag("dns-provider.available") + try: + goal_state_rels = hookenv.goal_state().get("relations", {}) + except NotImplementedError: + goal_state_rels = {} + dns_provider_missing = not dns_provider and "dns-provider" not in goal_state_rels + dns_provider_pending = not dns_provider and "dns-provider" in goal_state_rels + try: + dns_disabled_cfg = get_dns_provider() == "none" + except InvalidDnsProvider: + dns_disabled_cfg = False + if dns_provider_missing and dns_disabled_cfg: + kube_control.set_dns(None, None, None, False) + elif dns_provider_pending: + pass + elif dns_provider: + details = dns_provider.details() + kube_control.set_dns( + details["port"], details["domain"], details["sdn-ip"], True + ) + else: + try: + dns_provider = get_dns_provider() + except InvalidDnsProvider: + hookenv.log(traceback.format_exc()) + return + dns_domain = hookenv.config("dns_domain") + dns_ip = None + try: + dns_ip = kubernetes_master.get_dns_ip() + except CalledProcessError: + hookenv.log("DNS addon service not ready yet") + return + kube_control.set_dns(53, dns_domain, dns_ip, True) + + +def create_tokens_and_sign_auth_requests(): + """Create tokens for CK users and services.""" + clear_flag("kubernetes-master.auth-webhook-tokens.setup") + # NB: This may be called before kube-apiserver is up when bootstrapping new + # clusters with auth-webhook. In this case, setup_tokens will be a no-op. + # We will re-enter this function once master services are available to + # create proper secrets. + controller_manager_token = get_token("system:kube-controller-manager") + if not controller_manager_token: + setup_tokens(None, "system:kube-controller-manager", "kube-controller-manager") + + proxy_token = get_token("system:kube-proxy") + if not proxy_token: + setup_tokens(None, "system:kube-proxy", "kube-proxy") + proxy_token = get_token("system:kube-proxy") + + scheduler_token = get_token("system:kube-scheduler") + if not scheduler_token: + setup_tokens(None, "system:kube-scheduler", "system:kube-scheduler") + + client_token = get_token("admin") + if not client_token: + setup_tokens(None, "admin", "admin", "system:masters") + client_token = get_token("admin") + + monitoring_token = get_token("system:monitoring") + if not monitoring_token: + setup_tokens(None, "system:monitoring", "system:monitoring") + + if not (proxy_token and client_token): + # When bootstrapping a new cluster, we may not have all our secrets yet. + # Do not let the kubelets start without all the needed tokens. + hookenv.log( + "Missing required tokens for kubelet startup; will retry", hookenv.WARNING + ) + return False + + kube_control = endpoint_from_flag("kube-control.connected") + requests = kube_control.auth_user() if kube_control else [] + any_failed = False + for request in requests: + username = request[1]["user"] + group = request[1]["group"] + if not username or not group: + continue + kubelet_token = get_token(username) + if not kubelet_token: + # Username will be in the form of system:node:. + # User ID will be a worker , and while not used today, we store + # this in case it becomes useful to map a secret to a unit in the future. + userid = request[0] + setup_tokens(None, username, userid, group) + kubelet_token = get_token(username) + if not kubelet_token: + hookenv.log( + "Failed to create token for {}; will retry".format(username), + hookenv.WARNING, + ) + any_failed = True + continue + kube_control.sign_auth_request( + request[0], username, kubelet_token, proxy_token, client_token + ) + if not any_failed: + set_flag("kubernetes-master.auth-webhook-tokens.setup") + return True + else: + return False + + +@when("kube-api-endpoint.available") +def push_service_data(): + """Send configuration to the load balancer, and close access to the + public interface. + """ + kube_api = endpoint_from_flag("kube-api-endpoint.available") + + endpoints = kubernetes_master.get_endpoints_from_config() + if endpoints: + addresses = [e[0] for e in endpoints] + kube_api.configure(kubernetes_master.STANDARD_API_PORT, addresses, addresses) + else: + # no manually configured LBs, so rely on the interface layer + # to use the ingress address for each relation + kube_api.configure(kubernetes_master.STANDARD_API_PORT) + + +@when("leadership.is_leader") +@when_any( + "endpoint.loadbalancer-internal.available", + "endpoint.loadbalancer-external.available", +) +def request_load_balancers(): + """Request LBs from the related provider(s).""" + for lb_type in ("internal", "external"): + lb_provider = endpoint_from_name("loadbalancer-" + lb_type) + if not lb_provider.is_available: + continue + req = lb_provider.get_request("api-server-" + lb_type) + req.protocol = req.protocols.tcp + ext_api_port = kubernetes_master.EXTERNAL_API_PORT + int_api_port = kubernetes_master.STANDARD_API_PORT + api_port = ext_api_port if lb_type == "external" else int_api_port + req.port_mapping = {api_port: int_api_port} + req.public = lb_type == "external" + if not req.health_checks: + req.add_health_check( + protocol=req.protocols.http, + port=int_api_port, + path="/livez", + ) + lb_provider.send_request(req) + + +@when("kube-control.connected") +def send_api_urls(): + kube_control = endpoint_from_name("kube-control") + if not hasattr(kube_control, "set_api_endpoints"): + # built with an old version of the kube-control interface + # the old kube-api-endpoint relation must be used instead + return + endpoints = kubernetes_master.get_internal_api_endpoints() + if not endpoints: + return + kube_control.set_api_endpoints(kubernetes_master.get_api_urls(endpoints)) + + +@when("certificates.available", "cni.available") +def send_data(): + """Send the data that is required to create a server certificate for + this server.""" + # Use the public ip of this unit as the Common Name for the certificate. + common_name = hookenv.unit_public_ip() + + # Get the SDN gateways based on the service CIDRs. + k8s_service_ips = kubernetes_master.get_kubernetes_service_ips() + + cluster_cidr = kubernetes_common.cluster_cidr() + bind_ips = kubernetes_common.get_bind_addrs( + ipv4=kubernetes_common.is_ipv4(cluster_cidr), + ipv6=kubernetes_common.is_ipv6(cluster_cidr), + ) + + # Get ingress address (this is probably already covered by bind_ips, + # but list it explicitly as well just in case it's not). + old_ingress_ip = get_ingress_address("kube-api-endpoint") + new_ingress_ip = get_ingress_address("kube-control") + + local_endpoint = kubernetes_master.get_local_api_endpoint()[0][0] + + domain = hookenv.config("dns_domain") + # Create SANs that the tls layer will add to the server cert. + sans = ( + [ + # The CN field is checked as a hostname, so if it's an IP, it + # won't match unless also included in the SANs as an IP field. + common_name, + local_endpoint, + old_ingress_ip, + new_ingress_ip, + socket.gethostname(), + socket.getfqdn(), + "kubernetes", + "kubernetes.{0}".format(domain), + "kubernetes.default", + "kubernetes.default.svc", + "kubernetes.default.svc.{0}".format(domain), + ] + + k8s_service_ips + + bind_ips + ) + + sans.extend(e[0] for e in kubernetes_master.get_internal_api_endpoints()) + sans.extend(e[0] for e in kubernetes_master.get_external_api_endpoints()) + + # maybe they have extra names they want as SANs + extra_sans = hookenv.config("extra_sans") + if extra_sans and not extra_sans == "": + sans.extend(extra_sans.split()) + + # Request a server cert with this information. + tls_client.request_server_cert( + common_name, + sorted(set(sans)), + crt_path=server_crt_path, + key_path=server_key_path, + ) + + # Request a client cert for kubelet. + tls_client.request_client_cert( + "system:kube-apiserver", crt_path=client_crt_path, key_path=client_key_path + ) + + +@when( + "config.changed.extra_sans", "certificates.available", "kube-api-endpoint.available" +) +def update_certificates(): + # NOTE: This handler may be called by another function. Two relationships + # are required, otherwise the send_data function fails. + # (until the relations are available) + missing_relations = get_unset_flags( + "certificates.available", "kube-api-endpoint.available" + ) + if missing_relations: + hookenv.log( + "Missing relations: '{}'".format(", ".join(missing_relations)), + hookenv.ERROR, + ) + return + + # Using the config.changed.extra_sans flag to catch changes. + # IP changes will take ~5 minutes or so to propagate, but + # it will update. + send_data() + clear_flag("config.changed.extra_sans") + + +@when( + "kubernetes-master.components.started", + "leadership.is_leader", + "cdk-addons.reconfigure", +) +def reconfigure_cdk_addons(): + configure_cdk_addons() + + +@when( + "kubernetes-master.components.started", + "leadership.is_leader", + "leadership.set.cluster_tag", +) +@when_not("upgrade.series.in-progress") +def configure_cdk_addons(): + """Configure CDK addons""" + remove_state("cdk-addons.reconfigure") + remove_state("cdk-addons.configured") + remove_state("kubernetes-master.aws.changed") + remove_state("kubernetes-master.azure.changed") + remove_state("kubernetes-master.gcp.changed") + remove_state("kubernetes-master.openstack.changed") + load_gpu_plugin = hookenv.config("enable-nvidia-plugin").lower() + gpuEnable = ( + get_version("kube-apiserver") >= (1, 9) + and load_gpu_plugin == "auto" + and is_state("kubernetes-master.gpu.enabled") + ) + registry = hookenv.config("image-registry") + dbEnabled = str(hookenv.config("enable-dashboard-addons")).lower() + try: + dnsProvider = get_dns_provider() + except InvalidDnsProvider: + hookenv.log(traceback.format_exc()) + return + metricsEnabled = str(hookenv.config("enable-metrics")).lower() + default_storage = "" + ceph = {} + ceph_ep = endpoint_from_flag("ceph-storage.available") + cephfs_mounter = hookenv.config("cephfs-mounter") + if ( + ceph_ep + and ceph_ep.key() + and ceph_ep.fsid() + and ceph_ep.mon_hosts() + and is_state("kubernetes-master.ceph.configured") + and get_version("kube-apiserver") >= (1, 12) + ): + cephEnabled = "true" + b64_ceph_key = base64.b64encode(ceph_ep.key().encode("utf-8")) + ceph["admin_key"] = b64_ceph_key.decode("ascii") + ceph["fsid"] = ceph_ep.fsid() + ceph["kubernetes_key"] = b64_ceph_key.decode("ascii") + ceph["mon_hosts"] = ceph_ep.mon_hosts() + default_storage = hookenv.config("default-storage") + if kubernetes_master.query_cephfs_enabled(): + cephFsEnabled = "true" + ceph["fsname"] = kubernetes_master.get_cephfs_fsname() or "" + else: + cephFsEnabled = "false" + else: + cephEnabled = "false" + cephFsEnabled = "false" + + keystone = {} + ks = endpoint_from_flag("keystone-credentials.available") + if ks: + keystoneEnabled = "true" + keystone["cert"] = "/root/cdk/server.crt" + keystone["key"] = "/root/cdk/server.key" + keystone["url"] = "{}://{}:{}/v{}".format( + ks.credentials_protocol(), + ks.credentials_host(), + ks.credentials_port(), + ks.api_version(), + ) + keystone["keystone-ca"] = hookenv.config("keystone-ssl-ca") + else: + keystoneEnabled = "false" + + enable_aws = str(is_flag_set("endpoint.aws.ready")).lower() + enable_azure = str(is_flag_set("endpoint.azure.ready")).lower() + enable_gcp = str(is_flag_set("endpoint.gcp.ready")).lower() + enable_openstack = str(is_flag_set("endpoint.openstack.ready")).lower() + openstack = endpoint_from_flag("endpoint.openstack.ready") + + if is_state("kubernetes-master.cdk-addons.unique-cluster-tag"): + cluster_tag = leader_get("cluster_tag") + else: + # allow for older upgraded charms to control when they start sending + # the unique cluster tag to cdk-addons + cluster_tag = "kubernetes" + + args = [ + "kubeconfig=" + cdk_addons_kubectl_config_path, + "arch=" + arch(), + "dns-domain=" + hookenv.config("dns_domain"), + "registry=" + registry, + "enable-dashboard=" + dbEnabled, + "enable-metrics=" + metricsEnabled, + "enable-gpu=" + str(gpuEnable).lower(), + "enable-ceph=" + cephEnabled, + "enable-cephfs=" + cephFsEnabled, + "cephfs-mounter=" + cephfs_mounter, + "ceph-admin-key=" + (ceph.get("admin_key", "")), + "ceph-fsid=" + (ceph.get("fsid", "")), + "ceph-fsname=" + (ceph.get("fsname", "")), + "ceph-kubernetes-key=" + (ceph.get("admin_key", "")), + 'ceph-mon-hosts="' + (ceph.get("mon_hosts", "")) + '"', + "default-storage=" + default_storage, + "enable-keystone=" + keystoneEnabled, + "keystone-cert-file=" + keystone.get("cert", ""), + "keystone-key-file=" + keystone.get("key", ""), + "keystone-server-url=" + keystone.get("url", ""), + "keystone-server-ca=" + keystone.get("keystone-ca", ""), + "dashboard-auth=token", + "enable-aws=" + enable_aws, + "enable-azure=" + enable_azure, + "enable-gcp=" + enable_gcp, + "enable-openstack=" + enable_openstack, + "cluster-tag=" + cluster_tag, + ] + if openstack: + args.extend( + [ + "openstack-cloud-conf=" + + base64.b64encode( + generate_openstack_cloud_config().encode("utf-8") + ).decode("utf-8"), + "openstack-endpoint-ca=" + (openstack.endpoint_tls_ca or ""), + ] + ) + if get_version("kube-apiserver") >= (1, 14): + args.append("dns-provider=" + dnsProvider) + else: + enableKubeDNS = dnsProvider == "kube-dns" + args.append("enable-kube-dns=" + str(enableKubeDNS).lower()) + check_call(["snap", "set", "cdk-addons"] + args) + if not addons_ready(): + remove_state("cdk-addons.configured") + return + + set_state("cdk-addons.configured") + leader_set({"kubernetes-master-addons-ca-in-use": True}) + if ks: + leader_set({"keystone-cdk-addons-configured": True}) + else: + leader_set({"keystone-cdk-addons-configured": None}) + + +@retry(times=3, delay_secs=20) +def addons_ready(): + """ + Test if the add ons got installed + + Returns: True is the addons got applied + + """ + try: + check_call(["cdk-addons.apply"]) + return True + except CalledProcessError: + hookenv.log("Addons are not ready yet.") + return False + + +@when("ceph-storage.available") +def ceph_state_control(): + """Determine if we should remove the state that controls the re-render + and execution of the ceph-relation-changed event because there + are changes in the relationship data, and we should re-render any + configs, keys, and/or service pre-reqs""" + + ceph_admin = endpoint_from_flag("ceph-storage.available") + ceph_relation_data = { + "mon_hosts": ceph_admin.mon_hosts(), + "fsid": ceph_admin.fsid(), + "auth_supported": ceph_admin.auth(), + "hostname": socket.gethostname(), + "key": ceph_admin.key(), + } + + # Re-execute the rendering if the data has changed. + if data_changed("ceph-config", ceph_relation_data): + remove_state("kubernetes-master.ceph.configured") + + +@when("kubernetes-master.ceph.configured") +@when_not("ceph-storage.available") +def ceph_storage_gone(): + # ceph has left, so clean up + clear_flag("kubernetes-master.apiserver.configured") + remove_state("kubernetes-master.ceph.configured") + + +@when("kubernetes-master.ceph.pools.created") +@when_not("ceph-client.connected") +def ceph_client_gone(): + # can't nuke pools, but we can't be certain that they + # are still made when a new relation comes in + remove_state("kubernetes-master.ceph.pools.created") + + +@when("etcd.available") +@when("ceph-storage.available") +@when_not("kubernetes-master.privileged") +@when_not("kubernetes-master.ceph.configured") +def ceph_storage_privilege(): + """ + Before we configure Ceph, we + need to allow the master to + run privileged containers. + + :return: None + """ + clear_flag("kubernetes-master.apiserver.configured") + + +@when("ceph-client.connected") +@when("kubernetes-master.ceph.configured") +@when_not("kubernetes-master.ceph.pool.created") +def ceph_storage_pool(): + """Once Ceph relation is ready, + we need to add storage pools. + + :return: None + """ + hookenv.log("Creating Ceph pools.") + ceph_client = endpoint_from_flag("ceph-client.connected") + + pools = ["xfs-pool", "ext4-pool"] + + for pool in pools: + hookenv.status_set("maintenance", "Creating {} pool.".format(pool)) + try: + ceph_client.create_pool(name=pool, replicas=3) + except Exception as e: + hookenv.status_set("blocked", "Error creating {} pool: {}.".format(pool, e)) + + set_state("kubernetes-master.ceph.pool.created") + + +@when("ceph-storage.available") +@when("kubernetes-master.privileged") +@when_not("kubernetes-master.ceph.configured") +def ceph_storage(): + """Ceph on kubernetes will require a few things - namely a ceph + configuration, and the ceph secret key file used for authentication. + This method will install the client package, and render the requisit files + in order to consume the ceph-storage relation.""" + hookenv.log("Configuring Ceph.") + + ceph_admin = endpoint_from_flag("ceph-storage.available") + + # >=1.12 will use CSI. + if get_version("kube-apiserver") >= (1, 12) and not ceph_admin.key(): + return # Retry until Ceph gives us a key. + + # Enlist the ceph-admin key as a kubernetes secret + if ceph_admin.key(): + encoded_key = base64.b64encode(ceph_admin.key().encode("utf-8")) + else: + # We didn't have a key, and cannot proceed. Do not set state and + # allow this method to re-execute + return + + # CSI isn't available, so we need to do it ourselves, + if get_version("kube-apiserver") < (1, 12): + try: + # At first glance this is deceptive. The apply stanza will + # create if it doesn't exist, otherwise it will update the + # entry, ensuring our ceph-secret is always reflective of + # what we have in /etc/ceph assuming we have invoked this + # anytime that file would change. + context = {"secret": encoded_key.decode("ascii")} + render("ceph-secret.yaml", "/tmp/ceph-secret.yaml", context) + cmd = ["kubectl", "apply", "-f", "/tmp/ceph-secret.yaml"] + check_call(cmd) + os.remove("/tmp/ceph-secret.yaml") + set_state("kubernetes-master.ceph.pool.created") + except: # NOQA + # The enlistment in kubernetes failed, return and + # prepare for re-exec. + return + + # When complete, set a state relating to configuration of the storage + # backend that will allow other modules to hook into this and verify we + # have performed the necessary pre-req steps to interface with a ceph + # deployment. + set_state("kubernetes-master.ceph.configured") + + +@when("nrpe-external-master.available") +@when_not("nrpe-external-master.initial-config") +def initial_nrpe_config(): + set_state("nrpe-external-master.initial-config") + update_nrpe_config() + + +@when("config.changed.authorization-mode") +def switch_auth_mode(forced=False): + config = hookenv.config() + mode = config.get("authorization-mode") + + if data_changed("auth-mode", mode) or forced: + # manage flags to handle rbac related resources + if mode and "rbac" in mode.lower(): + remove_state("kubernetes-master.remove.rbac") + set_state("kubernetes-master.create.rbac") + else: + remove_state("kubernetes-master.create.rbac") + set_state("kubernetes-master.remove.rbac") + + # set ourselves up to restart since auth mode has changed + remove_state("kubernetes-master.components.started") + + +@when("leadership.is_leader", "kubernetes-master.components.started") +@when_not("kubernetes-master.pod-security-policy.applied") +def create_pod_security_policy_resources(): + pod_security_policy_path = "/root/cdk/pod-security-policy.yaml" + pod_security_policy = hookenv.config("pod-security-policy") + if pod_security_policy: + hookenv.log("Using configuration defined on pod-security-policy option") + write_file_with_autogenerated_header( + pod_security_policy_path, pod_security_policy + ) + else: + hookenv.log("Using the default rbac-pod-security-policy template") + render("rbac-pod-security-policy.yaml", pod_security_policy_path, {}) + + hookenv.log("Creating pod security policy resources.") + if kubectl_manifest("apply", pod_security_policy_path): + set_state("kubernetes-master.pod-security-policy.applied") + else: + msg = "Failed to apply {}, will retry.".format(pod_security_policy_path) + hookenv.log(msg) + + +@when( + "leadership.is_leader", + "kubernetes-master.components.started", + "kubernetes-master.create.rbac", +) +def create_rbac_resources(): + rbac_proxy_path = "/root/cdk/rbac-proxy.yaml" + + # NB: when metrics and logs are retrieved by proxy, the 'user' is the + # common name of the cert used to authenticate the proxied request. + # The CN for /root/cdk/client.crt is 'system:kube-apiserver' + # (see the send_data handler, above). + proxy_users = ["client", "system:kube-apiserver"] + + context = {"juju_application": hookenv.service_name(), "proxy_users": proxy_users} + render("rbac-proxy.yaml", rbac_proxy_path, context) + + hookenv.log("Creating proxy-related RBAC resources.") + if kubectl_manifest("apply", rbac_proxy_path): + remove_state("kubernetes-master.create.rbac") + else: + msg = "Failed to apply {}, will retry.".format(rbac_proxy_path) + hookenv.log(msg) + + +@when("leadership.is_leader", "kubernetes-master.components.started") +@when_not("kubernetes-master.system-monitoring-rbac-role.applied") +def apply_system_monitoring_rbac_role(): + try: + hookenv.status_set("maintenance", "Applying system:monitoring RBAC role") + path = "/root/cdk/system-monitoring-rbac-role.yaml" + render("system-monitoring-rbac-role.yaml", path, {}) + kubectl("apply", "-f", path) + set_state("kubernetes-master.system-monitoring-rbac-role.applied") + except Exception: + hookenv.log(traceback.format_exc()) + hookenv.log("Waiting to retry applying system:monitoring RBAC role") + return + + +@when( + "leadership.is_leader", + "kubernetes-master.components.started", + "kubernetes-master.remove.rbac", +) +def remove_rbac_resources(): + rbac_proxy_path = "/root/cdk/rbac-proxy.yaml" + if os.path.isfile(rbac_proxy_path): + hookenv.log("Removing proxy-related RBAC resources.") + if kubectl_manifest("delete", rbac_proxy_path): + os.remove(rbac_proxy_path) + remove_state("kubernetes-master.remove.rbac") + else: + msg = "Failed to delete {}, will retry.".format(rbac_proxy_path) + hookenv.log(msg) + else: + # if we dont have the yaml, there's nothing for us to do + remove_state("kubernetes-master.remove.rbac") + + +@when("kubernetes-master.components.started") +@when("nrpe-external-master.available") +@when_any("config.changed.nagios_context", "config.changed.nagios_servicegroups") +def update_nrpe_config(): + services = ["snap.{}.daemon".format(s) for s in master_services] + + plugin = install_nagios_plugin_from_file( + "templates/nagios_plugin.py", "check_k8s_master.py" + ) + hostname = nrpe.get_nagios_hostname() + current_unit = nrpe.get_nagios_unit_name() + nrpe_setup = nrpe.NRPE(hostname=hostname) + nrpe.add_init_service_checks(nrpe_setup, services, current_unit) + nrpe_setup.add_check( + "k8s-api-server", + "Verify that the Kubernetes API server is accessible", + str(plugin), + ) + nrpe_setup.write() + + +@when_not("nrpe-external-master.available") +@when("nrpe-external-master.initial-config") +def remove_nrpe_config(): + # List of systemd services for which the checks will be removed + services = ["snap.{}.daemon".format(s) for s in master_services] + + remove_nagios_plugin("check_k8s_master.py") + + # The current nrpe-external-master interface doesn't handle a lot of logic, + # use the charm-helpers code for now. + hostname = nrpe.get_nagios_hostname() + nrpe_setup = nrpe.NRPE(hostname=hostname) + + for service in services: + nrpe_setup.remove_check(shortname=service) + nrpe_setup.remove_check(shortname="k8s-api-server") + remove_state("nrpe-external-master.initial-config") + + +def is_privileged(): + """Return boolean indicating whether or not to set allow-privileged=true.""" + privileged = hookenv.config("allow-privileged").lower() + if privileged == "auto": + return ( + is_state("kubernetes-master.gpu.enabled") + or is_state("ceph-storage.available") + or is_state("endpoint.openstack.joined") + ) + else: + return privileged == "true" + + +@when("config.changed.allow-privileged") +@when("kubernetes-master.components.started") +def on_config_allow_privileged_change(): + """React to changed 'allow-privileged' config value.""" + remove_state("kubernetes-master.components.started") + remove_state("config.changed.allow-privileged") + + +@when_any( + "config.changed.api-extra-args", + "config.changed.audit-policy", + "config.changed.audit-webhook-config", + "config.changed.enable-keystone-authorization", + "config.changed.service-cidr", +) +@when("kubernetes-master.components.started") +@when("leadership.set.auto_storage_backend") +@when("etcd.available") +def reconfigure_apiserver(): + clear_flag("kubernetes-master.apiserver.configured") + + +@when("config.changed.controller-manager-extra-args") +@when("kubernetes-master.components.started") +def on_config_controller_manager_extra_args_change(): + configure_controller_manager() + + +@when("config.changed.scheduler-extra-args") +@when("kubernetes-master.components.started") +def on_config_scheduler_extra_args_change(): + configure_scheduler() + + +@when("kube-control.gpu.available") +@when("kubernetes-master.components.started") +@when_not("kubernetes-master.gpu.enabled") +def on_gpu_available(kube_control): + """The remote side (kubernetes-worker) is gpu-enabled. + + We need to run in privileged mode. + + """ + kube_version = get_version("kube-apiserver") + config = hookenv.config() + if config["allow-privileged"].lower() == "false" and kube_version < (1, 9): + return + + remove_state("kubernetes-master.components.started") + set_state("kubernetes-master.gpu.enabled") + + +@when("kubernetes-master.gpu.enabled") +@when("kubernetes-master.components.started") +@when_not("kubernetes-master.privileged") +def gpu_with_no_privileged(): + """We were in gpu mode, but the operator has set allow-privileged="false", + so we can't run in gpu mode anymore. + + """ + if get_version("kube-apiserver") < (1, 9): + remove_state("kubernetes-master.gpu.enabled") + + +@when("kube-control.connected") +@when_not("kube-control.gpu.available") +@when("kubernetes-master.gpu.enabled") +@when("kubernetes-master.components.started") +def gpu_departed(kube_control): + """We were in gpu mode, but the workers informed us there is + no gpu support anymore. + + """ + remove_state("kubernetes-master.gpu.enabled") + + +@hook("stop") +def shutdown(): + """Stop the kubernetes master services""" + for service in master_services: + service_stop("snap.%s.daemon" % service) + + +@when( + "certificates.ca.available", + "certificates.client.cert.available", + "authentication.setup", +) +def build_kubeconfig(): + """Gather the relevant data for Kubernetes configuration objects and create + a config object with that information.""" + local_endpoint = kubernetes_master.get_local_api_endpoint() + internal_endpoints = kubernetes_master.get_internal_api_endpoints() + external_endpoints = kubernetes_master.get_external_api_endpoints() + + # Do we have everything we need? + if ca_crt_path.exists() and internal_endpoints and external_endpoints: + local_url = kubernetes_master.get_api_url(local_endpoint) + internal_url = kubernetes_master.get_api_url(internal_endpoints) + external_url = kubernetes_master.get_api_url(external_endpoints) + client_pass = get_token("admin") + if not client_pass: + # If we made it this far without a password, we're bootstrapping a new + # cluster. Create a new token so we can build an admin kubeconfig. The + # auth-webhook service will ack this value from the kubeconfig file, + # allowing us to continue until the master is started and a proper + # secret can be created. + client_pass = ( + hookenv.config("client_password") or kubernetes_master.token_generator() + ) + client_pass = "admin::{}".format(client_pass) + + # drop keystone helper script? + ks = endpoint_from_flag("keystone-credentials.available") + if ks: + script_filename = "kube-keystone.sh" + keystone_path = os.path.join(os.sep, "home", "ubuntu", script_filename) + context = { + "protocol": ks.credentials_protocol(), + "address": ks.credentials_host(), + "port": ks.credentials_port(), + "version": ks.api_version(), + } + render(script_filename, keystone_path, context) + elif is_state("leadership.set.keystone-cdk-addons-configured"): + # if addons are configured, we're going to do keystone + # just not yet because we don't have creds + hookenv.log("Keystone endpoint not found, will retry.") + + cluster_id = None + aws_iam = endpoint_from_flag("endpoint.aws-iam.available") + if aws_iam: + cluster_id = aws_iam.get_cluster_id() + + # Create an absolute path for the kubeconfig file. + kubeconfig_path = os.path.join(os.sep, "home", "ubuntu", "config") + + # Create the kubeconfig on this system so users can access the cluster. + hookenv.log("Writing kubeconfig file.") + + if ks: + create_kubeconfig( + kubeconfig_path, + external_url, + ca_crt_path, + user="admin", + token=client_pass, + keystone=True, + aws_iam_cluster_id=cluster_id, + ) + else: + create_kubeconfig( + kubeconfig_path, + external_url, + ca_crt_path, + user="admin", + token=client_pass, + aws_iam_cluster_id=cluster_id, + ) + + # Make the config file readable by the ubuntu users so juju scp works. + cmd = ["chown", "ubuntu:ubuntu", kubeconfig_path] + check_call(cmd) + + # make a kubeconfig for root / the charm + create_kubeconfig( + kubeclientconfig_path, + local_url, + ca_crt_path, + user="admin", + token=client_pass, + ) + + # Create kubernetes configuration in the default location for ubuntu. + create_kubeconfig( + "/home/ubuntu/.kube/config", + internal_url, + ca_crt_path, + user="admin", + token=client_pass, + ) + # Make the config dir readable by the ubuntu user + check_call(["chown", "-R", "ubuntu:ubuntu", "/home/ubuntu/.kube"]) + + # make a kubeconfig for cdk-addons + create_kubeconfig( + cdk_addons_kubectl_config_path, + local_url, + ca_crt_path, + user="admin", + token=client_pass, + ) + + # make a kubeconfig for our services + proxy_token = get_token("system:kube-proxy") + if proxy_token: + create_kubeconfig( + kubeproxyconfig_path, + local_url, + ca_crt_path, + token=proxy_token, + user="kube-proxy", + ) + controller_manager_token = get_token("system:kube-controller-manager") + if controller_manager_token: + create_kubeconfig( + kubecontrollermanagerconfig_path, + local_url, + ca_crt_path, + token=controller_manager_token, + user="kube-controller-manager", + ) + scheduler_token = get_token("system:kube-scheduler") + if scheduler_token: + create_kubeconfig( + kubeschedulerconfig_path, + local_url, + ca_crt_path, + token=scheduler_token, + user="kube-scheduler", + ) + + cni = endpoint_from_name("cni") + if cni: + cni.notify_kubeconfig_changed() + + +def handle_etcd_relation(reldata): + """Save the client credentials and set appropriate daemon flags when + etcd declares itself as available""" + # Define where the etcd tls files will be kept. + etcd_dir = "/root/cdk/etcd" + + # Create paths to the etcd client ca, key, and cert file locations. + ca = os.path.join(etcd_dir, "client-ca.pem") + key = os.path.join(etcd_dir, "client-key.pem") + cert = os.path.join(etcd_dir, "client-cert.pem") + + # Save the client credentials (in relation data) to the paths provided. + reldata.save_client_credentials(key, cert, ca) + + +def remove_if_exists(path): + try: + os.remove(path) + except FileNotFoundError: + pass + + +def write_file_with_autogenerated_header(path, contents): + with open(path, "w") as f: + header = "# Autogenerated by kubernetes-master charm" + f.write(header + "\n" + contents) + + +@when( + "etcd.available", "cni.available", "kubernetes-master.auth-webhook-service.started" +) +@when_not("kubernetes-master.apiserver.configured") +def configure_apiserver(): + etcd_connection_string = endpoint_from_flag( + "etcd.available" + ).get_connection_string() + if not etcd_connection_string: + # etcd is not returning a connection string. This happens when + # the master unit disconnects from etcd and is ready to terminate. + # No point in trying to start master services and fail. Just return. + return + + # Update unit db service-cidr + was_service_cidr_expanded = kubernetes_master.is_service_cidr_expansion() + kubernetes_master.freeze_service_cidr() + + cluster_cidr = kubernetes_common.cluster_cidr() + service_cidr = kubernetes_master.service_cidr() + + api_opts = {} + + if is_privileged(): + api_opts["allow-privileged"] = "true" + set_state("kubernetes-master.privileged") + else: + api_opts["allow-privileged"] = "false" + remove_state("kubernetes-master.privileged") + + # Handle static options for now + api_opts["service-cluster-ip-range"] = service_cidr + # Enable StreamingProxyRedirects to work around `kubectl exec` failures + # when passing through kubeapi-load-balancer. This feature will be removed + # in k8s 1.24. + # https://bugs.launchpad.net/bugs/1940527 + feature_gates = ["StreamingProxyRedirects=true"] + if kubernetes_common.is_dual_stack(cluster_cidr): + feature_gates.append("IPv6DualStack=true") + api_opts["feature-gates"] = ",".join(feature_gates) + api_opts["min-request-timeout"] = "300" + api_opts["v"] = "4" + api_opts["tls-cert-file"] = str(server_crt_path) + api_opts["tls-private-key-file"] = str(server_key_path) + api_opts["kubelet-certificate-authority"] = str(ca_crt_path) + api_opts["kubelet-client-certificate"] = str(client_crt_path) + api_opts["kubelet-client-key"] = str(client_key_path) + api_opts["logtostderr"] = "true" + api_opts["storage-backend"] = getStorageBackend() + api_opts["insecure-port"] = "0" + api_opts["profiling"] = "false" + + api_opts["anonymous-auth"] = "false" + api_opts["authentication-token-webhook-cache-ttl"] = "1m0s" + api_opts["authentication-token-webhook-config-file"] = auth_webhook_conf + api_opts["service-account-issuer"] = "https://kubernetes.default.svc" + api_opts["service-account-signing-key-file"] = "/root/cdk/serviceaccount.key" + api_opts["service-account-key-file"] = "/root/cdk/serviceaccount.key" + api_opts[ + "kubelet-preferred-address-types" + ] = "InternalIP,Hostname,InternalDNS,ExternalDNS,ExternalIP" + api_opts["encryption-provider-config"] = str(encryption_config_path()) + if kubernetes_common.is_ipv6(cluster_cidr): + api_opts["bind-address"] = "::" + if kubernetes_common.is_ipv6_preferred(cluster_cidr): + api_opts["advertise-address"] = get_ingress_address6("kube-control") + else: + api_opts["advertise-address"] = get_ingress_address("kube-control") + + etcd_dir = "/root/cdk/etcd" + etcd_ca = os.path.join(etcd_dir, "client-ca.pem") + etcd_key = os.path.join(etcd_dir, "client-key.pem") + etcd_cert = os.path.join(etcd_dir, "client-cert.pem") + + api_opts["etcd-cafile"] = etcd_ca + api_opts["etcd-keyfile"] = etcd_key + api_opts["etcd-certfile"] = etcd_cert + api_opts["etcd-servers"] = etcd_connection_string + + # In Kubernetes 1.10 and later, some admission plugins are enabled by + # default. The current list of default plugins can be found at + # https://bit.ly/2meP9XT, listed under the '--enable-admission-plugins' + # option. + # + # The list below need only include the plugins we want to enable + # in addition to the defaults. + + admission_plugins = [ + "PersistentVolumeLabel", + "PodSecurityPolicy", + "NodeRestriction", + ] + + auth_mode = hookenv.config("authorization-mode") + + ks = endpoint_from_flag("keystone-credentials.available") + if ks: + ks_ip = None + ks_ip = get_service_ip("k8s-keystone-auth-service", errors_fatal=False) + if ks and ks_ip: + os.makedirs(keystone_root, exist_ok=True) + + keystone_webhook = keystone_root + "/webhook.yaml" + context = {} + context["keystone_service_cluster_ip"] = ks_ip + render("keystone-api-server-webhook.yaml", keystone_webhook, context) + + if hookenv.config("enable-keystone-authorization"): + # if user wants authorization, enable it + if "Webhook" not in auth_mode: + auth_mode += ",Webhook" + api_opts["authorization-webhook-config-file"] = keystone_webhook # noqa + set_state("keystone.apiserver.configured") + else: + if ks and not ks_ip: + hookenv.log( + "Unable to find k8s-keystone-auth-service " "service. Will retry" + ) + # Note that we can get into a nasty state here + # if the user has specified webhook and they're relying on + # keystone auth to handle that, the api server will fail to + # start because we push it Webhook and no webhook config. + # We can't generate the config because we can't talk to the + # apiserver to get the ip of the service to put into the + # webhook template. A chicken and egg problem. To fix this, + # remove Webhook if keystone is related and trying to come + # up until we can find the service IP. + if "Webhook" in auth_mode: + auth_mode = ",".join( + [i for i in auth_mode.split(",") if i != "Webhook"] + ) + elif is_state("leadership.set.keystone-cdk-addons-configured"): + hookenv.log("Unable to find keystone endpoint. Will retry") + remove_state("keystone.apiserver.configured") + + api_opts["authorization-mode"] = auth_mode + api_opts["enable-admission-plugins"] = ",".join(admission_plugins) + + kube_version = get_version("kube-apiserver") + + if kube_version > (1, 6) and hookenv.config("enable-metrics"): + api_opts["requestheader-client-ca-file"] = str(ca_crt_path) + api_opts["requestheader-allowed-names"] = "system:kube-apiserver,client" + api_opts["requestheader-extra-headers-prefix"] = "X-Remote-Extra-" + api_opts["requestheader-group-headers"] = "X-Remote-Group" + api_opts["requestheader-username-headers"] = "X-Remote-User" + api_opts["proxy-client-cert-file"] = str(client_crt_path) + api_opts["proxy-client-key-file"] = str(client_key_path) + api_opts["enable-aggregator-routing"] = "true" + api_opts["client-ca-file"] = str(ca_crt_path) + + api_cloud_config_path = cloud_config_path("kube-apiserver") + if is_state("endpoint.aws.ready"): + api_opts["cloud-provider"] = "aws" + elif is_state("endpoint.gcp.ready"): + api_opts["cloud-provider"] = "gce" + api_opts["cloud-config"] = str(api_cloud_config_path) + elif is_state("endpoint.vsphere.ready") and get_version("kube-apiserver") >= ( + 1, + 12, + ): + api_opts["cloud-provider"] = "vsphere" + api_opts["cloud-config"] = str(api_cloud_config_path) + elif is_state("endpoint.azure.ready"): + api_opts["cloud-provider"] = "azure" + api_opts["cloud-config"] = str(api_cloud_config_path) + + audit_root = "/root/cdk/audit" + os.makedirs(audit_root, exist_ok=True) + + audit_log_path = audit_root + "/audit.log" + api_opts["audit-log-path"] = audit_log_path + api_opts["audit-log-maxage"] = "30" + api_opts["audit-log-maxsize"] = "100" + api_opts["audit-log-maxbackup"] = "10" + + audit_policy_path = audit_root + "/audit-policy.yaml" + audit_policy = hookenv.config("audit-policy") + if audit_policy: + write_file_with_autogenerated_header(audit_policy_path, audit_policy) + api_opts["audit-policy-file"] = audit_policy_path + else: + remove_if_exists(audit_policy_path) + + audit_webhook_config_path = audit_root + "/audit-webhook-config.yaml" + audit_webhook_config = hookenv.config("audit-webhook-config") + if audit_webhook_config: + write_file_with_autogenerated_header( + audit_webhook_config_path, audit_webhook_config + ) + api_opts["audit-webhook-config-file"] = audit_webhook_config_path + else: + remove_if_exists(audit_webhook_config_path) + + configure_kubernetes_service( + configure_prefix, "kube-apiserver", api_opts, "api-extra-args" + ) + service_restart("snap.kube-apiserver.daemon") + + if was_service_cidr_expanded and is_state("leadership.is_leader"): + set_flag("kubernetes-master.had-service-cidr-expanded") + + set_flag("kubernetes-master.apiserver.configured") + if kubernetes_master.check_service("kube-apiserver"): + set_flag("kubernetes-master.apiserver.running") + + +@when("kubernetes-master.apiserver.configured") +@when_not("kubernetes-master.apiserver.running") +def check_apiserver(): + if kubernetes_master.check_service("kube-apiserver"): + set_flag("kubernetes-master.apiserver.running") + + +@when( + "kubernetes-master.had-service-cidr-expanded", + "kubernetes-master.apiserver.configured", + "leadership.is_leader", +) +def update_for_service_cidr_expansion(): + # We just restarted the API server, so there's a decent chance it's + # not up yet. Keep trying to get the svcs list until we can; get_svcs + # has a built-in retry and delay, so this should try for around 30s. + def _wait_for_svc_ip(): + for attempt in range(10): + svcs = get_svcs() + if svcs: + svc_ip = { + svc["metadata"]["name"]: svc["spec"]["clusterIP"] + for svc in svcs["items"] + }.get("kubernetes") + if svc_ip: + return svc_ip + else: + return None + + hookenv.log("service-cidr expansion: Waiting for API service") + # First network is the default, which is used for the API service's address. + # This logic will likely need to change once dual-stack services are + # supported: https://bit.ly/2YlbxOx + expected_service_ip = kubernetes_master.get_kubernetes_service_ips()[0] + actual_service_ip = _wait_for_svc_ip() + if not actual_service_ip: + hookenv.log("service-cidr expansion: Timed out waiting for API service") + return + try: + if actual_service_ip != expected_service_ip: + hookenv.log("service-cidr expansion: Deleting service kubernetes") + kubectl("delete", "service", "kubernetes") + actual_service_ip = _wait_for_svc_ip() + if not actual_service_ip: + # we might need another restart to get the service recreated + hookenv.log( + "service-cidr expansion: Timed out waiting for " + "the service to return; restarting API server" + ) + clear_flag("kubernetes-master.apiserver.configured") + return + if actual_service_ip != expected_service_ip: + raise ValueError( + "Unexpected service IP: {} != {}".format( + actual_service_ip, expected_service_ip + ) + ) + + # Restart the cdk-addons + # Get deployments/daemonsets/statefulsets + hookenv.log("service-cidr expansion: Restart the cdk-addons") + output = kubectl( + "get", + "daemonset,deployment,statefulset", + "-o", + "json", + "--all-namespaces", + "-l", + "cdk-restart-on-ca-change=true", + ).decode("UTF-8") + deployments = json.loads(output)["items"] + + # Now restart the addons + for deployment in deployments: + kind = deployment["kind"] + namespace = deployment["metadata"]["namespace"] + name = deployment["metadata"]["name"] + hookenv.log("Restarting addon: {0} {1} {2}".format(kind, namespace, name)) + kubectl("rollout", "restart", kind + "/" + name, "-n", namespace) + except CalledProcessError: + # the kubectl calls already log the command and don't capture stderr, + # so logging the exception is a bit superfluous + hookenv.log("service-cidr expansion: failed to restart components") + else: + clear_flag("kubernetes-master.had-service-cidr-expanded") + + +def configure_controller_manager(): + controller_opts = {} + cluster_cidr = kubernetes_common.cluster_cidr() + service_cidr = kubernetes_master.service_cidr() + + # Default to 3 minute resync. TODO: Make this configurable? + controller_opts["min-resync-period"] = "3m" + controller_opts["v"] = "2" + controller_opts["root-ca-file"] = str(ca_crt_path) + controller_opts["logtostderr"] = "true" + controller_opts["kubeconfig"] = kubecontrollermanagerconfig_path + controller_opts["authorization-kubeconfig"] = kubecontrollermanagerconfig_path + controller_opts["authentication-kubeconfig"] = kubecontrollermanagerconfig_path + controller_opts["use-service-account-credentials"] = "true" + controller_opts["service-account-private-key-file"] = "/root/cdk/serviceaccount.key" + controller_opts["tls-cert-file"] = str(server_crt_path) + controller_opts["tls-private-key-file"] = str(server_key_path) + controller_opts["cluster-name"] = leader_get("cluster_tag") + controller_opts["terminated-pod-gc-threshold"] = "12500" + controller_opts["profiling"] = "false" + controller_opts["feature-gates"] = "RotateKubeletServerCertificate=true" + controller_opts["service-cluster-ip-range"] = service_cidr + controller_opts["cluster-cidr"] = cluster_cidr + if kubernetes_common.is_dual_stack(cluster_cidr): + controller_opts["feature-gates"] = "IPv6DualStack=true" + net_ipv6 = kubernetes_common.get_ipv6_network(cluster_cidr) + if net_ipv6: + controller_opts["node-cidr-mask-size-ipv6"] = net_ipv6.prefixlen + + cm_cloud_config_path = cloud_config_path("kube-controller-manager") + if is_state("endpoint.aws.ready"): + controller_opts["cloud-provider"] = "aws" + elif is_state("endpoint.gcp.ready"): + controller_opts["cloud-provider"] = "gce" + controller_opts["cloud-config"] = str(cm_cloud_config_path) + elif is_state("endpoint.vsphere.ready") and get_version("kube-apiserver") >= ( + 1, + 12, + ): + controller_opts["cloud-provider"] = "vsphere" + controller_opts["cloud-config"] = str(cm_cloud_config_path) + elif is_state("endpoint.azure.ready"): + controller_opts["cloud-provider"] = "azure" + controller_opts["cloud-config"] = str(cm_cloud_config_path) + + configure_kubernetes_service( + configure_prefix, + "kube-controller-manager", + controller_opts, + "controller-manager-extra-args", + ) + service_restart("snap.kube-controller-manager.daemon") + + +def configure_scheduler(): + kube_scheduler_config_path = "/root/cdk/kube-scheduler-config.yaml" + + scheduler_opts = {} + + scheduler_opts["v"] = "2" + scheduler_opts["logtostderr"] = "true" + scheduler_opts["profiling"] = "false" + scheduler_opts["config"] = kube_scheduler_config_path + + scheduler_ver = get_version("kube-scheduler") + if scheduler_ver >= (1, 19): + api_ver = "v1beta1" + elif scheduler_ver >= (1, 18): + api_ver = "v1alpha2" + else: + api_ver = "v1alpha1" + + host.write_file( + path=kube_scheduler_config_path, + perms=0o600, + content=yaml.safe_dump( + { + "apiVersion": "kubescheduler.config.k8s.io/{}".format(api_ver), + "kind": "KubeSchedulerConfiguration", + "clientConnection": {"kubeconfig": kubeschedulerconfig_path}, + } + ), + ) + + configure_kubernetes_service( + configure_prefix, "kube-scheduler", scheduler_opts, "scheduler-extra-args" + ) + + service_restart("snap.kube-scheduler.daemon") + + +def setup_tokens(token, username, user, groups=None): + """Create a token for kubernetes authentication. + + Create a new secret if known_tokens have been migrated. Otherwise, + add an entry to the 'known_tokens.csv' file. + """ + if not token: + token = kubernetes_master.token_generator() + if is_flag_set("kubernetes-master.token-auth.migrated"): + # We need the apiserver before we can create secrets. + if is_flag_set("kubernetes-master.apiserver.configured"): + kubernetes_master.create_secret(token, username, user, groups) + else: + hookenv.log("Delaying secret creation until the apiserver is configured.") + else: + kubernetes_master.create_known_token(token, username, user, groups) + + +def get_token(username): + """Fetch a token for the given username. + + Grab a token from the given user's secret if known_tokens have been + migrated. Otherwise, fetch it from the 'known_tokens.csv' file. + """ + if is_flag_set("kubernetes-master.token-auth.migrated"): + return kubernetes_common.get_secret_password(username) + else: + return kubernetes_master.get_csv_password("known_tokens.csv", username) + + +def set_token(password, save_salt): + """Store a token so it can be recalled later by token_generator. + + param: password - the password to be stored + param: save_salt - the key to store the value of the token.""" + db.set(save_salt, password) + return db.get(save_salt) + + +@retry(times=3, delay_secs=1) +def get_pods(namespace="default"): + try: + output = kubectl( + "get", "po", "-n", namespace, "-o", "json", "--request-timeout", "10s" + ).decode("UTF-8") + result = json.loads(output) + except CalledProcessError: + hookenv.log("failed to get {} pod status".format(namespace)) + return None + return result + + +@retry(times=3, delay_secs=1) +def get_svcs(namespace="default"): + try: + output = kubectl( + "get", "svc", "-n", namespace, "-o", "json", "--request-timeout", "10s" + ).decode("UTF-8") + result = json.loads(output) + except CalledProcessError: + hookenv.log("failed to get {} service status".format(namespace)) + return None + return result + + +class FailedToGetPodStatus(Exception): + pass + + +def get_kube_system_pods_not_running(): + """Check pod status in the kube-system namespace. Throws + FailedToGetPodStatus if unable to determine pod status. This can + occur when the api server is not currently running. On success, + returns a list of pods that are not currently running + or an empty list if all are running.""" + + result = get_pods("kube-system") + if result is None: + raise FailedToGetPodStatus + + hookenv.log( + "Checking system pods status: {}".format( + ", ".join( + "=".join([pod["metadata"]["name"], pod["status"]["phase"]]) + for pod in result["items"] + ) + ) + ) + + # Pods that are Running or Evicted (which should re-spawn) are + # considered running + not_running = [ + pod + for pod in result["items"] + if pod["status"]["phase"] != "Running" + and pod["status"].get("reason", "") != "Evicted" + ] + + pending = [pod for pod in result["items"] if pod["status"]["phase"] == "Pending"] + any_pending = len(pending) > 0 + if is_state("endpoint.gcp.ready") and any_pending: + poke_network_unavailable() + return not_running + + return not_running + + +def poke_network_unavailable(): + """ + Work around https://github.com/kubernetes/kubernetes/issues/44254 by + manually poking the status into the API server to tell the nodes they have + a network route. + + This is needed because kubelet sets the NetworkUnavailable flag and expects + the network plugin to clear it, which only kubenet does. There is some + discussion about refactoring the affected code but nothing has happened + in a while. + """ + internal_endpoints = kubernetes_master.get_internal_api_endpoints() + internal_url = kubernetes_master.get_api_url(internal_endpoints) + + client_token = get_token("admin") + http_header = ("Authorization", "Bearer {}".format(client_token)) + + try: + output = kubectl("get", "nodes", "-o", "json").decode("utf-8") + nodes = json.loads(output)["items"] + except CalledProcessError: + hookenv.log("failed to get kube-system nodes") + return + except (KeyError, json.JSONDecodeError) as e: + hookenv.log( + "failed to parse kube-system node status " "({}): {}".format(e, output), + hookenv.ERROR, + ) + return + + for node in nodes: + node_name = node["metadata"]["name"] + url = "{}/api/v1/nodes/{}/status".format(internal_url, node_name) + req = Request(url) + req.add_header(*http_header) + with urlopen(req) as response: + code = response.getcode() + body = response.read().decode("utf8") + if code != 200: + hookenv.log( + "failed to get node status from {} [{}]: {}".format(url, code, body), + hookenv.ERROR, + ) + return + try: + node_info = json.loads(body) + conditions = node_info["status"]["conditions"] + i = [c["type"] for c in conditions].index("NetworkUnavailable") + if conditions[i]["status"] == "True": + hookenv.log("Clearing NetworkUnavailable from {}".format(node_name)) + conditions[i] = { + "type": "NetworkUnavailable", + "status": "False", + "reason": "RouteCreated", + "message": "Manually set through k8s api", + } + req = Request( + url, + method="PUT", + data=json.dumps(node_info).encode("utf8"), + headers={"Content-Type": "application/json"}, + ) + req.add_header(*http_header) + with urlopen(req) as response: + code = response.getcode() + body = response.read().decode("utf8") + if code not in (200, 201, 202): + hookenv.log( + "failed to update node status [{}]: {}".format(code, body), + hookenv.ERROR, + ) + return + except (json.JSONDecodeError, KeyError): + hookenv.log("failed to parse node status: {}".format(body), hookenv.ERROR) + return + + +def apiserverVersion(): + cmd = "kube-apiserver --version".split() + version_string = check_output(cmd).decode("utf-8") + return tuple(int(q) for q in re.findall("[0-9]+", version_string)[:3]) + + +def touch(fname): + try: + os.utime(fname, None) + except OSError: + open(fname, "a").close() + + +def getStorageBackend(): + storage_backend = hookenv.config("storage-backend") + if storage_backend == "auto": + storage_backend = leader_get("auto_storage_backend") + return storage_backend + + +@when("leadership.is_leader") +@when_not("leadership.set.cluster_tag") +def create_cluster_tag(): + cluster_tag = "kubernetes-{}".format(kubernetes_master.token_generator().lower()) + leader_set(cluster_tag=cluster_tag) + + +@when("leadership.set.cluster_tag", "kube-control.connected") +def send_cluster_tag(): + cluster_tag = leader_get("cluster_tag") + kube_control = endpoint_from_flag("kube-control.connected") + kube_control.set_cluster_tag(cluster_tag) + + +@when_not("kube-control.connected") +def clear_cluster_tag_sent(): + remove_state("kubernetes-master.cluster-tag-sent") + + +@when_any( + "endpoint.aws.joined", + "endpoint.gcp.joined", + "endpoint.openstack.joined", + "endpoint.vsphere.joined", + "endpoint.azure.joined", +) +@when_not("kubernetes-master.cloud.ready") +def set_cloud_pending(): + k8s_version = get_version("kube-apiserver") + k8s_1_11 = k8s_version >= (1, 11) + k8s_1_12 = k8s_version >= (1, 12) + vsphere_joined = is_state("endpoint.vsphere.joined") + azure_joined = is_state("endpoint.azure.joined") + if (vsphere_joined and not k8s_1_12) or (azure_joined and not k8s_1_11): + set_state("kubernetes-master.cloud.blocked") + else: + remove_state("kubernetes-master.cloud.blocked") + set_state("kubernetes-master.cloud.pending") + + +@when_any("endpoint.aws.joined", "endpoint.gcp.joined", "endpoint.azure.joined") +@when("leadership.set.cluster_tag") +@when_not("kubernetes-master.cloud.request-sent") +def request_integration(): + hookenv.status_set("maintenance", "requesting cloud integration") + cluster_tag = leader_get("cluster_tag") + if is_state("endpoint.aws.joined"): + cloud = endpoint_from_flag("endpoint.aws.joined") + cloud.tag_instance( + { + "kubernetes.io/cluster/{}".format(cluster_tag): "owned", + "k8s.io/role/master": "true", + } + ) + cloud.tag_instance_security_group( + { + "kubernetes.io/cluster/{}".format(cluster_tag): "owned", + } + ) + cloud.tag_instance_subnet( + { + "kubernetes.io/cluster/{}".format(cluster_tag): "owned", + } + ) + cloud.enable_object_storage_management(["kubernetes-*"]) + cloud.enable_load_balancer_management() + elif is_state("endpoint.gcp.joined"): + cloud = endpoint_from_flag("endpoint.gcp.joined") + cloud.label_instance( + { + "k8s-io-cluster-name": cluster_tag, + "k8s-io-role-master": "master", + } + ) + cloud.enable_object_storage_management() + cloud.enable_security_management() + elif is_state("endpoint.azure.joined"): + cloud = endpoint_from_flag("endpoint.azure.joined") + cloud.tag_instance( + { + "k8s-io-cluster-name": cluster_tag, + "k8s-io-role-master": "master", + } + ) + cloud.enable_object_storage_management() + cloud.enable_security_management() + cloud.enable_loadbalancer_management() + cloud.enable_instance_inspection() + cloud.enable_network_management() + cloud.enable_dns_management() + cloud.enable_block_storage_management() + set_state("kubernetes-master.cloud.request-sent") + + +@when_none( + "endpoint.aws.joined", + "endpoint.gcp.joined", + "endpoint.openstack.joined", + "endpoint.vsphere.joined", + "endpoint.azure.joined", +) +@when_any( + "kubernetes-master.cloud.pending", + "kubernetes-master.cloud.request-sent", + "kubernetes-master.cloud.blocked", + "kubernetes-master.cloud.ready", +) +def clear_cloud_flags(): + remove_state("kubernetes-master.cloud.pending") + remove_state("kubernetes-master.cloud.request-sent") + remove_state("kubernetes-master.cloud.blocked") + remove_state("kubernetes-master.cloud.ready") + clear_flag("kubernetes-master.apiserver.configured") + _kick_controller_manager() + + +@when_any( + "endpoint.aws.ready", + "endpoint.gcp.ready", + "endpoint.openstack.ready", + "endpoint.vsphere.ready", + "endpoint.azure.ready", +) +@when_not("kubernetes-master.cloud.blocked", "kubernetes-master.cloud.ready") +def cloud_ready(): + if is_state("endpoint.gcp.ready"): + write_gcp_snap_config("kube-apiserver") + write_gcp_snap_config("kube-controller-manager") + elif is_state("endpoint.vsphere.ready"): + _write_vsphere_snap_config("kube-apiserver") + _write_vsphere_snap_config("kube-controller-manager") + elif is_state("endpoint.azure.ready"): + write_azure_snap_config("kube-apiserver") + write_azure_snap_config("kube-controller-manager") + remove_state("kubernetes-master.cloud.pending") + set_state("kubernetes-master.cloud.ready") + remove_state("kubernetes-master.components.started") # force restart + + +@when("kubernetes-master.cloud.ready") +@when_any( + "endpoint.openstack.ready.changed", + "endpoint.vsphere.ready.changed", + "endpoint.azure.ready.changed", +) +def update_cloud_config(): + """Signal that cloud config has changed. + + Some clouds (openstack, vsphere) support runtime config that needs to be + reflected in the k8s cloud config files when changed. Manage flags to + ensure this happens. + """ + if is_state("endpoint.openstack.ready.changed"): + remove_state("endpoint.openstack.ready.changed") + set_state("kubernetes-master.openstack.changed") + if is_state("endpoint.vsphere.ready.changed"): + remove_state("kubernetes-master.cloud.ready") + remove_state("endpoint.vsphere.ready.changed") + if is_state("endpoint.azure.ready.changed"): + remove_state("kubernetes-master.cloud.ready") + remove_state("endpoint.azure.ready.changed") + + +def _cdk_addons_template_path(): + return Path("/snap/cdk-addons/current/templates") + + +def _write_vsphere_snap_config(component): + # vsphere requires additional cloud config + vsphere = endpoint_from_flag("endpoint.vsphere.ready") + + # NB: vsphere provider will ask kube-apiserver and -controller-manager to + # find a uuid from sysfs unless a global config value is set. Our strict + # snaps cannot read sysfs, so let's do it in the charm. An invalid uuid is + # not fatal for storage, but it will muddy the logs; try to get it right. + uuid = _get_vmware_uuid() + + comp_cloud_config_path = cloud_config_path(component) + comp_cloud_config_path.write_text( + "\n".join( + [ + "[Global]", + "insecure-flag = true", + 'datacenters = "{}"'.format(vsphere.datacenter), + 'vm-uuid = "VMware-{}"'.format(uuid), + '[VirtualCenter "{}"]'.format(vsphere.vsphere_ip), + 'user = "{}"'.format(vsphere.user), + 'password = "{}"'.format(vsphere.password), + "[Workspace]", + 'server = "{}"'.format(vsphere.vsphere_ip), + 'datacenter = "{}"'.format(vsphere.datacenter), + 'default-datastore = "{}"'.format(vsphere.datastore), + 'folder = "{}"'.format(vsphere.folder), + 'resourcepool-path = "{}"'.format(vsphere.respool_path), + "[Disk]", + 'scsicontrollertype = "pvscsi"', + ] + ) + ) + + +@when("config.changed.keystone-policy") +@when("kubernetes-master.keystone-policy-handled") +def regen_keystone_policy(): + clear_flag("kubernetes-master.keystone-policy-handled") + + +@when( + "keystone-credentials.available", + "leadership.is_leader", + "kubernetes-master.apiserver.configured", +) +@when_not("kubernetes-master.keystone-policy-handled") +def generate_keystone_configmap(): + keystone_policy = hookenv.config("keystone-policy") + if keystone_policy: + os.makedirs(keystone_root, exist_ok=True) + write_file_with_autogenerated_header(keystone_policy_path, keystone_policy) + if kubectl_manifest("apply", keystone_policy_path): + set_flag("kubernetes-master.keystone-policy-handled") + clear_flag("kubernetes-master.keystone-policy-error") + else: + set_flag("kubernetes-master.keystone-policy-error") + else: + # a missing policy configmap will crashloop the pods, but... + # what do we do in this situation. We could just do nothing, + # but that isn't cool for the user so we surface an error + # and wait for them to fix it. + set_flag("kubernetes-master.keystone-policy-error") + + # note that information is surfaced to the user in the code above where we + # write status. It will notify the user we are waiting on the policy file + # to apply if the keystone-credentials.available flag is set, but + # kubernetes-master.keystone-policy-handled is not set. + + +@when("leadership.is_leader", "kubernetes-master.keystone-policy-handled") +@when_not("keystone-credentials.available") +def remove_keystone(): + clear_flag("kubernetes-master.apiserver.configured") + if not os.path.exists(keystone_policy_path): + clear_flag("kubernetes-master.keystone-policy-handled") + elif kubectl_manifest("delete", keystone_policy_path): + os.remove(keystone_policy_path) + clear_flag("kubernetes-master.keystone-policy-handled") + + +@when("keystone-credentials.connected") +def setup_keystone_user(): + # This seems silly, but until we request a user from keystone + # we don't get information about the keystone server... + ks = endpoint_from_flag("keystone-credentials.connected") + ks.request_credentials("k8s") + + +def _kick_controller_manager(): + if is_flag_set("kubernetes-master.components.started"): + configure_controller_manager() + + +@when( + "keystone.credentials.configured", "leadership.set.keystone-cdk-addons-configured" +) +@when_not("keystone.apiserver.configured") +def keystone_kick_apiserver(): + clear_flag("kubernetes-master.apiserver.configured") + + +@when( + "keystone-credentials.available", + "certificates.ca.available", + "certificates.client.cert.available", + "authentication.setup", + "etcd.available", + "leadership.set.keystone-cdk-addons-configured", +) +def keystone_config(): + # first, we have to have the service set up before we can render this stuff + ks = endpoint_from_flag("keystone-credentials.available") + data = { + "host": ks.credentials_host(), + "proto": ks.credentials_protocol(), + "port": ks.credentials_port(), + "version": ks.api_version(), + } + if data_changed("keystone", data): + remove_state("keystone.credentials.configured") + clear_flag("kubernetes-master.apiserver.configured") + build_kubeconfig() + generate_keystone_configmap() + set_state("keystone.credentials.configured") + + +@when("layer.vault-kv.app-kv.set.encryption_key", "layer.vaultlocker.ready") +@when_not("kubernetes-master.secure-storage.created") +def create_secure_storage(): + encryption_conf_dir = encryption_config_path().parent + encryption_conf_dir.mkdir(mode=0o700, parents=True, exist_ok=True) + try: + vaultlocker.create_encrypted_loop_mount(encryption_conf_dir) + except vaultlocker.VaultLockerError: + # One common cause of this would be deploying on lxd. + # Should this be more fatal? + hookenv.log( + "Unable to create encrypted mount for storing encryption config.\n" + "{}".format(traceback.format_exc()), + level=hookenv.ERROR, + ) + set_flag("kubernetes-master.secure-storage.failed") + clear_flag("kubernetes-master.secure-storage.created") + else: + # TODO: If Vault isn't available, it's probably still better to encrypt + # anyway and store the key in plaintext and leadership than to just + # give up on encryption entirely. + _write_encryption_config() + # prevent an unnecessary service restart on this + # unit since we've already handled the change + clear_flag("layer.vault-kv.app-kv.changed.encryption_key") + # mark secure storage as ready + set_flag("kubernetes-master.secure-storage.created") + clear_flag("kubernetes-master.secure-storage.failed") + # restart to regen config + clear_flag("kubernetes-master.apiserver.configured") + + +@when_not("layer.vaultlocker.ready") +@when("kubernetes-master.secure-storage.created") +def revert_secure_storage(): + clear_flag("kubernetes-master.secure-storage.created") + clear_flag("kubernetes-master.secure-storage.failed") + clear_flag("kubernetes-master.apiserver.configured") + + +@when("leadership.is_leader", "layer.vault-kv.ready") +@when_not("layer.vault-kv.app-kv.set.encryption_key") +def generate_encryption_key(): + app_kv = vault_kv.VaultAppKV() + app_kv["encryption_key"] = kubernetes_master.token_generator(32) + + +@when( + "layer.vault-kv.app-kv.changed.encryption_key", + "kubernetes-master.secure-storage.created", +) +def restart_apiserver_for_encryption_key(): + clear_flag("kubernetes-master.apiserver.configured") + clear_flag("layer.vault-kv.app-kv.changed.encryption_key") + + +def _write_encryption_config(): + app_kv = vault_kv.VaultAppKV() + encryption_config_path().parent.mkdir(parents=True, exist_ok=True) + secret = app_kv["encryption_key"] + secret = base64.b64encode(secret.encode("utf8")).decode("utf8") + host.write_file( + path=str(encryption_config_path()), + perms=0o600, + content=yaml.safe_dump( + { + "kind": "EncryptionConfig", + "apiVersion": "v1", + "resources": [ + { + "resources": ["secrets"], + "providers": [ + { + "aescbc": { + "keys": [ + { + "name": "key1", + "secret": secret, + } + ], + } + }, + {"identity": {}}, + ], + } + ], + } + ), + ) + + +@when_any("config.changed.pod-security-policy") +def pod_security_policy_config_changed(): + clear_flag("kubernetes-master.pod-security-policy.applied") + + +@when_any("config.changed.ha-cluster-vip", "config.changed.ha-cluster-dns") +def haconfig_changed(): + clear_flag("hacluster-configured") + + +@when("ha.connected", "kubernetes-master.components.started") +@when_not("hacluster-configured") +def configure_hacluster(): + for service in master_services: + daemon = "snap.{}.daemon".format(service) + add_service_to_hacluster(service, daemon) + + # get a new cert + if is_flag_set("certificates.available"): + send_data() + + # update workers + if is_flag_set("kube-control.connected"): + send_api_urls() + if is_flag_set("kube-api-endpoint.available"): + push_service_data() + + set_flag("hacluster-configured") + + +@when_not("ha.connected") +@when("hacluster-configured") +def remove_hacluster(): + for service in master_services: + daemon = "snap.{}.daemon".format(service) + remove_service_from_hacluster(service, daemon) + + # get a new cert + if is_flag_set("certificates.available"): + send_data() + # update workers + if is_flag_set("kube-control.connected"): + send_api_urls() + if is_flag_set("kube-api-endpoint.available"): + push_service_data() + + clear_flag("hacluster-configured") + + +class InvalidDnsProvider(Exception): + def __init__(self, value): + self.value = value + + +def get_dns_provider(): + valid_dns_providers = ["auto", "core-dns", "kube-dns", "none"] + if get_version("kube-apiserver") < (1, 14): + valid_dns_providers.remove("core-dns") + + dns_provider = hookenv.config("dns-provider").lower() + if dns_provider not in valid_dns_providers: + raise InvalidDnsProvider(dns_provider) + + if dns_provider == "auto": + dns_provider = leader_get("auto_dns_provider") + # On new deployments, the first time this is called, auto_dns_provider + # hasn't been set yet. We need to make a choice now. + if not dns_provider: + if "core-dns" in valid_dns_providers: + dns_provider = "core-dns" + else: + dns_provider = "kube-dns" + + # LP: 1833089. Followers end up here when setting final status; ensure only + # leaders call leader_set. + if is_state("leadership.is_leader"): + leader_set(auto_dns_provider=dns_provider) + return dns_provider + + +@when("kube-control.connected") +@when_not("kubernetes-master.sent-registry") +def send_registry_location(): + registry_location = hookenv.config("image-registry") + kube_control = endpoint_from_flag("kube-control.connected") + + # Send registry to workers + kube_control.set_registry_location(registry_location) + + # Construct and send the sandbox image (pause container) to our runtime + runtime = endpoint_from_flag("endpoint.container-runtime.available") + if runtime: + uri = "{}/pause-{}:3.1".format(registry_location, arch()) + runtime.set_config(sandbox_image=uri) + + set_flag("kubernetes-master.sent-registry") + + +@when("config.changed.image-registry") +def send_new_registry_location(): + clear_flag("kubernetes-master.sent-registry") + + +@when( + "leadership.is_leader", + "leadership.set.kubernetes-master-addons-restart-for-ca", + "kubernetes-master.components.started", +) +def restart_addons_for_ca(): + try: + # Get deployments/daemonsets/statefulsets + output = kubectl( + "get", + "daemonset,deployment,statefulset", + "-o", + "json", + "--all-namespaces", + "-l", + "cdk-restart-on-ca-change=true", + ).decode("UTF-8") + deployments = json.loads(output)["items"] + + # Get ServiceAccounts + service_account_names = set( + ( + deployment["metadata"]["namespace"], + deployment["spec"]["template"]["spec"].get( + "serviceAccountName", "default" + ), + ) + for deployment in deployments + ) + service_accounts = [] + for namespace, name in service_account_names: + output = kubectl( + "get", "ServiceAccount", name, "-o", "json", "-n", namespace + ).decode("UTF-8") + service_account = json.loads(output) + service_accounts.append(service_account) + + # Get ServiceAccount secrets + secret_names = set() + for service_account in service_accounts: + namespace = service_account["metadata"]["namespace"] + for secret in service_account["secrets"]: + secret_names.add((namespace, secret["name"])) + secrets = [] + for namespace, name in secret_names: + output = kubectl( + "get", "Secret", name, "-o", "json", "-n", namespace + ).decode("UTF-8") + secret = json.loads(output) + secrets.append(secret) + + # Check secrets have updated CA + with open(ca_crt_path, "rb") as f: + ca = f.read() + encoded_ca = base64.b64encode(ca).decode("UTF-8") + mismatched_secrets = [ + secret for secret in secrets if secret["data"]["ca.crt"] != encoded_ca + ] + if mismatched_secrets: + hookenv.log( + "ServiceAccount secrets do not have correct ca.crt: " + + ",".join(secret["metadata"]["name"] for secret in mismatched_secrets) + ) + hookenv.log("Waiting to retry restarting addons") + return + + # Now restart the addons + for deployment in deployments: + kind = deployment["kind"] + namespace = deployment["metadata"]["namespace"] + name = deployment["metadata"]["name"] + hookenv.log("Restarting addon: %s %s %s" % (kind, namespace, name)) + kubectl("rollout", "restart", kind + "/" + name, "-n", namespace) + + leader_set({"kubernetes-master-addons-restart-for-ca": None}) + except Exception: + hookenv.log(traceback.format_exc()) + hookenv.log("Waiting to retry restarting addons") + + +def add_systemd_iptables_patch(): + source = "templates/kube-proxy-iptables-fix.sh" + dest = "/usr/local/bin/kube-proxy-iptables-fix.sh" + copyfile(source, dest) + os.chmod(dest, 0o775) + + template = "templates/service-iptables-fix.service" + dest_dir = "/etc/systemd/system" + os.makedirs(dest_dir, exist_ok=True) + service_name = "kube-proxy-iptables-fix.service" + copyfile(template, "{}/{}".format(dest_dir, service_name)) + + check_call(["systemctl", "daemon-reload"]) + + # enable and run the service + service_resume(service_name) + + +@when( + "leadership.is_leader", + "kubernetes-master.components.started", + "endpoint.prometheus.joined", + "certificates.ca.available", +) +def register_prometheus_jobs(): + prometheus = endpoint_from_flag("endpoint.prometheus.joined") + tls = endpoint_from_flag("certificates.ca.available") + monitoring_token = get_token("system:monitoring") + + for relation in prometheus.relations: + endpoints = kubernetes_master.get_internal_api_endpoints(relation) + if not endpoints: + continue + address, port = endpoints[0] + + templates_dir = Path("templates") + for job_file in Path("templates/prometheus").glob("*.yaml.j2"): + prometheus.register_job( + relation=relation, + job_name=job_file.name.split(".")[0], + job_data=yaml.safe_load( + render( + source=str(job_file.relative_to(templates_dir)), + target=None, # don't write file, just return data + context={ + "k8s_api_address": address, + "k8s_api_port": port, + "k8s_token": monitoring_token, + }, + ) + ), + ca_cert=tls.root_ca_cert, + ) + + +def detect_telegraf(): + # Telegraf uses the implicit juju-info relation, which makes it difficult + # to tell if it's related. The "best" option is to look for the subordinate + # charm on disk. + for charm_dir in Path("/var/lib/juju/agents").glob("unit-*/charm"): + metadata = yaml.safe_load((charm_dir / "metadata.yaml").read_text()) + if "telegraf" in metadata["name"]: + return True + else: + return False + + +@when( + "leadership.is_leader", + "kubernetes-master.components.started", + "endpoint.grafana.joined", +) +def register_grafana_dashboards(): + grafana = endpoint_from_flag("endpoint.grafana.joined") + + # load conditional dashboards + dash_dir = Path("templates/grafana/conditional") + if is_flag_set("endpoint.prometheus.joined"): + dashboard = (dash_dir / "prometheus.json").read_text() + grafana.register_dashboard("prometheus", json.loads(dashboard)) + if detect_telegraf(): + dashboard = (dash_dir / "telegraf.json").read_text() + grafana.register_dashboard("telegraf", json.loads(dashboard)) + + # load automatic dashboards + dash_dir = Path("templates/grafana/autoload") + for dash_file in dash_dir.glob("*.json"): + dashboard = dash_file.read_text() + grafana.register_dashboard(dash_file.stem, json.loads(dashboard)) + + +@when("endpoint.aws-iam.ready") +@when_not("kubernetes-master.aws-iam.configured") +def enable_aws_iam_webhook(): + # if etcd isn't available yet, we'll set this up later + # when we start the api server. + if is_flag_set("etcd.available"): + # call the other things we need to update + clear_flag("kubernetes-master.apiserver.configured") + build_kubeconfig() + set_flag("kubernetes-master.aws-iam.configured") + + +@when("kubernetes-master.components.started", "endpoint.aws-iam.available") +def api_server_started(): + aws_iam = endpoint_from_flag("endpoint.aws-iam.available") + if aws_iam: + aws_iam.set_api_server_status(True) + + +@when_not("kubernetes-master.components.started") +@when("endpoint.aws-iam.available") +def api_server_stopped(): + aws_iam = endpoint_from_flag("endpoint.aws-iam.available") + if aws_iam: + aws_iam.set_api_server_status(False) + + +@when("kube-control.connected") +def send_default_cni(): + """Send the value of the default-cni config to the kube-control relation. + This allows kubernetes-worker to use the same config value as well. + """ + default_cni = hookenv.config("default-cni") + kube_control = endpoint_from_flag("kube-control.connected") + kube_control.set_default_cni(default_cni) + + +@when("config.changed.default-cni") +def default_cni_changed(): + remove_state("kubernetes-master.components.started") diff --git a/kubernetes-master/reactive/kubernetes_master_worker_base.py b/kubernetes-master/reactive/kubernetes_master_worker_base.py new file mode 100644 index 0000000..0bb1de4 --- /dev/null +++ b/kubernetes-master/reactive/kubernetes_master_worker_base.py @@ -0,0 +1,88 @@ +from charms.layer import snap +from charms.leadership import ( + leader_get, + leader_set +) +from charms.reactive import ( + when, + when_not, + when_any, + data_changed +) + +from charmhelpers.core import hookenv +from charmhelpers.core.host import is_container +from charmhelpers.core.sysctl import create as create_sysctl + + +@when_any('kubernetes-master.snaps.installed', + 'kubernetes-worker.snaps.installed') +@when('snap.refresh.set') +@when('leadership.is_leader') +def process_snapd_timer(): + """ + Set the snapd refresh timer on the leader so all cluster members + (present and future) will refresh near the same time. + + :return: None + """ + # Get the current snapd refresh timer; we know layer-snap has set this + # when the 'snap.refresh.set' flag is present. + timer = snap.get( + snapname='core', key='refresh.timer').decode('utf-8').strip() + if not timer: + # The core snap timer is empty. This likely means a subordinate timer + # reset ours. Try to set it back to a previously leader-set value, + # falling back to config if needed. Luckily, this should only happen + # during subordinate install, so this should remain stable afterward. + timer = leader_get('snapd_refresh') or hookenv.config('snapd_refresh') + snap.set_refresh_timer(timer) + + # Ensure we have the timer known by snapd (it may differ from config). + timer = snap.get( + snapname='core', key='refresh.timer').decode('utf-8').strip() + + # The first time through, data_changed will be true. Subsequent calls + # should only update leader data if something changed. + if data_changed('snapd_refresh', timer): + hookenv.log('setting leader snapd_refresh timer to: {}'.format(timer)) + leader_set({'snapd_refresh': timer}) + + +@when_any('kubernetes-master.snaps.installed', + 'kubernetes-worker.snaps.installed') +@when('snap.refresh.set') +@when('leadership.changed.snapd_refresh') +@when_not('leadership.is_leader') +def set_snapd_timer(): + """ + Set the snapd refresh.timer on non-leader cluster members. + + :return: None + """ + # NB: This method should only be run when 'snap.refresh.set' is present. + # Layer-snap will always set a core refresh.timer, which may not be the + # same as our leader. Gating with 'snap.refresh.set' ensures layer-snap + # has finished and we are free to set our config to the leader's timer. + timer = leader_get('snapd_refresh') or '' # None will error + hookenv.log('setting snapd_refresh timer to: {}'.format(timer)) + snap.set_refresh_timer(timer) + + +@when('config.changed.sysctl') +def write_sysctl(): + """ + :return: None + """ + sysctl_settings = hookenv.config('sysctl') + if sysctl_settings and not is_container(): + create_sysctl( + sysctl_settings, + '/etc/sysctl.d/50-kubernetes-charm.conf', + # Some keys in the config may not exist in /proc/sys/net/. + # For example, the conntrack module may not be loaded when + # using lxd drivers insteam of kvm. In these cases, we + # simply ignore the missing keys, rather than making time + # consuming calls out to the filesystem to check for their + # existence. + ignore=True) diff --git a/kubernetes-master/reactive/leadership.py b/kubernetes-master/reactive/leadership.py new file mode 100644 index 0000000..29c6f3a --- /dev/null +++ b/kubernetes-master/reactive/leadership.py @@ -0,0 +1,68 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Leadership Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.leadership import leader_get, leader_set + + +__all__ = ['leader_get', 'leader_set'] # Backwards compatibility + + +def initialize_leadership_state(): + '''Initialize leadership.* states from the hook environment. + + Invoked by hookenv.atstart() so states are available in + @hook decorated handlers. + ''' + is_leader = hookenv.is_leader() + if is_leader: + hookenv.log('Initializing Leadership Layer (is leader)') + else: + hookenv.log('Initializing Leadership Layer (is follower)') + + reactive.helpers.toggle_state('leadership.is_leader', is_leader) + + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + current = hookenv.leader_get() + + # Handle deletions. + for key in set(previous.keys()) - set(current.keys()): + current[key] = None + + any_changed = False + for key, value in current.items(): + reactive.helpers.toggle_state('leadership.changed.{}'.format(key), + value != previous.get(key)) + if value != previous.get(key): + any_changed = True + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + reactive.helpers.toggle_state('leadership.changed', any_changed) + + unitdata.kv().update(current, prefix='leadership.settings.') + + +# 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, '_leadership_registered'): + hookenv.atstart(initialize_leadership_state) + reactive._leadership_registered = True diff --git a/kubernetes-master/reactive/snap.py b/kubernetes-master/reactive/snap.py new file mode 100644 index 0000000..1fda7b7 --- /dev/null +++ b/kubernetes-master/reactive/snap.py @@ -0,0 +1,349 @@ +# Copyright 2016-2019 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. +""" +from collections import OrderedDict +from distutils.version import LooseVersion +import os.path +from os import uname +import shutil +import subprocess +from textwrap import dedent +import time +from urllib.request import urlretrieve + +from charmhelpers.core import hookenv, host +from charmhelpers.core.hookenv import ERROR +from charmhelpers.core.host import write_file +from charms import layer +from charms import reactive +from charms.layer import snap +from charms.reactive import register_trigger, when, when_not, toggle_flag +from charms.reactive.helpers import data_changed + + +class UnsatisfiedMinimumVersionError(Exception): + def __init__(self, desired, actual): + super().__init__() + self.desired = desired + self.actual = actual + + def __str__(self): + return "Could not install snapd >= {0.desired}, got {0.actual}".format(self) + + +class InvalidBundleError(Exception): + pass + + +def sorted_snap_opts(): + opts = layer.options("snap") + opts = sorted(opts.items(), key=lambda item: item[0] != "core") + opts = OrderedDict(opts) + return opts + + +def install(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + opts = sorted_snap_opts() + # supported-architectures is EXPERIMENTAL and undocumented. + # It probably should live in the base layer, blocking the charm + # during bootstrap if the arch is unsupported. + arch = uname().machine + for snapname, snap_opts in opts.items(): + supported_archs = snap_opts.pop("supported-architectures", None) + if supported_archs and arch not in supported_archs: + # Note that this does *not* error. The charm will need to + # cope with the snaps it requested never getting installed, + # likely by doing its own check on supported-architectures. + hookenv.log( + "Snap {} not supported on {!r} architecture" "".format(snapname, arch), + ERROR, + ) + continue + installed_flag = "snap.installed.{}".format(snapname) + if not reactive.is_flag_set(installed_flag): + snap.install(snapname, **snap_opts) + if data_changed("snap.install.opts", opts): + snap.connect_all() + + +def check_refresh_available(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + available_refreshes = snap.get_available_refreshes() + for snapname in snap.get_installed_snaps(): + toggle_flag(snap.get_refresh_available_flag(snapname), snapname in available_refreshes) + + +def refresh(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + opts = sorted_snap_opts() + # supported-architectures is EXPERIMENTAL and undocumented. + # It probably should live in the base layer, blocking the charm + # during bootstrap if the arch is unsupported. + arch = uname()[4] + check_refresh_available() + for snapname, snap_opts in opts.items(): + supported_archs = snap_opts.pop("supported-architectures", None) + if supported_archs and arch not in supported_archs: + continue + snap.refresh(snapname, **snap_opts) + snap.connect_all() + + +@reactive.hook("upgrade-charm") +def upgrade_charm(): + refresh() + + +def get_series(): + return subprocess.check_output(["lsb_release", "-sc"], universal_newlines=True).strip() + + +def snapd_supported(): + # snaps are not supported in trusty lxc containers. + if get_series() == "trusty" and host.is_container(): + return False + return True # For all other cases, assume true. + + +def kernel_supported(): + kernel_version = uname().release + + if LooseVersion(kernel_version) < LooseVersion("4.4"): + hookenv.log( + "Snaps do not work on kernel {}, a reboot " + "into a supported kernel (>4.4) is required" + "".format(kernel_version) + ) + return False + return True + + +def ensure_snapd(): + if not snapd_supported(): + hookenv.log("Snaps do not work in this environment", hookenv.ERROR) + raise Exception("Snaps do not work in this environment") + + # 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"): + os.environ["DEBIAN_FRONTEND"] = "noninteractive" + cmd = ["apt-get", "install", "-y", "snapd"] + # LP:1699986: Force install of systemd on Trusty. + if get_series() == "trusty": + cmd.append("systemd") + 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 host.is_container() and not shutil.which("squashfuse"): + os.environ["DEBIAN_FRONTEND"] = "noninteractive" + cmd = ["apt-get", "install", "-y", "squashfuse", "fuse"] + subprocess.check_call(cmd, universal_newlines=True) + + +def proxy_settings(): + proxy_vars = ("http_proxy", "https_proxy") + proxy_env = {key: value for key, value in os.environ.items() if key in proxy_vars} + + snap_proxy = hookenv.config().get("snap_proxy") + if snap_proxy: + proxy_env["http_proxy"] = snap_proxy + proxy_env["https_proxy"] = snap_proxy + return proxy_env + + +def update_snap_proxy(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + # 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 = proxy_settings() + + override_dir = "/etc/systemd/system/snapd.service.d" + path = os.path.join(override_dir, "snap_layer_proxy.conf") + if not proxy and not os.path.exists(path): + return # No proxy asked for and proxy never configured. + + # It seems we cannot rely on this directory existing, so manually + # create it. + if not os.path.exists(override_dir): + host.mkdir(override_dir, perms=0o755) + + if not data_changed("snap.proxy", proxy): + return # Short circuit avoids unnecessary restarts. + + 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] + """ + ) + for proxy_key, proxy_value in proxy.items(): + content += "Environment={}={}\n".format(proxy_key, proxy_value) + 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" + + +def _get_snapd_version(): + stdout = subprocess.check_output(["snap", "version"], stdin=subprocess.DEVNULL, universal_newlines=True) + version_info = dict(line.split(None, 1) for line in stdout.splitlines()) + return LooseVersion(version_info["snapd"]) + + +PREFERENCES = """\ +Package: * +Pin: release a={}-proposed +Pin-Priority: 400 +""" + + +def ensure_snapd_min_version(min_version): + snapd_version = _get_snapd_version() + if snapd_version < LooseVersion(min_version): + from charmhelpers.fetch import add_source, apt_update, apt_install + + # Temporary until LP:1735344 lands + add_source("distro-proposed", fail_invalid=True) + distro = get_series() + # disable proposed by default, needs to explicit + write_file( + "/etc/apt/preferences.d/proposed", + PREFERENCES.format(distro), + ) + apt_update() + # explicitly install snapd from proposed + apt_install("snapd/{}-proposed".format(distro)) + snapd_version = _get_snapd_version() + if snapd_version < LooseVersion(min_version): + hookenv.log("Failed to install snapd >= {}".format(min_version), ERROR) + raise UnsatisfiedMinimumVersionError(min_version, snapd_version) + + +def download_assertion_bundle(proxy_url): + """Download proxy assertion bundle and store id""" + assertions_url = "{}/v2/auth/store/assertions".format(proxy_url) + local_bundle, headers = urlretrieve(assertions_url) + store_id = headers["X-Assertion-Store-Id"] + return local_bundle, store_id + + +def configure_snap_store_proxy(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + if not reactive.is_flag_set("config.changed.snap_proxy_url"): + return + config = hookenv.config() + if "snap_proxy_url" not in config: + # The deprecated snap_proxy_url config items have been removed + # from config.yaml. If the charm author hasn't added them back + # explicitly, there is nothing to do. Juju is maintaining these + # settings as model configuration. + return + snap_store_proxy_url = config.get("snap_proxy_url") + if not snap_store_proxy_url and not config.previous("snap_proxy_url"): + # Proxy url is not set, and was not set previous hook. Do nothing, + # to avoid overwriting the Juju maintained setting. + return + ensure_snapd_min_version("2.30") + if snap_store_proxy_url: + bundle, store_id = download_assertion_bundle(snap_store_proxy_url) + try: + subprocess.check_output( + ["snap", "ack", bundle], + stdin=subprocess.DEVNULL, + universal_newlines=True, + ) + except subprocess.CalledProcessError as e: + raise InvalidBundleError("snapd could not ack the proxy assertion: " + e.output) + else: + store_id = "" + + try: + subprocess.check_output( + ["snap", "set", "core", "proxy.store={}".format(store_id)], + stdin=subprocess.DEVNULL, + universal_newlines=True, + ) + except subprocess.CalledProcessError as e: + raise InvalidBundleError("Proxy ID from header did not match store assertion: " + e.output) + + +register_trigger(when="config.changed.snapd_refresh", clear_flag="snap.refresh.set") + + +@when_not("snap.refresh.set") +@when("snap.installed.core") +def change_snapd_refresh(): + """Set the system refresh.timer option""" + ensure_snapd_min_version("2.31") + timer = hookenv.config()["snapd_refresh"] + was_set = reactive.is_flag_set("snap.refresh.was-set") + if timer or was_set: + snap.set_refresh_timer(timer) + reactive.toggle_flag("snap.refresh.was-set", timer) + reactive.set_flag("snap.refresh.set") + + +# Bootstrap. We don't use standard reactive handlers to ensure that +# everything is bootstrapped before any charm handlers are run. +hookenv.atstart(hookenv.log, "Initializing Snap Layer") +hookenv.atstart(ensure_snapd) +hookenv.atstart(ensure_path) +hookenv.atstart(update_snap_proxy) +hookenv.atstart(configure_snap_store_proxy) +hookenv.atstart(install) diff --git a/kubernetes-master/reactive/status.py b/kubernetes-master/reactive/status.py new file mode 100644 index 0000000..2f33f3f --- /dev/null +++ b/kubernetes-master/reactive/status.py @@ -0,0 +1,4 @@ +from charms import layer + + +layer.status._initialize() diff --git a/kubernetes-master/reactive/tls_client.py b/kubernetes-master/reactive/tls_client.py new file mode 100644 index 0000000..afa2228 --- /dev/null +++ b/kubernetes-master/reactive/tls_client.py @@ -0,0 +1,208 @@ +import os + +from pathlib import Path +from subprocess import check_call + +from charms import layer +from charms.reactive import hook +from charms.reactive import set_state, remove_state +from charms.reactive import when +from charms.reactive import set_flag, clear_flag +from charms.reactive import endpoint_from_flag +from charms.reactive.helpers import data_changed + +from charmhelpers.core import hookenv, unitdata +from charmhelpers.core.hookenv import log + + +@when('certificates.ca.available') +def store_ca(tls): + '''Read the certificate authority from the relation object and install + the ca on this system.''' + # Get the CA from the relationship object. + certificate_authority = tls.get_ca() + if certificate_authority: + layer_options = layer.options('tls-client') + ca_path = layer_options.get('ca_certificate_path') + changed = data_changed('certificate_authority', certificate_authority) + if ca_path: + if changed or not os.path.exists(ca_path): + log('Writing CA certificate to {0}'.format(ca_path)) + # ensure we have a newline at the end of the certificate. + # some things will blow up without one. + # See https://bugs.launchpad.net/charm-kubernetes-master/+bug/1828034 + if not certificate_authority.endswith('\n'): + certificate_authority += '\n' + _write_file(ca_path, certificate_authority) + set_state('tls_client.ca.written') + set_state('tls_client.ca.saved') + if changed: + # Update /etc/ssl/certs and generate ca-certificates.crt + install_ca(certificate_authority) + + +@when('certificates.server.cert.available') +def store_server(tls): + '''Read the server certificate and server key from the relation object + and save them to the certificate directory..''' + server_cert, server_key = tls.get_server_cert() + chain = tls.get_chain() + if chain: + server_cert = server_cert + '\n' + chain + if server_cert and server_key: + layer_options = layer.options('tls-client') + cert_path = layer_options.get('server_certificate_path') + key_path = layer_options.get('server_key_path') + cert_changed = data_changed('server_certificate', server_cert) + key_changed = data_changed('server_key', server_key) + if cert_path: + if cert_changed or not os.path.exists(cert_path): + log('Writing server certificate to {0}'.format(cert_path)) + _write_file(cert_path, server_cert) + set_state('tls_client.server.certificate.written') + set_state('tls_client.server.certificate.saved') + if key_path: + if key_changed or not os.path.exists(key_path): + log('Writing server key to {0}'.format(key_path)) + _write_file(key_path, server_key) + set_state('tls_client.server.key.saved') + + +@when('certificates.client.cert.available') +def store_client(tls): + '''Read the client certificate and client key from the relation object + and copy them to the certificate directory.''' + client_cert, client_key = tls.get_client_cert() + chain = tls.get_chain() + if chain: + client_cert = client_cert + '\n' + chain + if client_cert and client_key: + layer_options = layer.options('tls-client') + cert_path = layer_options.get('client_certificate_path') + key_path = layer_options.get('client_key_path') + cert_changed = data_changed('client_certificate', client_cert) + key_changed = data_changed('client_key', client_key) + if cert_path: + if cert_changed or not os.path.exists(cert_path): + log('Writing client certificate to {0}'.format(cert_path)) + _write_file(cert_path, client_cert) + set_state('tls_client.client.certificate.written') + set_state('tls_client.client.certificate.saved') + if key_path: + if key_changed or not os.path.exists(key_path): + log('Writing client key to {0}'.format(key_path)) + _write_file(key_path, client_key) + set_state('tls_client.client.key.saved') + + +@when('certificates.certs.changed') +def update_certs(): + tls = endpoint_from_flag('certificates.certs.changed') + certs_paths = unitdata.kv().get('layer.tls-client.cert-paths', {}) + all_ready = True + any_changed = False + maps = { + 'server': tls.server_certs_map, + 'client': tls.client_certs_map, + } + + if maps.get('client') == {}: + log( + 'No client certs found using maps. Checking for global \ + client certificates.', + 'WARNING' + ) + # Check for global certs, + # Backwards compatibility https://bugs.launchpad.net/charm-kubernetes-master/+bug/1825819 + cert_pair = tls.get_client_cert() + if cert_pair is not None: + for client_name in certs_paths.get('client', {}).keys(): + maps.get('client').update({ + client_name: cert_pair + }) + + chain = tls.get_chain() + for cert_type in ('server', 'client'): + for common_name, paths in certs_paths.get(cert_type, {}).items(): + cert_pair = maps[cert_type].get(common_name) + if not cert_pair: + all_ready = False + continue + if not data_changed('layer.tls-client.' + '{}.{}'.format(cert_type, common_name), cert_pair): + continue + + cert = None + key = None + if type(cert_pair) is not tuple: + if paths['crt']: + cert = cert_pair.cert + if paths['key']: + key = cert_pair.key + else: + cert, key = cert_pair + + if cert: + if chain: + cert = cert + '\n' + chain + _ensure_directory(paths['crt']) + Path(paths['crt']).write_text(cert) + + if key: + _ensure_directory(paths['key']) + Path(paths['key']).write_text(key) + + any_changed = True + # clear flags first to ensure they are re-triggered if left set + clear_flag('tls_client.{}.certs.changed'.format(cert_type)) + clear_flag('tls_client.{}.cert.{}.changed'.format(cert_type, + common_name)) + set_flag('tls_client.{}.certs.changed'.format(cert_type)) + set_flag('tls_client.{}.cert.{}.changed'.format(cert_type, + common_name)) + if all_ready: + set_flag('tls_client.certs.saved') + if any_changed: + clear_flag('tls_client.certs.changed') + set_flag('tls_client.certs.changed') + clear_flag('certificates.certs.changed') + + +def install_ca(certificate_authority): + '''Install a certificiate authority on the system by calling the + update-ca-certificates command.''' + if certificate_authority: + name = hookenv.service_name() + # Create a path to install CAs on Debian systems. + ca_path = '/usr/local/share/ca-certificates/{0}.crt'.format(name) + log('Writing CA certificate to {0}'.format(ca_path)) + _write_file(ca_path, certificate_authority) + # Update the trusted CAs on this system (a time expensive operation). + check_call(['update-ca-certificates']) + log('Generated ca-certificates.crt for {0}'.format(name)) + set_state('tls_client.ca_installed') + + +@hook('upgrade-charm') +def remove_states(): + remove_state('tls_client.ca.saved') + remove_state('tls_client.server.certificate.saved') + remove_state('tls_client.server.key.saved') + remove_state('tls_client.client.certificate.saved') + remove_state('tls_client.client.key.saved') + + +def _ensure_directory(path): + '''Ensure the parent directory exists creating directories if necessary.''' + directory = os.path.dirname(path) + if not os.path.isdir(directory): + os.makedirs(directory) + os.chmod(directory, 0o770) + + +def _write_file(path, content): + '''Write the path to a file.''' + _ensure_directory(path) + with open(path, 'w') as stream: + stream.write(content) + os.chmod(path, 0o440) diff --git a/kubernetes-master/reactive/vault_kv.py b/kubernetes-master/reactive/vault_kv.py new file mode 100644 index 0000000..4396bf6 --- /dev/null +++ b/kubernetes-master/reactive/vault_kv.py @@ -0,0 +1,67 @@ +from charmhelpers.core import hookenv, host +from charms.reactive import when_all, when_not, set_flag, clear_flag +from charms.reactive import endpoint_from_flag, register_trigger +from charms.reactive import data_changed + +from charms.layer import vault_kv + + +register_trigger(when_not='vault-kv.connected', + clear_flag='layer.vault-kv.ready') +register_trigger(when_not='vault-kv.connected', + clear_flag='layer.vault-kv.requested') + + +@when_all('vault-kv.connected') +@when_not('layer.vault-kv.requested') +def request_vault_access(): + vault = endpoint_from_flag('vault-kv.connected') + backend_name = vault_kv._get_secret_backend() + # backend can't be isolated or VaultAppKV won't work; see issue #2 + vault.request_secret_backend(backend_name, isolated=False) + set_flag('layer.vault-kv.requested') + + +@when_all('vault-kv.available') +def set_ready(): + try: + vault_kv.get_vault_config() + except vault_kv.VaultNotReady: + clear_flag('layer.vault-kv.ready') + else: + set_flag('layer.vault-kv.ready') + + +@when_all('layer.vault-kv.ready') +def check_config_changed(): + try: + config = vault_kv.get_vault_config() + except vault_kv.VaultNotReady: + return + else: + if data_changed('layer.vault-kv.config', config): + set_flag('layer.vault-kv.config.changed') + + +def manage_app_kv_flags(): + try: + app_kv = vault_kv.VaultAppKV() + for key in app_kv.keys(): + app_kv._manage_flags(key) + except vault_kv.VaultNotReady: + vault_kv.VaultAppKV._clear_all_flags() + + +def update_app_kv_hashes(): + try: + app_kv = vault_kv.VaultAppKV() + if hookenv.is_leader() and app_kv.any_changed(): + # force hooks to run on non-leader units + hookenv.leader_set({'vault-kv-nonce': host.pwgen(8)}) + app_kv.update_hashes() + except vault_kv.VaultNotReady: + return + + +hookenv.atstart(manage_app_kv_flags) +hookenv.atexit(update_app_kv_hashes) diff --git a/kubernetes-master/reactive/vaultlocker.py b/kubernetes-master/reactive/vaultlocker.py new file mode 100644 index 0000000..d591c1e --- /dev/null +++ b/kubernetes-master/reactive/vaultlocker.py @@ -0,0 +1,49 @@ +import shutil + +from charms.reactive import when_all, when_not, set_flag, clear_flag +from charmhelpers.core import hookenv, host + +from charms import apt +from charms import layer + + +@when_not('apt.installed.vaultlocker') +def install_vaultlocker(): + '''Install vaultlocker. + + On bionic and higher, vaultlocker is available in the default system + sources. For xenial, we need to add the queens cloud archive. + ''' + dist = host.lsb_release() + dist_series = dist['DISTRIB_CODENAME'].lower() + if dist_series == 'xenial': + apt.add_source('cloud:queens') + apt.update() + apt.queue_install(['vaultlocker']) + + +@when_all('apt.installed.vaultlocker', + 'layer.vault-kv.ready', + 'layer.vault-kv.config.changed') +def configure_vaultlocker(): + # write VaultLocker config file + layer.vaultlocker.write_vaultlocker_conf(layer.vault_kv.get_vault_config()) + # create location for loop device service envs + layer.vaultlocker.LOOP_ENVS.mkdir(parents=True, exist_ok=True) + # create loop device service template + shutil.copyfile('templates/vaultlocker-loop@.service', + '/etc/systemd/system/vaultlocker-loop@.service') + # mark as complete + set_flag('layer.vaultlocker.configured') + clear_flag('layer.vault-kv.config.changed') + + +@when_all('layer.vaultlocker.configured') +@when_not('layer.vaultlocker.ready') +def auto_encrypt(): + metadata = hookenv.metadata() + for storage_name, storage_metadata in metadata.get('storage', {}).items(): + if storage_metadata.get('vaultlocker-encrypt', False): + mountbase = storage_metadata.get('vaultlocker-mountbase') + layer.vaultlocker.encrypt_storage(storage_name, mountbase) + set_flag('layer.vaultlocker.ready') diff --git a/kubernetes-master/requirements.txt b/kubernetes-master/requirements.txt new file mode 100644 index 0000000..55543d9 --- /dev/null +++ b/kubernetes-master/requirements.txt @@ -0,0 +1,3 @@ +mock +flake8 +pytest diff --git a/kubernetes-master/revision b/kubernetes-master/revision new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/kubernetes-master/revision @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/kubernetes-master/setup.py b/kubernetes-master/setup.py new file mode 100755 index 0000000..b30bff5 --- /dev/null +++ b/kubernetes-master/setup.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +import os +from setuptools import setup + +here = os.path.abspath(os.path.dirname(__file__)) + +with open(os.path.join(here, "README.md")) as f: + README = f.read() + +setup( + name="layer_snap", + version="1.0.0", + description="layer_snap", + long_description=README, + license="Apache License 2.0", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + ], + url="https://git.launchpad.net/layer-snap", + package_dir={"": "lib"}, + packages=["charms/layer"], + include_package_data=True, + zip_safe=False, + install_requires=["charmhelpers", "charms.reactive"], +) diff --git a/kubernetes-master/templates/cdk-service-kicker b/kubernetes-master/templates/cdk-service-kicker new file mode 100644 index 0000000..26d3740 --- /dev/null +++ b/kubernetes-master/templates/cdk-service-kicker @@ -0,0 +1,34 @@ +#!/bin/sh +set -eu + +# This service runs on boot to work around issues relating to LXD and snapd. + +# Workaround for https://github.com/conjure-up/conjure-up/issues/1448 +if [ -f '/proc/1/environ' ] && grep -q '^container=lxc' /proc/1/environ; then + echo "lxc detected, applying snapd apparmor profiles" + (set +e + apparmor_parser /var/lib/snapd/apparmor/profiles/* + echo "apparmor_parser: exit status $?" + ) +else + echo "lxc not detected, skipping snapd apparmor profiles" +fi + +# Workaround for https://github.com/juju-solutions/bundle-canonical-kubernetes/issues/357 +services="{{services}}" + +deadline="$(expr "$(date +%s)" + 600)" + +while [ "$(date +%s)" -lt "$deadline" ]; do + for service in $services; do + echo "$service: checking" + if ! systemctl is-active "$service"; then + echo "$service: not active, restarting" + systemctl restart "$service" || true + fi + done + + sleep 10 +done + +echo "deadline has passed, exiting gracefully" diff --git a/kubernetes-master/templates/cdk-service-kicker.service b/kubernetes-master/templates/cdk-service-kicker.service new file mode 100644 index 0000000..5c2105e --- /dev/null +++ b/kubernetes-master/templates/cdk-service-kicker.service @@ -0,0 +1,10 @@ +[Unit] +Description=cdk-service-kicker + +[Service] +ExecStart=/usr/bin/cdk-service-kicker +Restart=on-failure +Type=simple + +[Install] +WantedBy=multi-user.target diff --git a/kubernetes-master/templates/cdk.auth-webhook-secret.yaml b/kubernetes-master/templates/cdk.auth-webhook-secret.yaml new file mode 100644 index 0000000..a12c402 --- /dev/null +++ b/kubernetes-master/templates/cdk.auth-webhook-secret.yaml @@ -0,0 +1,13 @@ +# Manifest for CK secrets that auth-webhook expects +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ secret_name }} + namespace: {{ secret_namespace }} +type: {{ type }} +data: + uid: {{ user }} + username: {{ username }} + password: {{ password }} + groups: '{{ groups }}' diff --git a/kubernetes-master/templates/cdk.master.auth-webhook-conf.yaml b/kubernetes-master/templates/cdk.master.auth-webhook-conf.yaml new file mode 100644 index 0000000..e2d3fa0 --- /dev/null +++ b/kubernetes-master/templates/cdk.master.auth-webhook-conf.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Config +clusters: + - name: authn + cluster: + certificate-authority: /root/cdk/ca.crt + server: https://{{ host }}:{{ port }}/{{ api_ver }} +users: + - name: kube-apiserver +contexts: +- context: + cluster: authn + user: kube-apiserver + name: authn +current-context: authn diff --git a/kubernetes-master/templates/cdk.master.auth-webhook.logrotate b/kubernetes-master/templates/cdk.master.auth-webhook.logrotate new file mode 100644 index 0000000..aefa2b7 --- /dev/null +++ b/kubernetes-master/templates/cdk.master.auth-webhook.logrotate @@ -0,0 +1,11 @@ +{{ root_dir }}/*.log { + daily + rotate 10 + missingok + notifempty + compress + sharedscripts + postrotate + kill -USR1 $(cat {{ root_dir }}/{{ pidfile }}) + endscript +} diff --git a/kubernetes-master/templates/cdk.master.auth-webhook.py b/kubernetes-master/templates/cdk.master.auth-webhook.py new file mode 100644 index 0000000..394aa7f --- /dev/null +++ b/kubernetes-master/templates/cdk.master.auth-webhook.py @@ -0,0 +1,424 @@ +#!/usr/bin/env python3 + +import csv +import json +import logging +import aiohttp +import asyncio +import signal +from base64 import b64decode +from copy import deepcopy +from pathlib import Path +from yaml import safe_load, YAMLError + + +AWS_IAM_ENDPOINT = '{{ aws_iam_endpoint if aws_iam_endpoint }}' +KEYSTONE_ENDPOINT = '{{ keystone_endpoint if keystone_endpoint }}' +CUSTOM_AUTHN_ENDPOINT = '{{ custom_authn_endpoint if custom_authn_endpoint }}' + +app = aiohttp.web.Application() +routes = aiohttp.web.RouteTableDef() + +# Disable the gunicorn arbiter's SIGCHLD handler in this worker. The handler +# gets inherited by worker processes where it appears to serve no useful +# function. It also makes it impossible for workers to make subprocess calls +# safely, so, disable it. +# https://bugs.launchpad.net/charm-kubernetes-master/+bug/1938470 +signal.signal(signal.SIGCHLD, signal.SIG_DFL) + + +async def run(*args, timeout=10, **kwargs): + '''Run a CLI command. + + Returns retcode, stdout, and stderr (already decoded). + + If the process times out, the exit code will be 124 and stdout and stderr + will be empty. + + NOTE: + In Python 3.8+, the default process child watcher, ThreadedChildWatcher, + appears to have a race condition where it frequently attempts to wait for + the child process PID before it's visible, leading to a spurious warning + in the log about "Unknown child process", and a 255 exit code regardless + of what the child process actually exits with. The stdout and stderr will + still be available, however. + ''' + args = [str(arg) for arg in args] + kwargs.update( + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + async def _run(): + proc = await asyncio.create_subprocess_exec(*args, **kwargs) + stdout, stderr = await proc.communicate() + return proc.returncode, stdout.decode('utf8'), stderr.decode('utf8') + + try: + return await asyncio.wait_for(_run(), timeout=timeout) + except asyncio.TimeoutError: + app.logger.exception('Command timed out: {}'.format(' '.join(args))) + return 124, '', '' + + +async def kubectl(*args): + '''Run a kubectl CLI command with a config file. + + Returns retcode, stdout, and stderr. + ''' + # Try to use our service account kubeconfig; fall back to root if needed + kubectl_cmd = Path('/snap/bin/kubectl') + if not kubectl_cmd.is_file(): + # Fall back to anywhere on the path if the snap isn't available + kubectl_cmd = 'kubectl' + return await run(kubectl_cmd, '--kubeconfig=/root/.kube/config', *args) + + +def log_secret(text, obj, hide=True): + '''Log information about a TokenReview object. + + The message will always be logged at the 'debug' level and will be in the + form "text: obj". By default, secrets will be hidden. Set 'hide=False' to + have the secret printed in the output unobfuscated. + ''' + log_obj = obj + if obj and hide: + log_obj = deepcopy(obj) + try: + log_obj['spec']['token'] = '********' + except (KeyError, TypeError): + # No secret here, carry on + pass + app.logger.debug('{}: {}'.format(text, log_obj)) + + +async def check_token(token_review): + '''Populate user info if token is found in auth-related files.''' + app.logger.info('Checking token') + token_to_check = token_review['spec']['token'] + + # If we have an admin token, short-circuit all other checks. This prevents us + # from leaking our admin token to other authn services. + admin_kubeconfig = Path('/root/.kube/config') + data = None + try: + try: + data = safe_load(admin_kubeconfig.read_text()) + except Exception: + # Retry loading the file once, in case the charm was in the + # middle of rewriting it. See lp:1837930 for more info, but + # even without it being rewritten on every hook, there will + # always be a race condition to consider. + await asyncio.sleep(0.5) + data = safe_load(admin_kubeconfig.read_text()) + except YAMLError as e: + # we don't want to use logger.exception() or str(e) because it + # can leak tokens into the log + app.logger.error('Invalid kube config file: %s', type(e).__name__) + except Exception: + if not admin_kubeconfig.exists(): + app.logger.error('Missing kube config file') + elif data is None: + app.logger.error('Empty kube config file') + else: + app.logger.exception('Invalid kube config file') + else: + admin_token = data['users'][0]['user']['token'] + if token_to_check == admin_token: + # We have a valid admin + token_review['status'] = { + 'authenticated': True, + 'user': { + 'username': 'admin', + 'uid': 'admin', + 'groups': ['system:masters'] + } + } + return True + + # No admin? We're probably in an upgrade. Check an existing known_tokens.csv. + csv_fields = ['token', 'username', 'user', 'groups'] + known_tokens = Path('/root/cdk/known_tokens.csv') + try: + with known_tokens.open('r') as f: + data_by_token = {r['token']: r for r in csv.DictReader(f, csv_fields)} + except FileNotFoundError: + data_by_token = {} + + if token_to_check in data_by_token: + record = data_by_token[token_to_check] + # groups are optional; default to an empty string if we don't have any + groups = record.get('groups', '').split(',') + token_review['status'] = { + 'authenticated': True, + 'user': { + 'username': record['username'], + 'uid': record['user'], + 'groups': groups, + } + } + return True + return False + + +async def check_secrets(token_review): + '''Populate user info if token is found in k8s secrets.''' + # Only check secrets if kube-apiserver is up + app.logger.info('Checking secret') + token = token_review['spec']['token'] + + if token in app['secrets']: + token_review['status'] = { + 'authenticated': True, + 'user': app['secrets'][token], + } + return True + else: + return False + + +async def check_aws_iam(token_review): + '''Check the request with an AWS IAM authn server.''' + app.logger.info('Checking AWS IAM') + + # URL comes from /root/cdk/aws-iam-webhook.yaml + app.logger.debug('Forwarding to: {}'.format(AWS_IAM_ENDPOINT)) + + return await forward_request(token_review, AWS_IAM_ENDPOINT) + + +async def check_keystone(token_review): + '''Check the request with a Keystone authn server.''' + app.logger.info('Checking Keystone') + + # URL comes from /root/cdk/keystone/webhook.yaml + app.logger.debug('Forwarding to: {}'.format(KEYSTONE_ENDPOINT)) + + return await forward_request(token_review, KEYSTONE_ENDPOINT) + + +async def check_custom(token_review): + '''Check the request with a user-specified authn server.''' + app.logger.info('Checking Custom Endpoint') + + # User will set the URL in k8s-master config + app.logger.debug('Forwarding to: {}'.format(CUSTOM_AUTHN_ENDPOINT)) + + return await forward_request(token_review, CUSTOM_AUTHN_ENDPOINT) + + +async def forward_request(json_req, url): + '''Forward a JSON TokenReview request to a url. + + Returns True if the request is authenticated; False if the response is + either invalid or authn has been denied. + ''' + timeout = 10 + resp_text = '' + try: + async with aiohttp.ClientSession() as session: + try: + async with session.post(url, json=json_req, timeout=timeout) as resp: + resp_text = await resp.text() + except aiohttp.ClientSSLError: + app.logger.debug('SSLError with server; skipping cert validation') + async with session.post(url, + json=json_req, + verify_ssl=False, + timeout=timeout) as resp: + resp_text = await resp.text() + except asyncio.TimeoutError: + app.logger.error('Timed out contacting server') + return False + except Exception: + app.logger.exception('Failed to contact server') + return False + + # Check if the response is valid + try: + resp = json.loads(resp_text) + 'authenticated' in resp['status'] + except (KeyError, TypeError, ValueError): + log_secret(text='Invalid response from server', obj=resp_text) + return False + + # NB: When a forwarded request is authenticated, set the 'status' field to + # whatever the external server sends us. This ensures any status fields that + # the server wants to send makes it back to the kube apiserver. + if resp['status']['authenticated']: + json_req['status'] = resp['status'] + return True + return False + + +def ack(req, **kwargs): + # Successful checks will set auth and user data in the 'req' dict + log_secret(text='ACK', obj=req) + return aiohttp.web.json_response(req, **kwargs) + + +def nak(req, **kwargs): + # Force unauthenticated, just in case + req.setdefault('status', {})['authenticated'] = False + log_secret(text='NAK', obj=req) + return aiohttp.web.json_response(req, **kwargs) + + +@routes.post('/{{ api_ver }}') +async def webhook(request): + '''Listen on /$api_version for POST requests. + + For a POSTed TokenReview object, check every known authentication mechanism + for a user with a matching token. + + The /$api_version is expected to be the api version of the authentication.k8s.io + TokenReview that the k8s-apiserver will be sending. + + Returns: + TokenReview object with 'authenticated: True' and user attributes if a + token is found; otherwise, a TokenReview object with 'authenticated: False' + ''' + try: + req = await request.json() + except json.JSONDecodeError: + app.logger.debug('Unable to parse request') + return nak({}, status=400) + + # Make the request unauthenticated by deafult + req['status'] = {'authenticated': False} + + try: + valid = True if (req['kind'] == 'TokenReview' and + req['spec']['token']) else False + except (KeyError, TypeError): + valid = False + + if valid: + log_secret(text='REQ', obj=req) + else: + log_secret(text='Invalid request', obj=req) + return nak({}, status=400) + + if await check_token(req): + return ack(req) + + if not app['secrets']: + # If secrets aren't yet available, none of the system accounts will be + # functional and thus neither will the cluster, so there's no point to + # going any further. Additionally, we don't want to accidentally leak + # system account tokens to external auth endpoints. + app.logger.warning('Secrets not yet available; aborting') + return nak(req) + + if await check_secrets(req): + return ack(req) + + if AWS_IAM_ENDPOINT and await check_aws_iam(req): + return ack(req) + + if KEYSTONE_ENDPOINT and await check_keystone(req): + return ack(req) + + if CUSTOM_AUTHN_ENDPOINT and await check_custom(req): + return ack(req) + + return nak(req) + + +@routes.post('/slow-test') +async def slow_test(request): + app.logger.debug('Slow request started') + await asyncio.sleep(5) + app.logger.debug('Slow request finished') + return aiohttp.web.json_response({'status': {'authenticated': False}}) + + +async def refresh_secrets(app): + app.logger.info('Refreshing secrets') + retcode, stdout, stderr = await run( + 'systemctl', 'is-active', 'snap.kube-apiserver.daemon' + ) + # See note in run() docstring above about exit 255. + if retcode not in (0, 255) or stdout.strip() != 'active': + app.logger.info('Skipping secret refresh: kube-apiserver is not ready ' + '({}, {})'.format(retcode, stdout.strip())) + return + + retcode, stdout, stderr = await kubectl( + 'get', 'secrets', '-n', 'kube-system', '-o', 'json' + ) + # See note in run() docstring above about exit 255. + if retcode not in (0, 255) or stderr: + app.logger.warning('Unable to load secrets ({}): {}'.format(retcode, stderr)) + return + + try: + secrets = json.loads(stdout) + except json.JSONDecodeError: + app.logger.exception('Unable to parse secrets') + return + + new_secrets = {} + for secret in secrets.get('items', []): + try: + data_b64 = secret['data'] + username_b64 = data_b64['username'].encode('UTF-8') + password_b64 = data_b64['password'].encode('UTF-8') + groups_b64 = data_b64.get('groups', '').encode('UTF-8') + except (KeyError, TypeError): + # CK secrets will have populated 'data', but not all secrets do + continue + + username = uid = b64decode(username_b64).decode('UTF-8') + password = b64decode(password_b64).decode('UTF-8') + groups = b64decode(groups_b64).decode('UTF-8').split(',') + + # NB: CK creates k8s secrets with the 'password' field set as + # uid::token. Split the decoded password so we can send a 'uid' back. + # If there is no delimiter, set uid == username. + # TODO: make the delimeter less magical so it doesn't get out of + # sync with the function that creates secrets in k8s-master.py. + pw_delim = '::' + if pw_delim in password: + uid = password.rsplit(pw_delim, 1)[0] + new_secrets[password] = { + 'username': username, + 'uid': uid, + 'groups': groups, + } + app['secrets'] = new_secrets + + +async def startup(app): + # Log to gunicorn + glogger = logging.getLogger('gunicorn.error') + app.logger.handlers = glogger.handlers + app.logger.setLevel(glogger.level) + + async def _task(): + while True: + try: + await refresh_secrets(app) + await asyncio.sleep(60) + except asyncio.CancelledError: + break + except Exception: + app.logger.exception('Failed to get secrets') + + app['secrets'] = {} + app['secrets_task'] = asyncio.ensure_future(_task()) + + +async def cleanup(app): + task = app.get('secrets_task') + task.cancel() + await task + + +app.add_routes(routes) +app.on_startup.append(startup) +app.on_cleanup.append(cleanup) + + +if __name__ == '__main__': + aiohttp.web.run_app(app) diff --git a/kubernetes-master/templates/cdk.master.auth-webhook.service b/kubernetes-master/templates/cdk.master.auth-webhook.service new file mode 100644 index 0000000..d6e1515 --- /dev/null +++ b/kubernetes-master/templates/cdk.master.auth-webhook.service @@ -0,0 +1,25 @@ +[Unit] +Description=CDK master auth webhook +After=network.target +StartLimitIntervalSec=0 + +[Service] +User=root +WorkingDirectory={{ root_dir }} +ExecStart={{ charm_dir }}/../.venv/bin/gunicorn \ + --bind {{ host }}:{{ port }} \ + --capture-output \ + --certfile /root/cdk/server.crt \ + --disable-redirect-access-to-syslog \ + --error-logfile auth-webhook.log \ + --keyfile /root/cdk/server.key \ + --log-level debug \ + --pid {{ pidfile }} \ + --workers {{ num_workers }} \ + --worker-class aiohttp.worker.GunicornWebWorker \ + auth-webhook:app +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target diff --git a/kubernetes-master/templates/cdk.master.leader.file-watcher.path b/kubernetes-master/templates/cdk.master.leader.file-watcher.path new file mode 100644 index 0000000..3855649 --- /dev/null +++ b/kubernetes-master/templates/cdk.master.leader.file-watcher.path @@ -0,0 +1,7 @@ +[Path] +PathChanged=/root/cdk/basic_auth.csv +PathChanged=/root/cdk/known_tokens.csv +PathChanged=/root/cdk/serviceaccount.key + +[Install] +WantedBy=multi-user.target diff --git a/kubernetes-master/templates/cdk.master.leader.file-watcher.service b/kubernetes-master/templates/cdk.master.leader.file-watcher.service new file mode 100644 index 0000000..c192c2c --- /dev/null +++ b/kubernetes-master/templates/cdk.master.leader.file-watcher.service @@ -0,0 +1,10 @@ +[Unit] +Description=CDK master leader file-watcher +After=network.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/juju-run {{ unit }} /usr/local/sbin/cdk.master.leader.file-watcher.sh + +[Install] +WantedBy=multi-user.target diff --git a/kubernetes-master/templates/cdk.master.leader.file-watcher.sh b/kubernetes-master/templates/cdk.master.leader.file-watcher.sh new file mode 100644 index 0000000..01a2e86 --- /dev/null +++ b/kubernetes-master/templates/cdk.master.leader.file-watcher.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# This script is invoked by cdk.master.leader.file-watcher.service + +if [ is-leader ]; then + leader-set \ + "/root/cdk/basic_auth.csv=$(cat /root/cdk/basic_auth.csv)" \ + "/root/cdk/known_tokens.csv=$(cat /root/cdk/known_tokens.csv)" \ + "/root/cdk/serviceaccount.key=$(cat /root/cdk/serviceaccount.key)" +fi diff --git a/kubernetes-master/templates/ceph-secret.yaml b/kubernetes-master/templates/ceph-secret.yaml new file mode 100644 index 0000000..fc6b1fb --- /dev/null +++ b/kubernetes-master/templates/ceph-secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: ceph-secret +type: kubernetes.io/rbd +data: + key: {{ secret }} diff --git a/kubernetes-master/templates/ceph.conf b/kubernetes-master/templates/ceph.conf new file mode 100644 index 0000000..d27c522 --- /dev/null +++ b/kubernetes-master/templates/ceph.conf @@ -0,0 +1,18 @@ +[global] +auth cluster required = {{ auth_supported }} +auth service required = {{ auth_supported }} +auth client required = {{ auth_supported }} +keyring = /etc/ceph/$cluster.$name.keyring +mon host = {{ mon_hosts }} +fsid = {{ fsid }} + +log to syslog = {{ use_syslog }} +err to syslog = {{ use_syslog }} +clog to syslog = {{ use_syslog }} +mon cluster log to syslog = {{ use_syslog }} +debug mon = {{ loglevel }}/5 +debug osd = {{ loglevel }}/5 + +[client] +log file = /var/log/ceph.log + diff --git a/kubernetes-master/templates/create-namespace.yaml.j2 b/kubernetes-master/templates/create-namespace.yaml.j2 new file mode 100644 index 0000000..a121ecc --- /dev/null +++ b/kubernetes-master/templates/create-namespace.yaml.j2 @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: {{ name }} + labels: + name: {{ name }} diff --git a/kubernetes-master/templates/grafana/autoload/kubernetes.json b/kubernetes-master/templates/grafana/autoload/kubernetes.json new file mode 100644 index 0000000..8f26875 --- /dev/null +++ b/kubernetes-master/templates/grafana/autoload/kubernetes.json @@ -0,0 +1,4032 @@ +{ + "dashboard":{ + "annotations":{ + "list":[ + { + "builtIn":1, + "datasource":"-- Grafana --", + "enable":true, + "hide":true, + "iconColor":"rgba(0, 211, 255, 1)", + "name":"Annotations & Alerts", + "type":"dashboard" + } + ] + }, + "description":"Monitors Kubernetes cluster using Prometheus. Shows overall cluster CPU / Memory / Filesystem usage as well as individual pod, containers, systemd services statistics.", + "editable":true, + "gnetId":315, + "graphTooltip":0, + "id":null, + "iteration":1572969306389, + "links":[ + + ], + "panels":[ + { + "collapsed":false, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":0 + }, + "id":49, + "panels":[ + + ], + "repeat":null, + "title":"Total usage", + "type":"row" + }, + { + "cacheTimeout":null, + "colorBackground":false, + "colorValue":true, + "colors":[ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource":"prometheus - Juju generated source", + "editable":true, + "error":false, + "format":"percent", + "gauge":{ + "maxValue":100, + "minValue":0, + "show":true, + "thresholdLabels":false, + "thresholdMarkers":true + }, + "gridPos":{ + "h":5, + "w":8, + "x":0, + "y":1 + }, + "height":"180px", + "id":4, + "interval":null, + "links":[ + + ], + "mappingType":1, + "mappingTypes":[ + { + "name":"value to text", + "value":1 + }, + { + "name":"range to text", + "value":2 + } + ], + "maxDataPoints":100, + "nullPointMode":"connected", + "nullText":null, + "postfix":"", + "postfixFontSize":"50%", + "prefix":"", + "prefixFontSize":"50%", + "rangeMaps":[ + { + "from":"null", + "text":"N/A", + "to":"null" + } + ], + "sparkline":{ + "fillColor":"rgba(31, 118, 189, 0.18)", + "full":false, + "lineColor":"rgb(31, 120, 193)", + "show":false + }, + "tableColumn":"", + "targets":[ + { + "expr":"sum (container_memory_working_set_bytes{id=\"/\"}) / sum (machine_memory_bytes{}) * 100", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"", + "refId":"A", + "step":300 + } + ], + "thresholds":"65, 90", + "title":"Cluster memory usage", + "transparent":false, + "type":"singlestat", + "valueFontSize":"80%", + "valueMaps":[ + { + "op":"=", + "text":"N/A", + "value":"null" + } + ], + "valueName":"current" + }, + { + "cacheTimeout":null, + "colorBackground":false, + "colorValue":true, + "colors":[ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "format":"percent", + "gauge":{ + "maxValue":100, + "minValue":0, + "show":true, + "thresholdLabels":false, + "thresholdMarkers":true + }, + "gridPos":{ + "h":5, + "w":8, + "x":8, + "y":1 + }, + "height":"180px", + "id":6, + "interval":null, + "links":[ + + ], + "mappingType":1, + "mappingTypes":[ + { + "name":"value to text", + "value":1 + }, + { + "name":"range to text", + "value":2 + } + ], + "maxDataPoints":100, + "nullPointMode":"connected", + "nullText":null, + "postfix":"", + "postfixFontSize":"50%", + "prefix":"", + "prefixFontSize":"50%", + "rangeMaps":[ + { + "from":"null", + "text":"N/A", + "to":"null" + } + ], + "sparkline":{ + "fillColor":"rgba(31, 118, 189, 0.18)", + "full":false, + "lineColor":"rgb(31, 120, 193)", + "show":false + }, + "tableColumn":"", + "targets":[ + { + "expr":"sum (rate (container_cpu_usage_seconds_total{id=\"/\"}[1m])) / sum (machine_cpu_cores{}) * 100", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"", + "refId":"A", + "step":300 + } + ], + "thresholds":"65, 90", + "title":"Cluster CPU usage (1m avg)", + "type":"singlestat", + "valueFontSize":"80%", + "valueMaps":[ + { + "op":"=", + "text":"N/A", + "value":"null" + } + ], + "valueName":"current" + }, + { + "cacheTimeout":null, + "colorBackground":false, + "colorValue":true, + "colors":[ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "format":"percent", + "gauge":{ + "maxValue":100, + "minValue":0, + "show":true, + "thresholdLabels":false, + "thresholdMarkers":true + }, + "gridPos":{ + "h":5, + "w":8, + "x":16, + "y":1 + }, + "height":"180px", + "id":7, + "interval":null, + "links":[ + + ], + "mappingType":1, + "mappingTypes":[ + { + "name":"value to text", + "value":1 + }, + { + "name":"range to text", + "value":2 + } + ], + "maxDataPoints":100, + "nullPointMode":"connected", + "nullText":null, + "postfix":"", + "postfixFontSize":"50%", + "prefix":"", + "prefixFontSize":"50%", + "rangeMaps":[ + { + "from":"null", + "text":"N/A", + "to":"null" + } + ], + "sparkline":{ + "fillColor":"rgba(31, 118, 189, 0.18)", + "full":false, + "lineColor":"rgb(31, 120, 193)", + "show":false + }, + "tableColumn":"", + "targets":[ + { + "expr":"sum (container_fs_usage_bytes{}) / sum (container_fs_limit_bytes{id=\"/\"}) * 100", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"", + "metric":"", + "refId":"A", + "step":300 + } + ], + "thresholds":"65, 90", + "title":"Cluster filesystem usage", + "type":"singlestat", + "valueFontSize":"80%", + "valueMaps":[ + { + "op":"=", + "text":"N/A", + "value":"null" + } + ], + "valueName":"current" + }, + { + "cacheTimeout":null, + "colorBackground":false, + "colorValue":false, + "colors":[ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "format":"bytes", + "gauge":{ + "maxValue":100, + "minValue":0, + "show":false, + "thresholdLabels":false, + "thresholdMarkers":true + }, + "gridPos":{ + "h":3, + "w":4, + "x":0, + "y":6 + }, + "height":"1px", + "id":9, + "interval":null, + "links":[ + + ], + "mappingType":1, + "mappingTypes":[ + { + "name":"value to text", + "value":1 + }, + { + "name":"range to text", + "value":2 + } + ], + "maxDataPoints":100, + "nullPointMode":"connected", + "nullText":null, + "postfix":"", + "postfixFontSize":"20%", + "prefix":"", + "prefixFontSize":"20%", + "rangeMaps":[ + { + "from":"null", + "text":"N/A", + "to":"null" + } + ], + "sparkline":{ + "fillColor":"rgba(31, 118, 189, 0.18)", + "full":false, + "lineColor":"rgb(31, 120, 193)", + "show":false + }, + "tableColumn":"", + "targets":[ + { + "expr":"sum (container_memory_working_set_bytes{id=\"/\"})", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"", + "refId":"A", + "step":300 + } + ], + "thresholds":"", + "title":"Used", + "type":"singlestat", + "valueFontSize":"50%", + "valueMaps":[ + { + "op":"=", + "text":"N/A", + "value":"null" + } + ], + "valueName":"current" + }, + { + "cacheTimeout":null, + "colorBackground":false, + "colorValue":false, + "colors":[ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "format":"bytes", + "gauge":{ + "maxValue":100, + "minValue":0, + "show":false, + "thresholdLabels":false, + "thresholdMarkers":true + }, + "gridPos":{ + "h":3, + "w":4, + "x":4, + "y":6 + }, + "height":"1px", + "id":10, + "interval":null, + "links":[ + + ], + "mappingType":1, + "mappingTypes":[ + { + "name":"value to text", + "value":1 + }, + { + "name":"range to text", + "value":2 + } + ], + "maxDataPoints":100, + "nullPointMode":"connected", + "nullText":null, + "postfix":"", + "postfixFontSize":"50%", + "prefix":"", + "prefixFontSize":"50%", + "rangeMaps":[ + { + "from":"null", + "text":"N/A", + "to":"null" + } + ], + "sparkline":{ + "fillColor":"rgba(31, 118, 189, 0.18)", + "full":false, + "lineColor":"rgb(31, 120, 193)", + "show":false + }, + "tableColumn":"", + "targets":[ + { + "expr":"sum (machine_memory_bytes{})", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "refId":"A", + "step":300 + } + ], + "thresholds":"", + "title":"Total", + "type":"singlestat", + "valueFontSize":"50%", + "valueMaps":[ + { + "op":"=", + "text":"N/A", + "value":"null" + } + ], + "valueName":"current" + }, + { + "cacheTimeout":null, + "colorBackground":false, + "colorValue":false, + "colors":[ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "format":"none", + "gauge":{ + "maxValue":100, + "minValue":0, + "show":false, + "thresholdLabels":false, + "thresholdMarkers":true + }, + "gridPos":{ + "h":3, + "w":4, + "x":8, + "y":6 + }, + "height":"1px", + "id":11, + "interval":null, + "links":[ + + ], + "mappingType":1, + "mappingTypes":[ + { + "name":"value to text", + "value":1 + }, + { + "name":"range to text", + "value":2 + } + ], + "maxDataPoints":100, + "nullPointMode":"connected", + "nullText":null, + "postfix":" cores", + "postfixFontSize":"30%", + "prefix":"", + "prefixFontSize":"50%", + "rangeMaps":[ + { + "from":"null", + "text":"N/A", + "to":"null" + } + ], + "sparkline":{ + "fillColor":"rgba(31, 118, 189, 0.18)", + "full":false, + "lineColor":"rgb(31, 120, 193)", + "show":false + }, + "tableColumn":"", + "targets":[ + { + "expr":"sum (rate (container_cpu_usage_seconds_total{id=\"/\"}[1m]))", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "refId":"A", + "step":300 + } + ], + "thresholds":"", + "title":"Used", + "type":"singlestat", + "valueFontSize":"50%", + "valueMaps":[ + { + "op":"=", + "text":"N/A", + "value":"null" + } + ], + "valueName":"current" + }, + { + "cacheTimeout":null, + "colorBackground":false, + "colorValue":false, + "colors":[ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "format":"none", + "gauge":{ + "maxValue":100, + "minValue":0, + "show":false, + "thresholdLabels":false, + "thresholdMarkers":true + }, + "gridPos":{ + "h":3, + "w":4, + "x":12, + "y":6 + }, + "height":"1px", + "id":12, + "interval":null, + "links":[ + + ], + "mappingType":1, + "mappingTypes":[ + { + "name":"value to text", + "value":1 + }, + { + "name":"range to text", + "value":2 + } + ], + "maxDataPoints":100, + "nullPointMode":"connected", + "nullText":null, + "postfix":" cores", + "postfixFontSize":"30%", + "prefix":"", + "prefixFontSize":"50%", + "rangeMaps":[ + { + "from":"null", + "text":"N/A", + "to":"null" + } + ], + "sparkline":{ + "fillColor":"rgba(31, 118, 189, 0.18)", + "full":false, + "lineColor":"rgb(31, 120, 193)", + "show":false + }, + "tableColumn":"", + "targets":[ + { + "expr":"sum (machine_cpu_cores{})", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "refId":"A", + "step":300 + } + ], + "thresholds":"", + "title":"Total", + "type":"singlestat", + "valueFontSize":"50%", + "valueMaps":[ + { + "op":"=", + "text":"N/A", + "value":"null" + } + ], + "valueName":"current" + }, + { + "cacheTimeout":null, + "colorBackground":false, + "colorValue":false, + "colors":[ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "format":"bytes", + "gauge":{ + "maxValue":100, + "minValue":0, + "show":false, + "thresholdLabels":false, + "thresholdMarkers":true + }, + "gridPos":{ + "h":3, + "w":4, + "x":16, + "y":6 + }, + "height":"1px", + "id":13, + "interval":null, + "links":[ + + ], + "mappingType":1, + "mappingTypes":[ + { + "name":"value to text", + "value":1 + }, + { + "name":"range to text", + "value":2 + } + ], + "maxDataPoints":100, + "nullPointMode":"connected", + "nullText":null, + "postfix":"", + "postfixFontSize":"50%", + "prefix":"", + "prefixFontSize":"50%", + "rangeMaps":[ + { + "from":"null", + "text":"N/A", + "to":"null" + } + ], + "sparkline":{ + "fillColor":"rgba(31, 118, 189, 0.18)", + "full":false, + "lineColor":"rgb(31, 120, 193)", + "show":false + }, + "tableColumn":"", + "targets":[ + { + "expr":"sum (container_fs_usage_bytes{id=\"/\"})", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"", + "refId":"A", + "step":300 + } + ], + "thresholds":"", + "title":"Used", + "type":"singlestat", + "valueFontSize":"50%", + "valueMaps":[ + { + "op":"=", + "text":"N/A", + "value":"null" + } + ], + "valueName":"current" + }, + { + "cacheTimeout":null, + "colorBackground":false, + "colorValue":false, + "colors":[ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "format":"bytes", + "gauge":{ + "maxValue":100, + "minValue":0, + "show":false, + "thresholdLabels":false, + "thresholdMarkers":true + }, + "gridPos":{ + "h":3, + "w":4, + "x":20, + "y":6 + }, + "height":"1px", + "id":14, + "interval":null, + "links":[ + + ], + "mappingType":1, + "mappingTypes":[ + { + "name":"value to text", + "value":1 + }, + { + "name":"range to text", + "value":2 + } + ], + "maxDataPoints":100, + "nullPointMode":"connected", + "nullText":null, + "postfix":"", + "postfixFontSize":"50%", + "prefix":"", + "prefixFontSize":"50%", + "rangeMaps":[ + { + "from":"null", + "text":"N/A", + "to":"null" + } + ], + "sparkline":{ + "fillColor":"rgba(31, 118, 189, 0.18)", + "full":false, + "lineColor":"rgb(31, 120, 193)", + "show":false + }, + "tableColumn":"", + "targets":[ + { + "expr":"sum (container_fs_limit_bytes{id=\"/\"})", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"", + "refId":"A", + "step":300 + } + ], + "thresholds":"", + "title":"Total", + "type":"singlestat", + "valueFontSize":"50%", + "valueMaps":[ + { + "op":"=", + "text":"N/A", + "value":"null" + } + ], + "valueName":"current" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":9 + }, + "id":50, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "description":"", + "fill":1, + "gridPos":{ + "h":7, + "w":24, + "x":0, + "y":10 + }, + "id":31, + "legend":{ + "avg":false, + "current":false, + "max":false, + "min":false, + "show":true, + "total":false, + "values":false + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"cpu_usage_idle{cpu=\"cpu-total\",host=~\".*kubernetes-master.*\"}", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"{{host}}", + "refId":"A" + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"kubernetes-master CPU idle", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"percent", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + } + ], + "repeat":null, + "title":"kubernetes-master CPU idle", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":9 + }, + "id":51, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "description":"", + "fill":1, + "gridPos":{ + "h":7, + "w":24, + "x":0, + "y":10 + }, + "id":48, + "legend":{ + "avg":false, + "current":false, + "max":false, + "min":false, + "show":true, + "total":false, + "values":false + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"cpu_usage_idle{cpu=\"cpu-total\",host=~\".*kubernetes-worker.*\"}", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"{{host}}", + "refId":"A" + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"kubernetes-worker CPU idle", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"percent", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + } + ], + "repeat":null, + "title":"kubernetes-worker CPU idle", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":10 + }, + "id":52, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":0, + "y":11 + }, + "id":41, + "legend":{ + "avg":false, + "current":false, + "max":false, + "min":false, + "show":false, + "total":false, + "values":false + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":true, + "steppedLine":false, + "targets":[ + { + "expr":"sum(rate(apiserver_request_latencies_sum{}[1m])) / sum(rate(apiserver_request_latencies_count{}[1m]))", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"{{username}}", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"API request latency", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"ms", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + }, + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":12, + "y":11 + }, + "id":37, + "legend":{ + "avg":false, + "current":false, + "max":false, + "min":false, + "show":true, + "total":false, + "values":false + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"sum(rate(apiserver_request_count{}[1m])) by (code)", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"{{code}}", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"API server hits by code", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + }, + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":0, + "y":18 + }, + "id":38, + "legend":{ + "avg":false, + "current":false, + "max":false, + "min":false, + "show":true, + "total":false, + "values":false + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":true, + "steppedLine":false, + "targets":[ + { + "expr":"rate(authenticated_user_requests{}[1m])", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"{{username}}", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Authenticated user requests", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + }, + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":0, + "gridPos":{ + "h":7, + "w":12, + "x":12, + "y":18 + }, + "id":39, + "legend":{ + "avg":false, + "current":false, + "max":false, + "min":false, + "show":true, + "total":false, + "values":false + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"histogram_quantile(0.95, sum(rate(apiserver_request_latencies_bucket{}[5m])) by (le,resource) )", + "format":"time_series", + "hide":false, + "intervalFactor":2, + "legendFormat":"{{resource}}", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"API request latency by resource 95th percentile", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"ms", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + }, + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":0, + "y":25 + }, + "id":40, + "legend":{ + "avg":false, + "current":false, + "max":false, + "min":false, + "show":true, + "total":false, + "values":false + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"histogram_quantile(0.95, sum(rate(apiserver_request_latencies_bucket{}[5m])) by (le,verb) )", + "format":"time_series", + "hide":false, + "intervalFactor":2, + "legendFormat":"{{verb}}", + "refId":"A", + "step":30 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"API request latency by resource 95th percentile", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"ms", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + } + ], + "repeat":null, + "title":"API server", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":11 + }, + "id":53, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":0, + "y":32 + }, + "id":42, + "legend":{ + "avg":false, + "current":false, + "max":false, + "min":false, + "show":true, + "total":false, + "values":false + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"rate(admission_quota_controller_adds{}[10m])", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Admission requests (10min avg)", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"none", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + } + ] + }, + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":12, + "y":32 + }, + "id":44, + "legend":{ + "avg":false, + "current":false, + "max":false, + "min":false, + "show":true, + "total":false, + "values":false + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"rate(admission_quota_controller_queue_latency_sum{}[10m]) / rate(admission_quota_controller_queue_latency_count{}[10m])", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Admission controller queue latency (10min avg)", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"ms", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + } + ] + }, + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":0, + "y":39 + }, + "id":43, + "legend":{ + "avg":false, + "current":false, + "max":false, + "min":false, + "show":true, + "total":false, + "values":false + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"rate(admission_quota_controller_work_duration_sum{}[10m]) / rate(admission_quota_controller_work_duration_count{}[10m])", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Admission controller work (10min avg)", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"ms", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + } + ] + } + ], + "repeat":null, + "title":"Admission controller", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":12 + }, + "id":54, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":0, + "y":13 + }, + "id":36, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":true, + "min":true, + "show":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"etcd_request_cache_get_latencies_summary{}", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"Quantile {{quantile}}", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Cache request latencies (get)", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"ms", + "label":null, + "logBase":1, + "max":null, + "min":"0", + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + }, + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":12, + "y":13 + }, + "id":35, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":true, + "min":true, + "show":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"etcd_request_cache_add_latencies_summary{}", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"Quantile {{quantile}}", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Cache request latencies (add)", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"ms", + "label":null, + "logBase":1, + "max":null, + "min":"0", + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + }, + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":0, + "y":20 + }, + "id":33, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":true, + "min":true, + "show":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"etcd_helper_cache_hit_count{} / (etcd_helper_cache_miss_count{} + etcd_helper_cache_hit_count{}) * 100", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"Hit ratio", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Cache hit ratio", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"percent", + "label":null, + "logBase":1, + "max":"100", + "min":"0", + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + }, + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "fill":1, + "gridPos":{ + "h":7, + "w":12, + "x":12, + "y":20 + }, + "id":34, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":true, + "min":true, + "show":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":1, + "links":[ + + ], + "nullPointMode":"null", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"sum(rate(etcd_request_latencies_summary_sum{}[1m])) by (operation) / sum(rate(etcd_request_latencies_summary_count{}[1m])) by (operation)", + "format":"time_series", + "intervalFactor":2, + "legendFormat":"{{operation}}", + "refId":"A", + "step":60 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Average cache request latencies", + "tooltip":{ + "shared":true, + "sort":0, + "value_type":"individual" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"ms", + "label":null, + "logBase":1, + "max":null, + "min":"0", + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + } + ], + "repeat":null, + "title":"Etcd", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":13 + }, + "id":55, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":3, + "editable":true, + "error":false, + "fill":0, + "grid":{ + + }, + "gridPos":{ + "h":7, + "w":24, + "x":0, + "y":48 + }, + "height":"", + "id":17, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":false, + "min":false, + "rightSide":true, + "show":true, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":true, + "targets":[ + { + "expr":"sum(rate (container_cpu_usage_seconds_total{image!=\"\",container=~\"$container\",namespace=~\"$namespace\"}[1m])) by (pod)", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"{{pod}}", + "metric":"container_cpu", + "refId":"A", + "step":15 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Pods CPU usage (1m avg)", + "tooltip":{ + "msResolution":true, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "transparent":false, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"none", + "label":"cores", + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ] + } + ], + "repeat":null, + "title":"Pods CPU usage", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":14 + }, + "id":56, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":3, + "description":"", + "editable":true, + "error":false, + "fill":0, + "grid":{ + + }, + "gridPos":{ + "h":7, + "w":24, + "x":0, + "y":49 + }, + "height":"", + "id":24, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "hideEmpty":false, + "hideZero":false, + "max":false, + "min":false, + "rightSide":true, + "show":true, + "sideWidth":null, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":true, + "targets":[ + { + "expr":"sum(rate (container_cpu_usage_seconds_total{image!=\"\",container!=\"POD\",container=~\"$container\",namespace=~\"$namespace\"}[1m])) without (cpu)", + "format":"time_series", + "hide":false, + "interval":"10s", + "intervalFactor":1, + "legendFormat":"{{container}} (pod: {{pod}}", + "metric":"container_cpu", + "refId":"A", + "step":15 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Container CPU usage (1m avg)", + "tooltip":{ + "msResolution":true, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"none", + "label":"cores", + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ] + } + ], + "repeat":null, + "title":"Containers CPU usage", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":15 + }, + "id":57, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":3, + "editable":true, + "error":false, + "fill":0, + "grid":{ + + }, + "gridPos":{ + "h":7, + "w":24, + "x":0, + "y":50 + }, + "height":"", + "id":23, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":false, + "min":false, + "rightSide":true, + "show":true, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":true, + "targets":[ + { + "expr":"sum (rate (container_cpu_usage_seconds_total{id=~\".*systemd.*service$\"}[1m])) by (id)", + "format":"time_series", + "hide":false, + "interval":"10s", + "intervalFactor":1, + "legendFormat":"{{id}}", + "metric":"container_cpu", + "refId":"A", + "step":15 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"System services CPU usage (1m avg)", + "tooltip":{ + "msResolution":true, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"none", + "label":"cores", + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ] + } + ], + "repeat":null, + "title":"System services CPU usage", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":16 + }, + "id":58, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":3, + "editable":true, + "error":false, + "fill":0, + "grid":{ + + }, + "gridPos":{ + "h":13, + "w":24, + "x":0, + "y":51 + }, + "id":20, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":false, + "min":false, + "rightSide":false, + "show":true, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":true, + "targets":[ + { + "expr":"sum (rate (container_cpu_usage_seconds_total{id!=\"/\"}[1m])) by (id)", + "format":"time_series", + "hide":false, + "interval":"10s", + "intervalFactor":1, + "legendFormat":"{{id}}", + "metric":"container_cpu", + "refId":"A", + "step":15 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"All processes CPU usage (1m avg)", + "tooltip":{ + "msResolution":true, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"none", + "label":"cores", + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ] + } + ], + "repeat":null, + "title":"All processes CPU usage", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":17 + }, + "id":59, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "fill":0, + "grid":{ + + }, + "gridPos":{ + "h":7, + "w":24, + "x":0, + "y":52 + }, + "id":25, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":false, + "min":false, + "rightSide":true, + "show":true, + "sideWidth":200, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":true, + "targets":[ + { + "expr":"sum (container_memory_working_set_bytes{image!=\"\",container=~\"$container\",namespace=~\"$namespace\"}) by (pod)", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"{{pod}}", + "metric":"container_memory_usage:sort_desc", + "refId":"A", + "step":10 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Pods memory usage", + "tooltip":{ + "msResolution":false, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"bytes", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ] + } + ], + "repeat":null, + "title":"Pods memory usage", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":18 + }, + "id":60, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "fill":0, + "grid":{ + + }, + "gridPos":{ + "h":7, + "w":24, + "x":0, + "y":53 + }, + "id":27, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":false, + "min":false, + "rightSide":true, + "show":true, + "sideWidth":200, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":true, + "targets":[ + { + "expr":"sum (container_memory_working_set_bytes{image!=\"\",container!=\"POD\",container=~\"$container\",namespace=~\"$namespace\"}) by (container, pod)", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"{{container}} (pod: {{pod}})", + "metric":"container_memory_usage:sort_desc", + "refId":"A", + "step":10 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Containers memory usage", + "tooltip":{ + "msResolution":false, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"bytes", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ] + } + ], + "repeat":null, + "title":"Containers memory usage", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":19 + }, + "id":61, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "fill":0, + "grid":{ + + }, + "gridPos":{ + "h":7, + "w":24, + "x":0, + "y":54 + }, + "id":26, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":false, + "min":false, + "rightSide":true, + "show":true, + "sideWidth":200, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":true, + "targets":[ + { + "expr":"sum (container_memory_working_set_bytes{id=~\".*systemd.*service$\"}) by (id)", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"{{id}}", + "metric":"container_memory_usage:sort_desc", + "refId":"A", + "step":10 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"System services memory usage", + "tooltip":{ + "msResolution":false, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"bytes", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ] + } + ], + "repeat":null, + "title":"System services memory usage", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":20 + }, + "id":62, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "fill":0, + "grid":{ + + }, + "gridPos":{ + "h":13, + "w":24, + "x":0, + "y":55 + }, + "id":28, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":false, + "min":false, + "rightSide":false, + "show":true, + "sideWidth":200, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":true, + "targets":[ + { + "expr":"sum (container_memory_working_set_bytes{id!=\"/\"}) by (id)", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"{{id}}", + "metric":"container_memory_usage:sort_desc", + "refId":"A", + "step":10 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"All processes memory usage", + "tooltip":{ + "msResolution":false, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"bytes", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ] + } + ], + "repeat":null, + "title":"All processes memory usage", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":21 + }, + "id":63, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "fill":1, + "grid":{ + + }, + "gridPos":{ + "h":10, + "w":24, + "x":0, + "y":22 + }, + "height":"400px", + "id":32, + "legend":{ + "alignAsTable":true, + "avg":false, + "current":true, + "max":true, + "min":true, + "rightSide":false, + "show":true, + "sideWidth":200, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"sum (rate (container_network_receive_bytes_total{}[1m]))", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"Received", + "metric":"network", + "refId":"A", + "step":15 + }, + { + "expr":"- sum (rate (container_network_transmit_bytes_total{}[1m]))", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"Sent", + "metric":"network", + "refId":"B", + "step":15 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Network I/O pressure", + "tooltip":{ + "msResolution":false, + "shared":true, + "sort":0, + "value_type":"cumulative" + }, + "transparent":false, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"Bps", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"Bps", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + } + ], + "repeat":null, + "title":"Network I/O", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":22 + }, + "id":64, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "fill":1, + "grid":{ + + }, + "gridPos":{ + "h":7, + "w":24, + "x":0, + "y":23 + }, + "id":16, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":false, + "min":false, + "rightSide":true, + "show":true, + "sideWidth":200, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"sum (rate (container_network_receive_bytes_total{image!=\"\",pod=~\"$container.*\",namespace=~\"$namespace\"}[1m])) by (pod)", + "format":"time_series", + "interval":"10s", + "intervalFactor":1, + "legendFormat":"-> {{pod}}", + "metric":"network", + "refId":"A", + "step":15 + }, + { + "expr":"- sum (rate (container_network_transmit_bytes_total{image!=\"\",pod=~\"$container.*\",namespace=~\"$namespace\"}[1m])) by (pod)", + "format":"time_series", + "hide":false, + "interval":"10s", + "intervalFactor":1, + "legendFormat":"<- {{pod}}", + "metric":"network", + "refId":"B", + "step":15 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Pods network I/O (1m avg)", + "tooltip":{ + "msResolution":false, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"Bps", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + } + ], + "repeat":null, + "title":"Pods network I/O", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":23 + }, + "id":65, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":2, + "editable":true, + "error":false, + "fill":1, + "grid":{ + + }, + "gridPos":{ + "h":13, + "w":24, + "x":0, + "y":24 + }, + "id":29, + "legend":{ + "alignAsTable":true, + "avg":true, + "current":true, + "max":false, + "min":false, + "rightSide":false, + "show":true, + "sideWidth":200, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":false, + "targets":[ + { + "expr":"sum (rate (container_network_receive_bytes_total{id!=\"/\"}[1m])) by (id)", + "format":"time_series", + "hide":false, + "interval":"10s", + "intervalFactor":1, + "legendFormat":"-> {{id}}", + "metric":"network", + "refId":"A", + "step":15 + }, + { + "expr":"- sum (rate (container_network_transmit_bytes_total{id!=\"/\"}[1m])) by (id)", + "format":"time_series", + "hide":false, + "interval":"10s", + "intervalFactor":1, + "legendFormat":"<- {{id}}", + "metric":"network", + "refId":"B", + "step":15 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"All processes network I/O (1m avg)", + "tooltip":{ + "msResolution":false, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "format":"Bps", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ], + "yaxis":{ + "align":false, + "alignLevel":null + } + } + ], + "repeat":null, + "title":"All processes network I/O", + "type":"row" + }, + { + "collapsed":true, + "gridPos":{ + "h":1, + "w":24, + "x":0, + "y":24 + }, + "id":66, + "panels":[ + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":3, + "description":"", + "editable":true, + "error":false, + "fill":0, + "grid":{ + + }, + "gridPos":{ + "h":7, + "w":8, + "x":0, + "y":59 + }, + "height":"", + "id":45, + "legend":{ + "alignAsTable":true, + "avg":false, + "current":true, + "hideEmpty":false, + "hideZero":false, + "max":false, + "min":false, + "rightSide":true, + "show":true, + "sideWidth":null, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":true, + "targets":[ + { + "expr":"sum by (container) (container_fs_usage_bytes{id!=\"/\"})", + "format":"time_series", + "hide":false, + "interval":"10s", + "intervalFactor":1, + "legendFormat":"{{container}}", + "metric":"container_cpu", + "refId":"A", + "step":15 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Total Bytes Used By Containers", + "tooltip":{ + "msResolution":true, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "decimals":null, + "format":"bytes", + "label":"Bytes", + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ] + }, + { + "aliasColors":{ + + }, + "bars":false, + "dashLength":10, + "dashes":false, + "datasource":"prometheus - Juju generated source", + "decimals":3, + "description":"", + "editable":true, + "error":false, + "fill":0, + "grid":{ + + }, + "gridPos":{ + "h":7, + "w":8, + "x":8, + "y":59 + }, + "height":"", + "id":46, + "legend":{ + "alignAsTable":true, + "avg":false, + "current":true, + "hideEmpty":false, + "hideZero":false, + "max":false, + "min":false, + "rightSide":true, + "show":true, + "sideWidth":null, + "sort":"current", + "sortDesc":true, + "total":false, + "values":true + }, + "lines":true, + "linewidth":2, + "links":[ + + ], + "nullPointMode":"connected", + "percentage":false, + "pointradius":5, + "points":false, + "renderer":"flot", + "seriesOverrides":[ + + ], + "spaceLength":10, + "stack":false, + "steppedLine":true, + "targets":[ + { + "expr":"sum by (container) (container_fs_usage_bytes{id!=\"/\"}) / on (environment) group_left(id) sum (container_fs_usage_bytes{id=\"/\"}) * 100", + "format":"time_series", + "hide":false, + "interval":"10s", + "intervalFactor":1, + "legendFormat":"{{container}}", + "metric":"container_cpu", + "refId":"A", + "step":15 + } + ], + "thresholds":[ + + ], + "timeFrom":null, + "timeShift":null, + "title":"Current Usage Breakdown By Container", + "tooltip":{ + "msResolution":true, + "shared":true, + "sort":2, + "value_type":"cumulative" + }, + "type":"graph", + "xaxis":{ + "buckets":null, + "mode":"time", + "name":null, + "show":true, + "values":[ + + ] + }, + "yaxes":[ + { + "decimals":null, + "format":"percent", + "label":"Percent", + "logBase":1, + "max":null, + "min":null, + "show":true + }, + { + "format":"short", + "label":null, + "logBase":1, + "max":null, + "min":null, + "show":false + } + ] + }, + { + "cacheTimeout":null, + "colorBackground":false, + "colorValue":false, + "colors":[ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource":"prometheus - Juju generated source", + "decimals":2, + "format":"bytes", + "gauge":{ + "maxValue":100, + "minValue":0, + "show":false, + "thresholdLabels":false, + "thresholdMarkers":true + }, + "gridPos":{ + "h":7, + "w":8, + "x":16, + "y":59 + }, + "id":47, + "interval":null, + "links":[ + + ], + "mappingType":1, + "mappingTypes":[ + { + "name":"value to text", + "value":1 + }, + { + "name":"range to text", + "value":2 + } + ], + "maxDataPoints":100, + "nullPointMode":"connected", + "nullText":null, + "postfix":"", + "postfixFontSize":"50%", + "prefix":"", + "prefixFontSize":"50%", + "rangeMaps":[ + { + "from":"null", + "text":"N/A", + "to":"null" + } + ], + "sparkline":{ + "fillColor":"rgba(31, 118, 189, 0.18)", + "full":false, + "lineColor":"rgb(31, 120, 193)", + "show":false + }, + "tableColumn":"", + "targets":[ + { + "expr":"sum(container_fs_usage_bytes{id=\"/\"}) - sum(container_fs_usage_bytes{id!=\"/\"})", + "format":"time_series", + "instant":false, + "intervalFactor":2, + "refId":"A" + } + ], + "thresholds":"", + "title":"Total Bytes Not Used By Containers", + "transparent":false, + "type":"singlestat", + "valueFontSize":"80%", + "valueMaps":[ + { + "op":"=", + "text":"N/A", + "value":"null" + } + ], + "valueName":"avg" + } + ], + "repeat":null, + "title":"Container Disk Utilisation", + "type":"row" + } + ], + "schemaVersion":16, + "style":"dark", + "tags":[ + "Juju", + "Kubernetes" + ], + "templating":{ + "list":[ + { + "allValue":".*", + "current":{ + + }, + "datasource":"prometheus - Juju generated source", + "hide":0, + "includeAll":true, + "label":null, + "multi":false, + "name":"namespace", + "options":[ + + ], + "query":"label_values(container_memory_usage_bytes{namespace=~\".+\",container!=\"POD\"},namespace)", + "refresh":1, + "regex":"", + "sort":1, + "tagValuesQuery":"", + "tags":[ + + ], + "tagsQuery":"", + "type":"query", + "useTags":false + }, + { + "allValue":".*", + "current":{ + + }, + "datasource":"prometheus - Juju generated source", + "hide":0, + "includeAll":true, + "label":null, + "multi":false, + "name":"container", + "options":[ + + ], + "query":"label_values(container_memory_usage_bytes{namespace=~\"$namespace\",container!=\"POD\"},container)", + "refresh":1, + "regex":"", + "sort":1, + "tagValuesQuery":"", + "tags":[ + + ], + "tagsQuery":"", + "type":"query", + "useTags":false + } + ] + }, + "time":{ + "from":"now-6h", + "to":"now" + }, + "timepicker":{ + "refresh_intervals":[ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options":[ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone":"utc", + "title":"Charmed Kubernetes Dashboard", + "version":1 + } +} \ No newline at end of file diff --git a/kubernetes-master/templates/grafana/conditional/prometheus.json b/kubernetes-master/templates/grafana/conditional/prometheus.json new file mode 100644 index 0000000..35c428a --- /dev/null +++ b/kubernetes-master/templates/grafana/conditional/prometheus.json @@ -0,0 +1,2186 @@ +{ + "dashboard": { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Derived from https://grafana.com/dashboards/315", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1554419177157, + "links": [ + + ], + "panels": [ + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 0 + }, + "height": "200px", + "id": 32, + "legend": { + "alignAsTable": false, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": false, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum (rate (container_network_receive_bytes_total{kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m]))", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "Received", + "metric": "network", + "refId": "A", + "step": 10 + }, + { + "expr": "- sum (rate (container_network_transmit_bytes_total{kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m]))", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "Sent", + "metric": "network", + "refId": "B", + "step": 10 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeRegions": [ + + ], + "timeShift": null, + "title": "Network I/O pressure", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "transparent": false, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 0, + "y": 5 + }, + "height": "180px", + "id": 4, + "interval": null, + "links": [ + + ], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum (container_memory_working_set_bytes{kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}) / sum (machine_memory_bytes{kubernetes_io_hostname=~\"^$Node$\"}) * 100", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "thresholds": "", + "title": "Cluster memory usage", + "transparent": false, + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 8, + "y": 5 + }, + "height": "180px", + "id": 6, + "interval": null, + "links": [ + + ], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum (rate (container_cpu_usage_seconds_total{kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) / sum (machine_cpu_cores{kubernetes_io_hostname=~\"^$Node$\"}) * 100", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "thresholds": "", + "title": "Cluster CPU usage (1m avg)", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 5, + "w": 8, + "x": 16, + "y": 5 + }, + "height": "180px", + "id": 7, + "interval": null, + "links": [ + + ], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum (container_fs_usage_bytes{device=~\"^/dev/.*$\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}) / sum (container_fs_limit_bytes{device=~\"^/dev/.*$\",id=\"/\",kubernetes_io_hostname=~\"^$Node$\"}) * 100", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 10 + } + ], + "thresholds": "", + "title": "Cluster filesystem usage", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "format": "bytes", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 0, + "y": 10 + }, + "height": "1px", + "id": 9, + "interval": null, + "links": [ + + ], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "20%", + "prefix": "", + "prefixFontSize": "20%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum (container_memory_working_set_bytes{kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"})", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "thresholds": "", + "title": "Used", + "type": "singlestat", + "valueFontSize": "50%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "format": "bytes", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 4, + "y": 10 + }, + "height": "1px", + "id": 10, + "interval": null, + "links": [ + + ], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum (machine_memory_bytes{kubernetes_io_hostname=~\"^$Node$\"})", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "thresholds": "", + "title": "Total", + "type": "singlestat", + "valueFontSize": "50%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 8, + "y": 10 + }, + "height": "1px", + "id": 11, + "interval": null, + "links": [ + + ], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": " cores", + "postfixFontSize": "30%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum (rate (container_cpu_usage_seconds_total{kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m]))", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "thresholds": "", + "title": "Used", + "type": "singlestat", + "valueFontSize": "50%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 12, + "y": 10 + }, + "height": "1px", + "id": 12, + "interval": null, + "links": [ + + ], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": " cores", + "postfixFontSize": "30%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum (machine_cpu_cores{kubernetes_io_hostname=~\"^$Node$\"})", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "thresholds": "", + "title": "Total", + "type": "singlestat", + "valueFontSize": "50%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "format": "bytes", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 16, + "y": 10 + }, + "height": "1px", + "id": 13, + "interval": null, + "links": [ + + ], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum (container_fs_usage_bytes{device=~\"^/dev/.*$\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"})", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "thresholds": "", + "title": "Used", + "type": "singlestat", + "valueFontSize": "50%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "format": "bytes", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 20, + "y": 10 + }, + "height": "1px", + "id": 14, + "interval": null, + "links": [ + + ], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum (container_fs_limit_bytes{device=~\"^/dev/.*$\",id=\"/\",kubernetes_io_hostname=~\"^$Node$\"})", + "interval": "10s", + "intervalFactor": 1, + "refId": "A", + "step": 10 + } + ], + "thresholds": "", + "title": "Total", + "type": "singlestat", + "valueFontSize": "50%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "decimals": 3, + "editable": true, + "error": false, + "fill": 0, + "grid": { + + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 13 + }, + "height": "", + "id": 17, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "expr": "sum (rate (container_cpu_usage_seconds_total{image!=\"\",name=~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (pod_name)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ pod_name }}", + "metric": "container_cpu", + "refId": "A", + "step": 10 + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "B" + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeRegions": [ + + ], + "timeShift": null, + "title": "Pods CPU usage (1m avg)", + "tooltip": { + "msResolution": true, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "transparent": false, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "none", + "label": "cores", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "decimals": 3, + "editable": true, + "error": false, + "fill": 0, + "grid": { + + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 20 + }, + "height": "", + "id": 24, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": null, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "expr": "sum (rate (container_cpu_usage_seconds_total{image!=\"\",name=~\"^k8s_.*\",container_name!=\"POD\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (container_name, pod_name)", + "format": "time_series", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "pod: {{ pod_name }} | {{ container_name }}", + "metric": "container_cpu", + "refId": "A", + "step": 10 + }, + { + "expr": "sum (rate (container_cpu_usage_seconds_total{image!=\"\",name!~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (kubernetes_io_hostname, name, image)", + "format": "time_series", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "docker: {{ kubernetes_io_hostname }} | {{ image }} ({{ name }})", + "metric": "container_cpu", + "refId": "B", + "step": 10 + }, + { + "expr": "sum (rate (container_cpu_usage_seconds_total{rkt_container_name!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (kubernetes_io_hostname, rkt_container_name)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "rkt: {{ kubernetes_io_hostname }} | {{ rkt_container_name }}", + "metric": "container_cpu", + "refId": "C", + "step": 10 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeRegions": [ + + ], + "timeShift": null, + "title": "Containers CPU usage (1m avg)", + "tooltip": { + "msResolution": true, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "none", + "label": "cores", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "decimals": 3, + "editable": true, + "error": false, + "fill": 0, + "grid": { + + }, + "gridPos": { + "h": 13, + "w": 24, + "x": 0, + "y": 27 + }, + "id": 20, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "expr": "sum (rate (container_cpu_usage_seconds_total{id!=\"/\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (id)", + "format": "time_series", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ id }}", + "metric": "container_cpu", + "refId": "A", + "step": 10 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeRegions": [ + + ], + "timeShift": null, + "title": "All processes CPU usage (1m avg)", + "tooltip": { + "msResolution": true, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "none", + "label": "cores", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "fill": 0, + "grid": { + + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 40 + }, + "id": 25, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "expr": "sum (container_memory_working_set_bytes{image!=\"\",name=~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}) by (pod_name)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ pod_name }}", + "metric": "container_memory_usage:sort_desc", + "refId": "A", + "step": 10 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeRegions": [ + + ], + "timeShift": null, + "title": "Pods memory usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "fill": 0, + "grid": { + + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 47 + }, + "id": 27, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "expr": "sum (container_memory_working_set_bytes{image!=\"\",name=~\"^k8s_.*\",container_name!=\"POD\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}) by (container_name, pod_name)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "pod: {{ pod_name }} | {{ container_name }}", + "metric": "container_memory_usage:sort_desc", + "refId": "A", + "step": 10 + }, + { + "expr": "sum (container_memory_working_set_bytes{image!=\"\",name!~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}) by (kubernetes_io_hostname, name, image)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "docker: {{ kubernetes_io_hostname }} | {{ image }} ({{ name }})", + "metric": "container_memory_usage:sort_desc", + "refId": "B", + "step": 10 + }, + { + "expr": "sum (container_memory_working_set_bytes{rkt_container_name!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}) by (kubernetes_io_hostname, rkt_container_name)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "rkt: {{ kubernetes_io_hostname }} | {{ rkt_container_name }}", + "metric": "container_memory_usage:sort_desc", + "refId": "C", + "step": 10 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeRegions": [ + + ], + "timeShift": null, + "title": "Containers memory usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "fill": 0, + "grid": { + + }, + "gridPos": { + "h": 13, + "w": 24, + "x": 0, + "y": 54 + }, + "id": 28, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "expr": "sum (container_memory_working_set_bytes{id!=\"/\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}) by (id)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "{{ id }}", + "metric": "container_memory_usage:sort_desc", + "refId": "A", + "step": 10 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeRegions": [ + + ], + "timeShift": null, + "title": "All processes memory usage", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 67 + }, + "id": 16, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum (rate (container_network_receive_bytes_total{image!=\"\",name=~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (pod_name)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "-> {{ pod_name }}", + "metric": "network", + "refId": "A", + "step": 10 + }, + { + "expr": "- sum (rate (container_network_transmit_bytes_total{image!=\"\",name=~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (pod_name)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "<- {{ pod_name }}", + "metric": "network", + "refId": "B", + "step": 10 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeRegions": [ + + ], + "timeShift": null, + "title": "Pods network I/O (1m avg)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 74 + }, + "id": 30, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": true, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum (rate (container_network_receive_bytes_total{image!=\"\",name=~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (container_name, pod_name)", + "format": "time_series", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "-> pod: {{ pod_name }} | {{ container_name }}", + "metric": "network", + "refId": "B", + "step": 10 + }, + { + "expr": "- sum (rate (container_network_transmit_bytes_total{image!=\"\",name=~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (container_name, pod_name)", + "format": "time_series", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "<- pod: {{ pod_name }} | {{ container_name }}", + "metric": "network", + "refId": "D", + "step": 10 + }, + { + "expr": "sum (rate (container_network_receive_bytes_total{image!=\"\",name!~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (kubernetes_io_hostname, name, image)", + "format": "time_series", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "-> docker: {{ kubernetes_io_hostname }} | {{ image }} ({{ name }})", + "metric": "network", + "refId": "A", + "step": 10 + }, + { + "expr": "- sum (rate (container_network_transmit_bytes_total{image!=\"\",name!~\"^k8s_.*\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (kubernetes_io_hostname, name, image)", + "format": "time_series", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "<- docker: {{ kubernetes_io_hostname }} | {{ image }} ({{ name }})", + "metric": "network", + "refId": "C", + "step": 10 + }, + { + "expr": "sum (rate (container_network_transmit_bytes_total{rkt_container_name!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (kubernetes_io_hostname, rkt_container_name)", + "format": "time_series", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "-> rkt: {{ kubernetes_io_hostname }} | {{ rkt_container_name }}", + "metric": "network", + "refId": "E", + "step": 10 + }, + { + "expr": "- sum (rate (container_network_transmit_bytes_total{rkt_container_name!=\"\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (kubernetes_io_hostname, rkt_container_name)", + "format": "time_series", + "hide": false, + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "<- rkt: {{ kubernetes_io_hostname }} | {{ rkt_container_name }}", + "metric": "network", + "refId": "F", + "step": 10 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeRegions": [ + + ], + "timeShift": null, + "title": "Containers network I/O (1m avg)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "decimals": 2, + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "gridPos": { + "h": 13, + "w": 24, + "x": 0, + "y": 81 + }, + "id": 29, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "sideWidth": 200, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum (rate (container_network_receive_bytes_total{id!=\"/\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (id)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "-> {{ id }}", + "metric": "network", + "refId": "A", + "step": 10 + }, + { + "expr": "- sum (rate (container_network_transmit_bytes_total{id!=\"/\",kubernetes_io_hostname=~\"^$Node$\",namespace=~\"^$Namespace$\"}[1m])) by (id)", + "format": "time_series", + "interval": "10s", + "intervalFactor": 1, + "legendFormat": "<- {{ id }}", + "metric": "network", + "refId": "B", + "step": 10 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeRegions": [ + + ], + "timeShift": null, + "title": "All processes network I/O (1m avg)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 2, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "Juju" + ], + "templating": { + "list": [ + { + "allValue": ".*", + "current": { + "selected": true, + "text": "All", + "value": "$__all" + }, + "datasource": "prometheus - Juju generated source", + "definition": "", + "hide": 0, + "includeAll": true, + "label": null, + "multi": false, + "name": "Node", + "options": [ + + ], + "query": "label_values(kubernetes_io_hostname)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [ + + ], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "tags": [ + + ], + "text": "All", + "value": "$__all" + }, + "datasource": "prometheus - Juju generated source", + "definition": "label_values(namespace)", + "hide": 0, + "includeAll": true, + "label": null, + "multi": false, + "name": "Namespace", + "options": [ + + ], + "query": "label_values(namespace)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [ + + ], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Kubernetes Metrics (via Prometheus)", + "version": 35 + }, + "overwrite": false +} diff --git a/kubernetes-master/templates/grafana/conditional/telegraf.json b/kubernetes-master/templates/grafana/conditional/telegraf.json new file mode 100644 index 0000000..1e9c0f1 --- /dev/null +++ b/kubernetes-master/templates/grafana/conditional/telegraf.json @@ -0,0 +1,2094 @@ +{ + "dashboard": { + "annotations": { + "list": [ + + ] + }, + "description": "Derived from https://grafana.com/dashboards/941", + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "hideControls": false, + "id": null, + "links": [ + + ], + "rows": [ + { + "collapse": false, + "height": "250px", + "panels": [ + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_load5{host=~\"$node\"} ", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Node load average 5m", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_load15{host=~\"$node\"} ", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Node load average 15m", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_load1{host=~\"$node\"} ", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Node load average 1m", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": true, + "title": "Load average", + "titleSize": "h6" + }, + { + "collapse": true, + "height": "250px", + "panels": [ + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "processes_running{host=~\"$node\"} ", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 60 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Process running", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "processes_stopped{host=~\"$node\"} ", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 60 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Process stopped", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 6, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "processes_paging{host=~\"$node\"} ", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 60 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Process waiting", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": true, + "title": "Processes statistics", + "titleSize": "h6" + }, + { + "collapse": false, + "height": "250px", + "panels": [ + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 7, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "cpu_usage_steal{cpu=\"cpu-total\", host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "CPU steal", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "cpu_usage_iowait{cpu=\"cpu-total\", host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "CPU wait", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 9, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "cpu_usage_user{cpu=\"cpu-total\", host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "CPU user", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "cpu_usage_system{cpu=\"cpu-total\", host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "CPU system", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 11, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "cpu_usage_softirq{cpu=\"cpu-total\", host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "CPU soft interrupts", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "cpu_usage_irq{cpu=\"cpu-total\", host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "CPU interrupts", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 13, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "cpu_usage_nice{cpu=\"cpu-total\", host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "CPU nice", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 14, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "cpu_usage_idle{cpu=\"cpu-total\", host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "CPU Idle", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": true, + "title": "CPU usage", + "titleSize": "h6" + }, + { + "collapse": false, + "height": "250px", + "panels": [ + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 15, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "mem_cached{host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Mem cached", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 16, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "mem_buffered{host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Mem buffered", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 17, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "mem_free{host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Mem free", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 18, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "mem_used{host=~\"$node\"}", + "intervalFactor": 2, + "legendFormat": "{{host}}", + "refId": "A", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Mem used", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": true, + "title": "Memory usage", + "titleSize": "h6" + }, + { + "collapse": false, + "height": "250px", + "panels": [ + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 19, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(diskio_reads{name=~\"$disk\", host=~\"$node\"}[5m])", + "intervalFactor": 2, + "legendFormat": "Read {{host}} {{name}}", + "refId": "A", + "step": 2 + }, + { + "expr": "rate(diskio_writes{name=~\"$disk\", host=~\"$node\"}[5m])", + "intervalFactor": 2, + "legendFormat": "Write {{host}} {{name}}", + "refId": "B", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Disk read/s and write/s", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + }, + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 20, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 6, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(diskio_read_bytes{name=~\"$disk\", host=~\"$node\"}[5m])", + "intervalFactor": 2, + "legendFormat": "Read {{host}} {{name}}", + "refId": "A", + "step": 2 + }, + { + "expr": "rate(diskio_write_bytes{name=~\"$disk\", host=~\"$node\"}[5m])", + "intervalFactor": 2, + "legendFormat": "Write {{host}} {{name}}", + "refId": "B", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Disk read/s and write/s", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "Bps", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": true, + "title": "Disk statistics", + "titleSize": "h6" + }, + { + "collapse": false, + "height": "250px", + "panels": [ + { + "aliasColors": { + + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "prometheus - Juju generated source", + "editable": true, + "error": false, + "fill": 1, + "grid": { + + }, + "id": 22, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [ + + ], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + + ], + "spaceLength": 10, + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(net_bytes_sent{interface=~\"$interface\", host=~\"$node\"}[5m])*8", + "intervalFactor": 2, + "legendFormat": "Out {{host}} {{interface}}", + "refId": "A", + "step": 2 + }, + { + "expr": "rate(net_bytes_recv{interface=~\"$interface\", host=~\"$node\"}[5m])*8", + "intervalFactor": 2, + "legendFormat": "In {{host}} {{interface}}", + "refId": "B", + "step": 2 + } + ], + "thresholds": [ + + ], + "timeFrom": null, + "timeShift": null, + "title": "Network load", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [ + + ] + }, + "yaxes": [ + { + "format": "bps", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "repeat": null, + "repeatIteration": null, + "repeatRowId": null, + "showTitle": true, + "title": "Network", + "titleSize": "h6" + } + ], + "schemaVersion": 14, + "style": "dark", + "tags": [ + "Juju" + ], + "templating": { + "list": [ + { + "allValue": ".*", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "prometheus - Juju generated source", + "hide": 0, + "includeAll": true, + "label": null, + "multi": false, + "name": "node", + "options": [ + + ], + "query": "label_values(host)", + "refresh": 1, + "regex": "", + "sort": 0, + "tagValuesQuery": "", + "tags": [ + + ], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Node Metrics (via Telegraf)", + "version": 4 + }, + "overwrite": false +} diff --git a/kubernetes-master/templates/keystone-api-server-webhook.yaml b/kubernetes-master/templates/keystone-api-server-webhook.yaml new file mode 100644 index 0000000..684c3ee --- /dev/null +++ b/kubernetes-master/templates/keystone-api-server-webhook.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Config +preferences: {} +clusters: + - cluster: + server: https://{{ keystone_service_cluster_ip }}:8443/webhook + insecure-skip-tls-verify: true + name: webhook +users: + - name: webhook +contexts: + - context: + cluster: webhook + user: webhook + name: webhook +current-context: webhook diff --git a/kubernetes-master/templates/kube-keystone.sh b/kubernetes-master/templates/kube-keystone.sh new file mode 100644 index 0000000..b9bd5bd --- /dev/null +++ b/kubernetes-master/templates/kube-keystone.sh @@ -0,0 +1,51 @@ +# Copyright 2018 The Kubernetes Authors. +# +# 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. + +# Replace with your public address and port for keystone +export OS_AUTH_URL="{{ protocol }}://{{ address }}:{{ port }}/v{{ version }}" +#export OS_PROJECT_NAME=k8s +#export OS_DOMAIN_NAME=k8s +#export OS_USERNAME=myuser +#export OS_PASSWORD=secure_pw +get_keystone_token() { + data='{ + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "name": "'"${OS_USERNAME}"'", + "domain": { "name": "'"${OS_DOMAIN_NAME}"'" }, + "password": "'"${OS_PASSWORD}"'" + } + } + }, + "scope": { + "project": { + "domain": { + "name": "'"${OS_DOMAIN_NAME}"'" + }, + "name": "'"${OS_PROJECT_NAME}"'" + } + } + } +}' + token=$(curl -s -i -H "Content-Type: application/json" -d "${data}" "${OS_AUTH_URL}/auth/tokens" |grep 'X-Subject-Token') + if [ -z "$token" ]; then + echo "Invalid authentication information" + else + echo $(echo ${token} | awk -F ': ' '{print $2}' | sed -e 's/[[:space:]]*$//') + fi +} +echo "Function get_keystone_token created. Type get_keystone_token in order to generate a login token for the Kubernetes dashboard." diff --git a/kubernetes-master/templates/kube-proxy-iptables-fix.sh b/kubernetes-master/templates/kube-proxy-iptables-fix.sh new file mode 100644 index 0000000..a6d219e --- /dev/null +++ b/kubernetes-master/templates/kube-proxy-iptables-fix.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# add the chain, note that adding twice is ok as it will just error. +/sbin/iptables -t nat -N KUBE-MARK-DROP + +# need to check the creation of the rule to ensure we only create it once. +if ! /sbin/iptables -t nat -C KUBE-MARK-DROP -j MARK --set-xmark 0x8000/0x8000 &> /dev/null; then + /sbin/iptables -t nat -A KUBE-MARK-DROP -j MARK --set-xmark 0x8000/0x8000 +fi diff --git a/kubernetes-master/templates/nagios_plugin.py b/kubernetes-master/templates/nagios_plugin.py new file mode 100644 index 0000000..1b2a329 --- /dev/null +++ b/kubernetes-master/templates/nagios_plugin.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2019 Canonical Ltd. + +import nagios_plugin3 +import socket +from subprocess import check_output + +snap_resources = ['kubectl', 'kube-apiserver', 'kube-controller-manager', + 'kube-scheduler', 'cdk-addons', 'kube-proxy'] + + +def check_snaps_installed(): + """Confirm the snaps are installed, raise an error if not""" + for snap_name in snap_resources: + cmd = ['snap', 'list', snap_name] + try: + check_output(cmd).decode('UTF-8') + except Exception: + msg = '{} snap is not installed'.format(snap_name) + raise nagios_plugin3.CriticalError(msg) + + +def test_connection(host, port): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + s.connect((host, int(port))) + s.shutdown(socket.SHUT_RDWR) + finally: + s.close() + + +def verify_remote_connection_to_apiserver(): + try: + test_connection(socket.gethostbyname(socket.gethostname()), 6443) + except Exception: + raise nagios_plugin3.CriticalError("Unable to reach " + "API server on remote port") + + +def main(): + nagios_plugin3.try_check(check_snaps_installed) + nagios_plugin3.try_check(verify_remote_connection_to_apiserver) + print("OK - API server is up and accessible") + + +if __name__ == "__main__": + main() diff --git a/kubernetes-master/templates/prometheus/k8s-api-endpoints.yaml.j2 b/kubernetes-master/templates/prometheus/k8s-api-endpoints.yaml.j2 new file mode 100644 index 0000000..83625e6 --- /dev/null +++ b/kubernetes-master/templates/prometheus/k8s-api-endpoints.yaml.j2 @@ -0,0 +1,16 @@ +job_name: 'k8s-api-endpoints' +kubernetes_sd_configs: +- api_server: https://{{k8s_api_address}}:{{k8s_api_port}} + role: endpoints + tls_config: + ca_file: __placeholder__ + bearer_token: {{k8s_token}} +scrape_interval: 30s +scheme: https +tls_config: + ca_file: __placeholder__ +bearer_token: {{k8s_token}} +relabel_configs: +- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] + action: keep + regex: default;kubernetes;https diff --git a/kubernetes-master/templates/prometheus/kube-state-metrics.yaml.j2 b/kubernetes-master/templates/prometheus/kube-state-metrics.yaml.j2 new file mode 100644 index 0000000..a0692fe --- /dev/null +++ b/kubernetes-master/templates/prometheus/kube-state-metrics.yaml.j2 @@ -0,0 +1,10 @@ +job_name: 'kube-state-metrics' +scrape_interval: 30s +scheme: https +tls_config: + ca_file: __placeholder__ +bearer_token: {{k8s_token}} +metrics_path: /api/v1/namespaces/kube-system/services/kube-state-metrics:8080/proxy/metrics +static_configs: + - targets: + - {{k8s_api_address}}:{{k8s_api_port}} diff --git a/kubernetes-master/templates/prometheus/kube-state-telemetry.yaml.j2 b/kubernetes-master/templates/prometheus/kube-state-telemetry.yaml.j2 new file mode 100644 index 0000000..799ef39 --- /dev/null +++ b/kubernetes-master/templates/prometheus/kube-state-telemetry.yaml.j2 @@ -0,0 +1,10 @@ +job_name: 'kube-state-telemetry' +scrape_interval: 30s +scheme: https +tls_config: + ca_file: __placeholder__ +bearer_token: {{k8s_token}} +metrics_path: /api/v1/namespaces/kube-system/services/kube-state-metrics:8081/proxy/metrics +static_configs: + - targets: + - {{k8s_api_address}}:{{k8s_api_port}} diff --git a/kubernetes-master/templates/prometheus/kubernetes-cadvisor.yaml.j2 b/kubernetes-master/templates/prometheus/kubernetes-cadvisor.yaml.j2 new file mode 100644 index 0000000..0ff8ed5 --- /dev/null +++ b/kubernetes-master/templates/prometheus/kubernetes-cadvisor.yaml.j2 @@ -0,0 +1,21 @@ +job_name: 'kubernetes-cadvisor' +kubernetes_sd_configs: +- api_server: https://{{k8s_api_address}}:{{k8s_api_port}} + role: node + tls_config: + ca_file: __placeholder__ + bearer_token: {{k8s_token}} +scrape_interval: 30s +scheme: https +tls_config: + ca_file: __placeholder__ +bearer_token: {{k8s_token}} +relabel_configs: +- action: labelmap + regex: __meta_kubernetes_node_label_(.+) +- target_label: __address__ + replacement: {{k8s_api_address}}:{{k8s_api_port}} +- source_labels: [__meta_kubernetes_node_name] + regex: (.+) + target_label: __metrics_path__ + replacement: /api/v1/nodes/$1/proxy/metrics/cadvisor diff --git a/kubernetes-master/templates/prometheus/kubernetes-nodes.yaml.j2 b/kubernetes-master/templates/prometheus/kubernetes-nodes.yaml.j2 new file mode 100644 index 0000000..32ea077 --- /dev/null +++ b/kubernetes-master/templates/prometheus/kubernetes-nodes.yaml.j2 @@ -0,0 +1,21 @@ +job_name: 'kubernetes-nodes' +kubernetes_sd_configs: +- api_server: https://{{k8s_api_address}}:{{k8s_api_port}} + role: node + tls_config: + ca_file: __placeholder__ + bearer_token: {{k8s_token}} +scrape_interval: 30s +scheme: https +tls_config: + ca_file: __placeholder__ +bearer_token: {{k8s_token}} +relabel_configs: +- action: labelmap + regex: __meta_kubernetes_node_label_(.+) +- target_label: __address__ + replacement: {{k8s_api_address}}:{{k8s_api_port}} +- source_labels: [__meta_kubernetes_node_name] + regex: (.+) + target_label: __metrics_path__ + replacement: /api/v1/nodes/$1/proxy/metrics diff --git a/kubernetes-master/templates/rbac-pod-security-policy.yaml b/kubernetes-master/templates/rbac-pod-security-policy.yaml new file mode 100644 index 0000000..d95eb4d --- /dev/null +++ b/kubernetes-master/templates/rbac-pod-security-policy.yaml @@ -0,0 +1,55 @@ +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: privileged + annotations: + seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*' +spec: + privileged: true + allowPrivilegeEscalation: true + allowedCapabilities: + - '*' + volumes: + - '*' + hostNetwork: true + hostPorts: + - min: 0 + max: 65535 + hostIPC: true + hostPID: true + runAsUser: + rule: 'RunAsAny' + seLinux: + rule: 'RunAsAny' + supplementalGroups: + rule: 'RunAsAny' + fsGroup: + rule: 'RunAsAny' + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: privileged +rules: +- apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: + - privileged + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: privileged +roleRef: + kind: ClusterRole + name: privileged + apiGroup: rbac.authorization.k8s.io +subjects: +- kind: Group + name: system:serviceaccounts + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/kubernetes-master/templates/rbac-proxy.yaml b/kubernetes-master/templates/rbac-proxy.yaml new file mode 100644 index 0000000..7b570ca --- /dev/null +++ b/kubernetes-master/templates/rbac-proxy.yaml @@ -0,0 +1,24 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: proxy-clusterrole-cdk-{{ juju_application }} +rules: +- apiGroups: [""] + resources: + - nodes/metrics + - nodes/proxy + verbs: ["get", "list", "watch", "create", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: proxy-role-binding-cdk-{{ juju_application }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: proxy-clusterrole-cdk-{{ juju_application }} +subjects: {% for proxy_user in proxy_users %} +- apiGroup: rbac.authorization.k8s.io + kind: User + name: {{ proxy_user }} +{% endfor %} diff --git a/kubernetes-master/templates/service-always-restart.systemd-229.conf b/kubernetes-master/templates/service-always-restart.systemd-229.conf new file mode 100644 index 0000000..d5cf4b1 --- /dev/null +++ b/kubernetes-master/templates/service-always-restart.systemd-229.conf @@ -0,0 +1,5 @@ +[Unit] +StartLimitInterval=0 + +[Service] +RestartSec=10 diff --git a/kubernetes-master/templates/service-always-restart.systemd-latest.conf b/kubernetes-master/templates/service-always-restart.systemd-latest.conf new file mode 100644 index 0000000..3dd37ab --- /dev/null +++ b/kubernetes-master/templates/service-always-restart.systemd-latest.conf @@ -0,0 +1,5 @@ +[Unit] +StartLimitIntervalSec=0 + +[Service] +RestartSec=10 diff --git a/kubernetes-master/templates/service-iptables-fix.service b/kubernetes-master/templates/service-iptables-fix.service new file mode 100644 index 0000000..913aed3 --- /dev/null +++ b/kubernetes-master/templates/service-iptables-fix.service @@ -0,0 +1,11 @@ +[Unit] +Description=Apply iptables rule for KUBE-MARK-DROP +After=network.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/kube-proxy-iptables-fix.sh +RemainAfterExit=true + +[Install] +WantedBy=multi-user.target diff --git a/kubernetes-master/templates/system-monitoring-rbac-role.yaml b/kubernetes-master/templates/system-monitoring-rbac-role.yaml new file mode 100644 index 0000000..ebbd95e --- /dev/null +++ b/kubernetes-master/templates/system-monitoring-rbac-role.yaml @@ -0,0 +1,29 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: system:monitoring +rules: +- apiGroups: [""] + resources: + - "endpoints" + - "nodes" + - "nodes/proxy" + - "pods" + - "services" + - "services/proxy" + verbs: ["get", "list", "watch"] +- nonResourceURLs: ["/metrics"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: system:monitoring +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:monitoring +subjects: +- apiGroup: rbac.authorization.k8s.io + kind: User + name: system:monitoring diff --git a/kubernetes-master/templates/vaultlocker-loop@.service b/kubernetes-master/templates/vaultlocker-loop@.service new file mode 100644 index 0000000..8dd0230 --- /dev/null +++ b/kubernetes-master/templates/vaultlocker-loop@.service @@ -0,0 +1,12 @@ +[Install] +RequiredBy=vaultlocker-decrypt@%i.service + +[Unit] +Description=Ensure loop device for VaultLocker %i +Before=vaultlocker-decrypt@%i.service + +[Service] +Type=oneshot +RemainAfterExit=true +EnvironmentFile=/etc/vaultlocker/loop-envs/%i +ExecStart=/sbin/losetup -f ${BACK_FILE} diff --git a/kubernetes-master/templates/vaultlocker.conf.j2 b/kubernetes-master/templates/vaultlocker.conf.j2 new file mode 100644 index 0000000..911a54c --- /dev/null +++ b/kubernetes-master/templates/vaultlocker.conf.j2 @@ -0,0 +1,5 @@ +[vault] +url = {{ vault_url }} +approle = {{ role_id }} +backend = {{ secret_backend }} +secret_id = {{ secret_id }} diff --git a/kubernetes-master/tests/functional/conftest.py b/kubernetes-master/tests/functional/conftest.py new file mode 100644 index 0000000..a92e249 --- /dev/null +++ b/kubernetes-master/tests/functional/conftest.py @@ -0,0 +1,4 @@ +import charms.unit_test + + +charms.unit_test.patch_reactive() diff --git a/kubernetes-master/tests/functional/test_k8s_common.py b/kubernetes-master/tests/functional/test_k8s_common.py new file mode 100644 index 0000000..4b867e6 --- /dev/null +++ b/kubernetes-master/tests/functional/test_k8s_common.py @@ -0,0 +1,90 @@ +from functools import partial + +import pytest +from unittest import mock +from charms.layer import kubernetes_common + + +class TestCreateKubeConfig: + @pytest.fixture(autouse=True) + def _files(self, tmp_path): + self.cfg_file = tmp_path / "config" + self.ca_file = tmp_path / "ca.crt" + self.ca_file.write_text("foo") + self.ckc = partial( + kubernetes_common.create_kubeconfig, + self.cfg_file, + "server", + self.ca_file, + ) + + def test_guard_clauses(self): + with pytest.raises(ValueError): + self.ckc() + assert not self.cfg_file.exists() + with pytest.raises(ValueError): + self.ckc(token="token", password="password") + assert not self.cfg_file.exists() + with pytest.raises(ValueError): + self.ckc(key="key") + assert not self.cfg_file.exists() + + def test_file_creation(self): + self.ckc(password="password") + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert cfg_data_1 + + def test_idempotency(self): + self.ckc(password="password") + cfg_data_1 = self.cfg_file.read_text() + self.ckc(password="password") + cfg_data_2 = self.cfg_file.read_text() + # Verify that calling w/ the same data keeps the same file contents. + assert cfg_data_2 == cfg_data_1 + + def test_efficient_updates(self): + self.ckc(password="old_password") + cfg_stat_1 = self.cfg_file.stat() + self.ckc(password="old_password") + cfg_stat_2 = self.cfg_file.stat() + self.ckc(password="new_password") + cfg_stat_3 = self.cfg_file.stat() + # Verify that calling with the same data doesn't + # modify the file at all, but that new data does + assert cfg_stat_1.st_mtime == cfg_stat_2.st_mtime < cfg_stat_3.st_mtime + + def test_aws_iam(self): + self.ckc(password="password", aws_iam_cluster_id="aws-cluster") + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert "aws-cluster" in cfg_data_1 + + def test_keystone(self): + self.ckc(password="password", keystone=True) + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert "keystone-user" in cfg_data_1 + assert "exec" in cfg_data_1 + + def test_atomic_updates(self): + self.ckc(password="old_password") + with self.cfg_file.open("rt") as f: + # Perform a write in the middle of reading + self.ckc(password="new_password") + # Read data from existing FH after new data was written + cfg_data_1 = f.read() + # Read updated data + cfg_data_2 = self.cfg_file.read_text() + # Verify that the in-progress read didn't get any of the new data + assert cfg_data_1 != cfg_data_2 + assert "old_password" in cfg_data_1 + assert "new_password" in cfg_data_2 + + @mock.patch("charmhelpers.core.hookenv.network_get", autospec=True) + def test_get_ingress_address(self, network_get): + network_get.return_value = {"ingress-addresses": ["1.2.3.4", "5.6.7.8"]} + ingress = kubernetes_common.get_ingress_address("endpoint-name") + assert ingress == "1.2.3.4" + ingress = kubernetes_common.get_ingress_address("endpoint-name", ["1.2.3.4"]) + assert ingress == "5.6.7.8" diff --git a/kubernetes-master/tests/unit/conftest.py b/kubernetes-master/tests/unit/conftest.py new file mode 100644 index 0000000..a92e249 --- /dev/null +++ b/kubernetes-master/tests/unit/conftest.py @@ -0,0 +1,4 @@ +import charms.unit_test + + +charms.unit_test.patch_reactive() diff --git a/kubernetes-master/tests/unit/test_k8s_common.py b/kubernetes-master/tests/unit/test_k8s_common.py new file mode 100644 index 0000000..0dcad31 --- /dev/null +++ b/kubernetes-master/tests/unit/test_k8s_common.py @@ -0,0 +1,122 @@ +import json +import string +from subprocess import CalledProcessError +from unittest.mock import Mock + +from charms.layer import kubernetes_common as kc + + +def test_token_generator(): + alphanum = string.ascii_letters + string.digits + token = kc.token_generator(10) + assert len(token) == 10 + unknown_chars = set(token) - set(alphanum) + assert not unknown_chars + + +def test_get_secret_names(monkeypatch): + monkeypatch.setattr(kc, "kubectl", Mock()) + kc.kubectl.side_effect = [ + CalledProcessError(1, "none"), + FileNotFoundError, + "{}".encode("utf8"), + json.dumps( + { + "items": [ + { + "metadata": {"name": "secret-id"}, + "data": {"username": "dXNlcg=="}, + }, + ], + } + ).encode("utf8"), + ] + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {"user": "secret-id"} + + +def test_generate_rfc1123(): + alphanum = string.ascii_letters + string.digits + token = kc.generate_rfc1123(1000) + assert len(token) == 253 + unknown_chars = set(token) - set(alphanum) + assert not unknown_chars + + +def test_create_secret(monkeypatch): + monkeypatch.setattr(kc, "render", Mock()) + monkeypatch.setattr(kc, "kubectl_manifest", Mock()) + monkeypatch.setattr(kc, "get_secret_names", Mock()) + monkeypatch.setattr(kc, "generate_rfc1123", Mock()) + kc.kubectl_manifest.side_effect = [True, False] + kc.get_secret_names.side_effect = [{"username": "secret-id"}, {}] + kc.generate_rfc1123.return_value = "foo" + assert kc.create_secret("token", "username", "user", "groups") + assert kc.render.call_args[1]["context"] == { + "groups": "Z3JvdXBz", + "password": "dXNlcjo6dG9rZW4=", + "secret_name": "secret-id", + "secret_namespace": "kube-system", + "type": "juju.is/token-auth", + "user": "dXNlcg==", + "username": "dXNlcm5hbWU=", + } + assert not kc.create_secret("token", "username", "user", "groups") + assert kc.render.call_args[1]["context"] == { + "groups": "Z3JvdXBz", + "password": "dXNlcjo6dG9rZW4=", + "secret_name": "auth-user-foo", + "secret_namespace": "kube-system", + "type": "juju.is/token-auth", + "user": "dXNlcg==", + "username": "dXNlcm5hbWU=", + } + + +def test_get_secret_password(monkeypatch): + monkeypatch.setattr(kc, "kubectl", Mock()) + monkeypatch.setattr(kc, "Path", Mock()) + monkeypatch.setattr(kc, "yaml", Mock()) + kc.kubectl.side_effect = [ + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + FileNotFoundError, + json.dumps({}).encode("utf8"), + json.dumps({"items": []}).encode("utf8"), + json.dumps({"items": []}).encode("utf8"), + json.dumps({"items": [{}]}).encode("utf8"), + json.dumps({"items": [{"data": {}}]}).encode("utf8"), + json.dumps( + {"items": [{"data": {"username": "Ym9i", "password": "c2VjcmV0"}}]} + ).encode("utf8"), + json.dumps( + {"items": [{"data": {"username": "dXNlcm5hbWU=", "password": "c2VjcmV0"}}]} + ).encode("utf8"), + ] + kc.yaml.safe_load.side_effect = [ + {}, + {"users": None}, + {"users": []}, + {"users": [{"user": {}}]}, + {"users": [{"user": {"token": "secret"}}]}, + ] + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") == "secret" + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") == "secret" diff --git a/kubernetes-master/tox.ini b/kubernetes-master/tox.ini new file mode 100644 index 0000000..805089a --- /dev/null +++ b/kubernetes-master/tox.ini @@ -0,0 +1,18 @@ +[tox] +envlist = py3 +skipsdist = true + +[testenv] +basepython=python3 +envdir={toxworkdir}/py3 +deps= + ipdb + pytest + charms.reactive + pydoc-markdown + # needed to prevent apt installs during import + netifaces + psutil + +[testenv:docs] +commands=python make_docs diff --git a/kubernetes-master/version b/kubernetes-master/version new file mode 100644 index 0000000..20817dd --- /dev/null +++ b/kubernetes-master/version @@ -0,0 +1 @@ +ccfa68be \ No newline at end of file diff --git a/kubernetes-master/wheelhouse.txt b/kubernetes-master/wheelhouse.txt new file mode 100644 index 0000000..05d3d30 --- /dev/null +++ b/kubernetes-master/wheelhouse.txt @@ -0,0 +1,35 @@ +# layer:basic +# pip is pinned to <19.0 to avoid https://github.com/pypa/pip/issues/6164 +# even with installing setuptools before upgrading pip ends up with pip seeing +# the older setuptools at the system level if include_system_packages is true +pip>=18.1,<19.0 +# pin Jinja2, PyYAML and MarkupSafe to the last versions supporting python 3.5 +# for trusty +Jinja2<=2.10.1 +PyYAML<=5.2 +MarkupSafe<2.0.0 +setuptools<42 +setuptools-scm<=1.17.0 +charmhelpers>=0.4.0,<1.0.0 +charms.reactive>=0.1.0,<2.0.0 +wheel<0.34 +# pin netaddr to avoid pulling importlib-resources +netaddr<=0.7.19 + +# layer:snap +# Newer versions of tenacity rely on `typing` which is in stdlib in +# python3.5 but not python3.4. We want to continue to support +# python3.4 (Trusty) +tenacity<5.0.4 + +# layer:vault-kv +hvac +# needed to prevent apt installs during import +netifaces +psutil + +# kubernetes-master +aiohttp>=3.7.4,<4.0.0 +gunicorn>=20.0.0,<21.0.0 +loadbalancer-interface + diff --git a/kubernetes-master/wheelhouse/Jinja2-2.10.1.tar.gz b/kubernetes-master/wheelhouse/Jinja2-2.10.1.tar.gz new file mode 100644 index 0000000..ffd1054 Binary files /dev/null and b/kubernetes-master/wheelhouse/Jinja2-2.10.1.tar.gz differ diff --git a/kubernetes-master/wheelhouse/MarkupSafe-1.1.1.tar.gz b/kubernetes-master/wheelhouse/MarkupSafe-1.1.1.tar.gz new file mode 100644 index 0000000..a6dad8e Binary files /dev/null and b/kubernetes-master/wheelhouse/MarkupSafe-1.1.1.tar.gz differ diff --git a/kubernetes-master/wheelhouse/PyYAML-5.2.tar.gz b/kubernetes-master/wheelhouse/PyYAML-5.2.tar.gz new file mode 100644 index 0000000..666d12a Binary files /dev/null and b/kubernetes-master/wheelhouse/PyYAML-5.2.tar.gz differ diff --git a/kubernetes-master/wheelhouse/Tempita-0.5.2.tar.gz b/kubernetes-master/wheelhouse/Tempita-0.5.2.tar.gz new file mode 100644 index 0000000..755befc Binary files /dev/null and b/kubernetes-master/wheelhouse/Tempita-0.5.2.tar.gz differ diff --git a/kubernetes-master/wheelhouse/aiohttp-3.7.4.post0.tar.gz b/kubernetes-master/wheelhouse/aiohttp-3.7.4.post0.tar.gz new file mode 100644 index 0000000..bb0fc48 Binary files /dev/null and b/kubernetes-master/wheelhouse/aiohttp-3.7.4.post0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/async-timeout-3.0.1.tar.gz b/kubernetes-master/wheelhouse/async-timeout-3.0.1.tar.gz new file mode 100644 index 0000000..dfed0e0 Binary files /dev/null and b/kubernetes-master/wheelhouse/async-timeout-3.0.1.tar.gz differ diff --git a/kubernetes-master/wheelhouse/attrs-21.2.0.tar.gz b/kubernetes-master/wheelhouse/attrs-21.2.0.tar.gz new file mode 100644 index 0000000..c028019 Binary files /dev/null and b/kubernetes-master/wheelhouse/attrs-21.2.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/cached-property-1.5.2.tar.gz b/kubernetes-master/wheelhouse/cached-property-1.5.2.tar.gz new file mode 100644 index 0000000..501f2c0 Binary files /dev/null and b/kubernetes-master/wheelhouse/cached-property-1.5.2.tar.gz differ diff --git a/kubernetes-master/wheelhouse/certifi-2021.10.8.tar.gz b/kubernetes-master/wheelhouse/certifi-2021.10.8.tar.gz new file mode 100644 index 0000000..9e1581b Binary files /dev/null and b/kubernetes-master/wheelhouse/certifi-2021.10.8.tar.gz differ diff --git a/kubernetes-master/wheelhouse/chardet-4.0.0.tar.gz b/kubernetes-master/wheelhouse/chardet-4.0.0.tar.gz new file mode 100644 index 0000000..6bfc4e3 Binary files /dev/null and b/kubernetes-master/wheelhouse/chardet-4.0.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/charmhelpers-0.20.23.tar.gz b/kubernetes-master/wheelhouse/charmhelpers-0.20.23.tar.gz new file mode 100644 index 0000000..8fbc8ec Binary files /dev/null and b/kubernetes-master/wheelhouse/charmhelpers-0.20.23.tar.gz differ diff --git a/kubernetes-master/wheelhouse/charms.reactive-1.4.1.tar.gz b/kubernetes-master/wheelhouse/charms.reactive-1.4.1.tar.gz new file mode 100644 index 0000000..03bc1fe Binary files /dev/null and b/kubernetes-master/wheelhouse/charms.reactive-1.4.1.tar.gz differ diff --git a/kubernetes-master/wheelhouse/charset-normalizer-2.0.7.tar.gz b/kubernetes-master/wheelhouse/charset-normalizer-2.0.7.tar.gz new file mode 100644 index 0000000..61df022 Binary files /dev/null and b/kubernetes-master/wheelhouse/charset-normalizer-2.0.7.tar.gz differ diff --git a/kubernetes-master/wheelhouse/gunicorn-20.1.0.tar.gz b/kubernetes-master/wheelhouse/gunicorn-20.1.0.tar.gz new file mode 100644 index 0000000..b5da493 Binary files /dev/null and b/kubernetes-master/wheelhouse/gunicorn-20.1.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/hvac-0.11.2.tar.gz b/kubernetes-master/wheelhouse/hvac-0.11.2.tar.gz new file mode 100644 index 0000000..6aa6982 Binary files /dev/null and b/kubernetes-master/wheelhouse/hvac-0.11.2.tar.gz differ diff --git a/kubernetes-master/wheelhouse/idna-3.3.tar.gz b/kubernetes-master/wheelhouse/idna-3.3.tar.gz new file mode 100644 index 0000000..ff2bcbf Binary files /dev/null and b/kubernetes-master/wheelhouse/idna-3.3.tar.gz differ diff --git a/kubernetes-master/wheelhouse/idna-ssl-1.1.0.tar.gz b/kubernetes-master/wheelhouse/idna-ssl-1.1.0.tar.gz new file mode 100644 index 0000000..2380177 Binary files /dev/null and b/kubernetes-master/wheelhouse/idna-ssl-1.1.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/loadbalancer_interface-1.1.1.tar.gz b/kubernetes-master/wheelhouse/loadbalancer_interface-1.1.1.tar.gz new file mode 100644 index 0000000..265133c Binary files /dev/null and b/kubernetes-master/wheelhouse/loadbalancer_interface-1.1.1.tar.gz differ diff --git a/kubernetes-master/wheelhouse/marshmallow-3.14.0.tar.gz b/kubernetes-master/wheelhouse/marshmallow-3.14.0.tar.gz new file mode 100644 index 0000000..14e4efc Binary files /dev/null and b/kubernetes-master/wheelhouse/marshmallow-3.14.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/marshmallow-enum-1.5.1.tar.gz b/kubernetes-master/wheelhouse/marshmallow-enum-1.5.1.tar.gz new file mode 100644 index 0000000..642941a Binary files /dev/null and b/kubernetes-master/wheelhouse/marshmallow-enum-1.5.1.tar.gz differ diff --git a/kubernetes-master/wheelhouse/multidict-5.2.0.tar.gz b/kubernetes-master/wheelhouse/multidict-5.2.0.tar.gz new file mode 100644 index 0000000..9563429 Binary files /dev/null and b/kubernetes-master/wheelhouse/multidict-5.2.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/netaddr-0.7.19.tar.gz b/kubernetes-master/wheelhouse/netaddr-0.7.19.tar.gz new file mode 100644 index 0000000..cc31d9d Binary files /dev/null and b/kubernetes-master/wheelhouse/netaddr-0.7.19.tar.gz differ diff --git a/kubernetes-master/wheelhouse/netifaces-0.11.0.tar.gz b/kubernetes-master/wheelhouse/netifaces-0.11.0.tar.gz new file mode 100644 index 0000000..3a35596 Binary files /dev/null and b/kubernetes-master/wheelhouse/netifaces-0.11.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/ops-1.2.0.tar.gz b/kubernetes-master/wheelhouse/ops-1.2.0.tar.gz new file mode 100644 index 0000000..2cb4358 Binary files /dev/null and b/kubernetes-master/wheelhouse/ops-1.2.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/ops_reactive_interface-1.0.1.tar.gz b/kubernetes-master/wheelhouse/ops_reactive_interface-1.0.1.tar.gz new file mode 100644 index 0000000..14f5ded Binary files /dev/null and b/kubernetes-master/wheelhouse/ops_reactive_interface-1.0.1.tar.gz differ diff --git a/kubernetes-master/wheelhouse/pbr-5.6.0.tar.gz b/kubernetes-master/wheelhouse/pbr-5.6.0.tar.gz new file mode 100644 index 0000000..0d5c965 Binary files /dev/null and b/kubernetes-master/wheelhouse/pbr-5.6.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/pip-18.1.tar.gz b/kubernetes-master/wheelhouse/pip-18.1.tar.gz new file mode 100644 index 0000000..a18192d Binary files /dev/null and b/kubernetes-master/wheelhouse/pip-18.1.tar.gz differ diff --git a/kubernetes-master/wheelhouse/psutil-5.8.0.tar.gz b/kubernetes-master/wheelhouse/psutil-5.8.0.tar.gz new file mode 100644 index 0000000..7907528 Binary files /dev/null and b/kubernetes-master/wheelhouse/psutil-5.8.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/pyaml-21.10.1.tar.gz b/kubernetes-master/wheelhouse/pyaml-21.10.1.tar.gz new file mode 100644 index 0000000..b19aad3 Binary files /dev/null and b/kubernetes-master/wheelhouse/pyaml-21.10.1.tar.gz differ diff --git a/kubernetes-master/wheelhouse/requests-2.26.0.tar.gz b/kubernetes-master/wheelhouse/requests-2.26.0.tar.gz new file mode 100644 index 0000000..101dc79 Binary files /dev/null and b/kubernetes-master/wheelhouse/requests-2.26.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/setuptools-41.6.0.zip b/kubernetes-master/wheelhouse/setuptools-41.6.0.zip new file mode 100644 index 0000000..3345759 Binary files /dev/null and b/kubernetes-master/wheelhouse/setuptools-41.6.0.zip differ diff --git a/kubernetes-master/wheelhouse/setuptools_scm-1.17.0.tar.gz b/kubernetes-master/wheelhouse/setuptools_scm-1.17.0.tar.gz new file mode 100644 index 0000000..43b16c7 Binary files /dev/null and b/kubernetes-master/wheelhouse/setuptools_scm-1.17.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/six-1.16.0.tar.gz b/kubernetes-master/wheelhouse/six-1.16.0.tar.gz new file mode 100644 index 0000000..5bf3a27 Binary files /dev/null and b/kubernetes-master/wheelhouse/six-1.16.0.tar.gz differ diff --git a/kubernetes-master/wheelhouse/tenacity-5.0.3.tar.gz b/kubernetes-master/wheelhouse/tenacity-5.0.3.tar.gz new file mode 100644 index 0000000..c7d05ba Binary files /dev/null and b/kubernetes-master/wheelhouse/tenacity-5.0.3.tar.gz differ diff --git a/kubernetes-master/wheelhouse/typing_extensions-3.10.0.2.tar.gz b/kubernetes-master/wheelhouse/typing_extensions-3.10.0.2.tar.gz new file mode 100644 index 0000000..dad7a2c Binary files /dev/null and b/kubernetes-master/wheelhouse/typing_extensions-3.10.0.2.tar.gz differ diff --git a/kubernetes-master/wheelhouse/urllib3-1.26.7.tar.gz b/kubernetes-master/wheelhouse/urllib3-1.26.7.tar.gz new file mode 100644 index 0000000..990abe6 Binary files /dev/null and b/kubernetes-master/wheelhouse/urllib3-1.26.7.tar.gz differ diff --git a/kubernetes-master/wheelhouse/wheel-0.33.6.tar.gz b/kubernetes-master/wheelhouse/wheel-0.33.6.tar.gz new file mode 100644 index 0000000..c922c4e Binary files /dev/null and b/kubernetes-master/wheelhouse/wheel-0.33.6.tar.gz differ diff --git a/kubernetes-master/wheelhouse/yarl-1.7.0.tar.gz b/kubernetes-master/wheelhouse/yarl-1.7.0.tar.gz new file mode 100644 index 0000000..3acaed4 Binary files /dev/null and b/kubernetes-master/wheelhouse/yarl-1.7.0.tar.gz differ diff --git a/kubernetes-worker/.build.manifest b/kubernetes-worker/.build.manifest new file mode 100644 index 0000000..6f82c60 --- /dev/null +++ b/kubernetes-worker/.build.manifest @@ -0,0 +1,1601 @@ +{ + "layers": [ + { + "branch": "refs/heads/master", + "rev": "fcdcea4e5de3e1556c24e6704607862d0ba00a56", + "url": "layer:options" + }, + { + "branch": "refs/heads/master", + "rev": "a3ff62c32c993d80417f6e093e3ef95e42f62083", + "url": "layer:basic" + }, + { + "branch": "refs/heads/master", + "rev": "527dd64fc4b9a6b0f8d80a3c2c0b865155050275", + "url": "layer:debug" + }, + { + "branch": "refs/heads/master", + "rev": "d3acdf209cbaf5b732e9aba621778a0f56dbaeb9", + "url": "layer:snap" + }, + { + "branch": "refs/heads/master", + "rev": "cc5bd3f49b2fa5e6c3ab2336763c313ec8bf083f", + "url": "layer:leadership" + }, + { + "branch": "refs/heads/master", + "rev": "f491ebe32b503c9712d2f8cd602dcce18f4aab46", + "url": "layer:metrics" + }, + { + "branch": "refs/heads/master", + "rev": "47dfcd4920ef6317850a4837ef0057ab0092a18e", + "url": "layer:nagios" + }, + { + "branch": "refs/heads/master", + "rev": "fb46dec78d390571753d21876bbba689bbbca9e4", + "url": "layer:tls-client" + }, + { + "branch": "refs/heads/master", + "rev": "b60102068c6f0ddbeaf8a308549a3e88cfa35688", + "url": "layer:cdk-service-kicker" + }, + { + "branch": "refs/heads/master", + "rev": "023c67941e18663a4df49f53edba809f43ba5069", + "url": "layer:cis-benchmark" + }, + { + "branch": "refs/heads/master", + "rev": "fa27fc93e0b08000963e83a6bfe49812d890dfcf", + "url": "layer:coordinator" + }, + { + "branch": "refs/heads/master", + "rev": "bbeabfee52c4442cdaf3a34e5e35530a3bd71156", + "url": "layer:kubernetes-common" + }, + { + "branch": "refs/heads/master", + "rev": "809f06c6f6521be59e21859eaebeccd13f4d8c28", + "url": "layer:kubernetes-master-worker-base" + }, + { + "branch": "refs/heads/master", + "rev": "c753ea9346c2503a85464d11740bce60ff8f6a66", + "url": "kubernetes-worker" + }, + { + "branch": "refs/heads/master", + "rev": "2e0e1fdea6d83b55078200aacb537d60013ec5bc", + "url": "interface:nrpe-external-master" + }, + { + "branch": "refs/heads/master", + "rev": "d9850016d930a6d507b9fd45e2598d327922b140", + "url": "interface:tls-certificates" + }, + { + "branch": "refs/heads/master", + "rev": "6f927f10b97f45c566481cf57a29d433f17373e1", + "url": "interface:container-runtime" + }, + { + "branch": "refs/heads/master", + "rev": "632131b1f122daf6fb601fd4c9f1e4dbb1a92e09", + "url": "interface:http" + }, + { + "branch": "refs/heads/master", + "rev": "88b1e8fad78d06efdbf512cd75eaa0bb308eb1c1", + "url": "interface:kubernetes-cni" + }, + { + "branch": "refs/heads/master", + "rev": "534310f5bca8edde02cadaf6ac42231cea0b040b", + "url": "interface:kube-control" + }, + { + "branch": "refs/heads/master", + "rev": "d8d8c7ef17c99ad53383f3cabf4cf5c8191d16f7", + "url": "interface:aws-integration" + }, + { + "branch": "refs/heads/master", + "rev": "d8f093cb2930edf5f93678253dca2da70b73b4fb", + "url": "interface:gcp-integration" + }, + { + "branch": "refs/heads/master", + "rev": "0d7a994f04b9e92ed847829ce8349b1a9c672e47", + "url": "interface:openstack-integration" + }, + { + "branch": "refs/heads/master", + "rev": "d5caea55ced6785f391215ee457c3a964eaf3f4b", + "url": "interface:vsphere-integration" + }, + { + "branch": "refs/heads/master", + "rev": "8d2202e433d7c188de4df2fd4bddb355193e93ac", + "url": "interface:azure-integration" + }, + { + "branch": "refs/heads/master", + "rev": "d5a2526fec9c3e8581f18b56e84a86871583e080", + "url": "interface:mount" + } + ], + "signatures": { + ".build.manifest": [ + "build", + "dynamic", + "unchecked" + ], + ".github/workflows/main.yml": [ + "kubernetes-worker", + "static", + "ac49b46a293a04f1a25c86769b054ec4cee88de9bf0354f26627fb99f044a711" + ], + ".gitignore": [ + "kubernetes-worker", + "static", + "e028ad966843fa4e09963c008d1200117caf1a42163c70795d9c55406f801d8c" + ], + ".travis.yml": [ + "layer:cis-benchmark", + "static", + "b6dbe144aa288b8a89caf1119b9835b407b234c9b32a1c81013b12a0593a8be2" + ], + "CONTRIBUTING.md": [ + "kubernetes-worker", + "static", + "bbe14e93d7db43d022103e3088036dc6fc1ca0554538a8205bcc07ef730d1ded" + ], + "HACKING.md": [ + "kubernetes-worker", + "static", + "fc87d881098225b72f3ca6a1b8e01cce143b130e3dc8ac1ec484a0de19bc4d6c" + ], + "LICENSE": [ + "kubernetes-worker", + "static", + "f02fd85a4171482f6bb1d6f87fe0704d3a2da93eca04afe39a0310a00c409902" + ], + "Makefile": [ + "layer:basic", + "static", + "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301" + ], + "README.md": [ + "kubernetes-worker", + "static", + "f7bfc0dab834e67d9be6dda2e942a8c9c36d03bf9fef8b94d00ec9c51d91742d" + ], + "actions.yaml": [ + "kubernetes-worker", + "dynamic", + "e43fb57f67cffdf43f1af01d53154f4a21b5905b448dd0780a87059c97625b8e" + ], + "actions/cis-benchmark": [ + "layer:cis-benchmark", + "static", + "fd3c1b8ba478b7f933605897ace8ae9f3ee102d9992f46f1e36d95eb1b094b84" + ], + "actions/debug": [ + "layer:debug", + "static", + "db0a42dae4c5045b2c06385bf22209dfe0e2ded55822ef847d84b01d9ff2b046" + ], + "actions/microbot": [ + "kubernetes-worker", + "static", + "cfc909b8a86c5517dec02bf40201c946efad02d2b7ec20a811f19aa5c4e3e1a0" + ], + "actions/pause": [ + "kubernetes-worker", + "static", + "ba4a19dc800ff6381367ad2bd84b0ad0c06180a77834c762750a4bdedb9ff366" + ], + "actions/resume": [ + "kubernetes-worker", + "static", + "460d50796be763674cdadb5d88ccdc0c883eb21fb3cf86805c46da18922022a7" + ], + "actions/upgrade": [ + "kubernetes-worker", + "static", + "641458372b97c9a0ef15c00aa8934d631f3c3b159d53f6e30cf8022bd3ff705e" + ], + "bin/charm-env": [ + "layer:basic", + "static", + "fb6a20fac4102a6a4b6ffe903fcf666998f9a95a3647e6f9af7a1eeb44e58fd5" + ], + "bin/layer_option": [ + "layer:options", + "static", + "e959bf29da4c5edff28b2602c24113c4df9e25cdc9f2aa3b5d46c8577b2a40cc" + ], + "build-cni-resources.sh": [ + "kubernetes-worker", + "static", + "687a1ada1b9cc1aeefc83eb61939662a28fe4d6c1ce50c2a3828f4c46a90546d" + ], + "config.yaml": [ + "kubernetes-worker", + "dynamic", + "bd2f6fe7b40e230014051cf982f0d1cdb8418555a006c3210f48f353675339bb" + ], + "copyright": [ + "kubernetes-worker", + "static", + "badd4492d214890abd07b615f9e1a7a5ff3339b6c44655a826c746a9263ff00d" + ], + "copyright.layer-basic": [ + "layer:basic", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-coordinator": [ + "layer:coordinator", + "static", + "7d212a095a6143559fb51f26bc40c2ba24b977190f65c7e5c835104f54d5dfc5" + ], + "copyright.layer-leadership": [ + "layer:leadership", + "static", + "8ce407829378fc0f72ce44c7f624e4951c7ccb3db1cfb949bee026b701728cc9" + ], + "copyright.layer-metrics": [ + "layer:metrics", + "static", + "08509dcbade4c20761ba4382ef23c831744dbab1d4a8dd94a1c2b4d4e913334c" + ], + "copyright.layer-nagios": [ + "layer:nagios", + "static", + "47b2363574909e748bcc471d9004780ac084b301c154905654b5b6f088474749" + ], + "copyright.layer-options": [ + "layer:options", + "static", + "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" + ], + "copyright.layer-snap": [ + "layer:snap", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "debug-scripts/charm-unitdata": [ + "layer:debug", + "static", + "c952b9d31f3942e4e722cb3e70f5119707b69b8e76cc44e2e906bc6d9aef49b7" + ], + "debug-scripts/filesystem": [ + "layer:debug", + "static", + "d29cc8687f4422d024001c91b1ac756ee6bf8a2a125bc98db1199ba775eb8fd7" + ], + "debug-scripts/inotify": [ + "kubernetes-worker", + "static", + "8991354951b11e32a9edf4736e7ca0d5948d6c30a9a83673193aadf829032223" + ], + "debug-scripts/juju-logs": [ + "layer:debug", + "static", + "d260b35753a917368cb8c64c1312546a0a40ef49cba84c75bc6369549807c55e" + ], + "debug-scripts/juju-network-get": [ + "layer:debug", + "static", + "6d849a1f8e6569bd0d5ea38299f7937cb8b36a5f505e3532f6c756eabeb8b6c5" + ], + "debug-scripts/kubectl": [ + "kubernetes-worker", + "static", + "dadc2eae5818d818ac0b10029056d0db975406c17211864e08d1fa9780bb82c2" + ], + "debug-scripts/kubernetes-worker-services": [ + "kubernetes-worker", + "static", + "fca2c57d754d9968c80308031fd9de7cfd2ddda37de5b2ff49ba1ccf333c5a58" + ], + "debug-scripts/network": [ + "layer:debug", + "static", + "714afae5dcb45554ff1f05285501e3b7fcc656c8de51217e263b93dab25a9d2e" + ], + "debug-scripts/packages": [ + "layer:debug", + "static", + "e8177102dc2ca853cb9272c1257cf2cfd5253d2a074e602d07c8bc4ea8e27c75" + ], + "debug-scripts/sysctl": [ + "layer:debug", + "static", + "990035b320e09cc2228e1f2f880e795d51118b2959339eacddff9cbb74349c6a" + ], + "debug-scripts/systemd": [ + "layer:debug", + "static", + "23ddf533198bf5b1ce723acde31ada806aab8539292b514c721d8ec08af74106" + ], + "debug-scripts/tls-certs": [ + "layer:tls-client", + "static", + "ebf7f23ef6e39fb8e664bac2e9429e32aaeb673b4a51751724b835c007e85d3b" + ], + "exec.d/docker-compose/charm-pre-install": [ + "layer:kubernetes-master-worker-base", + "static", + "32482c2a88209cbe512990db5fb4deabdcff88282bf7c7dd71a265383139fc77" + ], + "exec.d/vmware-patch/charm-pre-install": [ + "kubernetes-worker", + "static", + "9f98f70669ddd949ff83c7b408b678ae170bf41e4faa2828b4d66bd47acca93e" + ], + "hooks/aws-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/aws-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/azure-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/azure-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/azure-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/azure-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/azure-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/certificates-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/cni-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/collect-metrics": [ + "layer:metrics", + "static", + "139fe18ce4cf2bed2155d3d0fce1c3b4cf1bc2598242cda42b3d772ec9bf8558" + ], + "hooks/config-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/container-runtime-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/container-runtime-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/container-runtime-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/container-runtime-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/container-runtime-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/coordinator-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/coordinator-relation-changed": [ + "layer:coordinator", + "static", + "e5138d13492aa9a90379e8fce4a85c612481e7bc27a49958edbbfcaaf06f03a6" + ], + "hooks/coordinator-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/coordinator-relation-departed": [ + "layer:coordinator", + "static", + "e5138d13492aa9a90379e8fce4a85c612481e7bc27a49958edbbfcaaf06f03a6" + ], + "hooks/coordinator-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/gcp-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/gcp-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/gcp-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/gcp-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/gcp-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/hook.template": [ + "layer:basic", + "static", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ingress-proxy-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ingress-proxy-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ingress-proxy-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ingress-proxy-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/ingress-proxy-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/install": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-api-endpoint-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-api-endpoint-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-api-endpoint-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-api-endpoint-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-api-endpoint-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-control-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-control-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-control-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-control-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/kube-control-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/leader-elected": [ + "layer:coordinator", + "static", + "e5138d13492aa9a90379e8fce4a85c612481e7bc27a49958edbbfcaaf06f03a6" + ], + "hooks/leader-settings-changed": [ + "layer:coordinator", + "static", + "e5138d13492aa9a90379e8fce4a85c612481e7bc27a49958edbbfcaaf06f03a6" + ], + "hooks/nfs-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nfs-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nfs-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nfs-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nfs-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/nrpe-external-master-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/openstack-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/openstack-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/openstack-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/openstack-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/openstack-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/post-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/pre-series-upgrade": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/relations/aws-integration/.gitignore": [ + "interface:aws-integration", + "static", + "315971ad9cc5d6ada2391f0940e1800149b211a18be3c7a8f396735d7978702b" + ], + "hooks/relations/aws-integration/LICENSE": [ + "interface:aws-integration", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/aws-integration/README.md": [ + "interface:aws-integration", + "static", + "1585d72b136158ce0741fc2ce0d7710c1ec55662f846afe2e768a4708c51057e" + ], + "hooks/relations/aws-integration/__init__.py": [ + "interface:aws-integration", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/aws-integration/copyright": [ + "interface:aws-integration", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/aws-integration/docs/provides.md": [ + "interface:aws-integration", + "static", + "a7669f49156173c27ede87105f6e65a07e1e5e41f3c154a24e1a82f307f65073" + ], + "hooks/relations/aws-integration/docs/requires.md": [ + "interface:aws-integration", + "static", + "09553e5f07f216e5234125fdf38a21af00ab11349cdb788b21703ae72b0aeed1" + ], + "hooks/relations/aws-integration/interface.yaml": [ + "interface:aws-integration", + "static", + "4449f48e5aaa99c0bb3e8e1c9833d11d3b20fc5f81ae1f15b6442af5ec873167" + ], + "hooks/relations/aws-integration/make_docs": [ + "interface:aws-integration", + "static", + "b471fefc7eaa5c377d47b2b63481d6c8f4c5e9d224428efe93c5abbd13a0817d" + ], + "hooks/relations/aws-integration/provides.py": [ + "interface:aws-integration", + "static", + "ee8f91b281d9112999f3d0e1d2ac17964fca3af5102fe5b072f3f3659b932ab7" + ], + "hooks/relations/aws-integration/pydocmd.yml": [ + "interface:aws-integration", + "static", + "8c242cde2b2517c74de8ad6b1b90d2f6d97b2eb86c54edaf2eb8a8f7d32913e8" + ], + "hooks/relations/aws-integration/requires.py": [ + "interface:aws-integration", + "static", + "3006d6a2607bc15507bec3e6144093c6938a51a22eee1f550d714ff702728c39" + ], + "hooks/relations/azure-integration/.gitignore": [ + "interface:azure-integration", + "static", + "9653f2820c79d92ac3518eedd0e1f43ffec128d5df9216c25d906fcba8ee46b8" + ], + "hooks/relations/azure-integration/LICENSE": [ + "interface:azure-integration", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/azure-integration/README.md": [ + "interface:azure-integration", + "static", + "c7799dba9471709e086dcd2ea272ad7a6e33f5058d875ce2bf5b3a6939d4a1e7" + ], + "hooks/relations/azure-integration/__init__.py": [ + "interface:azure-integration", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/azure-integration/copyright": [ + "interface:azure-integration", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/azure-integration/docs/provides.md": [ + "interface:azure-integration", + "static", + "60ae63187cac32c00d9f462f1723c9487960c728beae871f1a409c92196cc1f5" + ], + "hooks/relations/azure-integration/docs/requires.md": [ + "interface:azure-integration", + "static", + "b01e313c8ce3d02093e851bd84d5e8b7ae77b300c4b06b5048bddc78c1ad3eb3" + ], + "hooks/relations/azure-integration/interface.yaml": [ + "interface:azure-integration", + "static", + "cea5bfd87c278bd3f2e8dc00e654930f06d2bd91ef731a063edea14b04d9128a" + ], + "hooks/relations/azure-integration/make_docs": [ + "interface:azure-integration", + "static", + "e76f4a64c2fdc4a9f97a57d6515b4a25f9404d7043f2792db5206bc44213927c" + ], + "hooks/relations/azure-integration/provides.py": [ + "interface:azure-integration", + "static", + "33af701c7abd51e869de945c1f032749136c66560bb604e8e72521dc9d7e495b" + ], + "hooks/relations/azure-integration/pydocmd.yml": [ + "interface:azure-integration", + "static", + "4c17085efb4ec328891b49257413eed4d9a552eeea8e589509e48081effe51ed" + ], + "hooks/relations/azure-integration/requires.py": [ + "interface:azure-integration", + "static", + "2e60fecf8bc65d84124742d0833afc90d2e839f5dfa2923e8d1849063c51f47a" + ], + "hooks/relations/container-runtime/.gitignore": [ + "interface:container-runtime", + "static", + "a2ebfecdb6c1b58267fbe97e6e2ac02c2b963df7673fc1047270f0f0cff16732" + ], + "hooks/relations/container-runtime/LICENSE": [ + "interface:container-runtime", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "hooks/relations/container-runtime/README.md": [ + "interface:container-runtime", + "static", + "44273265818229d2c858c3af0e0eee3a7df05aaa9ab20d28c3872190d4b48611" + ], + "hooks/relations/container-runtime/__init__.py": [ + "interface:container-runtime", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/container-runtime/interface.yaml": [ + "interface:container-runtime", + "static", + "e5343dcb11a6817a6050df4ea1c463eeaa0dd4777098566d4e27b056775426c6" + ], + "hooks/relations/container-runtime/provides.py": [ + "interface:container-runtime", + "static", + "4e818da222f507604179a828629787a1250083c847277f6b5b8e028cfbbb6d06" + ], + "hooks/relations/container-runtime/requires.py": [ + "interface:container-runtime", + "static", + "95285168b02f1f70be15c03098833a85e60fa1658ed72a46acd42e8e85ded761" + ], + "hooks/relations/coordinator/peers.py": [ + "layer:coordinator", + "static", + "d615c442396422a30a0c5f7639750d15bb59247ae5d9362c4f5dc8dd2cc7fff2" + ], + "hooks/relations/gcp-integration/.gitignore": [ + "interface:gcp-integration", + "static", + "9653f2820c79d92ac3518eedd0e1f43ffec128d5df9216c25d906fcba8ee46b8" + ], + "hooks/relations/gcp-integration/LICENSE": [ + "interface:gcp-integration", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/gcp-integration/README.md": [ + "interface:gcp-integration", + "static", + "dab3f4a03f02dec0095883054780e3e3f1bf63262b06a9fd499364a3db8b1e97" + ], + "hooks/relations/gcp-integration/__init__.py": [ + "interface:gcp-integration", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/gcp-integration/copyright": [ + "interface:gcp-integration", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/gcp-integration/docs/provides.md": [ + "interface:gcp-integration", + "static", + "a67cda4094b4d601c8de63cf099ba2e83fecf3a8382e88f44e58b98be8872fa6" + ], + "hooks/relations/gcp-integration/docs/requires.md": [ + "interface:gcp-integration", + "static", + "d7e6d7dc90b74d35bf2bd10b00b3ba289ab856dc79ec51046508a85b9dda35a3" + ], + "hooks/relations/gcp-integration/interface.yaml": [ + "interface:gcp-integration", + "static", + "368e8ade9267b905dcb2e6843e7ed61bd6d246f0b0c18942e729f546d5db2260" + ], + "hooks/relations/gcp-integration/make_docs": [ + "interface:gcp-integration", + "static", + "5bf011da5045c31da97a67b8633d30ea90adc6c0d4d823f839fce6e07e5fe222" + ], + "hooks/relations/gcp-integration/provides.py": [ + "interface:gcp-integration", + "static", + "839f15cf978cf94343772889846ad3e2b8375372ef25ed08036207e5608b1f48" + ], + "hooks/relations/gcp-integration/pydocmd.yml": [ + "interface:gcp-integration", + "static", + "2d5a524cbde5ccf732b67382a85deb7c26dfb92315c30d26c2b2d5632a2a8f38" + ], + "hooks/relations/gcp-integration/requires.py": [ + "interface:gcp-integration", + "static", + "79c75c6c76b37bc5ac486ac2e14f853223c4c603850d2f231f187ab255cbdbf0" + ], + "hooks/relations/http/.gitignore": [ + "interface:http", + "static", + "83b4ca18cc39800b1d260b5633cd0252e21501b21e7c33e718db44f1a68a09b8" + ], + "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", + "8c72cd8a5a6ea24f53b6dba11f4353c75265bfa7d3ecc2dd096c8963eab8c877" + ], + "hooks/relations/http/requires.py": [ + "interface:http", + "static", + "76cc886368eaf9c2403a6dc46b40531c3f4eaf67b08829f890c57cb645430abd" + ], + "hooks/relations/kube-control/.travis.yml": [ + "interface:kube-control", + "static", + "c2bd1b88f26c88b883696cca155c28671359a256ed48b90a9ea724b376f2a829" + ], + "hooks/relations/kube-control/README.md": [ + "interface:kube-control", + "static", + "66ee58f59efceefa21f7f2d7f88c1d75c07a16bbec8d09a83a7fda6373eab421" + ], + "hooks/relations/kube-control/__init__.py": [ + "interface:kube-control", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/kube-control/interface.yaml": [ + "interface:kube-control", + "static", + "07e3d781283ecbb59c780cc8e4aeb9f030f22d2db6c28d731b74a36ab126960d" + ], + "hooks/relations/kube-control/provides.py": [ + "interface:kube-control", + "static", + "08e090bb3ad51e5825590ad0dee077288648b171764480afc20205c740fa15be" + ], + "hooks/relations/kube-control/requires.py": [ + "interface:kube-control", + "static", + "a064ad0b75081439faeda7fb948934bfd86a7ab2079d25b2ad47aa5fa32c2a6f" + ], + "hooks/relations/kubernetes-cni/.github/workflows/tests.yaml": [ + "interface:kubernetes-cni", + "static", + "d0015cd49675976ff87832f5ef7ea20ffca961786379c72bb6acdbdeddd9137c" + ], + "hooks/relations/kubernetes-cni/.gitignore": [ + "interface:kubernetes-cni", + "static", + "0594213ebf9c6ef87827b30405ee67d847f73f4185a865e0e5e9c0be9d29eabe" + ], + "hooks/relations/kubernetes-cni/README.md": [ + "interface:kubernetes-cni", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/kubernetes-cni/__init__.py": [ + "interface:kubernetes-cni", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/kubernetes-cni/interface.yaml": [ + "interface:kubernetes-cni", + "static", + "03affdaf7e879adfdf8c434aa31d40faa6d2872faa7dfd93a5d3a1ebae02487d" + ], + "hooks/relations/kubernetes-cni/provides.py": [ + "interface:kubernetes-cni", + "static", + "e436e187f2bab6e73add2b897cd43a2f000fde4726e40b772b66f27786c85dee" + ], + "hooks/relations/kubernetes-cni/requires.py": [ + "interface:kubernetes-cni", + "static", + "45398af27246eaf2005115bd3f270b78fc830d4345b02cc0c4d438711b7cd9fe" + ], + "hooks/relations/mount/.gitignore": [ + "interface:mount", + "static", + "f107e9960f299957deb6087dbc043b5ca51a7e78f5895f9444bb5bf91a6b579d" + ], + "hooks/relations/mount/LICENSE": [ + "interface:mount", + "static", + "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + ], + "hooks/relations/mount/README.md": [ + "interface:mount", + "static", + "b8dc1667fe75f4339b4a6cfcb2272eb2c066268c1de08d24dd95880c0ba32e2d" + ], + "hooks/relations/mount/__init__.py": [ + "interface:mount", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/mount/copyright": [ + "interface:mount", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/mount/interface.yaml": [ + "interface:mount", + "static", + "038465e3afcdc6344a43fe5e224cb3468866e311d9c0c83920b4454c4ac8b602" + ], + "hooks/relations/mount/provides.py": [ + "interface:mount", + "static", + "39a3c6f245f2df8e3df82f7995207eaec06e0beec4bc6c412d30c777a7794e88" + ], + "hooks/relations/mount/requires.py": [ + "interface:mount", + "static", + "c2e9ad42d6009818211bb28d11e365f90b073829d5cc847998060b6009e37ff3" + ], + "hooks/relations/nrpe-external-master/README.md": [ + "interface:nrpe-external-master", + "static", + "d8ed3bc7334f6581b12b6091923f58e6f5ef62075a095a4e78fb8f434a948636" + ], + "hooks/relations/nrpe-external-master/__init__.py": [ + "interface:nrpe-external-master", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/nrpe-external-master/interface.yaml": [ + "interface:nrpe-external-master", + "static", + "894f24ba56148044dae5b7febf874b427d199239bcbe1f2f55c3db06bb77b5f0" + ], + "hooks/relations/nrpe-external-master/provides.py": [ + "interface:nrpe-external-master", + "static", + "e6ba708d05b227b139a86be59c83ed95a2bad030bc81e5819167ba5e1e67ecd4" + ], + "hooks/relations/nrpe-external-master/requires.py": [ + "interface:nrpe-external-master", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/openstack-integration/.gitignore": [ + "interface:openstack-integration", + "static", + "9653f2820c79d92ac3518eedd0e1f43ffec128d5df9216c25d906fcba8ee46b8" + ], + "hooks/relations/openstack-integration/LICENSE": [ + "interface:openstack-integration", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/openstack-integration/README.md": [ + "interface:openstack-integration", + "static", + "ca58e21bd973f6e65f7a8a06b4aeabd50bf137ab6fab9c8defa8789b02df3aa5" + ], + "hooks/relations/openstack-integration/__init__.py": [ + "interface:openstack-integration", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/openstack-integration/copyright": [ + "interface:openstack-integration", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/openstack-integration/docs/provides.md": [ + "interface:openstack-integration", + "static", + "ec4b81da3dfeac892f94053d753b56e504f5fd9c6ec4e743efa40efade3aa651" + ], + "hooks/relations/openstack-integration/docs/requires.md": [ + "interface:openstack-integration", + "static", + "95424fe767a26e3208800b4099f8768212b0a72b989ee145f181b67d678e3bbe" + ], + "hooks/relations/openstack-integration/interface.yaml": [ + "interface:openstack-integration", + "static", + "11b07a41bd2e24765231c4b7c7218da15f2173398d8d73698ecb210e599d02f6" + ], + "hooks/relations/openstack-integration/make_docs": [ + "interface:openstack-integration", + "static", + "a564aac288cc0bf4ff14418a341f11b065988c2b64adf93ec451e09dd92dcea5" + ], + "hooks/relations/openstack-integration/provides.py": [ + "interface:openstack-integration", + "static", + "ad09fc79fa5eb7a142477d5bf7f48b53f6ede389708de0bb297c6d009aba502b" + ], + "hooks/relations/openstack-integration/pydocmd.yml": [ + "interface:openstack-integration", + "static", + "3568f8a3c1446dfd736f31050e2b470bf125cc41717d156a4b866c7ea53861be" + ], + "hooks/relations/openstack-integration/requires.py": [ + "interface:openstack-integration", + "static", + "a15f5a7ffa2391f75da6bde0007700ee75f058e62430924312ff39efc6ecea6b" + ], + "hooks/relations/tls-certificates/.gitignore": [ + "interface:tls-certificates", + "static", + "b485e74def213c534676224e655e9276b62d401ebc643508ddc545dd335cb6dc" + ], + "hooks/relations/tls-certificates/README.md": [ + "interface:tls-certificates", + "static", + "6851227de8fcca7edfd504159dbe3e3af31080af64df46f3d3b345da7630827a" + ], + "hooks/relations/tls-certificates/__init__.py": [ + "interface:tls-certificates", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/tls-certificates/docs/common.md": [ + "interface:tls-certificates", + "static", + "5e91d6637fc0ccc50af2776de9e59a0f8098244b627816b2e18fabb266e980ff" + ], + "hooks/relations/tls-certificates/docs/provides.md": [ + "interface:tls-certificates", + "static", + "5c12dfca99b5c15ba10b4e7f7cff4cb4c9b621b198deba5f2397d3c837d035fe" + ], + "hooks/relations/tls-certificates/docs/requires.md": [ + "interface:tls-certificates", + "static", + "148dd1de163d75253f0a9d3c35e108dcaacbc9bdf97e47186743e6c82a67b62e" + ], + "hooks/relations/tls-certificates/interface.yaml": [ + "interface:tls-certificates", + "static", + "e412e54b1d327bad15a882f7f0bf996212090db576b863cc9cff7a68afc0e4fa" + ], + "hooks/relations/tls-certificates/make_docs": [ + "interface:tls-certificates", + "static", + "3671543bddc9d277171263310e404df3f11660429582cb27b39b7e7ec8757a37" + ], + "hooks/relations/tls-certificates/provides.py": [ + "interface:tls-certificates", + "static", + "be2a4b9a411c770989c529fd887070ad91649481a13f5239cfd8751f234b637c" + ], + "hooks/relations/tls-certificates/pydocmd.yml": [ + "interface:tls-certificates", + "static", + "48a233f60a89f87d56e9bc715e05766f5d39bbea2bc8741ed31f67b30c8cfcb8" + ], + "hooks/relations/tls-certificates/requires.py": [ + "interface:tls-certificates", + "static", + "442d773112079bc674d3e6be75b00323fcad7efd2f03613a1972b575dd438dba" + ], + "hooks/relations/tls-certificates/tls_certificates_common.py": [ + "interface:tls-certificates", + "static", + "068bd32ba69bfa514e1da386919d18b348ee678b40c372f275c9110f2cc4677c" + ], + "hooks/relations/vsphere-integration/.gitignore": [ + "interface:vsphere-integration", + "static", + "9653f2820c79d92ac3518eedd0e1f43ffec128d5df9216c25d906fcba8ee46b8" + ], + "hooks/relations/vsphere-integration/LICENSE": [ + "interface:vsphere-integration", + "static", + "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + ], + "hooks/relations/vsphere-integration/README.md": [ + "interface:vsphere-integration", + "static", + "8de815f0f938cb8f58c536899ed87e55aac507a782093bd50d50bd3c1d6add1c" + ], + "hooks/relations/vsphere-integration/__init__.py": [ + "interface:vsphere-integration", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "hooks/relations/vsphere-integration/copyright": [ + "interface:vsphere-integration", + "static", + "7c0e36e618a8544faaaa3f8e0533c2f1f4a18bcacbdd8b99b537742e6b587d58" + ], + "hooks/relations/vsphere-integration/docs/provides.md": [ + "interface:vsphere-integration", + "static", + "daa3c44a2df6d774adc60bde1160f1e307129be9d696f018eab4a7e713ee775a" + ], + "hooks/relations/vsphere-integration/docs/requires.md": [ + "interface:vsphere-integration", + "static", + "4e79bb1b151f1de63b423d39a6e1831efbb6f767fe5b84963162f62c6bbb9123" + ], + "hooks/relations/vsphere-integration/interface.yaml": [ + "interface:vsphere-integration", + "static", + "20295b882dfb9a1750d8e988eaa3383cd3109fae510785ba4e415d7fa9b118af" + ], + "hooks/relations/vsphere-integration/make_docs": [ + "interface:vsphere-integration", + "static", + "cd9d91049ee3c6e6148f4bd9204a34463dde905ce665cff25be014ffc1b81b89" + ], + "hooks/relations/vsphere-integration/provides.py": [ + "interface:vsphere-integration", + "static", + "8ccb09c4a3009b59caea227ef40395fb063d3e8ce983338060fb59bbe74138c0" + ], + "hooks/relations/vsphere-integration/pydocmd.yml": [ + "interface:vsphere-integration", + "static", + "9f8eb566569977f10955da67def28886737e80914ae000e4acfae1313d08f105" + ], + "hooks/relations/vsphere-integration/requires.py": [ + "interface:vsphere-integration", + "static", + "d56702f60037f06259752d3bd7882f7ee46f60a4ce7b6d1071520d69ec9351f9" + ], + "hooks/start": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/stop": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/update-status": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/upgrade-charm": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vsphere-relation-broken": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vsphere-relation-changed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vsphere-relation-created": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vsphere-relation-departed": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "hooks/vsphere-relation-joined": [ + "layer:basic", + "dynamic", + "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" + ], + "icon.svg": [ + "kubernetes-worker", + "static", + "0afa1f0d13a72b9df63559bc950bc0f5881984a591ea994990a45066db1e665b" + ], + "layer.yaml": [ + "kubernetes-worker", + "dynamic", + "9ad8956799dcd00d6ab47306e99ea638f469dddf8f3f0a75f3436e3424ec056b" + ], + "lib/charms/coordinator.py": [ + "layer:coordinator", + "static", + "6dbacc87605be8efcbf19ec05341e4eb210327724495c79998a46947e034dbea" + ], + "lib/charms/layer/__init__.py": [ + "layer:basic", + "static", + "dfe0d26c6bf409767de6e2546bc648f150e1b396243619bad3aa0553ab7e0e6f" + ], + "lib/charms/layer/basic.py": [ + "layer:basic", + "static", + "98b47134770ed6e4c0b2d4aad73cd5bc200bec84aa9c1c4e075fd70c3222a0c9" + ], + "lib/charms/layer/execd.py": [ + "layer:basic", + "static", + "fda8bd491032db1db8ddaf4e99e7cc878c6fb5432efe1f91cadb5b34765d076d" + ], + "lib/charms/layer/kubernetes_common.py": [ + "layer:kubernetes-common", + "static", + "29cedffd490e6295273d195a7c9bace2fcdf149826e7427f2af9698f7f75055b" + ], + "lib/charms/layer/nagios.py": [ + "layer:nagios", + "static", + "0246710bdbea844356007a64409907d93e6e94a289d83266e8b7c5d921fb3a6c" + ], + "lib/charms/layer/options.py": [ + "layer:options", + "static", + "8ae7a07d22542fc964f2d2bee8219d1c78a68dace70a1b38d36d4aea47b1c3b2" + ], + "lib/charms/layer/snap.py": [ + "layer:snap", + "static", + "f278a3b06a1604e1c59f107d2ff3e9f5705e3c6c7be7a012c1a500d0fc8925df" + ], + "lib/charms/layer/tls_client.py": [ + "layer:tls-client", + "static", + "34531c3980777b661b913d77c432fc371ed10425473c2eb365b1dd5540c2ec6e" + ], + "lib/charms/leadership.py": [ + "layer:leadership", + "static", + "20ffcbbc08147506759726ad51567420659ffb8a2e0121079240b8706658e332" + ], + "lib/debug_script.py": [ + "layer:debug", + "static", + "a4d56f2d3e712b1b5cadb657c7195c6268d0aac6d228991049fd769e0ddaf453" + ], + "lxd-profile.yaml": [ + "kubernetes-worker", + "static", + "e62700f1993721652d83756f89e1f8b33c5d0dec6fb27554f61aaf96ccd4e379" + ], + "metadata.yaml": [ + "kubernetes-worker", + "dynamic", + "1aa8d365447c86fe0ab73979d0390728e771ffe7cdace09b97de4119d5ee2e69" + ], + "metrics.yaml": [ + "kubernetes-worker", + "static", + "94a5eb0b0966f8ba434d91ff1e9b99b1b4c3b3044657b236d4e742d3e0d57c47" + ], + "reactive/__init__.py": [ + "layer:coordinator", + "static", + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ], + "reactive/cdk_service_kicker.py": [ + "layer:cdk-service-kicker", + "static", + "cc2648443016a18324ecb26acb71d69c71610ba23df235f280383552136f7efc" + ], + "reactive/coordinator.py": [ + "layer:coordinator", + "static", + "18cda7ddf00ae0e47578d489fc3ebb376b4428cd0559797a87ddbead54360d02" + ], + "reactive/kubernetes_master_worker_base.py": [ + "layer:kubernetes-master-worker-base", + "static", + "4a2d098f2e54f96b7ecef19b9485534b2da911a4a62104bd0efa40e4cb8bb519" + ], + "reactive/kubernetes_worker.py": [ + "kubernetes-worker", + "static", + "384ce90f41b0abe38c5cafcbf8b485ad7cb31f093c16db56eb1d3405208499bc" + ], + "reactive/leadership.py": [ + "layer:leadership", + "static", + "e2b233cf861adc3b2d9e9c062134ce2f104953f03283cdddd88f49efee652e8f" + ], + "reactive/snap.py": [ + "layer:snap", + "static", + "e4625ff4190ed33625f50d94343eda100871052ef133028f5f0ff1edfa5a23c3" + ], + "reactive/tls_client.py": [ + "layer:tls-client", + "static", + "08e850e401d2004523dca6b5e6bc47c33d558bf575dd55969491e11cd3ed98c8" + ], + "requirements.txt": [ + "layer:basic", + "static", + "a00f75d80849e5b4fc5ad2e7536f947c25b1a4044b341caa8ee87a92d3a4c804" + ], + "setup.py": [ + "layer:snap", + "static", + "b219c8c6cb138a2f70a8ef9136d1cc3fe6210bd1e28c99fccb5e7ae90d547164" + ], + "templates/cdk-service-kicker": [ + "layer:cdk-service-kicker", + "static", + "b17adff995310e14d1b510337efa0af0531b55e2c487210168829e0dc1a6f99b" + ], + "templates/cdk-service-kicker.service": [ + "layer:cdk-service-kicker", + "static", + "c2d3977fa89d453f0f13a8a823621c44bb642ec7392d8b7462b631864f665029" + ], + "templates/cdk.auth-webhook-secret.yaml": [ + "layer:kubernetes-common", + "static", + "efaf34c12a5c961fa7843199070945ba05717b3656a0f3acc3327f45334bcaec" + ], + "templates/default-http-backend.yaml": [ + "kubernetes-worker", + "static", + "e31bb19574e6f23bb89cc30475d5e8b41fa96a8f67aa0b2d01316902584fa4e4" + ], + "templates/ingress-daemon-set.yaml": [ + "kubernetes-worker", + "static", + "c3b7e7d95c8a4cd0079145be797346b77a64f3b48b808cddbfdc33787b48d316" + ], + "templates/microbot-example.yaml": [ + "kubernetes-worker", + "static", + "fb8feb88979eb5d0cfcbf9a5169387667a2224c72b0aae7f01310caa8c094ebe" + ], + "templates/nagios_plugin.py": [ + "kubernetes-worker", + "static", + "8b425bb29ed41ee1b1c2fddc7acf5f24f5c6a0cf7432c86cf8486434032fcb14" + ], + "templates/nfs-provisioner.yaml": [ + "kubernetes-worker", + "static", + "e3ee7c995c9a3624daffdc9a09467e9e274b38a4bb6c3851d928bf7bf1151fac" + ], + "tests/functional/conftest.py": [ + "layer:kubernetes-common", + "static", + "fd53e0c38b4dda0c18096167889cd0d85b98b0a13225f9f8853261241e94078c" + ], + "tests/functional/test_k8s_common.py": [ + "layer:kubernetes-common", + "static", + "680a53724154771dd78422bbaf24b151788d86dd07960712c5d9e0d758499b50" + ], + "tests/unit/conftest.py": [ + "layer:kubernetes-common", + "static", + "fd53e0c38b4dda0c18096167889cd0d85b98b0a13225f9f8853261241e94078c" + ], + "tests/unit/test_k8s_common.py": [ + "layer:kubernetes-common", + "static", + "da9bcea8e75160311a4055c1cbf577b497ddd45dc00223c5f1667598f94d9be4" + ], + "tox.ini": [ + "layer:kubernetes-common", + "static", + "70b0bf95842031247336c8734981b72d991d64d108f19697429abd80c8739877" + ], + "version": [ + "kubernetes-worker", + "dynamic", + "fd5e95ef69d8cf8fc1f89f04cfe6d6f8a97cf271f75e20f285b9e1efbdc4ba79" + ], + "wheelhouse.txt": [ + "kubernetes-worker", + "dynamic", + "4062748728aabda25a3630f994d7801b1578a0a98dd5614b77251cd3e2ca8f57" + ], + "wheelhouse/Jinja2-2.10.1.tar.gz": [ + "layer:basic", + "dynamic", + "065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013" + ], + "wheelhouse/MarkupSafe-1.1.1.tar.gz": [ + "layer:basic", + "dynamic", + "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b" + ], + "wheelhouse/PyYAML-5.2.tar.gz": [ + "layer:basic", + "dynamic", + "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c" + ], + "wheelhouse/Tempita-0.5.2.tar.gz": [ + "__pip__", + "dynamic", + "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c" + ], + "wheelhouse/charmhelpers-0.20.23.tar.gz": [ + "layer:basic", + "dynamic", + "59a9776594e91cd3e3e000043f8668b4d7b279422dbb17e320f01dc16385b80e" + ], + "wheelhouse/charms.reactive-1.4.1.tar.gz": [ + "layer:basic", + "dynamic", + "bba21b4fd40b26c240c9ef2aa10c6fdf73592031c68591da4e7ccc46ca9cb616" + ], + "wheelhouse/charms.templating.jinja2-1.0.2.tar.gz": [ + "kubernetes-worker", + "dynamic", + "8193c6a1d40bdb66fe272c359b4e4780501c658acfaf2b1118c4230927815fe2" + ], + "wheelhouse/netaddr-0.7.19.tar.gz": [ + "layer:basic", + "dynamic", + "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd" + ], + "wheelhouse/pbr-5.6.0.tar.gz": [ + "__pip__", + "dynamic", + "42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd" + ], + "wheelhouse/pip-18.1.tar.gz": [ + "layer:basic", + "dynamic", + "c0a292bd977ef590379a3f05d7b7f65135487b67470f6281289a94e015650ea1" + ], + "wheelhouse/pyaml-21.10.1.tar.gz": [ + "__pip__", + "dynamic", + "c6519fee13bf06e3bb3f20cacdea8eba9140385a7c2546df5dbae4887f768383" + ], + "wheelhouse/setuptools-41.6.0.zip": [ + "layer:basic", + "dynamic", + "6afa61b391dcd16cb8890ec9f66cc4015a8a31a6e1c2b4e0c464514be1a3d722" + ], + "wheelhouse/setuptools_scm-1.17.0.tar.gz": [ + "layer:basic", + "dynamic", + "70a4cf5584e966ae92f54a764e6437af992ba42ac4bca7eb37cc5d02b98ec40a" + ], + "wheelhouse/six-1.16.0.tar.gz": [ + "__pip__", + "dynamic", + "1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926" + ], + "wheelhouse/tenacity-5.0.3.tar.gz": [ + "layer:snap", + "dynamic", + "24b7f302a1caa1801e58b39ea557129c095966e64e5b1ddad3c93a6cb033e38b" + ], + "wheelhouse/wheel-0.33.6.tar.gz": [ + "layer:basic", + "dynamic", + "10c9da68765315ed98850f8e048347c3eb06dd81822dc2ab1d4fde9dc9702646" + ] + } +} \ No newline at end of file diff --git a/kubernetes-worker/.github/workflows/main.yml b/kubernetes-worker/.github/workflows/main.yml new file mode 100644 index 0000000..6998dbc --- /dev/null +++ b/kubernetes-worker/.github/workflows/main.yml @@ -0,0 +1,60 @@ +name: Test Suite +on: [pull_request] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + - name: Run lint + run: tox -vve lint + unit-test: + name: Unit Tests + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Dependencies + run: | + pip install tox + - name: Run test + run: tox -vve unit + integration-test: + name: Integration test with VMWare + runs-on: self-hosted + timeout-minutes: 360 + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@master + with: + provider: vsphere + credentials-yaml: ${{ secrets.CREDENTIALS_YAML }} + clouds-yaml: ${{ secrets.CLOUDS_YAML }} + bootstrap-options: "--model-default datastore=vsanDatastore --model-default primary-network=VLAN_2764" + - name: Run test + run: tox -e integration diff --git a/kubernetes-worker/.gitignore b/kubernetes-worker/.gitignore new file mode 100644 index 0000000..15deb39 --- /dev/null +++ b/kubernetes-worker/.gitignore @@ -0,0 +1,5 @@ +.tox/ +__pycache__/ +*.pyc +placeholders/ +*.tgz diff --git a/kubernetes-worker/.travis.yml b/kubernetes-worker/.travis.yml new file mode 100644 index 0000000..66d8e1f --- /dev/null +++ b/kubernetes-worker/.travis.yml @@ -0,0 +1,7 @@ +language: python +python: + - "3.5" +install: + - pip install tox-travis +script: + - tox diff --git a/kubernetes-worker/CONTRIBUTING.md b/kubernetes-worker/CONTRIBUTING.md new file mode 100644 index 0000000..3de561f --- /dev/null +++ b/kubernetes-worker/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributor Guide + +This Juju charm is open source ([Apache License 2.0](./LICENSE)) and we actively seek any community contibutions +for code, suggestions and documentation. +This page details a few notes, workflows and suggestions for how to make contributions most effective and help us +all build a better charm - please give them a read before working on any contributions. + +## Licensing + +This charm has been created under the [Apache License 2.0](./LICENSE), which will cover any contributions you may +make to this project. Please familiarise yourself with the terms of the license. + +Additionally, this charm uses the Harmony CLA agreement. It’s the easiest way for you to give us permission to +use your contributions. +In effect, you’re giving us a license, but you still own the copyright — so you retain the right to modify your +code and use it in other projects. Please [sign the CLA here](https://ubuntu.com/legal/contributors/agreement) before +making any contributions. + +## Code of conduct + +We have adopted the Ubuntu code of Conduct. You can read this in full [here](https://ubuntu.com/community/code-of-conduct). + +## Contributing code + +To contribute code to this project, please use the following workflow: + +1. [Submit a bug](https://bugs.launchpad.net/charm-kubernetes-worker/+filebug) to explain the need for and track the change. +2. Create a branch on your fork of the repo with your changes, including a unit test covering the new or modified code. +3. Submit a PR. The PR description should include a link to the bug on Launchpad. +4. Update the Launchpad bug to include a link to the PR and the `review-needed` tag. +5. Once reviewed and merged, the change will become available on the edge channel and assigned to an appropriate milestone + for further release according to priority. + +## Documentation + +Documentation for this charm is currently maintained as part of the Charmed Kubernetes docs. +See [this page](https://github.com/charmed-kubernetes/kubernetes-docs/blob/master/pages/k8s/charm-kubernetes-worker.md) diff --git a/kubernetes-worker/HACKING.md b/kubernetes-worker/HACKING.md new file mode 100644 index 0000000..28e380c --- /dev/null +++ b/kubernetes-worker/HACKING.md @@ -0,0 +1,25 @@ + # Kubernetes Worker + +### Building from the layer + +You can clone the kubernetes-worker layer with git and build locally if you +have the charm package/snap installed. + +```shell +# Instal the snap +sudo snap install charm --channel=edge + +# Set the build environment +export JUJU_REPOSITORY=$HOME + +# Clone the layer and build it to our JUJU_REPOSITORY +git clone https://github.com/juju-solutions/kubernetes +cd kubernetes/cluster/juju/layers/kubernetes-worker +charm build -r +``` + +### Contributing + +TBD + + diff --git a/kubernetes-worker/LICENSE b/kubernetes-worker/LICENSE new file mode 100644 index 0000000..0543093 --- /dev/null +++ b/kubernetes-worker/LICENSE @@ -0,0 +1,202 @@ + + 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 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. diff --git a/kubernetes-worker/Makefile b/kubernetes-worker/Makefile new file mode 100644 index 0000000..a1ad3a5 --- /dev/null +++ b/kubernetes-worker/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/kubernetes-worker/README.md b/kubernetes-worker/README.md new file mode 100644 index 0000000..0bf63b3 --- /dev/null +++ b/kubernetes-worker/README.md @@ -0,0 +1,22 @@ +# Kubernetes Worker + +## Usage + +This charm deploys a container runtime, and additionally stands up the Kubernetes +worker applications: kubelet, and kube-proxy. + +In order for this charm to be useful, it should be deployed with its companion +charm [kubernetes-master](https://jujucharms.com/u/containers/kubernetes-master) +and linked with an SDN-Plugin and a container runtime such as +[containerd](https://jaas.ai/u/containers/containerd). + +This charm is a component of Charmed Kubernetes. For full information, +please visit the [official Charmed Kubernetes docs](https://www.ubuntu.com/kubernetes/docs/charm-kubernetes-worker). + +## Developers + +### Building the charm + +``` +make charm +``` diff --git a/kubernetes-worker/actions.yaml b/kubernetes-worker/actions.yaml new file mode 100644 index 0000000..f07e4ff --- /dev/null +++ b/kubernetes-worker/actions.yaml @@ -0,0 +1,75 @@ +"debug": + "description": "Collect debug data" +"cis-benchmark": + "description": | + Run the CIS Kubernetes Benchmark against snap-based components. + "params": + "apply": + "type": "string" + "default": "none" + "description": | + Apply remediations to address benchmark failures. The default, 'none', + will not attempt to fix any reported failures. Set to 'conservative' + to resolve simple failures. Set to 'dangerous' to attempt to resolve + all failures. + + Note: Applying any remediation may result in an unusable cluster. + "config": + "type": "string" + "default": "https://github.com/charmed-kubernetes/kube-bench-config/archive/cis-1.5.zip#sha1=811f21dbf6c841bafdbfbd8a21f912ad67582f46" + "description": | + Archive containing configuration files to use when running kube-bench. + The default value is known to be compatible with snap components. When + using a custom URL, append '#=' to verify the + archive integrity when downloaded. + "release": + "type": "string" + "default": "https://github.com/aquasecurity/kube-bench/releases/download/v0.3.1/kube-bench_0.3.1_linux_amd64.tar.gz#sha256=6616f1373987259285e2f676a225d4a3885cd62b7e7a116102ff2fb445724281" + "description": | + Archive containing the 'kube-bench' binary to run. The default value + points to a stable upstream release. When using a custom URL, append + '#=' to verify the archive integrity when + downloaded. + + This may also be set to the special keyword 'upstream'. In this case, + the action will compile and use a local kube-bench binary built from + the master branch of the upstream repository: + https://github.com/aquasecurity/kube-bench + +"pause": + "description": | + Mark the node as unschedulable to prevent new pods from arriving, and + evict existing pods. + "params": + "delete-local-data": + "type": "boolean" + "description": | + Continue even if there are pods using emptyDir (local data that will + be deleted when the node is drained). + "default": !!bool "false" + "force": + "type": "boolean" + "description": | + Continue even if there are pods not managed by a + ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet. + "default": !!bool "false" +"resume": + "description": | + Mark node as schedulable. +"microbot": + "description": "Launch microbot containers" + "params": + "delete": + "type": "boolean" + "default": !!bool "false" + "description": "Remove a microbots deployment, service, and ingress if True." + "registry": + "type": "string" + "default": "rocks.canonical.com:443/cdk" + "description": "Registry to use for the microbot image." + "replicas": + "type": "integer" + "default": !!int "3" + "description": "Number of microbots to launch in Kubernetes." +"upgrade": + "description": "Upgrade the kubernetes snaps" diff --git a/kubernetes-worker/actions/cis-benchmark b/kubernetes-worker/actions/cis-benchmark new file mode 100755 index 0000000..3f91dea --- /dev/null +++ b/kubernetes-worker/actions/cis-benchmark @@ -0,0 +1,371 @@ +#!/usr/local/sbin/charm-env python3 +import os +import json +import shlex +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +import charms.layer +import charms.reactive +from charmhelpers.core import hookenv, unitdata +from charmhelpers.fetch.archiveurl import ArchiveUrlFetchHandler +from charms.layer import snap +from charms.reactive import clear_flag, is_flag_set, set_flag + + +BENCH_HOME = '/home/ubuntu/kube-bench' +BENCH_BIN = '{}/kube-bench'.format(BENCH_HOME) +BENCH_CFG = '{}/cfg-ck'.format(BENCH_HOME) +GO_PKG = 'github.com/aquasecurity/kube-bench' +RESULTS_DIR = '/home/ubuntu/kube-bench-results' + +# Remediation dicts associate a failing test with a tuple to fix it. +# Conservative fixes will probably leave the cluster in a good state. +# Dangerous fixes will likely break the cluster. +# Tuple examples: +# {'1.2.3': ('manual -- we don't know how to auto fix this', None, None)} +# {'1.2.3': ('cli', 'command to run', None)} +# {'1.2.3': ('kv', 'snap', {cfg_key: value})} +CONSERVATIVE = { + '0.0.0': ('cli', 'echo "this is fine"', None), + + # etcd (no known failures with a default install) + + # k8s-master + '1.2.21': ('kv', 'kube-apiserver', {'profiling': 'false'}), + '1.2.23': ('kv', 'kube-apiserver', {'audit-log-maxage': '30'}), + '1.2.24': ('kv', 'kube-apiserver', {'audit-log-maxbackup': '10'}), + '1.3.1': ('kv', 'kube-controller-manager', {'terminated-pod-gc-threshold': '500'}), + '1.3.2': ('kv', 'kube-controller-manager', {'profiling': 'false'}), + '1.4.1': ('kv', 'kube-scheduler', {'profiling': 'false'}), + + # k8s-worker + '4.2.2': ('kv', 'kubelet', {'authorization-mode': 'Webhook'}), + '4.2.4': ('kv', 'kubelet', {'read-only-port': '0'}), + '4.2.6': ('kv', 'kubelet', {'protect-kernel-defaults': 'true'}), +} +ADMISSION_PLUGINS = {'enable-admission-plugins': ('PersistentVolumeLabel', + 'PodSecurityPolicy,' + 'ServiceAccount,' + 'NodeRestriction')} +DANGEROUS = { + '0.0.0': ('cli', 'echo "this is fine"', None), + + # etcd (no known failures with a default install) + + # k8s-master + '1.2.2': ('kv', 'kube-apiserver', {'basic-auth-file': None}), + '1.2.3': ('kv', 'kube-apiserver', {'token-auth-file': None}), + '1.2.7': ('kv', 'kube-apiserver', {'authorization-mode': 'RBAC,Node'}), + '1.2.8': ('kv', 'kube-apiserver', {'authorization-mode': 'RBAC,Node'}), + '1.2.9': ('kv', 'kube-apiserver', {'authorization-mode': 'RBAC,Node'}), + '1.2.14': ('kv', 'kube-apiserver', ADMISSION_PLUGINS), + '1.2.16': ('kv', 'kube-apiserver', ADMISSION_PLUGINS), + '1.2.17': ('kv', 'kube-apiserver', ADMISSION_PLUGINS), + '1.2.18': ('kv', 'kube-apiserver', {'insecure-bind-address': None}), + '1.2.19': ('kv', 'kube-apiserver', {'insecure-port': '0'}), + '1.2.33': ('manual', None, None), + '1.3.6': ('kv', 'kube-controller-manager', + {'feature-gates': 'RotateKubeletServerCertificate=true'}), + + # k8s-worker + '4.2.12': ('kv', 'kubelet', + {'feature-gates': 'RotateKubeletServerCertificate=true'}), +} + + +def _fail(msg): + '''Fail the action with a given message.''' + hookenv.action_fail(msg) + sys.exit() + + +def _move_matching_parent(dirpath, filename, dest): + '''Move a parent directory that contains a specific file. + + Helper function that walks a directory looking for a given file. If found, + the file's parent directory is moved to the given destination. + + :param: dirpath: String path to search + :param: filename: String file to find + :param: dest: String destination of the found parent directory + ''' + for root, _, files in os.walk(dirpath): + for name in files: + if name == filename: + hookenv.log('Moving {} to {}'.format(root, dest)) + shutil.move(root, dest) + return + else: + _fail('Could not find {} in {}'.format(filename, dirpath)) + + +def _restart_charm(): + '''Set charm-specific flags and call reactive.main().''' + app = hookenv.charm_name() or 'unknown' + if 'master' in app: + hookenv.log('Restarting master') + clear_flag('kubernetes-master.components.started') + elif 'worker' in app: + hookenv.log('Restarting worker') + set_flag('kubernetes-worker.restart-needed') + elif 'etcd' in app: + hookenv.log('No-op: etcd does not need to be restarted') + return + else: + _fail('Unable to determine the charm to restart: {}'.format(app)) + + # Invoke reactive so the charm will react to the flags we just managed + charms.layer.import_layer_libs() + charms.reactive.main() + + +def install(release, config): + '''Install kube-bench and related configuration. + + Release and configuration are set via action params. If installing an + upstream release, this method will also install 'go' if needed. + + :param: release: Archive URI or 'upstream' + :param: config: Archive URI of configuration files + ''' + if Path(BENCH_HOME).exists(): + shutil.rmtree(BENCH_HOME) + fetcher = ArchiveUrlFetchHandler() + + if release == 'upstream': + Path(BENCH_HOME).mkdir(parents=True, exist_ok=True) + + # Setup the 'go' environment + env = os.environ.copy() + go_bin = shutil.which('go', path='{}:/snap/bin'.format(env['PATH'])) + if not go_bin: + snap.install('go', channel='stable', classic=True) + go_bin = '/snap/bin/go' + go_cache = os.getenv('GOCACHE', '/var/snap/go/common/cache') + go_path = os.getenv('GOPATH', '/var/snap/go/common') + env['GOCACHE'] = go_cache + env['GOPATH'] = go_path + Path(go_path).mkdir(parents=True, exist_ok=True) + + # From https://github.com/aquasecurity/kube-bench#installing-from-sources + go_cmd = ('{bin} get {pkg} ' + 'github.com/golang/dep/cmd/dep'.format(bin=go_bin, pkg=GO_PKG)) + try: + subprocess.check_call(shlex.split(go_cmd), cwd=go_path, env=env) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(go_cmd)) + + go_cmd = ('{bin} build -o {out} {base}/src/{pkg}'.format( + bin=go_bin, out=BENCH_BIN, base=go_path, pkg=GO_PKG)) + try: + subprocess.check_call(shlex.split(go_cmd), cwd=go_path, env=env) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(go_cmd)) + else: + # Fetch the release URI and put it in the right place. + archive_path = fetcher.install(source=release) + # NB: We may not know the structure of the archive, but we know the + # directory containing 'kube-bench' belongs in our BENCH_HOME. + _move_matching_parent( + dirpath=archive_path, filename='kube-bench', dest=BENCH_HOME) + + # Fetch the config URI and put it in the right place. + archive_dir = fetcher.install(source=config) + # NB: We may not know the structure of the archive, but we know the + # directory containing 'config.yaml' belongs in our BENCH_CFG. + _move_matching_parent( + dirpath=archive_dir, filename='config.yaml', dest=BENCH_CFG) + + +def apply(remediations=None): + '''Apply remediations to address benchmark failures. + + :param: remediations: either 'conservative' or 'dangerous' + ''' + applied_fixes = 0 + danger = True if remediations == 'dangerous' else False + db = unitdata.kv() + + json_log = report(log_format='json') + hookenv.log('Loading JSON from: {}'.format(json_log)) + try: + with open(json_log, 'r') as f: + full_json = json.load(f) + except Exception: + _fail('Failed to load: {}'.format(json_log)) + + for test in full_json.get('tests', {}): + for result in test.get('results', {}): + test_num = result.get('test_number') + test_remediation = result.get('remediation') + test_status = result.get('status', '') + + if test_status.lower() == 'fail': + test_remedy = CONSERVATIVE.get(test_num) + if not test_remedy and danger: + # no conservative remedy, check dangerous if user wants + test_remedy = DANGEROUS.get(test_num) + if isinstance(test_remedy, tuple): + if test_remedy[0] == 'manual': + # we don't know how to autofix; log remediation text + hookenv.log('Test {}: unable to auto-apply remedy.\n' + 'Manual steps:\n{}'.format(test_num, + test_remediation)) + elif test_remedy[0] == 'cli': + cmd = shlex.split(test_remedy[1]) + try: + out = subprocess.check_output(cmd) + except subprocess.CalledProcessError: + _fail('Test {}: failed to run: {}'.format(test_num, cmd)) + else: + hookenv.log('Test {}: applied remedy: {}\n' + 'Output: {}'.format(test_num, cmd, out)) + applied_fixes += 1 + elif test_remedy[0] == 'kv': + cfg_key = 'cis-' + test_remedy[1] + cfg = db.get(cfg_key) or {} + cfg.update(test_remedy[2]) + db.set(cfg_key, cfg) + + hookenv.log('Test {}: updated configuration: {}\n'.format( + test_num, cfg)) + applied_fixes += 1 + else: + hookenv.log('Test {}: remediation is missing'.format(test_num)) + + # CLI and KV changes will require a charm restart; do it. + if applied_fixes > 0: + _restart_charm() + + msg = ('Applied {} remediations. Re-run with "apply=none" to generate a ' + 'new report.').format(applied_fixes) + hookenv.action_set({'summary': msg}) + + +def reset(): + '''Reset any remediations we applied to unitdata.kv(). + + This action does not track individual remediations to reset. Therefore, + this function unconditionally unsets all 'cis-' prefixed arguments that + this action may have set and restarts the relevant charm. + ''' + db = unitdata.kv() + + db.unset('cis-kube-apiserver') + db.unset('cis-kube-scheduler') + db.unset('cis-kube-controller-manager') + db.unset('cis-kubelet') + _restart_charm() + + hookenv.action_set({'summary': ('Reset is complete. Re-run with ' + '"apply=none" to generate a new report.')}) + + +def report(log_format='text'): + '''Run kube-bench and report results. + + By default, save the full plain-text results to our RESULTS_DIR and set + action output with a summary. This function can also save full results in + a machine-friendly json format. + + :param: log_format: String determines if output is text or json + :returns: Path to results log + ''' + Path(RESULTS_DIR).mkdir(parents=True, exist_ok=True) + + # Node type is different depending on the charm + app = hookenv.charm_name() or 'unknown' + version = 'cis-1.5' + if 'master' in app: + target = 'master' + elif 'worker' in app: + target = 'node' + elif 'etcd' in app: + target = 'etcd' + else: + _fail('Unable to determine the target to benchmark: {}'.format(app)) + + # Commands and log names are different depending on the format + if log_format == 'json': + log_prefix = 'results-json-' + verbose_cmd = ('{bin} -D {cfg} --benchmark {ver} --json run ' + '--targets {target}').format( + bin=BENCH_BIN, cfg=BENCH_CFG, ver=version, target=target) + else: + log_prefix = 'results-text-' + verbose_cmd = ('{bin} -D {cfg} --benchmark {ver} run ' + '--targets {target}').format( + bin=BENCH_BIN, cfg=BENCH_CFG, ver=version, target=target) + + summary_cmd = ('{bin} -D {cfg} --benchmark {ver} ' + '--noremediations --noresults run --targets {target}').format( + bin=BENCH_BIN, cfg=BENCH_CFG, ver=version, target=target) + + # Store full results for future consumption + with tempfile.NamedTemporaryFile(mode='w+b', prefix=log_prefix, + dir=RESULTS_DIR, delete=False) as res_file: + try: + subprocess.call(shlex.split(verbose_cmd), stdout=res_file) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(verbose_cmd)) + else: + # remember the filename for later (and make it readable, why not?) + Path(res_file.name).chmod(0o644) + log = res_file.name + + # When making a summary, we also have a verbose report. Set action output + # so operators can see everything related to this run. + try: + out = subprocess.check_output(shlex.split(summary_cmd), + universal_newlines=True) + except subprocess.CalledProcessError: + _fail('Failed to run: {}'.format(summary_cmd)) + else: + fetch_cmd = 'juju scp {unit}:{file} .'.format(unit=hookenv.local_unit(), + file=log) + hookenv.action_set({'cmd': summary_cmd, + 'report': fetch_cmd, + 'summary': out}) + + return log or None + + +if __name__ == '__main__': + if not (is_flag_set('snap.installed.etcd') or + is_flag_set('kubernetes-master.snaps.installed') or + is_flag_set('kubernetes-worker.snaps.installed')): + msg = 'Snaps are not yet installed on this unit.' + _fail(msg) + + # Validate action params + release = hookenv.action_get('release') or 'upstream' + config = hookenv.action_get('config') + if not config: + msg = 'Missing "config" parameter' + _fail(msg) + remediations = hookenv.action_get('apply') + if remediations not in ['none', 'conservative', 'dangerous', 'reset']: + msg = 'Invalid "apply" parameter: {}'.format(remediations) + _fail(msg) + + # TODO: may want an option to overwrite an existing install + if Path(BENCH_BIN).exists() and Path(BENCH_CFG).exists(): + hookenv.log('{} exists; skipping install'.format(BENCH_HOME)) + else: + hookenv.log('Installing benchmark from: {}'.format(release)) + install(release, config) + + # Reset, remediate, or report + if remediations == 'reset': + hookenv.log('Attempting to remove all remediations') + reset() + elif remediations != 'none': + hookenv.log('Applying "{}" remediations'.format(remediations)) + apply(remediations) + else: + hookenv.log('Report only; no remediations were requested') + report(log_format='text') diff --git a/kubernetes-worker/actions/debug b/kubernetes-worker/actions/debug new file mode 100755 index 0000000..8ba160e --- /dev/null +++ b/kubernetes-worker/actions/debug @@ -0,0 +1,102 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import tarfile +import tempfile +import traceback +from contextlib import contextmanager +from datetime import datetime +from charmhelpers.core.hookenv import action_set, local_unit + +archive_dir = None +log_file = None + + +@contextmanager +def archive_context(): + """ Open a context with a new temporary directory. + + When the context closes, the directory is archived, and the archive + location is added to Juju action output. """ + global archive_dir + global log_file + with tempfile.TemporaryDirectory() as temp_dir: + name = "debug-" + datetime.now().strftime("%Y%m%d%H%M%S") + archive_dir = os.path.join(temp_dir, name) + os.makedirs(archive_dir) + with open("%s/debug.log" % archive_dir, "w") as log_file: + yield + os.chdir(temp_dir) + tar_path = "/home/ubuntu/%s.tar.gz" % name + with tarfile.open(tar_path, "w:gz") as f: + f.add(name) + action_set({ + "path": tar_path, + "command": "juju scp %s:%s ." % (local_unit(), tar_path), + "message": " ".join([ + "Archive has been created on unit %s." % local_unit(), + "Use the juju scp command to copy it to your local machine." + ]) + }) + + +def log(msg): + """ Log a message that will be included in the debug archive. + + Must be run within archive_context """ + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for line in str(msg).splitlines(): + log_file.write(timestamp + " | " + line.rstrip() + "\n") + + +def run_script(script): + """ Run a single script. Must be run within archive_context """ + log("Running script: " + script) + script_dir = os.path.join(archive_dir, script) + os.makedirs(script_dir) + env = os.environ.copy() + env["PYTHONPATH"] = "lib" # allow same imports as reactive code + env["DEBUG_SCRIPT_DIR"] = script_dir + with open(script_dir + "/stdout", "w") as stdout: + with open(script_dir + "/stderr", "w") as stderr: + process = subprocess.Popen( + "debug-scripts/" + script, + stdout=stdout, stderr=stderr, env=env + ) + try: + exit_code = process.wait(timeout=300) + except subprocess.TimeoutExpired: + log("ERROR: still running, terminating") + process.terminate() + try: + exit_code = process.wait(timeout=10) + except subprocess.TimeoutExpired: + log("ERROR: still running, killing") + process.kill() + exit_code = process.wait(timeout=10) + if exit_code != 0: + log("ERROR: %s failed with exit code %d" % (script, exit_code)) + + +def run_all_scripts(): + """ Run all scripts. For the sake of robustness, log and ignore any + exceptions that occur. + + Must be run within archive_context """ + scripts = os.listdir("debug-scripts") + for script in scripts: + try: + run_script(script) + except: + log(traceback.format_exc()) + + +def main(): + """ Open an archive context and run all scripts. """ + with archive_context(): + run_all_scripts() + + +if __name__ == "__main__": + main() diff --git a/kubernetes-worker/actions/microbot b/kubernetes-worker/actions/microbot new file mode 100755 index 0000000..5a3d6db --- /dev/null +++ b/kubernetes-worker/actions/microbot @@ -0,0 +1,81 @@ +#!/usr/local/sbin/charm-env python3 + +# Copyright 2015 The Kubernetes Authors. +# +# 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 sys + +from charmhelpers.core.hookenv import action_fail, action_get, action_set +from charmhelpers.core.hookenv import unit_public_ip +from charms.reactive import endpoint_from_flag +from charms.templating.jinja2 import render +from subprocess import call, check_output + +os.environ['PATH'] += os.pathsep + os.path.join(os.sep, 'snap', 'bin') + +context = {} +context['delete'] = action_get('delete') +context['public_address'] = unit_public_ip() +context['registry'] = action_get('registry') +context['replicas'] = action_get('replicas') + +arch = check_output(['dpkg', '--print-architecture']).rstrip() +context['arch'] = arch.decode('utf-8') + +if not context['replicas']: + context['replicas'] = 3 + +# Declare a kubectl template when invoking kubectl +kubectl = ['kubectl', '--kubeconfig=/root/.kube/config'] + +# Remove deployment if requested +if context['delete']: + service_del = kubectl + ['delete', 'svc', 'microbot'] + service_response = call(service_del) + deploy_del = kubectl + ['delete', 'deployment', 'microbot'] + deploy_response = call(deploy_del) + ingress_del = kubectl + ['delete', 'ing', 'microbot-ingress'] + ingress_response = call(ingress_del) + + if ingress_response != 0: + action_set({'microbot-ing': + 'Failed removal of microbot ingress resource.'}) + if deploy_response != 0: + action_set({'microbot-deployment': + 'Failed removal of microbot deployment resource.'}) + if service_response != 0: + action_set({'microbot-service': + 'Failed removal of microbot service resource.'}) + sys.exit(0) + +kube_control = endpoint_from_flag('kube-control.registry_location.available') +if kube_control: + registry_location = kube_control.get_registry_location() + context['registry'] = registry_location + +# Creation request +render('microbot-example.yaml', '/root/cdk/addons/microbot.yaml', + context) + +create_command = kubectl + ['apply', '-f', + '/root/cdk/addons/microbot.yaml'] + +create_response = call(create_command) + +if create_response == 0: + action_set({'address': + 'microbot.{}.nip.io'.format(context['public_address'])}) +else: + action_fail('Failed to apply microbot manifest.') diff --git a/kubernetes-worker/actions/pause b/kubernetes-worker/actions/pause new file mode 100755 index 0000000..f3bcabb --- /dev/null +++ b/kubernetes-worker/actions/pause @@ -0,0 +1,34 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess + +from charms.layer.kubernetes_common import ( + get_node_name, + kubectl, +) + +from charmhelpers.core.hookenv import ( + action_fail, + action_get, + status_set, +) + +# Make sure the kubectl snap can be found +os.environ['PATH'] += os.pathsep + os.path.join(os.sep, 'snap', 'bin') + +drain_args = ['--ignore-daemonsets'] + +if action_get('delete-local-data'): + drain_args.append('--delete-local-data=true') + +if action_get('force'): + drain_args.append('--force') + +try: + kubectl('drain', get_node_name(), *drain_args) +except subprocess.CalledProcessError as e: + action_fail('{}. See unit logs for details.'.format(str(e))) + raise + +status_set('waiting', 'Kubernetes unit paused') diff --git a/kubernetes-worker/actions/resume b/kubernetes-worker/actions/resume new file mode 100755 index 0000000..a8b6422 --- /dev/null +++ b/kubernetes-worker/actions/resume @@ -0,0 +1,25 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess + +from charms.layer.kubernetes_common import ( + get_node_name, + kubectl, +) + +from charmhelpers.core.hookenv import ( + action_fail, + status_set, +) + +# make sure the kubectl snap can be found +os.environ['PATH'] += os.pathsep + os.path.join(os.sep, 'snap', 'bin') + +try: + kubectl('uncordon', get_node_name()) +except subprocess.CalledProcessError as e: + action_fail('{}. See unit logs for details.'.format(str(e))) + raise + +status_set('active', 'Kubernetes unit resumed') diff --git a/kubernetes-worker/actions/upgrade b/kubernetes-worker/actions/upgrade new file mode 100755 index 0000000..a97c19b --- /dev/null +++ b/kubernetes-worker/actions/upgrade @@ -0,0 +1,5 @@ +#!/bin/sh +set -eux + +charms.reactive set_state kubernetes-worker.snaps.upgrade-specified +exec hooks/config-changed diff --git a/kubernetes-worker/bin/charm-env b/kubernetes-worker/bin/charm-env new file mode 100755 index 0000000..d211ce9 --- /dev/null +++ b/kubernetes-worker/bin/charm-env @@ -0,0 +1,107 @@ +#!/bin/bash + +VERSION="1.0.0" + + +find_charm_dirs() { + # Hopefully, $JUJU_CHARM_DIR is set so which venv to use in unambiguous. + if [[ -n "$JUJU_CHARM_DIR" || -n "$CHARM_DIR" ]]; then + if [[ -z "$JUJU_CHARM_DIR" ]]; then + # accept $CHARM_DIR to be more forgiving + export JUJU_CHARM_DIR="$CHARM_DIR" + fi + if [[ -z "$CHARM_DIR" ]]; then + # set CHARM_DIR as well to help with backwards compatibility + export CHARM_DIR="$JUJU_CHARM_DIR" + fi + return + fi + # Try to guess the value for JUJU_CHARM_DIR by looking for a non-subordinate + # (because there's got to be at least one principle) charm directory; + # if there are several, pick the first by alpha order. + agents_dir="/var/lib/juju/agents" + if [[ -d "$agents_dir" ]]; then + desired_charm="$1" + found_charm_dir="" + if [[ -n "$desired_charm" ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + charm_name="$(grep -o '^['\''"]\?name['\''"]\?:.*' $charm_dir/metadata.yaml 2> /dev/null | sed -e 's/.*: *//' -e 's/['\''"]//g')" + if [[ "$charm_name" == "$desired_charm" ]]; then + if [[ -n "$found_charm_dir" ]]; then + >&2 echo "Ambiguous possibilities for JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + found_charm_dir="$charm_dir" + fi + done + if [[ -z "$found_charm_dir" ]]; then + >&2 echo "Unable to determine JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" + exit 1 + fi + export JUJU_CHARM_DIR="$found_charm_dir" + export CHARM_DIR="$found_charm_dir" + return + fi + # shellcheck disable=SC2126 + non_subordinates="$(grep -L 'subordinate"\?:.*true' "$agents_dir"/unit-*/charm/metadata.yaml | wc -l)" + if [[ "$non_subordinates" -gt 1 ]]; then + >&2 echo 'Ambiguous possibilities for JUJU_CHARM_DIR; please use --charm or run within a Juju hook context' + exit 1 + elif [[ "$non_subordinates" -eq 1 ]]; then + for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do + if grep -q 'subordinate"\?:.*true' "$charm_dir/metadata.yaml"; then + continue + fi + export JUJU_CHARM_DIR="$charm_dir" + export CHARM_DIR="$charm_dir" + return + done + fi + fi + >&2 echo 'Unable to determine JUJU_CHARM_DIR; please run within a Juju hook context' + exit 1 +} + +try_activate_venv() { + if [[ -d "$JUJU_CHARM_DIR/../.venv" ]]; then + . "$JUJU_CHARM_DIR/../.venv/bin/activate" + fi +} + +find_wrapped() { + PATH="${PATH/\/usr\/local\/sbin:}" which "$(basename "$0")" +} + + +if [[ "$1" == "--version" || "$1" == "-v" ]]; then + echo "$VERSION" + exit 0 +fi + + +# allow --charm option to hint which JUJU_CHARM_DIR to choose when ambiguous +# NB: --charm option must come first +# NB: option must be processed outside find_charm_dirs to modify $@ +charm_name="" +if [[ "$1" == "--charm" ]]; then + charm_name="$2" + shift; shift +fi + +find_charm_dirs "$charm_name" +try_activate_venv +export PYTHONPATH="$JUJU_CHARM_DIR/lib:$PYTHONPATH" + +if [[ "$(basename "$0")" == "charm-env" ]]; then + # being used as a shebang + exec "$@" +elif [[ "$0" == "$BASH_SOURCE" ]]; then + # being invoked as a symlink wrapping something to find in the venv + exec "$(find_wrapped)" "$@" +elif [[ "$(basename "$BASH_SOURCE")" == "charm-env" ]]; then + # being sourced directly; do nothing + /bin/true +else + # being sourced for wrapped bash helpers + . "$(find_wrapped)" +fi diff --git a/kubernetes-worker/bin/layer_option b/kubernetes-worker/bin/layer_option new file mode 100755 index 0000000..3253ef8 --- /dev/null +++ b/kubernetes-worker/bin/layer_option @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from charms import layer + + +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 = layer.options.get(args.section, 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/kubernetes-worker/build-cni-resources.sh b/kubernetes-worker/build-cni-resources.sh new file mode 100755 index 0000000..40e602d --- /dev/null +++ b/kubernetes-worker/build-cni-resources.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -eux + +CNI_VERSION="${CNI_VERSION:-v0.7.5}" +ARCH="${ARCH:-amd64 arm64 s390x}" + +build_script_commit="$(git show --oneline -q)" +temp_dir="$(readlink -f build-cni-resources.tmp)" +rm -rf "$temp_dir" +mkdir "$temp_dir" +(cd "$temp_dir" + git clone https://github.com/containernetworking/plugins.git cni-plugins \ + --branch "$CNI_VERSION" \ + --depth 1 + + # Grab the user id and group id of this current user. + GROUP_ID=$(id -g) + USER_ID=$(id -u) + + for arch in $ARCH; do + echo "Building cni $CNI_VERSION for $arch" + rm -f cni-plugins/bin/* + docker run \ + --rm \ + -e GOOS=linux \ + -e GOARCH="$arch" \ + -v "$temp_dir"/cni-plugins:/cni \ + golang:1.15 \ + /bin/bash -c "cd /cni && ./build.sh && chown -R ${USER_ID}:${GROUP_ID} /cni" + + (cd cni-plugins/bin + echo "cni-$arch $CNI_VERSION" >> BUILD_INFO + echo "Built $(date)" >> BUILD_INFO + echo "build script commit: $build_script_commit" >> BUILD_INFO + echo "cni-plugins commit: $(git show --oneline -q)" >> BUILD_INFO + tar -czf "$temp_dir/cni-$arch.tgz" . + ) + done +) +mv "$temp_dir"/cni-*.tgz . +rm -rf "$temp_dir" diff --git a/kubernetes-worker/config.yaml b/kubernetes-worker/config.yaml new file mode 100644 index 0000000..82c578b --- /dev/null +++ b/kubernetes-worker/config.yaml @@ -0,0 +1,200 @@ +# 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 and snap_proxy_url have been deprecated for some time. + # If your charm still needs them, add these config items manually + # to your charm's config.yaml. + # snap_proxy: + # description: > + # DEPRECATED. Use snap-http-proxy and snap-https-proxy model configuration settings. + # HTTP/HTTPS web proxy for Snappy to use when accessing the snap store. + # type: string + # default: "" + # snap_proxy_url: + # default: "" + # type: string + # description: > + # DEPRECATED. Use snap-store-proxy model configuration setting. + # The address of a Snap Store Proxy to use for snaps e.g. http://snap-proxy.example.com + "snapd_refresh": + "default": "max" + "type": "string" + "description": | + How often snapd handles updates for installed snaps. Setting an empty + string will check 4x per day. Set to "max" to delay the refresh as long + as possible. You may also set a custom string as described in the + 'refresh.timer' section here: + https://forum.snapcraft.io/t/system-options/87 + "nagios_context": + "default": "juju" + "type": "string" + "description": | + Used by the nrpe subordinate charms. + A string that will be prepended to instance name to set the host name + in nagios. So for instance the hostname would be something like: + juju-myservice-0 + If you're running multiple environments with the same services in them + this allows you to differentiate between them. + "nagios_servicegroups": + "default": "" + "type": "string" + "description": | + A comma-separated list of nagios servicegroups. + If left empty, the nagios_context will be used as the servicegroup + "sysctl": + "type": "string" + "default": "{ net.ipv4.conf.all.forwarding : 1, net.ipv4.neigh.default.gc_thresh1\ + \ : 128, net.ipv4.neigh.default.gc_thresh2 : 28672, net.ipv4.neigh.default.gc_thresh3\ + \ : 32768, net.ipv6.neigh.default.gc_thresh1 : 128, net.ipv6.neigh.default.gc_thresh2\ + \ : 28672, net.ipv6.neigh.default.gc_thresh3 : 32768, fs.inotify.max_user_instances\ + \ : 8192, fs.inotify.max_user_watches : 1048576, kernel.panic : 10, kernel.panic_on_oops:\ + \ 1, vm.overcommit_memory : 1 }" + "description": | + YAML formatted associative array of sysctl values, e.g.: + '{kernel.pid_max : 4194303 }'. Note that kube-proxy handles + the conntrack settings. The proper way to alter them is to + use the proxy-extra-args config to set them, e.g.: + juju config kubernetes-master proxy-extra-args="conntrack-min=1000000 conntrack-max-per-core=250000" + juju config kubernetes-worker proxy-extra-args="conntrack-min=1000000 conntrack-max-per-core=250000" + The proxy-extra-args conntrack-min and conntrack-max-per-core can be set to 0 to ignore + kube-proxy's settings and use the sysctl settings instead. Note the fundamental difference between + the setting of conntrack-max-per-core vs nf_conntrack_max. + "proxy-extra-args": + "type": "string" + "default": "" + "description": | + Space separated list of flags and key=value pairs that will be passed as arguments to + kube-proxy. For example a value like this: + runtime-config=batch/v2alpha1=true profiling=true + will result in kube-apiserver being run with the following options: + --runtime-config=batch/v2alpha1=true --profiling=true + "ingress": + "type": "boolean" + "default": !!bool "true" + "description": | + Deploy the default http backend and ingress controller to handle + ingress requests. + + Set to false if deploying an alternate ingress controller, and note + that you may need to manually open ports 80 and 443 on the nodes: + juju run --application kubernetes-worker -- open-port 80 && open-port 443 + "labels": + "type": "string" + "default": "" + "description": | + Labels can be used to organize and to select subsets of nodes in the + cluster. Declare node labels in key=value format, separated by spaces. + "channel": + "type": "string" + "default": "1.23/edge" + "description": | + Snap channel to install Kubernetes worker services from + "require-manual-upgrade": + "type": "boolean" + "default": !!bool "true" + "description": | + When true, worker services will not be upgraded until the user triggers + it manually by running the upgrade action. + "kubelet-extra-args": + "type": "string" + "default": "" + "description": | + Space separated list of flags and key=value pairs that will be passed as arguments to + kubelet. For example a value like this: + runtime-config=batch/v2alpha1=true profiling=true + will result in kubelet being run with the following options: + --runtime-config=batch/v2alpha1=true --profiling=true + Note: As of Kubernetes 1.10.x, many of Kubelet's args have been deprecated, and can + be set with kubelet-extra-config instead. + "ingress-default-ssl-certificate": + "type": "string" + "default": "" + "description": | + SSL certificate to be used by the default HTTPS server. If one of the + flag ingress-default-ssl-certificate or ingress-default-ssl-key is not + provided ingress will use a self-signed certificate. This parameter is + specific to nginx-ingress-controller. + "ingress-default-ssl-key": + "type": "string" + "default": "" + "description": | + Private key to be used by the default HTTPS server. If one of the flag + ingress-default-ssl-certificate or ingress-default-ssl-key is not + provided ingress will use a self-signed certificate. This parameter is + specific to nginx-ingress-controller. + "ingress-ssl-passthrough": + "type": "boolean" + "default": !!bool "false" + "description": | + Enable ssl passthrough on ingress server. This allows passing the ssl + connection through to the workloads and not terminating it at the ingress + controller. + "ingress-ssl-chain-completion": + "type": "boolean" + "default": !!bool "false" + "description": | + Enable chain completion for TLS certificates used by the nginx ingress + controller. Set this to true if you would like the ingress controller + to attempt auto-retrieval of intermediate certificates. The default + (false) is recommended for all production kubernetes installations, and + any environment which does not have outbound Internet access. + "ingress-use-forwarded-headers": + "type": "boolean" + "default": !!bool "false" + "description": | + If true, NGINX passes the incoming X-Forwarded-* headers to upstreams. Use this + option when NGINX is behind another L7 proxy / load balancer that is setting + these headers. + + If false, NGINX ignores incoming X-Forwarded-* headers, filling them with the + request information it sees. Use this option if NGINX is exposed directly to + the internet, or it's behind a L3/packet-based load balancer that doesn't alter + the source IP in the packets. + + Reference: https://github.com/kubernetes/ingress-nginx/blob/a9c706be12a8be418c49ab1f60a02f52f9b14e55/ + docs/user-guide/nginx-configuration/configmap.md#use-forwarded-headers. + "nginx-image": + "type": "string" + "default": "auto" + "description": | + Docker image to use for the nginx ingress controller. Using "auto" will select + an image based on architecture. + + Example: + quay.io/kubernetes-ingress-controller/nginx-ingress-controller-amd64:0.32.0 + "default-backend-image": + "type": "string" + "default": "auto" + "description": | + Docker image to use for the default backend. Auto will select an image + based on architecture. + "kubelet-extra-config": + "default": "{}" + "type": "string" + "description": | + Extra configuration to be passed to kubelet. Any values specified in this + config will be merged into a KubeletConfiguration file that is passed to + the kubelet service via the --config flag. This can be used to override + values provided by the charm. + + Requires Kubernetes 1.10+. + + The value for this config must be a YAML mapping that can be safely + merged with a KubeletConfiguration file. For example: + {evictionHard: {memory.available: 200Mi}} + + For more information about KubeletConfiguration, see upstream docs: + https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/ diff --git a/kubernetes-worker/copyright b/kubernetes-worker/copyright new file mode 100644 index 0000000..ac5e525 --- /dev/null +++ b/kubernetes-worker/copyright @@ -0,0 +1,13 @@ +Copyright 2016 The Kubernetes Authors. + + 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/kubernetes-worker/copyright.layer-basic b/kubernetes-worker/copyright.layer-basic new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/kubernetes-worker/copyright.layer-basic @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-worker/copyright.layer-coordinator b/kubernetes-worker/copyright.layer-coordinator new file mode 100644 index 0000000..b8518aa --- /dev/null +++ b/kubernetes-worker/copyright.layer-coordinator @@ -0,0 +1,15 @@ +Copyright 2015-2016 Canonical Ltd. + +This file is part of the Coordinator Layer for Juju. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/kubernetes-worker/copyright.layer-leadership b/kubernetes-worker/copyright.layer-leadership new file mode 100644 index 0000000..08b983f --- /dev/null +++ b/kubernetes-worker/copyright.layer-leadership @@ -0,0 +1,15 @@ +Copyright 2015-2016 Canonical Ltd. + +This file is part of the Leadership Layer for Juju. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License version 3, as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranties of +MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/kubernetes-worker/copyright.layer-metrics b/kubernetes-worker/copyright.layer-metrics new file mode 100644 index 0000000..2df15bd --- /dev/null +++ b/kubernetes-worker/copyright.layer-metrics @@ -0,0 +1,13 @@ +Copyright 2016 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. diff --git a/kubernetes-worker/copyright.layer-nagios b/kubernetes-worker/copyright.layer-nagios new file mode 100644 index 0000000..c80db95 --- /dev/null +++ b/kubernetes-worker/copyright.layer-nagios @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2016, Canonical Ltd. +License: GPL-3 + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 3, as + published by the Free Software Foundation. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranties of + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . diff --git a/kubernetes-worker/copyright.layer-options b/kubernetes-worker/copyright.layer-options new file mode 100644 index 0000000..d4fdd18 --- /dev/null +++ b/kubernetes-worker/copyright.layer-options @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-worker/copyright.layer-snap b/kubernetes-worker/copyright.layer-snap new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/kubernetes-worker/copyright.layer-snap @@ -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/kubernetes-worker/debug-scripts/charm-unitdata b/kubernetes-worker/debug-scripts/charm-unitdata new file mode 100755 index 0000000..d2aac60 --- /dev/null +++ b/kubernetes-worker/debug-scripts/charm-unitdata @@ -0,0 +1,12 @@ +#!/usr/local/sbin/charm-env python3 + +import debug_script +import json +from charmhelpers.core import unitdata + +kv = unitdata.kv() +data = kv.getrange("") + +with debug_script.open_file("unitdata.json", "w") as f: + json.dump(data, f, indent=2) + f.write("\n") diff --git a/kubernetes-worker/debug-scripts/filesystem b/kubernetes-worker/debug-scripts/filesystem new file mode 100755 index 0000000..c5ec6d8 --- /dev/null +++ b/kubernetes-worker/debug-scripts/filesystem @@ -0,0 +1,17 @@ +#!/bin/sh +set -ux + +# report file system disk space usage +df -hT > $DEBUG_SCRIPT_DIR/df-hT +# estimate file space usage +du -h / 2>&1 > $DEBUG_SCRIPT_DIR/du-h +# list the mounted filesystems +mount > $DEBUG_SCRIPT_DIR/mount +# list the mounted systems with ascii trees +findmnt -A > $DEBUG_SCRIPT_DIR/findmnt +# list block devices +lsblk > $DEBUG_SCRIPT_DIR/lsblk +# list open files +lsof 2>&1 > $DEBUG_SCRIPT_DIR/lsof +# list local system locks +lslocks > $DEBUG_SCRIPT_DIR/lslocks diff --git a/kubernetes-worker/debug-scripts/inotify b/kubernetes-worker/debug-scripts/inotify new file mode 100755 index 0000000..350e20f --- /dev/null +++ b/kubernetes-worker/debug-scripts/inotify @@ -0,0 +1,8 @@ +#!/bin/sh +set -ux + +# We had to bump inotify limits once in the past, hence why this oddly specific +# script lives here in kubernetes-worker. + +sysctl fs.inotify > $DEBUG_SCRIPT_DIR/sysctl-limits +ls -l /proc/*/fd/* | grep inotify > $DEBUG_SCRIPT_DIR/inotify-instances diff --git a/kubernetes-worker/debug-scripts/juju-logs b/kubernetes-worker/debug-scripts/juju-logs new file mode 100755 index 0000000..d27c458 --- /dev/null +++ b/kubernetes-worker/debug-scripts/juju-logs @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +cp -v /var/log/juju/* $DEBUG_SCRIPT_DIR diff --git a/kubernetes-worker/debug-scripts/juju-network-get b/kubernetes-worker/debug-scripts/juju-network-get new file mode 100755 index 0000000..983c8c4 --- /dev/null +++ b/kubernetes-worker/debug-scripts/juju-network-get @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import subprocess +import yaml +import debug_script + +with open('metadata.yaml') as f: + metadata = yaml.load(f) + +relations = [] +for key in ['requires', 'provides', 'peers']: + relations += list(metadata.get(key, {}).keys()) + +os.mkdir(os.path.join(debug_script.dir, 'relations')) + +for relation in relations: + path = 'relations/' + relation + with debug_script.open_file(path, 'w') as f: + cmd = ['network-get', relation] + subprocess.call(cmd, stdout=f, stderr=subprocess.STDOUT) diff --git a/kubernetes-worker/debug-scripts/kubectl b/kubernetes-worker/debug-scripts/kubectl new file mode 100755 index 0000000..1192c3c --- /dev/null +++ b/kubernetes-worker/debug-scripts/kubectl @@ -0,0 +1,15 @@ +#!/bin/sh +set -ux + +export PATH=$PATH:/snap/bin + +alias kubectl="kubectl --kubeconfig=/root/cdk/kubeconfig" + +kubectl cluster-info > $DEBUG_SCRIPT_DIR/cluster-info +kubectl cluster-info dump > $DEBUG_SCRIPT_DIR/cluster-info-dump +for obj in pods svc ingress secrets pv pvc rc; do + kubectl describe $obj --all-namespaces > $DEBUG_SCRIPT_DIR/describe-$obj +done +for obj in nodes; do + kubectl describe $obj > $DEBUG_SCRIPT_DIR/describe-$obj +done diff --git a/kubernetes-worker/debug-scripts/kubernetes-worker-services b/kubernetes-worker/debug-scripts/kubernetes-worker-services new file mode 100755 index 0000000..4f9dfa2 --- /dev/null +++ b/kubernetes-worker/debug-scripts/kubernetes-worker-services @@ -0,0 +1,9 @@ +#!/bin/sh +set -ux + +for service in kubelet kube-proxy; do + systemctl status snap.$service.daemon > $DEBUG_SCRIPT_DIR/$service-systemctl-status + journalctl -u snap.$service.daemon > $DEBUG_SCRIPT_DIR/$service-journal +done + +# FIXME: get the snap config or something diff --git a/kubernetes-worker/debug-scripts/network b/kubernetes-worker/debug-scripts/network new file mode 100755 index 0000000..944a355 --- /dev/null +++ b/kubernetes-worker/debug-scripts/network @@ -0,0 +1,11 @@ +#!/bin/sh +set -ux + +ifconfig -a > $DEBUG_SCRIPT_DIR/ifconfig +cp -v /etc/resolv.conf $DEBUG_SCRIPT_DIR/resolv.conf +cp -v /etc/network/interfaces $DEBUG_SCRIPT_DIR/interfaces +netstat -planut > $DEBUG_SCRIPT_DIR/netstat +route -n > $DEBUG_SCRIPT_DIR/route +iptables-save > $DEBUG_SCRIPT_DIR/iptables-save +dig google.com > $DEBUG_SCRIPT_DIR/dig-google +ping -w 2 -i 0.1 google.com > $DEBUG_SCRIPT_DIR/ping-google diff --git a/kubernetes-worker/debug-scripts/packages b/kubernetes-worker/debug-scripts/packages new file mode 100755 index 0000000..b60a9cf --- /dev/null +++ b/kubernetes-worker/debug-scripts/packages @@ -0,0 +1,7 @@ +#!/bin/sh +set -ux + +dpkg --list > $DEBUG_SCRIPT_DIR/dpkg-list +snap list > $DEBUG_SCRIPT_DIR/snap-list +pip2 list > $DEBUG_SCRIPT_DIR/pip2-list +pip3 list > $DEBUG_SCRIPT_DIR/pip3-list diff --git a/kubernetes-worker/debug-scripts/sysctl b/kubernetes-worker/debug-scripts/sysctl new file mode 100755 index 0000000..a86a6c8 --- /dev/null +++ b/kubernetes-worker/debug-scripts/sysctl @@ -0,0 +1,4 @@ +#!/bin/sh +set -ux + +sysctl -a > $DEBUG_SCRIPT_DIR/sysctl diff --git a/kubernetes-worker/debug-scripts/systemd b/kubernetes-worker/debug-scripts/systemd new file mode 100755 index 0000000..8bb9b6f --- /dev/null +++ b/kubernetes-worker/debug-scripts/systemd @@ -0,0 +1,9 @@ +#!/bin/sh +set -ux + +systemctl --all > $DEBUG_SCRIPT_DIR/systemctl +journalctl > $DEBUG_SCRIPT_DIR/journalctl +systemd-analyze time > $DEBUG_SCRIPT_DIR/systemd-analyze-time +systemd-analyze blame > $DEBUG_SCRIPT_DIR/systemd-analyze-blame +systemd-analyze critical-chain > $DEBUG_SCRIPT_DIR/systemd-analyze-critical-chain +systemd-analyze dump > $DEBUG_SCRIPT_DIR/systemd-analyze-dump diff --git a/kubernetes-worker/debug-scripts/tls-certs b/kubernetes-worker/debug-scripts/tls-certs new file mode 100755 index 0000000..2692e51 --- /dev/null +++ b/kubernetes-worker/debug-scripts/tls-certs @@ -0,0 +1,21 @@ +#!/usr/local/sbin/charm-env python3 + +import os +import shutil +import traceback +import debug_script +from charms import layer + +options = layer.options.get('tls-client') + +def copy_cert(source_key, name): + try: + source = options[source_key] + dest = os.path.join(debug_script.dir, name) + shutil.copy(source, dest) + except Exception: + traceback.print_exc() + +copy_cert('client_certificate_path', 'client.crt') +copy_cert('server_certificate_path', 'server.crt') +copy_cert('ca_certificate_path', 'ca.crt') diff --git a/kubernetes-worker/exec.d/docker-compose/charm-pre-install b/kubernetes-worker/exec.d/docker-compose/charm-pre-install new file mode 100644 index 0000000..f0202c5 --- /dev/null +++ b/kubernetes-worker/exec.d/docker-compose/charm-pre-install @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# This stubs out charm-pre-install coming from layer-docker as a workaround for +# offline installs until https://github.com/juju/charm-tools/issues/301 is fixed. diff --git a/kubernetes-worker/exec.d/vmware-patch/charm-pre-install b/kubernetes-worker/exec.d/vmware-patch/charm-pre-install new file mode 100755 index 0000000..b5e6d97 --- /dev/null +++ b/kubernetes-worker/exec.d/vmware-patch/charm-pre-install @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +MY_HOSTNAME=$(hostname) + +: ${JUJU_UNIT_NAME:=`uuidgen`} + + +if [ "${MY_HOSTNAME}" == "ubuntuguest" ]; then + juju-log "Detected broken vsphere integration. Applying hostname override" + + FRIENDLY_HOSTNAME=$(echo $JUJU_UNIT_NAME | tr / -) + juju-log "Setting hostname to $FRIENDLY_HOSTNAME" + if [ ! -f /etc/hostname.orig ]; then + mv /etc/hostname /etc/hostname.orig + fi + echo "${FRIENDLY_HOSTNAME}" > /etc/hostname + hostname $FRIENDLY_HOSTNAME +fi diff --git a/kubernetes-worker/hooks/aws-relation-broken b/kubernetes-worker/hooks/aws-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/aws-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/aws-relation-changed b/kubernetes-worker/hooks/aws-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/aws-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/aws-relation-created b/kubernetes-worker/hooks/aws-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/aws-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/aws-relation-departed b/kubernetes-worker/hooks/aws-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/aws-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/aws-relation-joined b/kubernetes-worker/hooks/aws-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/aws-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/azure-relation-broken b/kubernetes-worker/hooks/azure-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/azure-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/azure-relation-changed b/kubernetes-worker/hooks/azure-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/azure-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/azure-relation-created b/kubernetes-worker/hooks/azure-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/azure-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/azure-relation-departed b/kubernetes-worker/hooks/azure-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/azure-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/azure-relation-joined b/kubernetes-worker/hooks/azure-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/azure-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/certificates-relation-broken b/kubernetes-worker/hooks/certificates-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/certificates-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/certificates-relation-changed b/kubernetes-worker/hooks/certificates-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/certificates-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/certificates-relation-created b/kubernetes-worker/hooks/certificates-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/certificates-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/certificates-relation-departed b/kubernetes-worker/hooks/certificates-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/certificates-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/certificates-relation-joined b/kubernetes-worker/hooks/certificates-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/certificates-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/cni-relation-broken b/kubernetes-worker/hooks/cni-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/cni-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/cni-relation-changed b/kubernetes-worker/hooks/cni-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/cni-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/cni-relation-created b/kubernetes-worker/hooks/cni-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/cni-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/cni-relation-departed b/kubernetes-worker/hooks/cni-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/cni-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/cni-relation-joined b/kubernetes-worker/hooks/cni-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/cni-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/collect-metrics b/kubernetes-worker/hooks/collect-metrics new file mode 100755 index 0000000..8a27863 --- /dev/null +++ b/kubernetes-worker/hooks/collect-metrics @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +import yaml +import os +from subprocess import check_output, check_call, CalledProcessError + + +def build_command(doc): + values = {} + metrics = doc.get("metrics", {}) + for metric, mdoc in metrics.items(): + if not mdoc: + continue + cmd = mdoc.get("command") + if cmd: + try: + value = check_output(cmd, shell=True, universal_newlines=True) + except CalledProcessError as e: + check_call(['juju-log', '-lERROR', + 'Error collecting metric {}:\n{}'.format( + metric, e.output)]) + continue + value = value.strip() + if value: + values[metric] = value + + if not values: + return None + command = ["add-metric"] + for metric, value in values.items(): + command.append("%s=%s" % (metric, value)) + return command + + +if __name__ == '__main__': + charm_dir = os.path.dirname(os.path.abspath(os.path.join(__file__, ".."))) + metrics_yaml = os.path.join(charm_dir, "metrics.yaml") + with open(metrics_yaml) as f: + doc = yaml.load(f) + command = build_command(doc) + if command: + check_call(command) diff --git a/kubernetes-worker/hooks/config-changed b/kubernetes-worker/hooks/config-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/config-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/container-runtime-relation-broken b/kubernetes-worker/hooks/container-runtime-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/container-runtime-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/container-runtime-relation-changed b/kubernetes-worker/hooks/container-runtime-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/container-runtime-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/container-runtime-relation-created b/kubernetes-worker/hooks/container-runtime-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/container-runtime-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/container-runtime-relation-departed b/kubernetes-worker/hooks/container-runtime-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/container-runtime-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/container-runtime-relation-joined b/kubernetes-worker/hooks/container-runtime-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/container-runtime-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/coordinator-relation-broken b/kubernetes-worker/hooks/coordinator-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/coordinator-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/coordinator-relation-changed b/kubernetes-worker/hooks/coordinator-relation-changed new file mode 100755 index 0000000..fe39f65 --- /dev/null +++ b/kubernetes-worker/hooks/coordinator-relation-changed @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer.basic import bootstrap_charm_deps +bootstrap_charm_deps() + + +# This will load and run the appropriate @hook and other decorated +# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive, +# and $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/kubernetes-worker/hooks/coordinator-relation-created b/kubernetes-worker/hooks/coordinator-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/coordinator-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/coordinator-relation-departed b/kubernetes-worker/hooks/coordinator-relation-departed new file mode 100755 index 0000000..fe39f65 --- /dev/null +++ b/kubernetes-worker/hooks/coordinator-relation-departed @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer.basic import bootstrap_charm_deps +bootstrap_charm_deps() + + +# This will load and run the appropriate @hook and other decorated +# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive, +# and $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/kubernetes-worker/hooks/coordinator-relation-joined b/kubernetes-worker/hooks/coordinator-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/coordinator-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/gcp-relation-broken b/kubernetes-worker/hooks/gcp-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/gcp-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/gcp-relation-changed b/kubernetes-worker/hooks/gcp-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/gcp-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/gcp-relation-created b/kubernetes-worker/hooks/gcp-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/gcp-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/gcp-relation-departed b/kubernetes-worker/hooks/gcp-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/gcp-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/gcp-relation-joined b/kubernetes-worker/hooks/gcp-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/gcp-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/hook.template b/kubernetes-worker/hooks/hook.template new file mode 100644 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/hook.template @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/ingress-proxy-relation-broken b/kubernetes-worker/hooks/ingress-proxy-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/ingress-proxy-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/ingress-proxy-relation-changed b/kubernetes-worker/hooks/ingress-proxy-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/ingress-proxy-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/ingress-proxy-relation-created b/kubernetes-worker/hooks/ingress-proxy-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/ingress-proxy-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/ingress-proxy-relation-departed b/kubernetes-worker/hooks/ingress-proxy-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/ingress-proxy-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/ingress-proxy-relation-joined b/kubernetes-worker/hooks/ingress-proxy-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/ingress-proxy-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/install b/kubernetes-worker/hooks/install new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/install @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/kube-api-endpoint-relation-broken b/kubernetes-worker/hooks/kube-api-endpoint-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/kube-api-endpoint-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/kube-api-endpoint-relation-changed b/kubernetes-worker/hooks/kube-api-endpoint-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/kube-api-endpoint-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/kube-api-endpoint-relation-created b/kubernetes-worker/hooks/kube-api-endpoint-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/kube-api-endpoint-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/kube-api-endpoint-relation-departed b/kubernetes-worker/hooks/kube-api-endpoint-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/kube-api-endpoint-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/kube-api-endpoint-relation-joined b/kubernetes-worker/hooks/kube-api-endpoint-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/kube-api-endpoint-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/kube-control-relation-broken b/kubernetes-worker/hooks/kube-control-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/kube-control-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/kube-control-relation-changed b/kubernetes-worker/hooks/kube-control-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/kube-control-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/kube-control-relation-created b/kubernetes-worker/hooks/kube-control-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/kube-control-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/kube-control-relation-departed b/kubernetes-worker/hooks/kube-control-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/kube-control-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/kube-control-relation-joined b/kubernetes-worker/hooks/kube-control-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/kube-control-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/leader-elected b/kubernetes-worker/hooks/leader-elected new file mode 100755 index 0000000..fe39f65 --- /dev/null +++ b/kubernetes-worker/hooks/leader-elected @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer.basic import bootstrap_charm_deps +bootstrap_charm_deps() + + +# This will load and run the appropriate @hook and other decorated +# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive, +# and $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/kubernetes-worker/hooks/leader-settings-changed b/kubernetes-worker/hooks/leader-settings-changed new file mode 100755 index 0000000..fe39f65 --- /dev/null +++ b/kubernetes-worker/hooks/leader-settings-changed @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Load modules from $CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer.basic import bootstrap_charm_deps +bootstrap_charm_deps() + + +# This will load and run the appropriate @hook and other decorated +# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive, +# and $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/kubernetes-worker/hooks/nfs-relation-broken b/kubernetes-worker/hooks/nfs-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/nfs-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/nfs-relation-changed b/kubernetes-worker/hooks/nfs-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/nfs-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/nfs-relation-created b/kubernetes-worker/hooks/nfs-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/nfs-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/nfs-relation-departed b/kubernetes-worker/hooks/nfs-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/nfs-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/nfs-relation-joined b/kubernetes-worker/hooks/nfs-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/nfs-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/nrpe-external-master-relation-broken b/kubernetes-worker/hooks/nrpe-external-master-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/nrpe-external-master-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/nrpe-external-master-relation-changed b/kubernetes-worker/hooks/nrpe-external-master-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/nrpe-external-master-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/nrpe-external-master-relation-created b/kubernetes-worker/hooks/nrpe-external-master-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/nrpe-external-master-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/nrpe-external-master-relation-departed b/kubernetes-worker/hooks/nrpe-external-master-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/nrpe-external-master-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/nrpe-external-master-relation-joined b/kubernetes-worker/hooks/nrpe-external-master-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/nrpe-external-master-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/openstack-relation-broken b/kubernetes-worker/hooks/openstack-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/openstack-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/openstack-relation-changed b/kubernetes-worker/hooks/openstack-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/openstack-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/openstack-relation-created b/kubernetes-worker/hooks/openstack-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/openstack-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/openstack-relation-departed b/kubernetes-worker/hooks/openstack-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/openstack-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/openstack-relation-joined b/kubernetes-worker/hooks/openstack-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/openstack-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/post-series-upgrade b/kubernetes-worker/hooks/post-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/post-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/pre-series-upgrade b/kubernetes-worker/hooks/pre-series-upgrade new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/pre-series-upgrade @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/relations/aws-integration/.gitignore b/kubernetes-worker/hooks/relations/aws-integration/.gitignore new file mode 100644 index 0000000..ba1431e --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/.gitignore @@ -0,0 +1,2 @@ +.tox +__pycache__ diff --git a/kubernetes-worker/hooks/relations/aws-integration/LICENSE b/kubernetes-worker/hooks/relations/aws-integration/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-worker/hooks/relations/aws-integration/README.md b/kubernetes-worker/hooks/relations/aws-integration/README.md new file mode 100644 index 0000000..59abfcf --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/README.md @@ -0,0 +1,28 @@ +# Overview + +This layer encapsulates the `aws-integration` interface communciation protocol +and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:aws-integration` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:aws-integration'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `aws-integration` interface protocol: + +```yaml +requires: + aws: + interface: aws-integration +``` + +For documentation on how to use the API for this interface, see: + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) (this will only be used by the aws-integrator charm) diff --git a/kubernetes-worker/hooks/relations/aws-integration/__init__.py b/kubernetes-worker/hooks/relations/aws-integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/aws-integration/copyright b/kubernetes-worker/hooks/relations/aws-integration/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-worker/hooks/relations/aws-integration/docs/provides.md b/kubernetes-worker/hooks/relations/aws-integration/docs/provides.md new file mode 100644 index 0000000..57ecb25 --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/docs/provides.md @@ -0,0 +1,179 @@ +

provides

+ + +This is the provides side of the interface layer, for use only by the AWS +integrator charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for AWS integration features. + The AWS integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. + +

AWSIntegrationProvides

+ +```python +AWSIntegrationProvides(self, endpoint_name, relation_ids=None) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag +from charms import layer + +@when('endpoint.aws.requested') +def handle_requests(): + aws = endpoint_from_flag('endpoint.aws.requested') + for request in aws.requests: + if request.instance_tags: + tag_instance( + request.instance_id, + request.region, + request.instance_tags) + if request.requested_load_balancer_management: + layer.aws.enable_load_balancer_management( + request.application_name, + request.instance_id, + request.region, + ) + # ... + request.mark_completed() +``` + +

application_names

+ + +Set of names of all applications that are still joined. + +

requests

+ + +A list of the new or updated `IntegrationRequests` that +have been made. + +

unit_instances

+ + +Mapping of unit names to instance IDs and regions for all joined units. + +

IntegrationRequest

+ +```python +IntegrationRequest(self, unit) +``` + +A request for integration from a single remote unit. + +

application_name

+ + +The name of the application making the request. + +

changed

+ + +Whether this request has changed since the last time it was +marked completed. + +

hash

+ + +SHA hash of the data for this request. + +

instance_id

+ + +The instance ID reported for this request. + +

instance_security_group_tags

+ + +Mapping of tag names to values (or `None`) to apply to this instance's +machine-specific security group (firewall). + +

instance_subnet_tags

+ + +Mapping of tag names to values (or `None`) to apply to this instance's +subnet. + +

instance_tags

+ + +Mapping of tag names to values (or `None`) to apply to this instance. + +

object_storage_access_patterns

+ + +List of patterns to which to restrict object storage access. + +

object_storage_management_patterns

+ + +List of patterns to which to restrict object storage management. + +

region

+ + +The region reported for this request. + +

requested_block_storage_management

+ + +Flag indicating whether block storage management was requested. + +

requested_dns_management

+ + +Flag indicating whether DNS management was requested. + +

requested_instance_inspection

+ + +Flag indicating whether the ability to inspect instances was requested. + +

requested_load_balancer_management

+ + +Flag indicating whether load balancer management was requested. + +

requested_network_management

+ + +Flag indicating whether the ability to manage networking (firewalls, +subnets, etc) was requested. + +

requested_object_storage_access

+ + +Flag indicating whether object storage access was requested. + +

requested_object_storage_management

+ + +Flag indicating whether object storage management was requested. + +

unit_name

+ + +The name of the unit making the request. + +

mark_completed

+ +```python +IntegrationRequest.mark_completed(self) +``` + +Mark this request as having been completed. + +

clear

+ +```python +IntegrationRequest.clear(self) +``` + +Clear this request's cached data. + diff --git a/kubernetes-worker/hooks/relations/aws-integration/docs/requires.md b/kubernetes-worker/hooks/relations/aws-integration/docs/requires.md new file mode 100644 index 0000000..41607f4 --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/docs/requires.md @@ -0,0 +1,178 @@ +

requires

+ + +This is the requires side of the interface layer, for use in charms that +wish to request integration with AWS native features. The integration will +be provided by the AWS integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of AWS specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific AWS features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the AWS instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. + +

AWSIntegrationRequires

+ +```python +AWSIntegrationRequires(self, *args, **kwargs) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag + +@when('endpoint.aws.joined') +def request_aws_integration(): + aws = endpoint_from_flag('endpoint.aws.joined') + aws.request_instance_tags({ + 'tag1': 'value1', + 'tag2': None, + }) + aws.request_load_balancer_management() + # ... + +@when('endpoint.aws.ready') +def aws_integration_ready(): + update_config_enable_aws() +``` + +

instance_id

+ + +This unit's instance-id. + +

region

+ + +The region this unit is in. + +

tag_instance

+ +```python +AWSIntegrationRequires.tag_instance(self, tags) +``` + +Request that the given tags be applied to this instance. + +__Parameters__ + +- __`tags` (dict)__: Mapping of tag names to values (or `None`). + +

tag_instance_security_group

+ +```python +AWSIntegrationRequires.tag_instance_security_group(self, tags) +``` + +Request that the given tags be applied to this instance's +machine-specific security group (firewall) created by Juju. + +__Parameters__ + +- __`tags` (dict)__: Mapping of tag names to values (or `None`). + +

tag_instance_subnet

+ +```python +AWSIntegrationRequires.tag_instance_subnet(self, tags) +``` + +Request that the given tags be applied to this instance's subnet. + +__Parameters__ + +- __`tags` (dict)__: Mapping of tag names to values (or `None`). + +

enable_acm_readonly

+ +```python +AWSIntegrationRequires.enable_acm_readonly(self) +``` + +Request readonly for ACM. + +

enable_acm_fullaccess

+ +```python +AWSIntegrationRequires.enable_acm_fullaccess(self) +``` + +Request fullaccess for ACM. + +

enable_instance_inspection

+ +```python +AWSIntegrationRequires.enable_instance_inspection(self) +``` + +Request the ability to inspect instances. + +

enable_network_management

+ +```python +AWSIntegrationRequires.enable_network_management(self) +``` + +Request the ability to manage networking (firewalls, subnets, etc). + +

enable_load_balancer_management

+ +```python +AWSIntegrationRequires.enable_load_balancer_management(self) +``` + +Request the ability to manage load balancers. + +

enable_block_storage_management

+ +```python +AWSIntegrationRequires.enable_block_storage_management(self) +``` + +Request the ability to manage block storage. + +

enable_dns_management

+ +```python +AWSIntegrationRequires.enable_dns_management(self) +``` + +Request the ability to manage DNS. + +

enable_object_storage_access

+ +```python +AWSIntegrationRequires.enable_object_storage_access(self, patterns=None) +``` + +Request the ability to access object storage. + +__Parameters__ + +- __`patterns` (list)__: If given, restrict access to the resources matching + the patterns. If patterns do not start with the S3 ARN prefix +- __(`arn__:aws:s3:::`), it will be prepended. + +

enable_object_storage_management

+ +```python +AWSIntegrationRequires.enable_object_storage_management(self, patterns=None) +``` + +Request the ability to manage object storage. + +__Parameters__ + +- __`patterns` (list)__: If given, restrict management to the resources + matching the patterns. If patterns do not start with the S3 ARN +- __prefix (`arn__:aws:s3:::`), it will be prepended. + diff --git a/kubernetes-worker/hooks/relations/aws-integration/interface.yaml b/kubernetes-worker/hooks/relations/aws-integration/interface.yaml new file mode 100644 index 0000000..fe3da6d --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/interface.yaml @@ -0,0 +1,4 @@ +name: aws-integration +summary: Interface for connecting to the AWS integrator charm. +version: 1 +maintainer: Cory Johns diff --git a/kubernetes-worker/hooks/relations/aws-integration/make_docs b/kubernetes-worker/hooks/relations/aws-integration/make_docs new file mode 100644 index 0000000..72b69c2 --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'aws': {'interface': 'aws-integration'}}, + 'provides': {'aws': {'interface': 'aws-integration'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-worker/hooks/relations/aws-integration/provides.py b/kubernetes-worker/hooks/relations/aws-integration/provides.py new file mode 100644 index 0000000..ae94211 --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/provides.py @@ -0,0 +1,288 @@ +""" +This is the provides side of the interface layer, for use only by the AWS +integrator charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for AWS integration features. + The AWS integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. +""" + +import json +from hashlib import sha256 + +from charmhelpers.core import unitdata + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class AWSIntegrationProvides(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + + @when('endpoint.aws.requested') + def handle_requests(): + aws = endpoint_from_flag('endpoint.aws.requested') + for request in aws.requests: + if request.instance_tags: + tag_instance( + request.instance_id, + request.region, + request.instance_tags) + if request.requested_load_balancer_management: + layer.aws.enable_load_balancer_management( + request.application_name, + request.instance_id, + request.region, + ) + # ... + request.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + requests = self.requests + toggle_flag(self.expand_name('requested'), len(requests) > 0) + clear_flag(self.expand_name('changed')) + + @when('endpoint.{endpoint_name}.departed') + def cleanup(self): + for unit in self.all_departed_units: + request = IntegrationRequest(unit) + request.clear() + self.all_departed_units.clear() + clear_flag(self.expand_name('departed')) + + @property + def requests(self): + """ + A list of the new or updated #IntegrationRequests that + have been made. + """ + return [request for request in self.all_requests if request.changed] + + @property + def all_requests(self): + """ + A list of all the #IntegrationRequests that have been made, + even if unchanged. + """ + return [IntegrationRequest(unit) for unit in self.all_joined_units] + + @property + def application_names(self): + """ + Set of names of all applications that are still joined. + """ + return {unit.application_name for unit in self.all_joined_units} + + @property + def unit_instances(self): + """ + Mapping of unit names to instance IDs and regions for all joined units. + """ + return { + unit.unit_name: { + 'instance-id': unit.received['instance-id'], + 'region': unit.received['region'], + } for unit in self.all_joined_units + } + + +class IntegrationRequest: + """ + A request for integration from a single remote unit. + """ + def __init__(self, unit): + self._unit = unit + self._hash = sha256(json.dumps(dict(unit.received), + sort_keys=True).encode('utf8') + ).hexdigest() + + @property + def hash(self): + """ + SHA hash of the data for this request. + """ + return self._hash + + @property + def _hash_key(self): + endpoint = self._unit.relation.endpoint + return endpoint.expand_name('request.{}'.format(self.instance_id)) + + @property + def changed(self): + """ + Whether this request has changed since the last time it was + marked completed. + """ + if not (self.instance_id and self._requested): + return False + saved_hash = unitdata.kv().get(self._hash_key) + result = saved_hash != self.hash + return result + + def mark_completed(self): + """ + Mark this request as having been completed. + """ + completed = self._unit.relation.to_publish.get('completed', {}) + completed[self.instance_id] = self.hash + unitdata.kv().set(self._hash_key, self.hash) + self._unit.relation.to_publish['completed'] = completed + + def clear(self): + """ + Clear this request's cached data. + """ + unitdata.kv().unset(self._hash_key) + + @property + def unit_name(self): + """ + The name of the unit making the request. + """ + return self._unit.unit_name + + @property + def application_name(self): + """ + The name of the application making the request. + """ + return self._unit.application_name + + @property + def _requested(self): + return self._unit.received['requested'] + + @property + def instance_id(self): + """ + The instance ID reported for this request. + """ + return self._unit.received['instance-id'] + + @property + def region(self): + """ + The region reported for this request. + """ + return self._unit.received['region'] + + @property + def instance_tags(self): + """ + Mapping of tag names to values (or `None`) to apply to this instance. + """ + # uses dict() here to make a copy, just to be safe + return dict(self._unit.received.get('instance-tags', {})) + + @property + def instance_security_group_tags(self): + """ + Mapping of tag names to values (or `None`) to apply to this instance's + machine-specific security group (firewall). + """ + # uses dict() here to make a copy, just to be safe + return dict(self._unit.received.get('instance-security-group-tags', + {})) + + @property + def instance_subnet_tags(self): + """ + Mapping of tag names to values (or `None`) to apply to this instance's + subnet. + """ + # uses dict() here to make a copy, just to be safe + return dict(self._unit.received.get('instance-subnet-tags', {})) + + @property + def requested_instance_inspection(self): + """ + Flag indicating whether the ability to inspect instances was requested. + """ + return bool(self._unit.received['enable-instance-inspection']) + + @property + def requested_acm_readonly(self): + """ + Flag indicating whether acm readonly was requested. + """ + return bool(self._unit.received['enable-acm-readonly']) + + @property + def requested_acm_fullaccess(self): + """ + Flag indicating whether acm fullaccess was requested. + """ + return bool(self._unit.received['enable-acm-fullaccess']) + + @property + def requested_network_management(self): + """ + Flag indicating whether the ability to manage networking (firewalls, + subnets, etc) was requested. + """ + return bool(self._unit.received['enable-network-management']) + + @property + def requested_load_balancer_management(self): + """ + Flag indicating whether load balancer management was requested. + """ + return bool(self._unit.received['enable-load-balancer-management']) + + @property + def requested_block_storage_management(self): + """ + Flag indicating whether block storage management was requested. + """ + return bool(self._unit.received['enable-block-storage-management']) + + @property + def requested_dns_management(self): + """ + Flag indicating whether DNS management was requested. + """ + return bool(self._unit.received['enable-dns-management']) + + @property + def requested_object_storage_access(self): + """ + Flag indicating whether object storage access was requested. + """ + return bool(self._unit.received['enable-object-storage-access']) + + @property + def object_storage_access_patterns(self): + """ + List of patterns to which to restrict object storage access. + """ + return list( + self._unit.received['object-storage-access-patterns'] or []) + + @property + def requested_object_storage_management(self): + """ + Flag indicating whether object storage management was requested. + """ + return bool(self._unit.received['enable-object-storage-management']) + + @property + def object_storage_management_patterns(self): + """ + List of patterns to which to restrict object storage management. + """ + return list( + self._unit.received['object-storage-management-patterns'] or []) diff --git a/kubernetes-worker/hooks/relations/aws-integration/pydocmd.yml b/kubernetes-worker/hooks/relations/aws-integration/pydocmd.yml new file mode 100644 index 0000000..70a2e75 --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'AWS Integration Interface' + +generate: + - requires.md: + - requires + - requires.AWSIntegrationRequires+ + - provides.md: + - provides + - provides.AWSIntegrationProvides+ + - provides.IntegrationRequest+ + +pages: + - Requires: requires.md + - Provides: provides.md + +gens_dir: docs diff --git a/kubernetes-worker/hooks/relations/aws-integration/requires.py b/kubernetes-worker/hooks/relations/aws-integration/requires.py new file mode 100644 index 0000000..c457e02 --- /dev/null +++ b/kubernetes-worker/hooks/relations/aws-integration/requires.py @@ -0,0 +1,262 @@ +""" +This is the requires side of the interface layer, for use in charms that +wish to request integration with AWS native features. The integration will +be provided by the AWS integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of AWS specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific AWS features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the AWS instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. +""" + + +import json +import string +from hashlib import sha256 +from urllib.parse import urljoin +from urllib.request import urlopen + +from charmhelpers.core import unitdata + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import clear_flag, toggle_flag + + +# block size to read data from AWS metadata service +# (realistically, just needs to be bigger than ~20 chars) +READ_BLOCK_SIZE = 2048 + + +class AWSIntegrationRequires(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + + @when('endpoint.aws.joined') + def request_aws_integration(): + aws = endpoint_from_flag('endpoint.aws.joined') + aws.request_instance_tags({ + 'tag1': 'value1', + 'tag2': None, + }) + aws.request_load_balancer_management() + # ... + + @when('endpoint.aws.ready') + def aws_integration_ready(): + update_config_enable_aws() + ``` + """ + # the IP is the AWS metadata service, documented here: + # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html + _metadata_url = 'http://169.254.169.254/latest/meta-data/' + _instance_id_url = urljoin(_metadata_url, 'instance-id') + _az_url = urljoin(_metadata_url, 'placement/availability-zone') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._instance_id = None + self._region = None + + @property + def _received(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single AWS integration application with a + single unit. + """ + return self.relations[0].joined_units.received + + @property + def _to_publish(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single AWS integration application with a + single unit. + """ + return self.relations[0].to_publish + + @when('endpoint.{endpoint_name}.joined') + def send_instance_info(self): + self._to_publish['instance-id'] = self.instance_id + self._to_publish['region'] = self.region + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + completed = self._received.get('completed', {}) + actual_hash = completed.get(self.instance_id) + # My middle name is ready. No, that doesn't sound right. + # I eat ready for breakfast. + toggle_flag(self.expand_name('ready'), + self._requested and actual_hash == self._expected_hash) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + @property + def instance_id(self): + """ + This unit's instance-id. + """ + if self._instance_id is None: + cache_key = self.expand_name('instance-id') + cached = unitdata.kv().get(cache_key) + if cached: + self._instance_id = cached + else: + with urlopen(self._instance_id_url) as fd: + self._instance_id = fd.read(READ_BLOCK_SIZE).decode('utf8') + unitdata.kv().set(cache_key, self._instance_id) + return self._instance_id + + @property + def region(self): + """ + The region this unit is in. + """ + if self._region is None: + cache_key = self.expand_name('region') + cached = unitdata.kv().get(cache_key) + if cached: + self._region = cached + else: + with urlopen(self._az_url) as fd: + az = fd.read(READ_BLOCK_SIZE).decode('utf8') + self._region = az.rstrip(string.ascii_lowercase) + unitdata.kv().set(cache_key, self._region) + return self._region + + @property + def _expected_hash(self): + return sha256(json.dumps(dict(self._to_publish), + sort_keys=True).encode('utf8')).hexdigest() + + @property + def _requested(self): + # whether or not a request has been issued + return self._to_publish['requested'] + + def _request(self, keyvals): + self._to_publish.update(keyvals) + self._to_publish['requested'] = True + clear_flag(self.expand_name('ready')) + + def tag_instance(self, tags): + """ + Request that the given tags be applied to this instance. + + # Parameters + `tags` (dict): Mapping of tag names to values (or `None`). + """ + self._request({'instance-tags': dict(tags)}) + + def tag_instance_security_group(self, tags): + """ + Request that the given tags be applied to this instance's + machine-specific security group (firewall) created by Juju. + + # Parameters + `tags` (dict): Mapping of tag names to values (or `None`). + """ + self._request({'instance-security-group-tags': dict(tags)}) + + def tag_instance_subnet(self, tags): + """ + Request that the given tags be applied to this instance's subnet. + + # Parameters + `tags` (dict): Mapping of tag names to values (or `None`). + """ + self._request({'instance-subnet-tags': dict(tags)}) + + def enable_acm_readonly(self): + """ + Request readonly for ACM. + """ + self._request({'enable-acm-readonly': True}) + + def enable_acm_fullaccess(self): + """ + Request fullaccess for ACM. + """ + self._request({'enable-acm-fullaccess': True}) + + def enable_instance_inspection(self): + """ + Request the ability to inspect instances. + """ + self._request({'enable-instance-inspection': True}) + + def enable_network_management(self): + """ + Request the ability to manage networking (firewalls, subnets, etc). + """ + self._request({'enable-network-management': True}) + + def enable_load_balancer_management(self): + """ + Request the ability to manage load balancers. + """ + self._request({'enable-load-balancer-management': True}) + + def enable_block_storage_management(self): + """ + Request the ability to manage block storage. + """ + self._request({'enable-block-storage-management': True}) + + def enable_dns_management(self): + """ + Request the ability to manage DNS. + """ + self._request({'enable-dns-management': True}) + + def enable_object_storage_access(self, patterns=None): + """ + Request the ability to access object storage. + + # Parameters + `patterns` (list): If given, restrict access to the resources matching + the patterns. If patterns do not start with the S3 ARN prefix + (`arn:aws:s3:::`), it will be prepended. + """ + if patterns: + for i, pattern in enumerate(patterns): + if not pattern.startswith('arn:aws:s3:::'): + patterns[i] = 'arn:aws:s3:::{}'.format(pattern) + self._request({ + 'enable-object-storage-access': True, + 'object-storage-access-patterns': patterns, + }) + + def enable_object_storage_management(self, patterns=None): + """ + Request the ability to manage object storage. + + # Parameters + `patterns` (list): If given, restrict management to the resources + matching the patterns. If patterns do not start with the S3 ARN + prefix (`arn:aws:s3:::`), it will be prepended. + """ + if patterns: + for i, pattern in enumerate(patterns): + if not pattern.startswith('arn:aws:s3:::'): + patterns[i] = 'arn:aws:s3:::{}'.format(pattern) + self._request({ + 'enable-object-storage-management': True, + 'object-storage-management-patterns': patterns, + }) diff --git a/kubernetes-worker/hooks/relations/azure-integration/.gitignore b/kubernetes-worker/hooks/relations/azure-integration/.gitignore new file mode 100644 index 0000000..5f9f2c5 --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/.gitignore @@ -0,0 +1,3 @@ +.tox +__pycache__ +*.pyc diff --git a/kubernetes-worker/hooks/relations/azure-integration/LICENSE b/kubernetes-worker/hooks/relations/azure-integration/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-worker/hooks/relations/azure-integration/README.md b/kubernetes-worker/hooks/relations/azure-integration/README.md new file mode 100644 index 0000000..ddcae26 --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/README.md @@ -0,0 +1,28 @@ +# Overview + +This layer encapsulates the `azure-integration` interface communciation +protocol and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:azure-integration` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:azure-integration'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `azure-integration` interface protocol: + +```yaml +requires: + azure: + interface: azure-integration +``` + +For documentation on how to use the API for this interface, see: + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) (this will only be used by the azure-integrator charm) diff --git a/kubernetes-worker/hooks/relations/azure-integration/__init__.py b/kubernetes-worker/hooks/relations/azure-integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/azure-integration/copyright b/kubernetes-worker/hooks/relations/azure-integration/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-worker/hooks/relations/azure-integration/docs/provides.md b/kubernetes-worker/hooks/relations/azure-integration/docs/provides.md new file mode 100644 index 0000000..4348dff --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/docs/provides.md @@ -0,0 +1,175 @@ +

provides

+ + +This is the provides side of the interface layer, for use only by the Azure +integrator charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for Azure integration features. + The Azure integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. + +

AzureIntegrationProvides

+ +```python +AzureIntegrationProvides(self, endpoint_name, relation_ids=None) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag +from charms import layer + +@when('endpoint.azure.requests-pending') +def handle_requests(): + azure = endpoint_from_flag('endpoint.azure.requests-pending') + for request in azure.requests: + if request.instance_tags: + layer.azure.tag_instance( + request.vm_name, + request.resource_group, + request.instance_tags) + if request.requested_load_balancer_management: + layer.azure.enable_load_balancer_management( + request.charm, + request.vm_name, + request.resource_group, + ) + # ... + azure.mark_completed() +``` + +

relation_ids

+ + +A list of the IDs of all established relations. + +

requests

+ + +A list of the new or updated `IntegrationRequests` that +have been made. + +

get_departed_charms

+ +```python +AzureIntegrationProvides.get_departed_charms(self) +``` + +Get a list of all charms that have had all units depart since the +last time this was called. + +

mark_completed

+ +```python +AzureIntegrationProvides.mark_completed(self) +``` + +Mark all requests as completed and remove the `requests-pending` flag. + +

IntegrationRequest

+ +```python +IntegrationRequest(self, unit) +``` + +A request for integration from a single remote unit. + +

application_name

+ + +The name of the application making the request. + +

charm

+ + +The charm name reported for this request. + +

instance_tags

+ + +Mapping of tag names to values to apply to this instance. + +

is_changed

+ + +Whether this request has changed since the last time it was +marked completed (if ever). + +

model_uuid

+ + +The UUID of the model containing the application making this request. + +

relation_id

+ + +The ID of the relation for the unit making the request. + +

requested_block_storage_management

+ + +Flag indicating whether block storage management was requested. + +

requested_dns_management

+ + +Flag indicating whether DNS management was requested. + +

requested_instance_inspection

+ + +Flag indicating whether the ability to inspect instances was requested. + +

requested_network_management

+ + +Flag indicating whether the ability to manage networking was requested. + +

requested_object_storage_access

+ + +Flag indicating whether object storage access was requested. + +

requested_object_storage_management

+ + +Flag indicating whether object storage management was requested. + +

requested_security_management

+ + +Flag indicating whether security management was requested. + +

resource_group

+ + +The resource group reported for this request. + +

unit_name

+ + +The name of the unit making the request. + +

vm_id

+ + +The instance ID reported for this request. + +

vm_name

+ + +The instance name reported for this request. + +

mark_completed

+ +```python +IntegrationRequest.mark_completed(self) +``` + +Mark this request as having been completed. + diff --git a/kubernetes-worker/hooks/relations/azure-integration/docs/requires.md b/kubernetes-worker/hooks/relations/azure-integration/docs/requires.md new file mode 100644 index 0000000..608b4ee --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/docs/requires.md @@ -0,0 +1,145 @@ +

requires

+ + +This is the requires side of the interface layer, for use in charms that +wish to request integration with Azure native features. The integration will +be provided by the Azure integrator charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of Azure specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific Azure features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the Azure instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. + +

AzureIntegrationRequires

+ +```python +AzureIntegrationRequires(self, *args, **kwargs) +``` + +Interface to request integration access. + +Note that due to resource limits and permissions granularity, policies are +limited to being applied at the charm level. That means that, if any +permissions are requested (i.e., any of the enable methods are called), +what is granted will be the sum of those ever requested by any instance of +the charm on this cloud. + +Labels, on the other hand, will be instance specific. + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag + +@when('endpoint.azure.joined') +def request_azure_integration(): + azure = endpoint_from_flag('endpoint.azure.joined') + azure.tag_instance({ + 'tag1': 'value1', + 'tag2': None, + }) + azure.request_load_balancer_management() + # ... + +@when('endpoint.azure.ready') +def azure_integration_ready(): + update_config_enable_azure() +``` + +

is_ready

+ + +Whether or not the request for this instance has been completed. + +

resource_group

+ + +The resource group this unit is in. + +

vm_id

+ + +This unit's instance ID. + +

vm_name

+ + +This unit's instance name. + +

tag_instance

+ +```python +AzureIntegrationRequires.tag_instance(self, tags) +``` + +Request that the given tags be applied to this instance. + +__Parameters__ + +- __`tags` (dict)__: Mapping of tags names to values. + +

enable_instance_inspection

+ +```python +AzureIntegrationRequires.enable_instance_inspection(self) +``` + +Request the ability to inspect instances. + +

enable_network_management

+ +```python +AzureIntegrationRequires.enable_network_management(self) +``` + +Request the ability to manage networking. + +

enable_security_management

+ +```python +AzureIntegrationRequires.enable_security_management(self) +``` + +Request the ability to manage security (e.g., firewalls). + +

enable_block_storage_management

+ +```python +AzureIntegrationRequires.enable_block_storage_management(self) +``` + +Request the ability to manage block storage. + +

enable_dns_management

+ +```python +AzureIntegrationRequires.enable_dns_management(self) +``` + +Request the ability to manage DNS. + +

enable_object_storage_access

+ +```python +AzureIntegrationRequires.enable_object_storage_access(self) +``` + +Request the ability to access object storage. + +

enable_object_storage_management

+ +```python +AzureIntegrationRequires.enable_object_storage_management(self) +``` + +Request the ability to manage object storage. + diff --git a/kubernetes-worker/hooks/relations/azure-integration/interface.yaml b/kubernetes-worker/hooks/relations/azure-integration/interface.yaml new file mode 100644 index 0000000..a77a7cb --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/interface.yaml @@ -0,0 +1,4 @@ +name: azure-integration +summary: Interface for connecting to the Azure integrator charm. +version: 1 +maintainer: Cory Johns diff --git a/kubernetes-worker/hooks/relations/azure-integration/make_docs b/kubernetes-worker/hooks/relations/azure-integration/make_docs new file mode 100644 index 0000000..84df5ee --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'azure': {'interface': 'azure-integration'}}, + 'provides': {'azure': {'interface': 'azure-integration'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-worker/hooks/relations/azure-integration/provides.py b/kubernetes-worker/hooks/relations/azure-integration/provides.py new file mode 100644 index 0000000..5ff7d3a --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/provides.py @@ -0,0 +1,275 @@ +""" +This is the provides side of the interface layer, for use only by the Azure +integrator charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for Azure integration features. + The Azure integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. +""" + +from operator import attrgetter + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class AzureIntegrationProvides(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + + @when('endpoint.azure.requests-pending') + def handle_requests(): + azure = endpoint_from_flag('endpoint.azure.requests-pending') + for request in azure.requests: + if request.instance_tags: + layer.azure.tag_instance( + request.vm_name, + request.resource_group, + request.instance_tags) + if request.requested_load_balancer_management: + layer.azure.enable_load_balancer_management( + request.charm, + request.vm_name, + request.resource_group, + ) + # ... + azure.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + toggle_flag(self.expand_name('requests-pending'), + len(self.requests) > 0) + clear_flag(self.expand_name('changed')) + + @property + def requests(self): + """ + A list of the new or updated #IntegrationRequests that + have been made. + """ + if not hasattr(self, '_requests'): + all_requests = [IntegrationRequest(unit) + for unit in self.all_joined_units] + is_changed = attrgetter('is_changed') + self._requests = list(filter(is_changed, all_requests)) + return self._requests + + @property + def relation_ids(self): + """ + A list of the IDs of all established relations. + """ + return [relation.relation_id for relation in self.relations] + + def get_departed_charms(self): + """ + Get a list of all charms that have had all units depart since the + last time this was called. + """ + joined_charms = {unit.received['charm'] + for unit in self.all_joined_units + if unit.received['charm']} + departed_charms = [unit.received['charm'] + for unit in self.all_departed_units + if unit.received['charm'] not in joined_charms] + self.all_departed_units.clear() + return departed_charms + + def mark_completed(self): + """ + Mark all requests as completed and remove the `requests-pending` flag. + """ + for request in self.requests: + request.mark_completed() + clear_flag(self.expand_name('requests-pending')) + self._requests = [] + + +class IntegrationRequest: + """ + A request for integration from a single remote unit. + """ + def __init__(self, unit): + self._unit = unit + + @property + def _to_publish(self): + return self._unit.relation.to_publish + + @property + def _completed(self): + return self._to_publish.get('completed', {}) + + @property + def _requested(self): + return self._unit.received['requested'] + + @property + def is_changed(self): + """ + Whether this request has changed since the last time it was + marked completed (if ever). + """ + if not all([self.charm, self.vm_id, self.vm_name, + self.resource_group, self._requested]): + return False + return self._completed.get(self.vm_id) != self._requested + + def mark_completed(self): + """ + Mark this request as having been completed. + """ + completed = self._completed + completed[self.vm_id] = self._requested + self._to_publish['completed'] = completed # have to explicitly update + + def send_additional_metadata(self, resource_group_location, + vnet_name, vnet_resource_group, + subnet_name, security_group_name, + security_group_resource_group, + use_managed_identity=True, aad_client=None, + aad_secret=None, tenant_id=None): + self._to_publish.update({ + 'resource-group-location': resource_group_location, + 'vnet-name': vnet_name, + 'vnet-resource-group': vnet_resource_group, + 'subnet-name': subnet_name, + 'security-group-name': security_group_name, + 'security-group-resource-group': security_group_resource_group, + 'use-managed-identity': use_managed_identity, + 'aad-client': aad_client, + 'aad-client-secret': aad_secret, + 'tenant-id': tenant_id + }) + + @property + def relation_id(self): + """ + The ID of the relation for the unit making the request. + """ + return self._unit.relation.relation_id + + @property + def unit_name(self): + """ + The name of the unit making the request. + """ + return self._unit.unit_name + + @property + def application_name(self): + """ + The name of the application making the request. + """ + return self._unit.application_name + + @property + def charm(self): + """ + The charm name reported for this request. + """ + return self._unit.received['charm'] + + @property + def vm_id(self): + """ + The instance ID reported for this request. + """ + return self._unit.received['vm-id'] + + @property + def vm_name(self): + """ + The instance name reported for this request. + """ + return self._unit.received['vm-name'] + + @property + def resource_group(self): + """ + The resource group reported for this request. + """ + return self._unit.received['res-group'] + + @property + def model_uuid(self): + """ + The UUID of the model containing the application making this request. + """ + return self._unit.received['model-uuid'] + + @property + def instance_tags(self): + """ + Mapping of tag names to values to apply to this instance. + """ + # uses dict() here to make a copy, just to be safe + return dict(self._unit.received.get('instance-tags', {})) + + @property + def requested_instance_inspection(self): + """ + Flag indicating whether the ability to inspect instances was requested. + """ + return bool(self._unit.received['enable-instance-inspection']) + + @property + def requested_network_management(self): + """ + Flag indicating whether the ability to manage networking was requested. + """ + return bool(self._unit.received['enable-network-management']) + + @property + def requested_loadbalancer_management(self): + """ + Flag indicating whether the ability to manage networking was requested. + """ + return bool(self._unit.received['enable-loadbalancer-management']) + + + @property + def requested_security_management(self): + """ + Flag indicating whether security management was requested. + """ + return bool(self._unit.received['enable-security-management']) + + @property + def requested_block_storage_management(self): + """ + Flag indicating whether block storage management was requested. + """ + return bool(self._unit.received['enable-block-storage-management']) + + @property + def requested_dns_management(self): + """ + Flag indicating whether DNS management was requested. + """ + return bool(self._unit.received['enable-dns-management']) + + @property + def requested_object_storage_access(self): + """ + Flag indicating whether object storage access was requested. + """ + return bool(self._unit.received['enable-object-storage-access']) + + @property + def requested_object_storage_management(self): + """ + Flag indicating whether object storage management was requested. + """ + return bool(self._unit.received['enable-object-storage-management']) diff --git a/kubernetes-worker/hooks/relations/azure-integration/pydocmd.yml b/kubernetes-worker/hooks/relations/azure-integration/pydocmd.yml new file mode 100644 index 0000000..6414c29 --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'Azure Integration Interface' + +generate: + - requires.md: + - requires + - requires.AzureIntegrationRequires+ + - provides.md: + - provides + - provides.AzureIntegrationProvides+ + - provides.IntegrationRequest+ + +pages: + - Requires: requires.md + - Provides: provides.md + +gens_dir: docs diff --git a/kubernetes-worker/hooks/relations/azure-integration/requires.py b/kubernetes-worker/hooks/relations/azure-integration/requires.py new file mode 100644 index 0000000..600d69e --- /dev/null +++ b/kubernetes-worker/hooks/relations/azure-integration/requires.py @@ -0,0 +1,298 @@ +""" +This is the requires side of the interface layer, for use in charms that +wish to request integration with Azure native features. The integration will +be provided by the Azure integrator charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of Azure specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific Azure features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the Azure instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. +""" + + +import json +import os +import random +import string +from urllib.request import urlopen, Request + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import clear_flag, toggle_flag + + +# block size to read data from Azure metadata service +# (realistically, just needs to be bigger than ~20 chars) +READ_BLOCK_SIZE = 2048 + + +class AzureIntegrationRequires(Endpoint): + """ + Interface to request integration access. + + Note that due to resource limits and permissions granularity, policies are + limited to being applied at the charm level. That means that, if any + permissions are requested (i.e., any of the enable methods are called), + what is granted will be the sum of those ever requested by any instance of + the charm on this cloud. + + Labels, on the other hand, will be instance specific. + + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + + @when('endpoint.azure.joined') + def request_azure_integration(): + azure = endpoint_from_flag('endpoint.azure.joined') + azure.tag_instance({ + 'tag1': 'value1', + 'tag2': None, + }) + azure.request_load_balancer_management() + # ... + + @when('endpoint.azure.ready') + def azure_integration_ready(): + update_config_enable_azure() + ``` + """ + # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service + _metadata_url = 'http://169.254.169.254/metadata/instance?api-version=2017-12-01' # noqa + _metadata_headers = {'Metadata': 'true'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._vm_metadata = None + + @property + def _received(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single Azure integration application with a + single unit. + """ + return self.relations[0].joined_units.received + + @property + def _to_publish(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single Azure integration application with a + single unit. + """ + return self.relations[0].to_publish + + @when('endpoint.{endpoint_name}.joined') + def send_instance_info(self): + self._to_publish['charm'] = hookenv.charm_name() + self._to_publish['vm-id'] = self.vm_id + self._to_publish['vm-name'] = self.vm_name + self._to_publish['res-group'] = self.resource_group + self._to_publish['model-uuid'] = os.environ['JUJU_MODEL_UUID'] + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + # My middle name is ready. No, that doesn't sound right. + # I eat ready for breakfast. + toggle_flag(self.expand_name('ready'), self.is_ready) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + @property + def vm_metadata(self): + if self._vm_metadata is None: + cache_key = self.expand_name('vm-metadata') + cached = unitdata.kv().get(cache_key) + if cached: + self._vm_metadata = cached + else: + req = Request(self._metadata_url, + headers=self._metadata_headers) + with urlopen(req) as fd: + metadata = fd.read(READ_BLOCK_SIZE).decode('utf8').strip() + self._vm_metadata = json.loads(metadata) + unitdata.kv().set(cache_key, self._vm_metadata) + return self._vm_metadata + + @property + def vm_id(self): + """ + This unit's instance ID. + """ + return self.vm_metadata['compute']['vmId'] + + @property + def vm_name(self): + """ + This unit's instance name. + """ + return self.vm_metadata['compute']['name'] + + @property + def vm_location(self): + """ + The location (region) the instance is running in. + """ + return self.vm_metadata['compute']['location'] + + @property + def resource_group(self): + """ + The resource group this unit is in. + """ + return self.vm_metadata['compute']['resourceGroupName'] + + @property + def resource_group_location(self): + """ + The location (region) the resource group is in. + """ + return self._received['resource-group-location'] + + @property + def subscription_id(self): + """ + The ID of the Azure Subscription this unit is in. + """ + return self.vm_metadata['compute']['subscriptionId'] + + @property + def vnet_name(self): + """ + The name of the virtual network the instance is in. + """ + return self._received['vnet-name'] + + @property + def vnet_resource_group(self): + """ + The name of the virtual network the instance is in. + """ + return self._received['vnet-resource-group'] + + @property + def subnet_name(self): + """ + The name of the subnet the instance is in. + """ + return self._received['subnet-name'] + + @property + def security_group_name(self): + """ + The name of the security group attached to the cluster's subnet. + """ + return self._received['security-group-name'] + + @property + def is_ready(self): + """ + Whether or not the request for this instance has been completed. + """ + requested = self._to_publish['requested'] + completed = self._received.get('completed', {}).get(self.vm_id) + return requested and requested == completed + + @property + def security_group_resource_group(self): + return self._received['security-group-resource-group'] + + @property + def managed_identity(self): + return self._received['use-managed-identity'] + + @property + def aad_client_id(self): + return self._received['aad-client'] + + @property + def aad_client_secret(self): + return self._received['aad-client-secret'] + + @property + def tenant_id(self): + return self._received['tenant-id'] + + def _request(self, keyvals): + alphabet = string.ascii_letters + string.digits + nonce = ''.join(random.choice(alphabet) for _ in range(8)) + self._to_publish.update(keyvals) + self._to_publish['requested'] = nonce + clear_flag(self.expand_name('ready')) + + def tag_instance(self, tags): + """ + Request that the given tags be applied to this instance. + + # Parameters + `tags` (dict): Mapping of tags names to values. + """ + self._request({'instance-tags': dict(tags)}) + + def enable_instance_inspection(self): + """ + Request the ability to inspect instances. + """ + self._request({'enable-instance-inspection': True}) + + def enable_network_management(self): + """ + Request the ability to manage networking. + """ + self._request({'enable-network-management': True}) + + def enable_loadbalancer_management(self): + """ + Request the ability to manage networking. + """ + self._request({'enable-loadbalancer-management': True}) + + + def enable_security_management(self): + """ + Request the ability to manage security (e.g., firewalls). + """ + self._request({'enable-security-management': True}) + + def enable_block_storage_management(self): + """ + Request the ability to manage block storage. + """ + self._request({'enable-block-storage-management': True}) + + def enable_dns_management(self): + """ + Request the ability to manage DNS. + """ + self._request({'enable-dns': True}) + + def enable_object_storage_access(self): + """ + Request the ability to access object storage. + """ + self._request({'enable-object-storage-access': True}) + + def enable_object_storage_management(self): + """ + Request the ability to manage object storage. + """ + self._request({'enable-object-storage-management': True}) + + diff --git a/kubernetes-worker/hooks/relations/container-runtime/.gitignore b/kubernetes-worker/hooks/relations/container-runtime/.gitignore new file mode 100644 index 0000000..894a44c --- /dev/null +++ b/kubernetes-worker/hooks/relations/container-runtime/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/kubernetes-worker/hooks/relations/container-runtime/LICENSE b/kubernetes-worker/hooks/relations/container-runtime/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/kubernetes-worker/hooks/relations/container-runtime/LICENSE @@ -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/kubernetes-worker/hooks/relations/container-runtime/README.md b/kubernetes-worker/hooks/relations/container-runtime/README.md new file mode 100644 index 0000000..4620013 --- /dev/null +++ b/kubernetes-worker/hooks/relations/container-runtime/README.md @@ -0,0 +1,45 @@ +# interface-container-runtime + +## Overview + +This interface handles communication between subordinate charms, that provide a container runtime and charms requiring a container runtime. + +## Usage + +### Provides + +The providing side of the container interface provides a place for a container runtime to connect to. + +Your charm should respond to the `endpoint.{endpoint_name}.available` state, +which indicates that there is a container runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def update_kubelet_config(containerd): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + config = endpoint.get_config() + kubelet.config['container-runtime'] = \ + config['runtime'] +``` + +### Requires + +The requiring side of the container interface requires a place for a container runtime to connect to. + +Your charm should set `{endpoint_name}.available` state, +which indicates that the container is runtime connected. + +A trivial example of handling this interface would be: + +```python +@when('endpoint.containerd.joined') +def pubish_config(): + endpoint = endpoint_from_flag('endpoint.containerd.joined') + endpoint.set_config( + socket='unix:///var/run/containerd/containerd.sock', + runtime='remote', + nvidia_enabled=False + ) +``` diff --git a/kubernetes-worker/hooks/relations/container-runtime/__init__.py b/kubernetes-worker/hooks/relations/container-runtime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/container-runtime/interface.yaml b/kubernetes-worker/hooks/relations/container-runtime/interface.yaml new file mode 100644 index 0000000..294be1e --- /dev/null +++ b/kubernetes-worker/hooks/relations/container-runtime/interface.yaml @@ -0,0 +1,4 @@ +name: container-runtime +summary: Interface for relating to container runtimes +version: 1 +maintainer: "Joe Borg " diff --git a/kubernetes-worker/hooks/relations/container-runtime/provides.py b/kubernetes-worker/hooks/relations/container-runtime/provides.py new file mode 100644 index 0000000..a9768a8 --- /dev/null +++ b/kubernetes-worker/hooks/relations/container-runtime/provides.py @@ -0,0 +1,55 @@ +from charms.reactive import ( + Endpoint, + toggle_flag +) + + +class ContainerRuntimeProvides(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined) + + def _get_config(self, key): + """ + Get the published configuration for a given key. + + :param key: String dict key + :return: String value for given key + """ + return self.all_joined_units.received.get(key) + + def get_nvidia_enabled(self): + """ + Get the published nvidia config. + + :return: String + """ + return self._get_config(key='nvidia_enabled') + + def get_runtime(self): + """ + Get the published runtime config. + + :return: String + """ + return self._get_config(key='runtime') + + def get_socket(self): + """ + Get the published socket config. + + :return: String + """ + return self._get_config(key='socket') + + def set_config(self, sandbox_image=None): + """ + Set the configuration to be published. + + :param sandbox_image: String to optionally override the sandbox image + :return: None + """ + for relation in self.relations: + relation.to_publish.update({ + 'sandbox_image': sandbox_image + }) diff --git a/kubernetes-worker/hooks/relations/container-runtime/requires.py b/kubernetes-worker/hooks/relations/container-runtime/requires.py new file mode 100644 index 0000000..c461b68 --- /dev/null +++ b/kubernetes-worker/hooks/relations/container-runtime/requires.py @@ -0,0 +1,61 @@ +from charms.reactive import ( + Endpoint, + clear_flag, + data_changed, + is_data_changed, + toggle_flag +) + + +class ContainerRuntimeRequires(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name('endpoint.{endpoint_name}.available'), + self.is_joined) + toggle_flag(self.expand_name('endpoint.{endpoint_name}.reconfigure'), + self.is_joined and self._config_changed()) + + def _config_changed(self): + """ + Determine if our received data has changed. + + :return: Boolean + """ + # NB: this call should match whatever we're tracking in handle_remote_config + return is_data_changed('containerd.remote_config', + [self.get_sandbox_image()]) + + def handle_remote_config(self): + """ + Keep track of received data so we can know if it changes. + + :return: None + """ + clear_flag(self.expand_name('endpoint.{endpoint_name}.reconfigure')) + # Presently, we only care about one piece of remote config. Expand + # the list as needed. + data_changed('containerd.remote_config', + [self.get_sandbox_image()]) + + def get_sandbox_image(self): + """ + Get the sandbox image URI if a remote has published one. + + :return: String: remotely configured sandbox image + """ + return self.all_joined_units.received.get('sandbox_image') + + def set_config(self, socket, runtime, nvidia_enabled): + """ + Set the configuration to be published. + + :param socket: String uri to runtime socket + :param runtime: String runtime executable + :param nvidia_enabled: Boolean nvidia runtime enabled + :return: None + """ + for relation in self.relations: + relation.to_publish.update({ + 'socket': socket, + 'runtime': runtime, + 'nvidia_enabled': nvidia_enabled + }) diff --git a/kubernetes-worker/hooks/relations/coordinator/peers.py b/kubernetes-worker/hooks/relations/coordinator/peers.py new file mode 100644 index 0000000..f443bf6 --- /dev/null +++ b/kubernetes-worker/hooks/relations/coordinator/peers.py @@ -0,0 +1,21 @@ +# Copyright 2016-2018 Canonical Ltd. +# +# This file is part of the Coordinator Layer for Juju charms. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charms import reactive + + +class CoordinatorPeer(reactive.Endpoint): + pass diff --git a/kubernetes-worker/hooks/relations/gcp-integration/.gitignore b/kubernetes-worker/hooks/relations/gcp-integration/.gitignore new file mode 100644 index 0000000..5f9f2c5 --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/.gitignore @@ -0,0 +1,3 @@ +.tox +__pycache__ +*.pyc diff --git a/kubernetes-worker/hooks/relations/gcp-integration/LICENSE b/kubernetes-worker/hooks/relations/gcp-integration/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-worker/hooks/relations/gcp-integration/README.md b/kubernetes-worker/hooks/relations/gcp-integration/README.md new file mode 100644 index 0000000..42861fb --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/README.md @@ -0,0 +1,28 @@ +# Overview + +This layer encapsulates the `gcp-integration` interface communication protocol +and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:gcp-integration` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:gcp-integration'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `gcp-integration` interface protocol: + +```yaml +requires: + gcp: + interface: gcp-integration +``` + +For documentation on how to use the API for this interface, see: + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) (this will only be used by the gcp-integrator charm) diff --git a/kubernetes-worker/hooks/relations/gcp-integration/__init__.py b/kubernetes-worker/hooks/relations/gcp-integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/gcp-integration/copyright b/kubernetes-worker/hooks/relations/gcp-integration/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-worker/hooks/relations/gcp-integration/docs/provides.md b/kubernetes-worker/hooks/relations/gcp-integration/docs/provides.md new file mode 100644 index 0000000..6f29a39 --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/docs/provides.md @@ -0,0 +1,183 @@ +

provides

+ + +This is the provides side of the interface layer, for use only by the GCP +integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for GCP integration features. + The GCP integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. + +

GCPIntegrationProvides

+ +```python +GCPIntegrationProvides(self, endpoint_name, relation_ids=None) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag +from charms import layer + +@when('endpoint.gcp.requests-pending') +def handle_requests(): + gcp = endpoint_from_flag('endpoint.gcp.requests-pending') + for request in gcp.requests: + if request.instance_labels: + layer.gcp.label_instance( + request.instance, + request.zone, + request.instance_labels) + if request.requested_load_balancer_management: + layer.gcp.enable_load_balancer_management( + request.charm, + request.instance, + request.zone, + ) + # ... + gcp.mark_completed() +``` + +

relation_ids

+ + +A list of the IDs of all established relations. + +

requests

+ + +A list of the new or updated `IntegrationRequests` that +have been made. + +

get_departed_charms

+ +```python +GCPIntegrationProvides.get_departed_charms(self) +``` + +Get a list of all charms that have had all units depart since the +last time this was called. + +

mark_completed

+ +```python +GCPIntegrationProvides.mark_completed(self) +``` + +Mark all requests as completed and remove the `requests-pending` flag. + +

IntegrationRequest

+ +```python +IntegrationRequest(self, unit) +``` + +A request for integration from a single remote unit. + +

application_name

+ + +The name of the application making the request. + +

charm

+ + +The charm name reported for this request. + +

has_credentials

+ + +Whether or not credentials have been set via `set_credentials`. + +

instance

+ + +The instance name reported for this request. + +

instance_labels

+ + +Mapping of label names to values to apply to this instance. + +

is_changed

+ + +Whether this request has changed since the last time it was +marked completed (if ever). + +

model_uuid

+ + +The UUID of the model containing the application making this request. + +

relation_id

+ + +The ID of the relation for the unit making the request. + +

requested_block_storage_management

+ + +Flag indicating whether block storage management was requested. + +

requested_dns_management

+ + +Flag indicating whether DNS management was requested. + +

requested_instance_inspection

+ + +Flag indicating whether the ability to inspect instances was requested. + +

requested_network_management

+ + +Flag indicating whether the ability to manage networking was requested. + +

requested_object_storage_access

+ + +Flag indicating whether object storage access was requested. + +

requested_object_storage_management

+ + +Flag indicating whether object storage management was requested. + +

requested_security_management

+ + +Flag indicating whether security management was requested. + +

unit_name

+ + +The name of the unit making the request. + +

zone

+ + +The zone reported for this request. + +

mark_completed

+ +```python +IntegrationRequest.mark_completed(self) +``` + +Mark this request as having been completed. + +

set_credentials

+ +```python +IntegrationRequest.set_credentials(self, credentials) +``` + +Set the credentials for this request. + diff --git a/kubernetes-worker/hooks/relations/gcp-integration/docs/requires.md b/kubernetes-worker/hooks/relations/gcp-integration/docs/requires.md new file mode 100644 index 0000000..36e23c2 --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/docs/requires.md @@ -0,0 +1,140 @@ +

requires

+ + +This is the requires side of the interface layer, for use in charms that +wish to request integration with GCP native features. The integration will +be provided by the GCP integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of GCP specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific GCP features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the GCP instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. + +

GCPIntegrationRequires

+ +```python +GCPIntegrationRequires(self, *args, **kwargs) +``` + +Interface to request integration access. + +Note that due to resource limits and permissions granularity, policies are +limited to being applied at the charm level. That means that, if any +permissions are requested (i.e., any of the enable methods are called), +what is granted will be the sum of those ever requested by any instance of +the charm on this cloud. + +Labels, on the other hand, will be instance specific. + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag + +@when('endpoint.gcp.joined') +def request_gcp_integration(): + gcp = endpoint_from_flag('endpoint.gcp.joined') + gcp.label_instance({ + 'tag1': 'value1', + 'tag2': None, + }) + gcp.request_load_balancer_management() + # ... + +@when('endpoint.gcp.ready') +def gcp_integration_ready(): + update_config_enable_gcp() +``` + +

instance

+ + +This unit's instance name. + +

is_ready

+ + +Whether or not the request for this instance has been completed. + +

zone

+ + +The zone this unit is in. + +

label_instance

+ +```python +GCPIntegrationRequires.label_instance(self, labels) +``` + +Request that the given labels be applied to this instance. + +__Parameters__ + +- __`labels` (dict)__: Mapping of labels names to values. + +

enable_instance_inspection

+ +```python +GCPIntegrationRequires.enable_instance_inspection(self) +``` + +Request the ability to inspect instances. + +

enable_network_management

+ +```python +GCPIntegrationRequires.enable_network_management(self) +``` + +Request the ability to manage networking. + +

enable_security_management

+ +```python +GCPIntegrationRequires.enable_security_management(self) +``` + +Request the ability to manage security (e.g., firewalls). + +

enable_block_storage_management

+ +```python +GCPIntegrationRequires.enable_block_storage_management(self) +``` + +Request the ability to manage block storage. + +

enable_dns_management

+ +```python +GCPIntegrationRequires.enable_dns_management(self) +``` + +Request the ability to manage DNS. + +

enable_object_storage_access

+ +```python +GCPIntegrationRequires.enable_object_storage_access(self) +``` + +Request the ability to access object storage. + +

enable_object_storage_management

+ +```python +GCPIntegrationRequires.enable_object_storage_management(self) +``` + +Request the ability to manage object storage. + diff --git a/kubernetes-worker/hooks/relations/gcp-integration/interface.yaml b/kubernetes-worker/hooks/relations/gcp-integration/interface.yaml new file mode 100644 index 0000000..9966e3f --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/interface.yaml @@ -0,0 +1,4 @@ +name: gcp-integration +summary: Interface for connecting to the GCP integrator charm. +version: 1 +maintainer: Cory Johns diff --git a/kubernetes-worker/hooks/relations/gcp-integration/make_docs b/kubernetes-worker/hooks/relations/gcp-integration/make_docs new file mode 100644 index 0000000..bd4e54e --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'gcp': {'interface': 'gcp-integration'}}, + 'provides': {'gcp': {'interface': 'gcp-integration'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-worker/hooks/relations/gcp-integration/provides.py b/kubernetes-worker/hooks/relations/gcp-integration/provides.py new file mode 100644 index 0000000..ba34b0d --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/provides.py @@ -0,0 +1,253 @@ +""" +This is the provides side of the interface layer, for use only by the GCP +integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for GCP integration features. + The GCP integration charm should then iterate over each request, perform + whatever actions are necessary to satisfy those requests, and then mark + them as complete. +""" + +from operator import attrgetter + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class GCPIntegrationProvides(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + + @when('endpoint.gcp.requests-pending') + def handle_requests(): + gcp = endpoint_from_flag('endpoint.gcp.requests-pending') + for request in gcp.requests: + if request.instance_labels: + layer.gcp.label_instance( + request.instance, + request.zone, + request.instance_labels) + if request.requested_load_balancer_management: + layer.gcp.enable_load_balancer_management( + request.charm, + request.instance, + request.zone, + ) + # ... + gcp.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + toggle_flag(self.expand_name('requests-pending'), + len(self.requests) > 0) + clear_flag(self.expand_name('changed')) + + @property + def requests(self): + """ + A list of the new or updated #IntegrationRequests that + have been made. + """ + if not hasattr(self, '_requests'): + all_requests = [IntegrationRequest(unit) + for unit in self.all_joined_units] + is_changed = attrgetter('is_changed') + self._requests = list(filter(is_changed, all_requests)) + return self._requests + + @property + def relation_ids(self): + """ + A list of the IDs of all established relations. + """ + return [relation.relation_id for relation in self.relations] + + def get_departed_charms(self): + """ + Get a list of all charms that have had all units depart since the + last time this was called. + """ + joined_charms = {unit.received['charm'] + for unit in self.all_joined_units + if unit.received['charm']} + departed_charms = [unit.received['charm'] + for unit in self.all_departed_units + if unit.received['charm'] not in joined_charms] + self.all_departed_units.clear() + return departed_charms + + def mark_completed(self): + """ + Mark all requests as completed and remove the `requests-pending` flag. + """ + for request in self.requests: + request.mark_completed() + clear_flag(self.expand_name('requests-pending')) + self._requests = [] + + +class IntegrationRequest: + """ + A request for integration from a single remote unit. + """ + def __init__(self, unit): + self._unit = unit + + @property + def _to_publish(self): + return self._unit.relation.to_publish + + @property + def _completed(self): + return self._to_publish.get('completed', {}) + + @property + def _requested(self): + return self._unit.received['requested'] + + @property + def is_changed(self): + """ + Whether this request has changed since the last time it was + marked completed (if ever). + """ + if not all([self.charm, self.instance, self.zone, self._requested]): + return False + return self._completed.get(self.instance) != self._requested + + def mark_completed(self): + """ + Mark this request as having been completed. + """ + completed = self._completed + completed[self.instance] = self._requested + self._to_publish['completed'] = completed # have to explicitly update + + def set_credentials(self, credentials): + """ + Set the credentials for this request. + """ + self._unit.relation.to_publish['credentials'] = credentials + + @property + def has_credentials(self): + """ + Whether or not credentials have been set via `set_credentials`. + """ + return 'credentials' in self._unit.relation.to_publish + + @property + def relation_id(self): + """ + The ID of the relation for the unit making the request. + """ + return self._unit.relation.relation_id + + @property + def unit_name(self): + """ + The name of the unit making the request. + """ + return self._unit.unit_name + + @property + def application_name(self): + """ + The name of the application making the request. + """ + return self._unit.application_name + + @property + def charm(self): + """ + The charm name reported for this request. + """ + return self._unit.received['charm'] + + @property + def instance(self): + """ + The instance name reported for this request. + """ + return self._unit.received['instance'] + + @property + def zone(self): + """ + The zone reported for this request. + """ + return self._unit.received['zone'] + + @property + def model_uuid(self): + """ + The UUID of the model containing the application making this request. + """ + return self._unit.received['model-uuid'] + + @property + def instance_labels(self): + """ + Mapping of label names to values to apply to this instance. + """ + # uses dict() here to make a copy, just to be safe + return dict(self._unit.received.get('instance-labels', {})) + + @property + def requested_instance_inspection(self): + """ + Flag indicating whether the ability to inspect instances was requested. + """ + return bool(self._unit.received['enable-instance-inspection']) + + @property + def requested_network_management(self): + """ + Flag indicating whether the ability to manage networking was requested. + """ + return bool(self._unit.received['enable-network-management']) + + @property + def requested_security_management(self): + """ + Flag indicating whether security management was requested. + """ + return bool(self._unit.received['enable-security-management']) + + @property + def requested_block_storage_management(self): + """ + Flag indicating whether block storage management was requested. + """ + return bool(self._unit.received['enable-block-storage-management']) + + @property + def requested_dns_management(self): + """ + Flag indicating whether DNS management was requested. + """ + return bool(self._unit.received['enable-dns-management']) + + @property + def requested_object_storage_access(self): + """ + Flag indicating whether object storage access was requested. + """ + return bool(self._unit.received['enable-object-storage-access']) + + @property + def requested_object_storage_management(self): + """ + Flag indicating whether object storage management was requested. + """ + return bool(self._unit.received['enable-object-storage-management']) diff --git a/kubernetes-worker/hooks/relations/gcp-integration/pydocmd.yml b/kubernetes-worker/hooks/relations/gcp-integration/pydocmd.yml new file mode 100644 index 0000000..9ef5e78 --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'GCP Integration Interface' + +generate: + - requires.md: + - requires + - requires.GCPIntegrationRequires+ + - provides.md: + - provides + - provides.GCPIntegrationProvides+ + - provides.IntegrationRequest+ + +pages: + - Requires: requires.md + - Provides: provides.md + +gens_dir: docs diff --git a/kubernetes-worker/hooks/relations/gcp-integration/requires.py b/kubernetes-worker/hooks/relations/gcp-integration/requires.py new file mode 100644 index 0000000..bbd191f --- /dev/null +++ b/kubernetes-worker/hooks/relations/gcp-integration/requires.py @@ -0,0 +1,227 @@ +""" +This is the requires side of the interface layer, for use in charms that +wish to request integration with GCP native features. The integration will +be provided by the GCP integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of GCP specific +API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific GCP features. This flag is automatically removed if + the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the GCP instance on which the charm is + running. This flag is automatically removed if new integration features + are requested. It should not be removed by the charm. +""" + + +import os +import random +import string +from urllib.parse import urljoin +from urllib.request import urlopen, Request + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import clear_flag, toggle_flag + + +# block size to read data from GCP metadata service +# (realistically, just needs to be bigger than ~20 chars) +READ_BLOCK_SIZE = 2048 + + +class GCPIntegrationRequires(Endpoint): + """ + Interface to request integration access. + + Note that due to resource limits and permissions granularity, policies are + limited to being applied at the charm level. That means that, if any + permissions are requested (i.e., any of the enable methods are called), + what is granted will be the sum of those ever requested by any instance of + the charm on this cloud. + + Labels, on the other hand, will be instance specific. + + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + + @when('endpoint.gcp.joined') + def request_gcp_integration(): + gcp = endpoint_from_flag('endpoint.gcp.joined') + gcp.label_instance({ + 'tag1': 'value1', + 'tag2': None, + }) + gcp.request_load_balancer_management() + # ... + + @when('endpoint.gcp.ready') + def gcp_integration_ready(): + update_config_enable_gcp() + ``` + """ + # https://cloud.google.com/compute/docs/storing-retrieving-metadata + _metadata_url = 'http://metadata.google.internal/computeMetadata/v1/' + _instance_url = urljoin(_metadata_url, 'instance/name') + _zone_url = urljoin(_metadata_url, 'instance/zone') + _metadata_headers = {'Metadata-Flavor': 'Google'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._instance = None + self._zone = None + + @property + def _received(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single GCP integration application with a + single unit. + """ + return self.relations[0].joined_units.received + + @property + def _to_publish(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single GCP integration application with a + single unit. + """ + return self.relations[0].to_publish + + @when('endpoint.{endpoint_name}.joined') + def send_instance_info(self): + self._to_publish['charm'] = hookenv.charm_name() + self._to_publish['instance'] = self.instance + self._to_publish['zone'] = self.zone + self._to_publish['model-uuid'] = os.environ['JUJU_MODEL_UUID'] + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + # My middle name is ready. No, that doesn't sound right. + # I eat ready for breakfast. + toggle_flag(self.expand_name('ready'), self.is_ready) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + @property + def instance(self): + """ + This unit's instance name. + """ + if self._instance is None: + cache_key = self.expand_name('instance') + cached = unitdata.kv().get(cache_key) + if cached: + self._instance = cached + else: + req = Request(self._instance_url, + headers=self._metadata_headers) + with urlopen(req) as fd: + instance = fd.read(READ_BLOCK_SIZE).decode('utf8').strip() + self._instance = instance + unitdata.kv().set(cache_key, self._instance) + return self._instance + + @property + def zone(self): + """ + The zone this unit is in. + """ + if self._zone is None: + cache_key = self.expand_name('zone') + cached = unitdata.kv().get(cache_key) + if cached: + self._zone = cached + else: + req = Request(self._zone_url, + headers=self._metadata_headers) + with urlopen(req) as fd: + zone = fd.read(READ_BLOCK_SIZE).decode('utf8').strip() + self._zone = zone.split('/')[-1] + unitdata.kv().set(cache_key, self._zone) + return self._zone + + @property + def is_ready(self): + """ + Whether or not the request for this instance has been completed. + """ + requested = self._to_publish['requested'] + completed = self._received.get('completed', {}).get(self.instance) + return requested and requested == completed + + @property + def credentials(self): + return self._received['credentials'] + + def _request(self, keyvals): + alphabet = string.ascii_letters + string.digits + nonce = ''.join(random.choice(alphabet) for _ in range(8)) + self._to_publish.update(keyvals) + self._to_publish['requested'] = nonce + clear_flag(self.expand_name('ready')) + + def label_instance(self, labels): + """ + Request that the given labels be applied to this instance. + + # Parameters + `labels` (dict): Mapping of labels names to values. + """ + self._request({'instance-labels': dict(labels)}) + + def enable_instance_inspection(self): + """ + Request the ability to inspect instances. + """ + self._request({'enable-instance-inspection': True}) + + def enable_network_management(self): + """ + Request the ability to manage networking. + """ + self._request({'enable-network-management': True}) + + def enable_security_management(self): + """ + Request the ability to manage security (e.g., firewalls). + """ + self._request({'enable-security-management': True}) + + def enable_block_storage_management(self): + """ + Request the ability to manage block storage. + """ + self._request({'enable-block-storage-management': True}) + + def enable_dns_management(self): + """ + Request the ability to manage DNS. + """ + self._request({'enable-dns': True}) + + def enable_object_storage_access(self): + """ + Request the ability to access object storage. + """ + self._request({'enable-object-storage-access': True}) + + def enable_object_storage_management(self): + """ + Request the ability to manage object storage. + """ + self._request({'enable-object-storage-management': True}) diff --git a/kubernetes-worker/hooks/relations/http/.gitignore b/kubernetes-worker/hooks/relations/http/.gitignore new file mode 100644 index 0000000..3374ec2 --- /dev/null +++ b/kubernetes-worker/hooks/relations/http/.gitignore @@ -0,0 +1,5 @@ +# Emacs save files +*~ +\#*\# +.\#* + diff --git a/kubernetes-worker/hooks/relations/http/README.md b/kubernetes-worker/hooks/relations/http/README.md new file mode 100644 index 0000000..3d7822a --- /dev/null +++ b/kubernetes-worker/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/kubernetes-worker/hooks/relations/http/__init__.py b/kubernetes-worker/hooks/relations/http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/http/interface.yaml b/kubernetes-worker/hooks/relations/http/interface.yaml new file mode 100644 index 0000000..54e7748 --- /dev/null +++ b/kubernetes-worker/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/kubernetes-worker/hooks/relations/http/provides.py b/kubernetes-worker/hooks/relations/http/provides.py new file mode 100644 index 0000000..86fa9b3 --- /dev/null +++ b/kubernetes-worker/hooks/relations/http/provides.py @@ -0,0 +1,67 @@ +import json + +from charmhelpers.core import hookenv +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag +from charms.reactive import Endpoint + + +class HttpProvides(Endpoint): + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + + def get_ingress_address(self, rel_id=None): + # If no rel_id is provided, we fallback to the first one + if rel_id is None: + rel_id = self.relations[0].relation_id + return hookenv.ingress_address(rel_id, hookenv.local_unit()) + + def configure(self, port, private_address=None, hostname=None): + ''' configure the address(es). private_address and hostname can + be None, a single string address/hostname, or a list of addresses + and hostnames. Note that if a list is passed, it is assumed both + private_address and hostname are either lists or None ''' + for relation in self.relations: + ingress_address = self.get_ingress_address(relation.relation_id) + if type(private_address) is list or type(hostname) is list: + # build 3 lists to zip together that are the same length + length = max(len(private_address), len(hostname)) + p = [port] * length + a = private_address + [ingress_address] *\ + (length - len(private_address)) + h = hostname + [ingress_address] * (length - len(hostname)) + zipped_list = zip(p, a, h) + # now build an array of dictionaries from that in the desired + # format for the interface + data_list = [{'hostname': h, 'port': p, 'private-address': a} + for p, a, h in zipped_list] + # for backwards compatibility, we just send a single entry + # and have an array of dictionaries in a field of that + # entry for the other entries. + data = data_list.pop(0) + data['extended_data'] = json.dumps(data_list) + + relation.to_publish_raw.update(data) + else: + relation.to_publish_raw.update({ + 'hostname': hostname or ingress_address, + 'private-address': private_address or ingress_address, + 'port': port, + }) + + def set_remote(self, **kwargs): + # NB: This method provides backwards compatibility for charms that + # called RelationBase.set_remote. Most commonly, this was done by + # charms that needed to pass reverse proxy stanzas to http proxies. + # This type of interaction with base relation classes is discouraged, + # and should be handled with logic encapsulated in appropriate + # interfaces. Eventually, this method will be deprecated in favor of + # that behavior. + for relation in self.relations: + relation.to_publish_raw.update(kwargs) diff --git a/kubernetes-worker/hooks/relations/http/requires.py b/kubernetes-worker/hooks/relations/http/requires.py new file mode 100644 index 0000000..17ea6b7 --- /dev/null +++ b/kubernetes-worker/hooks/relations/http/requires.py @@ -0,0 +1,76 @@ +import json + +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag +from charms.reactive import Endpoint + + +class HttpRequires(Endpoint): + + @when('endpoint.{endpoint_name}.changed') + def changed(self): + if any(unit.received_raw['port'] for unit in self.all_joined_units): + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_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, + 'private-address': private_address_of_host, + 'port': port_for_host, + }, + # ... + ], + }, + # ... + ] + """ + def build_service_host(data): + private_address = data['private-address'] + host = data['hostname'] or private_address + if host and data['port']: + return (host, private_address, data['port']) + else: + return None + + services = {} + for relation in self.relations: + service_name = relation.application_name + service = services.setdefault(service_name, { + 'service_name': service_name, + 'hosts': [], + }) + host_set = set() + for unit in relation.joined_units: + data = unit.received_raw + host = build_service_host(data) + if host: + host_set.add(host) + + # if we have extended data, add it + if 'extended_data' in data: + for ed in json.loads(data['extended_data']): + host = build_service_host(ed) + if host: + host_set.add(host) + + service['hosts'] = [ + {'hostname': h, 'private-address': pa, 'port': p} + for h, pa, p in sorted(host_set) + ] + + ret = [s for s in services.values() if s['hosts']] + return ret diff --git a/kubernetes-worker/hooks/relations/kube-control/.travis.yml b/kubernetes-worker/hooks/relations/kube-control/.travis.yml new file mode 100644 index 0000000..d2be8be --- /dev/null +++ b/kubernetes-worker/hooks/relations/kube-control/.travis.yml @@ -0,0 +1,9 @@ +language: python +python: + - "3.5" + - "3.6" + - "3.7" +install: + - pip install tox-travis +script: + - tox diff --git a/kubernetes-worker/hooks/relations/kube-control/README.md b/kubernetes-worker/hooks/relations/kube-control/README.md new file mode 100644 index 0000000..6f9ecb7 --- /dev/null +++ b/kubernetes-worker/hooks/relations/kube-control/README.md @@ -0,0 +1,171 @@ +# kube-control interface + +This interface provides communication between master and workers in a +Kubernetes cluster. + + +## Provides (kubernetes-master side) + + +### States + +* `kube-control.connected` + + Enabled when a worker has joined the relation. + +* `kube-control.gpu.available` + + Enabled when any worker has indicated that it is running in gpu mode. + +* `kube-control.departed` + + Enabled when any worker has indicated that it is leaving the cluster. + + +* `kube-control.auth.requested` + + Enabled when an authentication credential is requested. This state is + temporary and will be removed once the units authentication request has + been fulfilled. + +### Methods + +* `kube_control.set_dns(port, domain, sdn_ip)` + + Sends DNS info to the connected worker(s). + + +* `kube_control.auth_user()` + + Returns a list of the requested username and group requested for + authentication. + +* `kube_control.sign_auth_request(scope, user, kubelet_token, proxy_token, client_token)` + + Sends authentication tokens to the unit scope for the requested user + and kube-proxy services. + +* `kube_control.set_cluster_tag(cluster_tag)` + + Sends a tag used to identify resources that are part of the cluster to the + connected worker(s). + +* `kube_control.flush_departed()` + + Returns the unit departing the kube_control relationship so you can do any + post removal cleanup. Such as removing authentication tokens for the unit. + Invoking this method will also remove the `kube-control.departed` state + +* `kube_control.set_registry_location(registry_location)` + Sends the container image registry location to the connected worker(s). + +### Examples + +```python + +@when('kube-control.connected') +def send_dns(kube_control): + # send port, domain, sdn_ip to the remote side + kube_control.set_dns(53, "cluster.local", "10.1.0.10") + +@when('kube-control.gpu.available') +def on_gpu_available(kube_control): + # The remote side is gpu-enable, handle it somehow + assert kube_control.get_gpu() == True + + +@when('kube-control.departed') +@when('leadership.is_leader') +def flush_auth_for_departed(kube_control): + ''' Unit has left the cluster and needs to have its authentication + tokens removed from the token registry ''' + departing_unit = kube_control.flush_departed() + +``` + +## Requires (kubernetes-worker side) + + +### States + +* `kube-control.connected` + + Enabled when a master has joined the relation. + +* `kube-control.dns.available` + + Enabled when DNS info is available from the master. + +* `kube-control.auth.available` + + Enabled when authentication credentials are present from the master. + +* `kube-control.cluster_tag.available` + + Enabled when cluster tag is present from the master. + +* `kube-control.registry_location.available` + + Enabled when registry location is present from the master. + +### Methods + +* `kube_control.get_dns()` + + Returns a dictionary of DNS info sent by the master. The keys in the + dict are: domain, private-address, sdn-ip, port. + +* `kube_control.set_gpu(enabled=True)` + + Tell the master that we are gpu-enabled. + +* `kube_control.get_auth_credentials(user)` + + Returns a dict with the users authentication credentials. + +* `set_auth_request(kubelet, group='system:nodes')` + + Issue an authentication request against the master to receive token based + auth credentials in return. + +* `kube_control.get_cluster_tag()` + + Returns the cluster tag provided by the master. + +* `kube_control.get_registry_location()` + + Returns the container image registry location provided by the master. + +### Examples + +```python + +@when('kube-control.dns.available') +def on_dns_available(kube_control): + # Remote side has sent DNS info + dns = kube_control.get_dns() + print(context['domain']) + print(context['private-address']) + print(context['sdn-ip']) + print(context['port']) + +@when('kube-control.connected') +def send_gpu(kube_control): + # Tell the master that we're gpu-enabled + kube_control.set_gpu(True) + +@when('kube-control.auth.available') +def display_auth_tokens(kube_control): + # Remote side has sent auth info + auth = kube_control.get_auth_credentials('root') + print(auth['kubelet_token']) + print(auth['proxy_token']) + print(auth['client_token']) + +@when('kube-control.connected') +@when_not('kube-control.auth.available') +def request_auth_credentials(kube_control): + # Request an admin user with sudo level access named 'root' + kube_control.set_auth_request('root', group='system:masters') + +``` diff --git a/kubernetes-worker/hooks/relations/kube-control/__init__.py b/kubernetes-worker/hooks/relations/kube-control/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/kube-control/interface.yaml b/kubernetes-worker/hooks/relations/kube-control/interface.yaml new file mode 100644 index 0000000..2f0b187 --- /dev/null +++ b/kubernetes-worker/hooks/relations/kube-control/interface.yaml @@ -0,0 +1,6 @@ +name: kube-control +summary: Provides master-worker communication. +version: 1 +maintainer: "Tim Van Steenburgh " +ignore: +- tests diff --git a/kubernetes-worker/hooks/relations/kube-control/provides.py b/kubernetes-worker/hooks/relations/kube-control/provides.py new file mode 100644 index 0000000..918ace1 --- /dev/null +++ b/kubernetes-worker/hooks/relations/kube-control/provides.py @@ -0,0 +1,160 @@ +#!/usr/local/sbin/charm-env python3 +# 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. +from charms.reactive import ( + Endpoint, + toggle_flag, + set_flag, + data_changed +) + +from charmhelpers.core import ( + hookenv, + unitdata +) + + +DB = unitdata.kv() + + +class KubeControlProvider(Endpoint): + """ + Implements the kubernetes-master side of the kube-control interface. + """ + def manage_flags(self): + toggle_flag(self.expand_name('{endpoint_name}.connected'), + self.is_joined) + toggle_flag(self.expand_name('{endpoint_name}.gpu.available'), + self.is_joined and self._get_gpu()) + requests_data_id = self.expand_name('{endpoint_name}.requests') + requests = self.auth_user() + if data_changed(requests_data_id, requests): + set_flag(self.expand_name('{endpoint_name}.requests.changed')) + + def set_dns(self, port, domain, sdn_ip, enable_kube_dns): + """ + Send DNS info to the remote units. + + We'll need the port, domain, and sdn_ip of the dns service. If + sdn_ip is not required in your deployment, the units private-ip + is available implicitly. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'port': port, + 'domain': domain, + 'sdn-ip': sdn_ip, + 'enable-kube-dns': enable_kube_dns, + }) + + def auth_user(self): + """ + Return the kubelet_user value on the wire from the requestors. + """ + requests = [] + + for unit in self.all_joined_units: + requests.append( + (unit.unit_name, + {'user': unit.received_raw.get('kubelet_user'), + 'group': unit.received_raw.get('auth_group')}) + ) + + requests.sort() + return requests + + def sign_auth_request(self, scope, user, kubelet_token, proxy_token, + client_token): + """ + Send authorization tokens to the requesting unit. + """ + cred = { + 'scope': scope, + 'kubelet_token': kubelet_token, + 'proxy_token': proxy_token, + 'client_token': client_token + } + + if not DB.get('creds'): + DB.set('creds', {}) + + all_creds = DB.get('creds') + all_creds[user] = cred + DB.set('creds', all_creds) + + for relation in self.relations: + relation.to_publish.update({ + 'creds': all_creds + }) + + def clear_creds(self): + """ + Clear creds from the relation. This is used by non-leader units to stop + advertising creds so that the leader can assume full control of them. + """ + DB.unset('creds') + for relation in self.relations: + relation.to_publish_raw['creds'] = '' + + def _get_gpu(self): + """ + Return True if any remote worker is gpu-enabled. + """ + for unit in self.all_joined_units: + if unit.received_raw.get('gpu') == 'True': + hookenv.log('Unit {} has gpu enabled'.format(unit)) + return True + + return False + + def set_cluster_tag(self, cluster_tag): + """ + Send the cluster tag to the remote units. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'cluster-tag': cluster_tag + }) + + def set_registry_location(self, registry_location): + """ + Send the registry location to the remote units. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'registry-location': registry_location + }) + + def set_cohort_keys(self, cohort_keys): + """ + Send the cohort snapshot keys. + """ + for relation in self.relations: + relation.to_publish['cohort-keys'] = cohort_keys + + def set_default_cni(self, default_cni): + """ + Send the default CNI. The default_cni value should be a string + containing the name of a related CNI application to use as the + default CNI. For example: "flannel" or "calico". If no default has + been chosen then "" can be sent instead. + """ + for relation in self.relations: + relation.to_publish['default-cni'] = default_cni + + def set_api_endpoints(self, endpoints): + """ + Send the list of API endpoint URLs to which workers should connect. + """ + endpoints = sorted(endpoints) + for relation in self.relations: + relation.to_publish['api-endpoints'] = endpoints diff --git a/kubernetes-worker/hooks/relations/kube-control/requires.py b/kubernetes-worker/hooks/relations/kube-control/requires.py new file mode 100644 index 0000000..a0c3b0d --- /dev/null +++ b/kubernetes-worker/hooks/relations/kube-control/requires.py @@ -0,0 +1,161 @@ +#!/usr/local/sbin/charm-env python3 +# 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. + +from charms.reactive import ( + Endpoint, + toggle_flag, +) + +from charmhelpers.core.hookenv import log + + +class KubeControlRequirer(Endpoint): + """ + Implements the kubernetes-worker side of the kube-control interface. + """ + def manage_flags(self): + """ + Set states corresponding to the data we have. + """ + toggle_flag( + self.expand_name('{endpoint_name}.connected'), + self.is_joined) + toggle_flag( + self.expand_name('{endpoint_name}.dns.available'), + self.is_joined and self.dns_ready()) + toggle_flag( + self.expand_name('{endpoint_name}.auth.available'), + self.is_joined and self._has_auth_credentials()) + toggle_flag( + self.expand_name('{endpoint_name}.cluster_tag.available'), + self.is_joined and self.get_cluster_tag()) + toggle_flag( + self.expand_name('{endpoint_name}.registry_location.available'), + self.is_joined and self.get_registry_location()) + toggle_flag( + self.expand_name('{endpoint_name}.cohort_keys.available'), + self.is_joined and self.cohort_keys) + toggle_flag( + self.expand_name('{endpoint_name}.default_cni.available'), + self.is_joined and self.get_default_cni() is not None) + toggle_flag( + self.expand_name('{endpoint_name}.api_endpoints.available'), + self.is_joined and self.get_api_endpoints()) + + def get_auth_credentials(self, user): + """ + Return the authentication credentials. + """ + rx = {} + for unit in self.all_joined_units: + rx.update(unit.received.get('creds', {})) + if not rx: + return None + + if user in rx: + return { + 'user': user, + 'kubelet_token': rx[user]['kubelet_token'], + 'proxy_token': rx[user]['proxy_token'], + 'client_token': rx[user]['client_token'] + } + else: + return None + + def get_dns(self): + """ + Return DNS info provided by the master. + """ + rx = self.all_joined_units.received_raw + + return { + 'port': rx.get('port'), + 'domain': rx.get('domain'), + 'sdn-ip': rx.get('sdn-ip'), + 'enable-kube-dns': rx.get('enable-kube-dns'), + } + + def dns_ready(self): + """ + Return True if we have all DNS info from the master. + """ + keys = ['port', 'domain', 'sdn-ip', 'enable-kube-dns'] + dns_info = self.get_dns() + return (set(dns_info.keys()) == set(keys) and + dns_info['enable-kube-dns'] is not None) + + def set_auth_request(self, kubelet, group='system:nodes'): + """ + Tell the master that we are requesting auth, and to use this + hostname for the kubelet system account. + + Param groups - Determines the level of eleveted privleges of the + requested user. Can be overridden to request sudo level access on the + cluster via changing to system:masters. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'kubelet_user': kubelet, + 'auth_group': group + }) + + def set_gpu(self, enabled=True): + """ + Tell the master that we're gpu-enabled (or not). + """ + log('Setting gpu={} on kube-control relation'.format(enabled)) + for relation in self.relations: + relation.to_publish_raw.update({ + 'gpu': enabled + }) + + def _has_auth_credentials(self): + """ + Predicate method to signal we have authentication credentials. + """ + if self.all_joined_units.received_raw.get('creds'): + return True + + def get_cluster_tag(self): + """ + Tag for identifying resources that are part of the cluster. + """ + return self.all_joined_units.received_raw.get('cluster-tag') + + def get_registry_location(self): + """ + URL for container image registry. + """ + return self.all_joined_units.received_raw.get('registry-location') + + @property + def cohort_keys(self): + """ + The cohort snapshot keys sent by the masters. + """ + return self.all_joined_units.received['cohort-keys'] + + def get_default_cni(self): + """ + Default CNI network to use. + """ + return self.all_joined_units.received['default-cni'] + + def get_api_endpoints(self): + """ + Returns a list of API endpoint URLs. + """ + endpoints = set() + for unit in self.all_joined_units: + endpoints.update(unit.received['api-endpoints'] or []) + return sorted(endpoints) diff --git a/kubernetes-worker/hooks/relations/kubernetes-cni/.github/workflows/tests.yaml b/kubernetes-worker/hooks/relations/kubernetes-cni/.github/workflows/tests.yaml new file mode 100644 index 0000000..9801450 --- /dev/null +++ b/kubernetes-worker/hooks/relations/kubernetes-cni/.github/workflows/tests.yaml @@ -0,0 +1,24 @@ +name: Test Suite for K8s Service Interface + +on: + - pull_request + +jobs: + lint-and-unit-tests: + name: Lint & Unit tests + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Tox + run: pip install tox + - name: Run lint & unit tests + run: tox + diff --git a/kubernetes-worker/hooks/relations/kubernetes-cni/.gitignore b/kubernetes-worker/hooks/relations/kubernetes-cni/.gitignore new file mode 100644 index 0000000..8d150f3 --- /dev/null +++ b/kubernetes-worker/hooks/relations/kubernetes-cni/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.tox +__pycache__ +*.pyc diff --git a/kubernetes-worker/hooks/relations/kubernetes-cni/README.md b/kubernetes-worker/hooks/relations/kubernetes-cni/README.md new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/kubernetes-cni/__init__.py b/kubernetes-worker/hooks/relations/kubernetes-cni/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/kubernetes-cni/interface.yaml b/kubernetes-worker/hooks/relations/kubernetes-cni/interface.yaml new file mode 100644 index 0000000..7e3c123 --- /dev/null +++ b/kubernetes-worker/hooks/relations/kubernetes-cni/interface.yaml @@ -0,0 +1,6 @@ +name: kubernetes-cni +summary: Interface for relating various CNI implementations +version: 0 +maintainer: "George Kraft " +ignore: +- tests diff --git a/kubernetes-worker/hooks/relations/kubernetes-cni/provides.py b/kubernetes-worker/hooks/relations/kubernetes-cni/provides.py new file mode 100644 index 0000000..9095c19 --- /dev/null +++ b/kubernetes-worker/hooks/relations/kubernetes-cni/provides.py @@ -0,0 +1,89 @@ +#!/usr/bin/python + +from charmhelpers.core import hookenv +from charmhelpers.core.host import file_hash +from charms.layer.kubernetes_common import kubeclientconfig_path +from charms.reactive import Endpoint +from charms.reactive import toggle_flag, is_flag_set, clear_flag, set_flag + + +class CNIPluginProvider(Endpoint): + def manage_flags(self): + toggle_flag(self.expand_name("{endpoint_name}.connected"), self.is_joined) + toggle_flag( + self.expand_name("{endpoint_name}.available"), self.config_available() + ) + if is_flag_set(self.expand_name("endpoint.{endpoint_name}.changed")): + clear_flag(self.expand_name("{endpoint_name}.configured")) + clear_flag(self.expand_name("endpoint.{endpoint_name}.changed")) + + def set_config(self, is_master): + """Relays a dict of kubernetes configuration information.""" + for relation in self.relations: + relation.to_publish_raw.update({"is_master": is_master}) + set_flag(self.expand_name("{endpoint_name}.configured")) + + def config_available(self): + """Ensures all config from the CNI plugin is available.""" + goal_state = hookenv.goal_state() + related_apps = [ + app + for app in goal_state.get("relations", {}).get(self.endpoint_name, "") + if "/" not in app + ] + if not related_apps: + return False + configs = self.get_configs() + return all( + "cidr" in config and "cni-conf-file" in config + for config in [configs.get(related_app, {}) for related_app in related_apps] + ) + + def get_config(self, default=None): + """Get CNI config for one related application. + + If default is specified, and there is a related application with a + matching name, then that application is chosen. Otherwise, the + application is chosen alphabetically. + + Whichever application is chosen, that application's CNI config is + returned. + """ + configs = self.get_configs() + if not configs: + return {} + elif default and default not in configs: + msg = "relation not found for default CNI %s, ignoring" % default + hookenv.log(msg, level="WARN") + return self.get_config() + elif default: + return configs.get(default, {}) + else: + return configs.get(sorted(configs)[0], {}) + + def get_configs(self): + """Get CNI configs for all related applications. + + This returns a mapping of application names to CNI configs. Here's an + example return value: + { + 'flannel': { + 'cidr': '10.1.0.0/16', + 'cni-conf-file': '10-flannel.conflist' + }, + 'calico': { + 'cidr': '192.168.0.0/16', + 'cni-conf-file': '10-calico.conflist' + } + } + """ + return { + relation.application_name: relation.joined_units.received_raw + for relation in self.relations + if relation.application_name + } + + def notify_kubeconfig_changed(self): + kubeconfig_hash = file_hash(kubeclientconfig_path) + for relation in self.relations: + relation.to_publish_raw.update({"kubeconfig-hash": kubeconfig_hash}) diff --git a/kubernetes-worker/hooks/relations/kubernetes-cni/requires.py b/kubernetes-worker/hooks/relations/kubernetes-cni/requires.py new file mode 100644 index 0000000..2067826 --- /dev/null +++ b/kubernetes-worker/hooks/relations/kubernetes-cni/requires.py @@ -0,0 +1,54 @@ +#!/usr/bin/python + +from charmhelpers.core import unitdata +from charms.reactive import Endpoint +from charms.reactive import when_any, when_not +from charms.reactive import set_state, remove_state + +db = unitdata.kv() + + +class CNIPluginClient(Endpoint): + def manage_flags(self): + kubeconfig_hash = self.get_config().get("kubeconfig-hash") + kubeconfig_hash_key = self.expand_name("{endpoint_name}.kubeconfig-hash") + if kubeconfig_hash: + set_state(self.expand_name("{endpoint_name}.kubeconfig.available")) + if kubeconfig_hash != db.get(kubeconfig_hash_key): + set_state(self.expand_name("{endpoint_name}.kubeconfig.changed")) + db.set(kubeconfig_hash_key, kubeconfig_hash) + + @when_any("endpoint.{endpoint_name}.joined", "endpoint.{endpoint_name}.changed") + def changed(self): + """Indicate the relation is connected, and if the relation data is + set it is also available.""" + set_state(self.expand_name("{endpoint_name}.connected")) + config = self.get_config() + if config["is_master"] == "True": + set_state(self.expand_name("{endpoint_name}.is-master")) + set_state(self.expand_name("{endpoint_name}.configured")) + elif config["is_master"] == "False": + set_state(self.expand_name("{endpoint_name}.is-worker")) + set_state(self.expand_name("{endpoint_name}.configured")) + else: + remove_state(self.expand_name("{endpoint_name}.configured")) + remove_state(self.expand_name("endpoint.{endpoint_name}.changed")) + + @when_not("endpoint.{endpoint_name}.joined") + def broken(self): + """Indicate the relation is no longer available and not connected.""" + remove_state(self.expand_name("{endpoint_name}.connected")) + remove_state(self.expand_name("{endpoint_name}.is-master")) + remove_state(self.expand_name("{endpoint_name}.is-worker")) + remove_state(self.expand_name("{endpoint_name}.configured")) + + def get_config(self): + """Get the kubernetes configuration information.""" + return self.all_joined_units.received_raw + + def set_config(self, cidr, cni_conf_file): + """Sets the CNI configuration information.""" + for relation in self.relations: + relation.to_publish_raw.update( + {"cidr": cidr, "cni-conf-file": cni_conf_file} + ) diff --git a/kubernetes-worker/hooks/relations/mount/.gitignore b/kubernetes-worker/hooks/relations/mount/.gitignore new file mode 100644 index 0000000..f3558c7 --- /dev/null +++ b/kubernetes-worker/hooks/relations/mount/.gitignore @@ -0,0 +1,105 @@ +# emacs files +*~ +\#*\# + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/kubernetes-worker/hooks/relations/mount/LICENSE b/kubernetes-worker/hooks/relations/mount/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/kubernetes-worker/hooks/relations/mount/LICENSE @@ -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/kubernetes-worker/hooks/relations/mount/README.md b/kubernetes-worker/hooks/relations/mount/README.md new file mode 100644 index 0000000..99c2394 --- /dev/null +++ b/kubernetes-worker/hooks/relations/mount/README.md @@ -0,0 +1,2 @@ +# interface-mount +Interface layer for connecting to mounts to a charm such as NFS diff --git a/kubernetes-worker/hooks/relations/mount/__init__.py b/kubernetes-worker/hooks/relations/mount/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/mount/copyright b/kubernetes-worker/hooks/relations/mount/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-worker/hooks/relations/mount/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-worker/hooks/relations/mount/interface.yaml b/kubernetes-worker/hooks/relations/mount/interface.yaml new file mode 100644 index 0000000..ff68ab1 --- /dev/null +++ b/kubernetes-worker/hooks/relations/mount/interface.yaml @@ -0,0 +1,4 @@ +name: mount +summary: Interface for mounting filesystems like NFS. +version: 1 +maintainer: Mike Wilson diff --git a/kubernetes-worker/hooks/relations/mount/provides.py b/kubernetes-worker/hooks/relations/mount/provides.py new file mode 100644 index 0000000..b68b0a8 --- /dev/null +++ b/kubernetes-worker/hooks/relations/mount/provides.py @@ -0,0 +1,39 @@ +from charms.reactive import when_any +from charms.reactive import set_flag, clear_flag +from charms.reactive import Endpoint + + +class MountProvides(Endpoint): + + @when_any('endpoint.{endpoint_name}.changed', + 'endpoint.{endpoint_name}.departed') + def changed(self): + set_flag(self.expand_name('{endpoint_name}.changed')) + clear_flag(self.expand_name('endpoint.{endpoint_name}.changed')) + clear_flag(self.expand_name('endpoint.{endpoint_name}.departed')) + + def get_mount_requests(self): + return [{ + 'identifier': relation.relation_id, + 'application_name': relation.joined_units.received_raw.get( + 'export_name', relation.application_name), + 'addresses': [ + unit.received_raw.get('ingress-address', + unit.received_raw['private-address']) + for unit in relation.joined_units], + } for relation in self.relations] + + def configure(self, responses): + for response in responses: + relation = self.relations[response['identifier']] + relation.to_publish_raw.update({ + 'mountpoint': response['mountpoint'], + 'fstype': response['fstype'], + 'options': response['options'], + }) + for key in ('export_name', 'hostname'): + if key in response: + relation.to_publish_raw[key] = response[key] + elif key in relation.to_publish_raw: + del relation.to_publish_raw[key] + clear_flag(self.expand_name('{endpoint_name}.changed')) diff --git a/kubernetes-worker/hooks/relations/mount/requires.py b/kubernetes-worker/hooks/relations/mount/requires.py new file mode 100644 index 0000000..6f503ed --- /dev/null +++ b/kubernetes-worker/hooks/relations/mount/requires.py @@ -0,0 +1,71 @@ +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag +from charms.reactive import Endpoint + + +class MountRequires(Endpoint): + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('{endpoint_name}.joined')) + + @when('endpoint.{endpoint_name}.changed') + def changed(self): + if any(unit.received_raw['mountpoint'] + for unit in self.all_joined_units): + set_flag(self.expand_name('{endpoint_name}.available')) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.joined')) + clear_flag(self.expand_name('{endpoint_name}.available')) + + def set_export_name(self, export_name): + for relation in self.relations: + relation.to_publish_raw['export_name'] = export_name + + def mounts(self): + """ + Returns a list of available mounts and their associated data. + + The return value is a list of dicts of the following form:: + + [ + { + 'mount_name': name_of_mount, + 'mounts': [ + { + 'hostname': hostname, + 'mountpoint': mountpoint, + 'fstype': mounttype, + 'options': options + }, + # ... + ], + }, + # ... + ] + """ + mounts = {} + for relation in self.relations: + for unit in relation.joined_units: + mount_name = unit.received_raw.get( + 'export_name', relation.application_name) + mount = mounts.setdefault(mount_name, { + 'mount_name': mount_name, + 'mounts': [], + }) + data = unit.received_raw + mountpoint = data['mountpoint'] + fstype = data['fstype'] + options = data['options'] + host = data['hostname'] or \ + data['private-address'] + if host and mountpoint and fstype and options: + mount['mounts'].append({ + 'hostname': host, + 'mountpoint': mountpoint, + 'fstype': fstype, + 'options': options + }) + return [m for m in mounts.values() if m['mounts']] diff --git a/kubernetes-worker/hooks/relations/nrpe-external-master/README.md b/kubernetes-worker/hooks/relations/nrpe-external-master/README.md new file mode 100644 index 0000000..e33deb8 --- /dev/null +++ b/kubernetes-worker/hooks/relations/nrpe-external-master/README.md @@ -0,0 +1,66 @@ +# nrpe-external-master interface + +Use this interface to register nagios checks in your charm layers. + +## Purpose + +This interface is designed to interoperate with the +[nrpe-external-master](https://jujucharms.com/nrpe-external-master) subordinate charm. + +## How to use in your layers + +The event handler for `nrpe-external-master.available` is called with an object +through which you can register your own custom nagios checks, when a relation +is established with `nrpe-external-master:nrpe-external-master`. + +This object provides a method, + +_add_check_(args, name=_check_name_, description=_description_, context=_context_, unit=_unit_) + +which is called to register a nagios plugin check for your service. + +All arguments are required. + +*args* is a list of nagios plugin command line arguments, starting with the path to the plugin executable. + +*name* is the name of the check registered in nagios + +*description* is some text that describes what the check is for and what it does + +*context* is the nagios context name, something that identifies your application + +*unit* is `hookenv.local_unit()` + +The nrpe subordinate installs `check_http`, so you can use it like this: + +``` +@when('nrpe-external-master.available') +def setup_nagios(nagios): + config = hookenv.config() + unit_name = hookenv.local_unit() + nagios.add_check(['/usr/lib/nagios/plugins/check_http', + '-I', '127.0.0.1', '-p', str(config['port']), + '-e', " 200 OK", '-u', '/publickey'], + name="check_http", + description="Verify my awesome service is responding", + context=config["nagios_context"], + unit=unit_name, + ) +``` +If your `nagios.add_check` defines a custom plugin, you will also need to restart the `nagios-nrpe-server` service. + +Consult the nagios documentation for more information on [how to write your own +plugins](https://assets.nagios.com/downloads/nagioscore/docs/nagioscore/4/en/pluginapi.html) +or [find one](https://www.nagios.org/projects/nagios-plugins/) that does what you need. + +## Example deployment + +``` +$ juju deploy your-awesome-charm +$ juju deploy nrpe-external-master --config site-nagios.yaml +$ juju add-relation your-awesome-charm nrpe-external-master +``` + +where `site-nagios.yaml` has the necessary configuration settings for the +subordinate to connect to nagios. + diff --git a/kubernetes-worker/hooks/relations/nrpe-external-master/__init__.py b/kubernetes-worker/hooks/relations/nrpe-external-master/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/nrpe-external-master/interface.yaml b/kubernetes-worker/hooks/relations/nrpe-external-master/interface.yaml new file mode 100644 index 0000000..859a423 --- /dev/null +++ b/kubernetes-worker/hooks/relations/nrpe-external-master/interface.yaml @@ -0,0 +1,3 @@ +name: nrpe-external-master +summary: Nagios interface +version: 1 diff --git a/kubernetes-worker/hooks/relations/nrpe-external-master/provides.py b/kubernetes-worker/hooks/relations/nrpe-external-master/provides.py new file mode 100644 index 0000000..b6c7f0d --- /dev/null +++ b/kubernetes-worker/hooks/relations/nrpe-external-master/provides.py @@ -0,0 +1,91 @@ +import datetime +import os + +from charmhelpers.core import hookenv + +from charms.reactive import hook +from charms.reactive import RelationBase +from charms.reactive import scopes + + +class NrpeExternalMasterProvides(RelationBase): + scope = scopes.GLOBAL + + @hook('{provides:nrpe-external-master}-relation-{joined,changed}') + def changed_nrpe(self): + self.set_state('{relation_name}.available') + + @hook('{provides:nrpe-external-master}-relation-{broken,departed}') + def broken_nrpe(self): + self.remove_state('{relation_name}.available') + + def add_check(self, args, name=None, description=None, context=None, + servicegroups=None, unit=None): + nagios_files = self.get_local('nagios.check.files', []) + + if not unit: + unit = hookenv.local_unit() + unit = unit.replace('/', '-') + context = self.get_remote('nagios_host_context', context) + host_name = self.get_remote('nagios_hostname', + '%s-%s' % (context, unit)) + + check_tmpl = """ +#--------------------------------------------------- +# This file is Juju managed +#--------------------------------------------------- +command[%(check_name)s]=%(check_args)s +""" + service_tmpl = """ +#--------------------------------------------------- +# This file is Juju managed +#--------------------------------------------------- +define service { + use active-service + host_name %(host_name)s + service_description %(description)s + check_command check_nrpe!%(check_name)s + servicegroups %(servicegroups)s +} +""" + check_filename = "/etc/nagios/nrpe.d/check_%s.cfg" % (name) + with open(check_filename, "w") as fh: + fh.write(check_tmpl % { + 'check_args': ' '.join(args), + 'check_name': name, + }) + nagios_files.append(check_filename) + + service_filename = "/var/lib/nagios/export/service__%s_%s.cfg" % ( + unit, name) + with open(service_filename, "w") as fh: + fh.write(service_tmpl % { + 'servicegroups': servicegroups or context, + 'context': context, + 'description': description, + 'check_name': name, + 'host_name': host_name, + 'unit_name': unit, + }) + nagios_files.append(service_filename) + + self.set_local('nagios.check.files', nagios_files) + + def removed(self): + files = self.get_local('nagios.check.files', []) + for f in files: + try: + os.unlink(f) + except Exception as e: + hookenv.log("failed to remove %s: %s" % (f, e)) + self.set_local('nagios.check.files', []) + self.remove_state('{relation_name}.removed') + + def added(self): + self.updated() + + def updated(self): + relation_info = { + 'timestamp': datetime.datetime.now().isoformat(), + } + self.set_remote(**relation_info) diff --git a/kubernetes-worker/hooks/relations/nrpe-external-master/requires.py b/kubernetes-worker/hooks/relations/nrpe-external-master/requires.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/openstack-integration/.gitignore b/kubernetes-worker/hooks/relations/openstack-integration/.gitignore new file mode 100644 index 0000000..5f9f2c5 --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/.gitignore @@ -0,0 +1,3 @@ +.tox +__pycache__ +*.pyc diff --git a/kubernetes-worker/hooks/relations/openstack-integration/LICENSE b/kubernetes-worker/hooks/relations/openstack-integration/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-worker/hooks/relations/openstack-integration/README.md b/kubernetes-worker/hooks/relations/openstack-integration/README.md new file mode 100644 index 0000000..ae021c2 --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/README.md @@ -0,0 +1,28 @@ +# Overview + +This layer encapsulates the `openstack-integration` interface communciation +protocol and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:openstack-integration` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:openstack-integration'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `openstack-integration` interface protocol: + +```yaml +requires: + openstack: + interface: openstack-integration +``` + +For documentation on how to use the API for this interface, see: + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) (this will only be used by the openstack-integrator charm) diff --git a/kubernetes-worker/hooks/relations/openstack-integration/__init__.py b/kubernetes-worker/hooks/relations/openstack-integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/openstack-integration/copyright b/kubernetes-worker/hooks/relations/openstack-integration/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-worker/hooks/relations/openstack-integration/docs/provides.md b/kubernetes-worker/hooks/relations/openstack-integration/docs/provides.md new file mode 100644 index 0000000..ee17ac6 --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/docs/provides.md @@ -0,0 +1,108 @@ +

provides

+ + +This is the provides side of the interface layer, for use only by the +OpenStack integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for OpenStack integration + features. The OpenStack integration charm should then iterate over each + request, perform whatever actions are necessary to satisfy those requests, + and then mark them as complete. + +

OpenStackIntegrationProvides

+ +```python +OpenStackIntegrationProvides(endpoint_name, relation_ids=None) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag +from charms import layer + +@when('endpoint.openstack.requests-pending') +def handle_requests(): + openstack = endpoint_from_flag('endpoint.openstack.requests-pending') + for request in openstack.requests: + request.set_credentials(layer.openstack.get_user_credentials()) + openstack.mark_completed() +``` + +

all_requests

+ + +A list of all of the [`IntegrationRequests`](#provides.OpenStackIntegrationProvides.all_requests.IntegrationRequests) that have been made. + +

new_requests

+ + +A list of the new or updated [`IntegrationRequests`](#provides.OpenStackIntegrationProvides.new_requests.IntegrationRequests) that have been made. + +

mark_completed

+ +```python +OpenStackIntegrationProvides.mark_completed() +``` + +Mark all requests as completed and remove the `requests-pending` flag. + +

IntegrationRequest

+ +```python +IntegrationRequest(unit) +``` + +A request for integration from a single remote unit. + +

has_credentials

+ + +Whether or not credentials have been set via `set_credentials`. + +

is_changed

+ + +Whether this request has changed since the last time it was +marked completed (if ever). + +

set_credentials

+ +```python +IntegrationRequest.set_credentials(auth_url, + region, + username, + password, + user_domain_name, + project_domain_name, + project_name, + endpoint_tls_ca, + version=None) +``` + +Set the credentials for this request. + +

set_lbaas_config

+ +```python +IntegrationRequest.set_lbaas_config(subnet_id, + floating_network_id, + lb_method, + manage_security_groups, + has_octavia=None) +``` + +Set the load-balancer-as-a-service config for this request. + +

set_block_storage_config

+ +```python +IntegrationRequest.set_block_storage_config(bs_version, trust_device_path, + ignore_volume_az) +``` + +Set the block storage config for this request. + diff --git a/kubernetes-worker/hooks/relations/openstack-integration/docs/requires.md b/kubernetes-worker/hooks/relations/openstack-integration/docs/requires.md new file mode 100644 index 0000000..510e292 --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/docs/requires.md @@ -0,0 +1,160 @@ +

requires

+ + +This is the requires side of the interface layer, for use in charms that wish +to request integration with OpenStack native features. The integration will be +provided by the OpenStack integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of OpenStack +specific API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific OpenStack features. This flag is automatically removed + if the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the OpenStack instance on which the charm is + running. This flag is automatically removed if new integration features are + requested. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready.changed`** This flag is set if the data + changes after the ready flag was set. This flag should be removed by the + charm once handled. + +

OpenStackIntegrationRequires

+ +```python +OpenStackIntegrationRequires(endpoint_name, relation_ids=None) +``` + +Interface to request integration access. + +Note that due to resource limits and permissions granularity, policies are +limited to being applied at the charm level. That means that, if any +permissions are requested (i.e., any of the enable methods are called), +what is granted will be the sum of those ever requested by any instance of +the charm on this cloud. + +Labels, on the other hand, will be instance specific. + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag + +@when('endpoint.openstack.ready') +def openstack_integration_ready(): + openstack = endpoint_from_flag('endpoint.openstack.ready') + update_config_enable_openstack(openstack) +``` + +

auth_url

+ + +The authentication endpoint URL. + +

bs_version

+ + +What block storage API version to use, `auto` if autodetection is +desired, or `None` to use the default. + +

endpoint_tls_ca

+ + +Optional base64-encoded CA certificate for the authentication endpoint, +or None. + +

floating_network_id

+ + +Optional floating network ID, or None. + +

has_octavia

+ + +Whether the underlying OpenStack supports Octavia instead of +Neutron-based LBaaS. + +Will either be True, False, or None if it could not be determined for +some reason (typically due to connecting to an older integrator charm). + +

ignore_volume_az

+ + +Whether to ignore availability zones when attaching Cinder volumes. + +Will be `True`, `False`, or `None`. + +

is_changed

+ + +Whether or not the request for this instance has changed. + +

is_ready

+ + +Whether or not the request for this instance has been completed. + +

lb_method

+ + +Optional load-balancer method, or None. + +

manage_security_groups

+ + +Whether or not the Load Balancer should automatically manage security +group rules. + +Will be `True` or `False`. + +

password

+ + +The password. + +

project_domain_name

+ + +The project domain name. + +

project_name

+ + +The project name, also known as the tenant ID. + +

region

+ + +The region name. + +

subnet_id

+ + +Optional subnet ID to work in, or None. + +

trust_device_path

+ + +Whether to trust the block device name provided by Ceph. + +Will be `True`, `False`, or `None`. + +

user_domain_name

+ + +The user domain name. + +

username

+ + +The username. + +

version

+ + +Optional version number for the APIs or None. + diff --git a/kubernetes-worker/hooks/relations/openstack-integration/interface.yaml b/kubernetes-worker/hooks/relations/openstack-integration/interface.yaml new file mode 100644 index 0000000..a94fed4 --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/interface.yaml @@ -0,0 +1,4 @@ +name: openstack-integration +summary: Interface for connecting to the OpenStack integrator charm. +version: 1 +maintainer: Cory Johns diff --git a/kubernetes-worker/hooks/relations/openstack-integration/make_docs b/kubernetes-worker/hooks/relations/openstack-integration/make_docs new file mode 100644 index 0000000..a09c66f --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'openstack': {'interface': 'openstack'}}, + 'provides': {'openstack': {'interface': 'openstack'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-worker/hooks/relations/openstack-integration/provides.py b/kubernetes-worker/hooks/relations/openstack-integration/provides.py new file mode 100644 index 0000000..7aa8146 --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/provides.py @@ -0,0 +1,152 @@ +""" +This is the provides side of the interface layer, for use only by the +OpenStack integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for OpenStack integration + features. The OpenStack integration charm should then iterate over each + request, perform whatever actions are necessary to satisfy those requests, + and then mark them as complete. +""" + +from operator import attrgetter + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class OpenStackIntegrationProvides(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + + @when('endpoint.openstack.requests-pending') + def handle_requests(): + openstack = endpoint_from_flag('endpoint.openstack.requests-pending') + for request in openstack.requests: + request.set_credentials(layer.openstack.get_user_credentials()) + openstack.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + toggle_flag(self.expand_name('requests-pending'), + len(self.all_requests) > 0) + clear_flag(self.expand_name('changed')) + + @property + def all_requests(self): + """ + A list of all of the #IntegrationRequests that have been made. + """ + if not hasattr(self, '_all_requests'): + self._all_requests = [IntegrationRequest(unit) + for unit in self.all_joined_units] + return self._all_requests + + @property + def new_requests(self): + """ + A list of the new or updated #IntegrationRequests that have been made. + """ + is_changed = attrgetter('is_changed') + return list(filter(is_changed, self.all_requests)) + + def mark_completed(self): + """ + Mark all requests as completed and remove the `requests-pending` flag. + """ + clear_flag(self.expand_name('requests-pending')) + + +class IntegrationRequest: + """ + A request for integration from a single remote unit. + """ + def __init__(self, unit): + self._unit = unit + + @property + def _to_publish(self): + return self._unit.relation.to_publish + + @property + def is_changed(self): + """ + Whether this request has changed since the last time it was + marked completed (if ever). + """ + return not self.has_credentials + + @property + def unit_name(self): + return self._unit.unit_name + + def set_credentials(self, + auth_url, + region, + username, + password, + user_domain_name, + project_domain_name, + project_name, + endpoint_tls_ca, + version=None): + """ + Set the credentials for this request. + """ + self._unit.relation.to_publish.update({ + 'auth_url': auth_url, + 'region': region, + 'username': username, + 'password': password, + 'user_domain_name': user_domain_name, + 'project_domain_name': project_domain_name, + 'project_name': project_name, + 'endpoint_tls_ca': endpoint_tls_ca, + 'version': version, + }) + + def set_lbaas_config(self, + subnet_id, + floating_network_id, + lb_method, + manage_security_groups, + has_octavia=None): + """ + Set the load-balancer-as-a-service config for this request. + """ + self._unit.relation.to_publish.update({ + 'subnet_id': subnet_id, + 'floating_network_id': floating_network_id, + 'lb_method': lb_method, + 'manage_security_groups': manage_security_groups, + 'has_octavia': has_octavia, + }) + + def set_block_storage_config(self, + bs_version, + trust_device_path, + ignore_volume_az): + """ + Set the block storage config for this request. + """ + self._unit.relation.to_publish.update({ + 'bs_version': bs_version, + 'trust_device_path': trust_device_path, + 'ignore_volume_az': ignore_volume_az, + }) + + @property + def has_credentials(self): + """ + Whether or not credentials have been set via `set_credentials`. + """ + return 'credentials' in self._unit.relation.to_publish diff --git a/kubernetes-worker/hooks/relations/openstack-integration/pydocmd.yml b/kubernetes-worker/hooks/relations/openstack-integration/pydocmd.yml new file mode 100644 index 0000000..aa0a286 --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'OpenStack Integration Interface' + +generate: + - requires.md: + - requires + - requires.OpenStackIntegrationRequires+ + - provides.md: + - provides + - provides.OpenStackIntegrationProvides+ + - provides.IntegrationRequest+ + +pages: + - Requires: requires.md + - Provides: provides.md + +gens_dir: docs diff --git a/kubernetes-worker/hooks/relations/openstack-integration/requires.py b/kubernetes-worker/hooks/relations/openstack-integration/requires.py new file mode 100644 index 0000000..420f767 --- /dev/null +++ b/kubernetes-worker/hooks/relations/openstack-integration/requires.py @@ -0,0 +1,254 @@ +""" +This is the requires side of the interface layer, for use in charms that wish +to request integration with OpenStack native features. The integration will be +provided by the OpenStack integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of OpenStack +specific API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific OpenStack features. This flag is automatically removed + if the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the OpenStack instance on which the charm is + running. This flag is automatically removed if new integration features are + requested. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready.changed`** This flag is set if the data + changes after the ready flag was set. This flag should be removed by the + charm once handled. +""" + + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag, is_flag_set +from charms.reactive import data_changed + + +class OpenStackIntegrationRequires(Endpoint): + """ + Interface to request integration access. + + Note that due to resource limits and permissions granularity, policies are + limited to being applied at the charm level. That means that, if any + permissions are requested (i.e., any of the enable methods are called), + what is granted will be the sum of those ever requested by any instance of + the charm on this cloud. + + Labels, on the other hand, will be instance specific. + + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + + @when('endpoint.openstack.ready') + def openstack_integration_ready(): + openstack = endpoint_from_flag('endpoint.openstack.ready') + update_config_enable_openstack(openstack) + ``` + """ + + @property + def _received(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single OpenStack integration application with a + single unit. + """ + return self.relations[0].joined_units.received + + @property + def _to_publish(self): + """ + Helper to streamline access to received data since we expect to only + ever be connected to a single OpenStack integration application with a + single unit. + """ + return self.relations[0].to_publish + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + # My middle name is ready. No, that doesn't sound right. + # I eat ready for breakfast. + was_ready = is_flag_set(self.expand_name('ready')) + toggle_flag(self.expand_name('ready'), self.is_ready) + if self.is_ready and was_ready and self.is_changed: + set_flag(self.expand_name('ready.changed')) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + @property + def is_ready(self): + """ + Whether or not the request for this instance has been completed. + """ + # Although more information can be passed, such as LBaaS access + # the minimum needed to be considered ready is defined here + return all(field is not None for field in [ + self.auth_url, + self.username, + self.password, + self.user_domain_name, + self.project_domain_name, + self.project_name, + ]) + + @property + def is_changed(self): + """ + Whether or not the request for this instance has changed. + """ + return data_changed(self.expand_name('all-data'), [ + self.auth_url, + self.region, + self.username, + self.password, + self.user_domain_name, + self.project_domain_name, + self.project_name, + self.endpoint_tls_ca, + self.subnet_id, + self.floating_network_id, + self.lb_method, + self.manage_security_groups, + ]) + + @property + def auth_url(self): + """ + The authentication endpoint URL. + """ + return self._received['auth_url'] + + @property + def region(self): + """ + The region name. + """ + return self._received['region'] + + @property + def username(self): + """ + The username. + """ + return self._received['username'] + + @property + def password(self): + """ + The password. + """ + return self._received['password'] + + @property + def user_domain_name(self): + """ + The user domain name. + """ + return self._received['user_domain_name'] + + @property + def project_domain_name(self): + """ + The project domain name. + """ + return self._received['project_domain_name'] + + @property + def project_name(self): + """ + The project name, also known as the tenant ID. + """ + return self._received['project_name'] + + @property + def endpoint_tls_ca(self): + """ + Optional base64-encoded CA certificate for the authentication endpoint, + or None. + """ + return self._received['endpoint_tls_ca'] or None + + @property + def version(self): + """ + Optional version number for the APIs or None. + """ + return self._received['version'] or None + + @property + def subnet_id(self): + """ + Optional subnet ID to work in, or None. + """ + return self._received['subnet_id'] + + @property + def floating_network_id(self): + """ + Optional floating network ID, or None. + """ + return self._received['floating_network_id'] + + @property + def lb_method(self): + """ + Optional load-balancer method, or None. + """ + return self._received['lb_method'] + + @property + def manage_security_groups(self): + """ + Whether or not the Load Balancer should automatically manage security + group rules. + + Will be `True` or `False`. + """ + return self._received['manage_security_groups'] or False + + @property + def bs_version(self): + """ + What block storage API version to use, `auto` if autodetection is + desired, or `None` to use the default. + """ + return self._received['bs_version'] + + @property + def trust_device_path(self): + """ + Whether to trust the block device name provided by Ceph. + + Will be `True`, `False`, or `None`. + """ + return self._received['trust_device_path'] + + @property + def ignore_volume_az(self): + """ + Whether to ignore availability zones when attaching Cinder volumes. + + Will be `True`, `False`, or `None`. + """ + return self._received['ignore_volume_az'] + + @property + def has_octavia(self): + """ + Whether the underlying OpenStack supports Octavia instead of + Neutron-based LBaaS. + + Will either be True, False, or None if it could not be determined for + some reason (typically due to connecting to an older integrator charm). + """ + return self._received['has_octavia'] diff --git a/kubernetes-worker/hooks/relations/tls-certificates/.gitignore b/kubernetes-worker/hooks/relations/tls-certificates/.gitignore new file mode 100644 index 0000000..93813bc --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/.gitignore @@ -0,0 +1,4 @@ +.tox +__pycache__ +*.pyc +_build diff --git a/kubernetes-worker/hooks/relations/tls-certificates/README.md b/kubernetes-worker/hooks/relations/tls-certificates/README.md new file mode 100644 index 0000000..733da6d --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/README.md @@ -0,0 +1,90 @@ +# Interface tls-certificates + +This is a [Juju][] interface layer that enables a charm which requires TLS +certificates to relate to a charm which can provide them, such as [Vault][] or +[EasyRSA][] + +To get started please read the [Introduction to PKI][] which defines some PKI +terms, concepts and processes used in this document. + +# Example Usage + +Let's say you have a charm which needs a server certificate for a service it +provides to other charms and a client certificate for a database it consumes +from another charm. The charm provides its own service on the `clients` +relation endpoint, and it consumes the database on the `db` relation endpoint. + +First, you must define the relation endpoint in your charm's `metadata.yaml`: + +```yaml +requires: + cert-provider: + interface: tls-certificates +``` + +Next, you must ensure the interface layer is included in your `layer.yaml`: + +```yaml +includes: + - interface:tls-certificates +``` + +Then, in your reactive code, add the following, changing `update_certs` to +handle the certificates however your charm needs: + +```python +from charmhelpers.core import hookenv, host +from charms.reactive import endpoint_from_flag + + +@when('cert-provider.ca.changed') +def install_root_ca_cert(): + cert_provider = endpoint_from_flag('cert-provider.ca.available') + host.install_ca_cert(cert_provider.root_ca_cert) + clear_flag('cert-provider.ca.changed') + + +@when('cert-provider.available') +def request_certificates(): + cert_provider = endpoint_from_flag('cert-provider.available') + + # get ingress info + ingress_for_clients = hookenv.network_get('clients')['ingress-addresses'] + ingress_for_db = hookenv.network_get('db')['ingress-addresses'] + + # use first ingress address as primary and any additional as SANs + server_cn, server_sans = ingress_for_clients[0], ingress_for_clients[:1] + client_cn, client_sans = ingress_for_db[0], ingress_for_db[:1] + + # request a single server and single client cert; note that multiple certs + # of either type can be requested as long as they have unique common names + cert_provider.request_server_cert(server_cn, server_sans) + cert_provider.request_client_cert(client_cn, client_sans) + + +@when('cert-provider.certs.changed') +def update_certs(): + cert_provider = endpoint_from_flag('cert-provider.available') + server_cert = cert_provider.server_certs[0] # only requested one + myserver.update_server_cert(server_cert.cert, server_cert.key) + + client_cert = cert_provider.client_certs[0] # only requested one + myclient.update_client_cert(client_cert.cert, client_cert.key) + clear_flag('cert-provider.certs.changed') +``` + + +# Reference + + * [Requires](docs/requires.md) + * [Provides](docs/provides.md) + +# Contact Information + +Maintainer: Cory Johns <Cory.Johns@canonical.com> + + +[Juju]: https://jujucharms.com +[Vault]: https://jujucharms.com/u/openstack-charmers/vault +[EasyRSA]: https://jujucharms.com/u/containers/easyrsa +[Introduction to PKI]: https://github.com/OpenVPN/easy-rsa/blob/master/doc/Intro-To-PKI.md diff --git a/kubernetes-worker/hooks/relations/tls-certificates/__init__.py b/kubernetes-worker/hooks/relations/tls-certificates/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/tls-certificates/docs/common.md b/kubernetes-worker/hooks/relations/tls-certificates/docs/common.md new file mode 100644 index 0000000..25d0e08 --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/docs/common.md @@ -0,0 +1,51 @@ +

CertificateRequest

+ +```python +CertificateRequest(self, unit, cert_type, cert_name, common_name, sans) +``` + +

application_name

+ +Name of the application which the request came from. + +:returns: Name of application +:rtype: str + +

cert

+ + +The cert published for this request, if any. + +

cert_type

+ + +Type of certificate, 'server' or 'client', being requested. + +

resolve_unit_name

+ +```python +CertificateRequest.resolve_unit_name(unit) +``` +Return name of unit associated with this request. + +unit_name should be provided in the relation data to ensure +compatability with cross-model relations. If the unit name +is absent then fall back to unit_name attribute of the +unit associated with this request. + +:param unit: Unit to extract name from +:type unit: charms.reactive.endpoints.RelatedUnit +:returns: Name of unit +:rtype: str + +

Certificate

+ +```python +Certificate(self, cert_type, common_name, cert, key) +``` + +Represents a created certificate and key. + +The ``cert_type``, ``common_name``, ``cert``, and ``key`` values can +be accessed either as properties or as the contents of the dict. + diff --git a/kubernetes-worker/hooks/relations/tls-certificates/docs/provides.md b/kubernetes-worker/hooks/relations/tls-certificates/docs/provides.md new file mode 100644 index 0000000..c213546 --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/docs/provides.md @@ -0,0 +1,212 @@ +

provides

+ + +

TlsProvides

+ +```python +TlsProvides(self, endpoint_name, relation_ids=None) +``` + +The provider's side of the interface protocol. + +The following flags may be set: + + * `{endpoint_name}.available` + Whenever any clients are joined. + + * `{endpoint_name}.certs.requested` + When there are new certificate requests of any kind to be processed. + The requests can be accessed via [new_requests][]. + + * `{endpoint_name}.server.certs.requested` + When there are new server certificate requests to be processed. + The requests can be accessed via [new_server_requests][]. + + * `{endpoint_name}.client.certs.requested` + When there are new client certificate requests to be processed. + The requests can be accessed via [new_client_requests][]. + +[Certificate]: common.md#tls_certificates_common.Certificate +[CertificateRequest]: common.md#tls_certificates_common.CertificateRequest +[all_requests]: provides.md#provides.TlsProvides.all_requests +[new_requests]: provides.md#provides.TlsProvides.new_requests +[new_server_requests]: provides.md#provides.TlsProvides.new_server_requests +[new_client_requests]: provides.md#provides.TlsProvides.new_client_requests + +

all_published_certs

+ + +List of all [Certificate][] instances that this provider has published +for all related applications. + +

all_requests

+ + +List of all requests that have been made. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('certs.regen', + 'tls.certs.available') +def regen_all_certs(): + tls = endpoint_from_flag('tls.certs.available') + for request in tls.all_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_application_requests

+ + +Filtered view of [new_requests][] that only includes application cert +requests. + +Each will be an instance of [ApplicationCertificateRequest][]. + +Example usage: + +```python +@when('tls.application.certs.requested') +def gen_application_certs(): + tls = endpoint_from_flag('tls.application.certs.requested') + for request in tls.new_application_requests: + cert, key = generate_application_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_client_requests

+ + +Filtered view of [new_requests][] that only includes client cert +requests. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('tls.client.certs.requested') +def gen_client_certs(): + tls = endpoint_from_flag('tls.client.certs.requested') + for request in tls.new_client_requests: + cert, key = generate_client_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_requests

+ + +Filtered view of [all_requests][] that only includes requests that +haven't been handled. + +Each will be an instance of [CertificateRequest][]. + +This collection can also be further filtered by request type using +[new_server_requests][] or [new_client_requests][]. + +Example usage: + +```python +@when('tls.certs.requested') +def gen_certs(): + tls = endpoint_from_flag('tls.certs.requested') + for request in tls.new_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

new_server_requests

+ + +Filtered view of [new_requests][] that only includes server cert +requests. + +Each will be an instance of [CertificateRequest][]. + +Example usage: + +```python +@when('tls.server.certs.requested') +def gen_server_certs(): + tls = endpoint_from_flag('tls.server.certs.requested') + for request in tls.new_server_requests: + cert, key = generate_server_cert(request.common_name, + request.sans) + request.set_cert(cert, key) +``` + +

set_ca

+ +```python +TlsProvides.set_ca(certificate_authority) +``` + +Publish the CA to all related applications. + +

set_chain

+ +```python +TlsProvides.set_chain(chain) +``` + +Publish the chain of trust to all related applications. + +

set_client_cert

+ +```python +TlsProvides.set_client_cert(cert, key) +``` + +Deprecated. This is only for backwards compatibility. + +Publish a globally shared client cert and key. + +

set_server_cert

+ +```python +TlsProvides.set_server_cert(scope, cert, key) +``` + +Deprecated. Use one of the [new_requests][] collections and +`request.set_cert()` instead. + +Set the server cert and key for the request identified by `scope`. + +

set_server_multicerts

+ +```python +TlsProvides.set_server_multicerts(scope) +``` + +Deprecated. Done automatically. + +

add_server_cert

+ +```python +TlsProvides.add_server_cert(scope, cn, cert, key) +``` + +Deprecated. Use `request.set_cert()` instead. + +

get_server_requests

+ +```python +TlsProvides.get_server_requests() +``` + +Deprecated. Use the [new_requests][] or [server_requests][] +collections instead. + +One provider can have many requests to generate server certificates. +Return a map of all server request objects indexed by a unique +identifier. + diff --git a/kubernetes-worker/hooks/relations/tls-certificates/docs/requires.md b/kubernetes-worker/hooks/relations/tls-certificates/docs/requires.md new file mode 100644 index 0000000..fdec902 --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/docs/requires.md @@ -0,0 +1,207 @@ +

requires

+ + +

TlsRequires

+ +```python +TlsRequires(self, endpoint_name, relation_ids=None) +``` + +The client's side of the interface protocol. + +The following flags may be set: + + * `{endpoint_name}.available` + Whenever the relation is joined. + + * `{endpoint_name}.ca.available` + When the root CA information is available via the [root_ca_cert][] and + [root_ca_chain][] properties. + + * `{endpoint_name}.ca.changed` + When the root CA information has changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.certs.available` + When the requested server or client certs are available. + + * `{endpoint_name}.certs.changed` + When the requested server or client certs have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.server.certs.available` + When the server certificates requested by [request_server_cert][] are + available via the [server_certs][] collection. + + * `{endpoint_name}.server.certs.changed` + When the requested server certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.client.certs.available` + When the client certificates requested by [request_client_cert][] are + available via the [client_certs][] collection. + + * `{endpoint_name}.client.certs.changed` + When the requested client certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + +The following flags have been deprecated: + + * `{endpoint_name}.server.cert.available` + * `{endpoint_name}.client.cert.available` + * `{endpoint_name}.batch.cert.available` + +[Certificate]: common.md#tls_certificates_common.Certificate +[CertificateRequest]: common.md#tls_certificates_common.CertificateRequest +[root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert +[root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain +[request_server_cert]: requires.md#requires.TlsRequires.request_server_cert +[request_client_cert]: requires.md#requires.TlsRequires.request_client_cert +[server_certs]: requires.md#requires.TlsRequires.server_certs +[server_certs_map]: requires.md#requires.TlsRequires.server_certs_map +[client_certs]: requires.md#requires.TlsRequires.server_certs + +

application_certs

+ + +List of [Certificate][] instances for all available application certs. + +

client_certs

+ + +List of [Certificate][] instances for all available client certs. + +

client_certs_map

+ + +Mapping of client [Certificate][] instances by their `common_name`. + +

root_ca_cert

+ + +Root CA certificate. + +

root_ca_chain

+ + +The chain of trust for the root CA. + +

server_certs

+ + +List of [Certificate][] instances for all available server certs. + +

server_certs_map

+ + +Mapping of server [Certificate][] instances by their `common_name`. + +

get_ca

+ +```python +TlsRequires.get_ca() +``` + +Return the root CA certificate. + +Same as [root_ca_cert][]. + +

get_chain

+ +```python +TlsRequires.get_chain() +``` + +Return the chain of trust for the root CA. + +Same as [root_ca_chain][]. + +

get_client_cert

+ +```python +TlsRequires.get_client_cert() +``` + +Deprecated. Use [request_client_cert][] and the [client_certs][] +collection instead. + +Return a globally shared client certificate and key. + +

get_server_cert

+ +```python +TlsRequires.get_server_cert() +``` + +Deprecated. Use the [server_certs][] collection instead. + +Return the cert and key of the first server certificate requested. + +

get_batch_requests

+ +```python +TlsRequires.get_batch_requests() +``` + +Deprecated. Use [server_certs_map][] instead. + +Mapping of server [Certificate][] instances by their `common_name`. + +

request_server_cert

+ +```python +TlsRequires.request_server_cert(cn, sans=None, cert_name=None) +``` + +Request a server certificate and key be generated for the given +common name (`cn`) and optional list of alternative names (`sans`). + +The `cert_name` is deprecated and not needed. + +This can be called multiple times to request more than one server +certificate, although the common names must be unique. If called +again with the same common name, it will be ignored. + +

add_request_server_cert

+ +```python +TlsRequires.add_request_server_cert(cn, sans) +``` + +Deprecated. Use [request_server_cert][] instead. + +

request_server_certs

+ +```python +TlsRequires.request_server_certs() +``` + +Deprecated. Just use [request_server_cert][]; this does nothing. + +

request_client_cert

+ +```python +TlsRequires.request_client_cert(cn, sans) +``` + +Request a client certificate and key be generated for the given +common name (`cn`) and list of alternative names (`sans`). + +This can be called multiple times to request more than one client +certificate, although the common names must be unique. If called +again with the same common name, it will be ignored. + +

request_application_cert

+ +```python +TlsRequires.request_application_cert(cn, sans) +``` + +Request an application certificate and key be generated for the given +common name (`cn`) and list of alternative names (`sans` ) of this +unit and all peer units. All units will share a single certificates. + diff --git a/kubernetes-worker/hooks/relations/tls-certificates/interface.yaml b/kubernetes-worker/hooks/relations/tls-certificates/interface.yaml new file mode 100644 index 0000000..beec53b --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/interface.yaml @@ -0,0 +1,6 @@ +name: tls-certificates +summary: | + A Transport Layer Security (TLS) charm layer that uses requires and provides + to exchange certifcates. +version: 1 +repo: https://github.com/juju-solutions/interface-tls-certificates diff --git a/kubernetes-worker/hooks/relations/tls-certificates/make_docs b/kubernetes-worker/hooks/relations/tls-certificates/make_docs new file mode 100644 index 0000000..2f2274a --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/make_docs @@ -0,0 +1,23 @@ +#!.tox/py3/bin/python + +import sys +import importlib +from pathlib import Path +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'cert': {'interface': 'tls-certificates'}}, + 'provides': {'cert': {'interface': 'tls-certificates'}}, + } + sys.path.append('..') + sys.modules[''] = importlib.import_module(Path.cwd().name) + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-worker/hooks/relations/tls-certificates/provides.py b/kubernetes-worker/hooks/relations/tls-certificates/provides.py new file mode 100644 index 0000000..0262baa --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/provides.py @@ -0,0 +1,301 @@ +if not __package__: + # fix relative imports when building docs + import sys + __package__ = sys.modules[''].__name__ + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag + +from .tls_certificates_common import ( + ApplicationCertificateRequest, + CertificateRequest +) + + +class TlsProvides(Endpoint): + """ + The provider's side of the interface protocol. + + The following flags may be set: + + * `{endpoint_name}.available` + Whenever any clients are joined. + + * `{endpoint_name}.certs.requested` + When there are new certificate requests of any kind to be processed. + The requests can be accessed via [new_requests][]. + + * `{endpoint_name}.server.certs.requested` + When there are new server certificate requests to be processed. + The requests can be accessed via [new_server_requests][]. + + * `{endpoint_name}.client.certs.requested` + When there are new client certificate requests to be processed. + The requests can be accessed via [new_client_requests][]. + + [Certificate]: common.md#tls_certificates_common.Certificate + [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest + [all_requests]: provides.md#provides.TlsProvides.all_requests + [new_requests]: provides.md#provides.TlsProvides.new_requests + [new_server_requests]: provides.md#provides.TlsProvides.new_server_requests + [new_client_requests]: provides.md#provides.TlsProvides.new_client_requests + """ + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + set_flag(self.expand_name('{endpoint_name}.available')) + toggle_flag(self.expand_name('{endpoint_name}.certs.requested'), + self.new_requests) + toggle_flag(self.expand_name('{endpoint_name}.server.certs.requested'), + self.new_server_requests) + toggle_flag(self.expand_name('{endpoint_name}.client.certs.requested'), + self.new_client_requests) + toggle_flag( + self.expand_name('{endpoint_name}.application.certs.requested'), + self.new_application_requests) + # For backwards compatibility, set the old "cert" flags as well + toggle_flag(self.expand_name('{endpoint_name}.server.cert.requested'), + self.new_server_requests) + toggle_flag(self.expand_name('{endpoint_name}.client.cert.requested'), + self.new_client_requests) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + clear_flag(self.expand_name('{endpoint_name}.available')) + clear_flag(self.expand_name('{endpoint_name}.certs.requested')) + clear_flag(self.expand_name('{endpoint_name}.server.certs.requested')) + clear_flag(self.expand_name('{endpoint_name}.client.certs.requested')) + clear_flag( + self.expand_name('{endpoint_name}.application.certs.requested')) + + def set_ca(self, certificate_authority): + """ + Publish the CA to all related applications. + """ + for relation in self.relations: + # All the clients get the same CA, so send it to them. + relation.to_publish_raw['ca'] = certificate_authority + + def set_chain(self, chain): + """ + Publish the chain of trust to all related applications. + """ + for relation in self.relations: + # All the clients get the same chain, so send it to them. + relation.to_publish_raw['chain'] = chain + + def set_client_cert(self, cert, key): + """ + Deprecated. This is only for backwards compatibility. + + Publish a globally shared client cert and key. + """ + for relation in self.relations: + relation.to_publish_raw.update({ + 'client.cert': cert, + 'client.key': key, + }) + + def set_server_cert(self, scope, cert, key): + """ + Deprecated. Use one of the [new_requests][] collections and + `request.set_cert()` instead. + + Set the server cert and key for the request identified by `scope`. + """ + request = self.get_server_requests()[scope] + request.set_cert(cert, key) + + def set_server_multicerts(self, scope): + """ + Deprecated. Done automatically. + """ + pass + + def add_server_cert(self, scope, cn, cert, key): + ''' + Deprecated. Use `request.set_cert()` instead. + ''' + self.set_server_cert(scope, cert, key) + + def get_server_requests(self): + """ + Deprecated. Use the [new_requests][] or [server_requests][] + collections instead. + + One provider can have many requests to generate server certificates. + Return a map of all server request objects indexed by a unique + identifier. + """ + return {req._key: req for req in self.new_server_requests} + + @property + def all_requests(self): + """ + List of all requests that have been made. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('certs.regen', + 'tls.certs.available') + def regen_all_certs(): + tls = endpoint_from_flag('tls.certs.available') + for request in tls.all_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + requests = [] + for unit in self.all_joined_units: + # handle older single server cert request + if unit.received_raw['common_name']: + requests.append(CertificateRequest( + unit, + 'server', + unit.received_raw['certificate_name'], + unit.received_raw['common_name'], + unit.received['sans'], + )) + + # handle mutli server cert requests + reqs = unit.received['cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(CertificateRequest( + unit, + 'server', + common_name, + common_name, + req['sans'], + )) + + # handle client cert requests + reqs = unit.received['client_cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(CertificateRequest( + unit, + 'client', + common_name, + common_name, + req['sans'], + )) + # handle application cert requests + reqs = unit.received['application_cert_requests'] or {} + for common_name, req in reqs.items(): + requests.append(ApplicationCertificateRequest( + unit, + 'application', + common_name, + common_name, + req['sans'] + )) + return requests + + @property + def new_requests(self): + """ + Filtered view of [all_requests][] that only includes requests that + haven't been handled. + + Each will be an instance of [CertificateRequest][]. + + This collection can also be further filtered by request type using + [new_server_requests][] or [new_client_requests][]. + + Example usage: + + ```python + @when('tls.certs.requested') + def gen_certs(): + tls = endpoint_from_flag('tls.certs.requested') + for request in tls.new_requests: + cert, key = generate_cert(request.cert_type, + request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.all_requests if not req.is_handled] + + @property + def new_server_requests(self): + """ + Filtered view of [new_requests][] that only includes server cert + requests. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('tls.server.certs.requested') + def gen_server_certs(): + tls = endpoint_from_flag('tls.server.certs.requested') + for request in tls.new_server_requests: + cert, key = generate_server_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.new_requests if req.cert_type == 'server'] + + @property + def new_client_requests(self): + """ + Filtered view of [new_requests][] that only includes client cert + requests. + + Each will be an instance of [CertificateRequest][]. + + Example usage: + + ```python + @when('tls.client.certs.requested') + def gen_client_certs(): + tls = endpoint_from_flag('tls.client.certs.requested') + for request in tls.new_client_requests: + cert, key = generate_client_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + """ + return [req for req in self.new_requests if req.cert_type == 'client'] + + @property + def new_application_requests(self): + """ + Filtered view of [new_requests][] that only includes application cert + requests. + + Each will be an instance of [ApplicationCertificateRequest][]. + + Example usage: + + ```python + @when('tls.application.certs.requested') + def gen_application_certs(): + tls = endpoint_from_flag('tls.application.certs.requested') + for request in tls.new_application_requests: + cert, key = generate_application_cert(request.common_name, + request.sans) + request.set_cert(cert, key) + ``` + + :returns: List of certificate requests. + :rtype: [CertificateRequest, ] + """ + return [req for req in self.new_requests + if req.cert_type == 'application'] + + @property + def all_published_certs(self): + """ + List of all [Certificate][] instances that this provider has published + for all related applications. + """ + return [req.cert for req in self.all_requests if req.cert] diff --git a/kubernetes-worker/hooks/relations/tls-certificates/pydocmd.yml b/kubernetes-worker/hooks/relations/tls-certificates/pydocmd.yml new file mode 100644 index 0000000..c568913 --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/pydocmd.yml @@ -0,0 +1,19 @@ +site_name: 'TLS Certificates Interface' + +generate: + - requires.md: + - requires + - requires.TlsRequires+ + - provides.md: + - provides + - provides.TlsProvides+ + - common.md: + - tls_certificates_common.CertificateRequest+ + - tls_certificates_common.Certificate+ + +pages: + - Requires: requires.md + - Provides: provides.md + - Common: common.md + +gens_dir: docs diff --git a/kubernetes-worker/hooks/relations/tls-certificates/requires.py b/kubernetes-worker/hooks/relations/tls-certificates/requires.py new file mode 100644 index 0000000..951f953 --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/requires.py @@ -0,0 +1,342 @@ +if not __package__: + # fix relative imports when building docs + import sys + __package__ = sys.modules[''].__name__ + +import uuid + +from charmhelpers.core import hookenv + +from charms.reactive import when, when_not +from charms.reactive import set_flag, clear_flag, toggle_flag +from charms.reactive import Endpoint +from charms.reactive import data_changed + +from .tls_certificates_common import Certificate + + +class TlsRequires(Endpoint): + """ + The client's side of the interface protocol. + + The following flags may be set: + + * `{endpoint_name}.available` + Whenever the relation is joined. + + * `{endpoint_name}.ca.available` + When the root CA information is available via the [root_ca_cert][] and + [root_ca_chain][] properties. + + * `{endpoint_name}.ca.changed` + When the root CA information has changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.certs.available` + When the requested server or client certs are available. + + * `{endpoint_name}.certs.changed` + When the requested server or client certs have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.server.certs.available` + When the server certificates requested by [request_server_cert][] are + available via the [server_certs][] collection. + + * `{endpoint_name}.server.certs.changed` + When the requested server certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + * `{endpoint_name}.client.certs.available` + When the client certificates requested by [request_client_cert][] are + available via the [client_certs][] collection. + + * `{endpoint_name}.client.certs.changed` + When the requested client certificates have changed, whether because + they have just become available or if they were regenerated by the CA. + Once processed this flag should be removed by the charm. + + The following flags have been deprecated: + + * `{endpoint_name}.server.cert.available` + * `{endpoint_name}.client.cert.available` + * `{endpoint_name}.batch.cert.available` + + [Certificate]: common.md#tls_certificates_common.Certificate + [CertificateRequest]: common.md#tls_certificates_common.CertificateRequest + [root_ca_cert]: requires.md#requires.TlsRequires.root_ca_cert + [root_ca_chain]: requires.md#requires.TlsRequires.root_ca_chain + [request_server_cert]: requires.md#requires.TlsRequires.request_server_cert + [request_client_cert]: requires.md#requires.TlsRequires.request_client_cert + [server_certs]: requires.md#requires.TlsRequires.server_certs + [server_certs_map]: requires.md#requires.TlsRequires.server_certs_map + [client_certs]: requires.md#requires.TlsRequires.server_certs + """ + + @when('endpoint.{endpoint_name}.joined') + def joined(self): + self.relations[0].to_publish_raw['unit_name'] = self._unit_name + prefix = self.expand_name('{endpoint_name}.') + ca_available = self.root_ca_cert + ca_changed = ca_available and data_changed(prefix + 'ca', + self.root_ca_cert) + server_available = self.server_certs + server_changed = server_available and data_changed(prefix + 'servers', + self.server_certs) + client_available = self.client_certs + client_changed = client_available and data_changed(prefix + 'clients', + self.client_certs) + certs_available = server_available or client_available + certs_changed = server_changed or client_changed + + set_flag(prefix + 'available') + toggle_flag(prefix + 'ca.available', ca_available) + toggle_flag(prefix + 'ca.changed', ca_changed) + toggle_flag(prefix + 'server.certs.available', server_available) + toggle_flag(prefix + 'server.certs.changed', server_changed) + toggle_flag(prefix + 'client.certs.available', client_available) + toggle_flag(prefix + 'client.certs.changed', client_changed) + toggle_flag(prefix + 'certs.available', certs_available) + toggle_flag(prefix + 'certs.changed', certs_changed) + # deprecated + toggle_flag(prefix + 'server.cert.available', self.server_certs) + toggle_flag(prefix + 'client.cert.available', self.get_client_cert()) + toggle_flag(prefix + 'batch.cert.available', self.server_certs) + + @when_not('endpoint.{endpoint_name}.joined') + def broken(self): + prefix = self.expand_name('{endpoint_name}.') + clear_flag(prefix + 'available') + clear_flag(prefix + 'ca.available') + clear_flag(prefix + 'ca.changed') + clear_flag(prefix + 'server.certs.available') + clear_flag(prefix + 'server.certs.changed') + clear_flag(prefix + 'client.certs.available') + clear_flag(prefix + 'client.certs.changed') + clear_flag(prefix + 'certs.available') + clear_flag(prefix + 'certs.changed') + # deprecated + clear_flag(prefix + 'server.cert.available') + clear_flag(prefix + 'client.cert.available') + clear_flag(prefix + 'batch.cert.available') + + @property + def _unit_name(self): + return hookenv.local_unit().replace('/', '_') + + @property + def root_ca_cert(self): + """ + Root CA certificate. + """ + # only the leader of the provider should set the CA, or all units + # had better agree + return self.all_joined_units.received_raw['ca'] + + def get_ca(self): + """ + Return the root CA certificate. + + Same as [root_ca_cert][]. + """ + return self.root_ca_cert + + @property + def root_ca_chain(self): + """ + The chain of trust for the root CA. + """ + # only the leader of the provider should set the CA, or all units + # had better agree + return self.all_joined_units.received_raw['chain'] + + def get_chain(self): + """ + Return the chain of trust for the root CA. + + Same as [root_ca_chain][]. + """ + return self.root_ca_chain + + def get_client_cert(self): + """ + Deprecated. Use [request_client_cert][] and the [client_certs][] + collection instead. + + Return a globally shared client certificate and key. + """ + data = self.all_joined_units.received_raw + return (data['client.cert'], data['client.key']) + + def get_server_cert(self): + """ + Deprecated. Use the [server_certs][] collection instead. + + Return the cert and key of the first server certificate requested. + """ + if not self.server_certs: + return (None, None) + cert = self.server_certs[0] + return (cert.cert, cert.key) + + @property + def server_certs(self): + """ + List of [Certificate][] instances for all available server certs. + """ + certs = [] + raw_data = self.all_joined_units.received_raw + json_data = self.all_joined_units.received + + # for backwards compatibility, the first cert goes in its own fields + if self.relations: + common_name = self.relations[0].to_publish_raw['common_name'] + cert = raw_data['{}.server.cert'.format(self._unit_name)] + key = raw_data['{}.server.key'.format(self._unit_name)] + if cert and key: + certs.append(Certificate('server', + common_name, + cert, + key)) + + # subsequent requests go in the collection + field = '{}.processed_requests'.format(self._unit_name) + certs_data = json_data[field] or {} + certs.extend(Certificate('server', + common_name, + cert['cert'], + cert['key']) + for common_name, cert in certs_data.items()) + return certs + + @property + def application_certs(self): + """ + List containg the application Certificate cert. + + :returns: A list containing one certificate + :rtype: [Certificate()] + """ + certs = [] + json_data = self.all_joined_units.received + field = '{}.processed_application_requests'.format(self._unit_name) + certs_data = json_data[field] or {} + app_cert_data = certs_data.get('app_data') + if app_cert_data: + certs = [Certificate( + 'server', + 'app_data', + app_cert_data['cert'], + app_cert_data['key'])] + return certs + + @property + def server_certs_map(self): + """ + Mapping of server [Certificate][] instances by their `common_name`. + """ + return {cert.common_name: cert for cert in self.server_certs} + + def get_batch_requests(self): + """ + Deprecated. Use [server_certs_map][] instead. + + Mapping of server [Certificate][] instances by their `common_name`. + """ + return self.server_certs_map + + @property + def client_certs(self): + """ + List of [Certificate][] instances for all available client certs. + """ + field = '{}.processed_client_requests'.format(self._unit_name) + certs_data = self.all_joined_units.received[field] or {} + return [Certificate('client', + common_name, + cert['cert'], + cert['key']) + for common_name, cert in certs_data.items()] + + @property + def client_certs_map(self): + """ + Mapping of client [Certificate][] instances by their `common_name`. + """ + return {cert.common_name: cert for cert in self.client_certs} + + def request_server_cert(self, cn, sans=None, cert_name=None): + """ + Request a server certificate and key be generated for the given + common name (`cn`) and optional list of alternative names (`sans`). + + The `cert_name` is deprecated and not needed. + + This can be called multiple times to request more than one server + certificate, although the common names must be unique. If called + again with the same common name, it will be ignored. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + to_publish_raw = self.relations[0].to_publish_raw + if to_publish_raw['common_name'] in (None, '', cn): + # for backwards compatibility, first request goes in its own fields + to_publish_raw['common_name'] = cn + to_publish_json['sans'] = sans or [] + cert_name = to_publish_raw.get('certificate_name') or cert_name + if cert_name is None: + cert_name = str(uuid.uuid4()) + to_publish_raw['certificate_name'] = cert_name + else: + # subsequent requests go in the collection + requests = to_publish_json.get('cert_requests', {}) + requests[cn] = {'sans': sans or []} + to_publish_json['cert_requests'] = requests + + def add_request_server_cert(self, cn, sans): + """ + Deprecated. Use [request_server_cert][] instead. + """ + self.request_server_cert(cn, sans) + + def request_server_certs(self): + """ + Deprecated. Just use [request_server_cert][]; this does nothing. + """ + pass + + def request_client_cert(self, cn, sans): + """ + Request a client certificate and key be generated for the given + common name (`cn`) and list of alternative names (`sans`). + + This can be called multiple times to request more than one client + certificate, although the common names must be unique. If called + again with the same common name, it will be ignored. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + requests = to_publish_json.get('client_cert_requests', {}) + requests[cn] = {'sans': sans} + to_publish_json['client_cert_requests'] = requests + + def request_application_cert(self, cn, sans): + """ + Request an application certificate and key be generated for the given + common name (`cn`) and list of alternative names (`sans` ) of this + unit and all peer units. All units will share a single certificates. + """ + if not self.relations: + return + # assume we'll only be connected to one provider + to_publish_json = self.relations[0].to_publish + requests = to_publish_json.get('application_cert_requests', {}) + requests[cn] = {'sans': sans} + to_publish_json['application_cert_requests'] = requests diff --git a/kubernetes-worker/hooks/relations/tls-certificates/tls_certificates_common.py b/kubernetes-worker/hooks/relations/tls-certificates/tls_certificates_common.py new file mode 100644 index 0000000..99a2f8c --- /dev/null +++ b/kubernetes-worker/hooks/relations/tls-certificates/tls_certificates_common.py @@ -0,0 +1,302 @@ +from charms.reactive import clear_flag, is_data_changed, data_changed + + +class CertificateRequest(dict): + def __init__(self, unit, cert_type, cert_name, common_name, sans): + self._unit = unit + self._cert_type = cert_type + super().__init__({ + 'certificate_name': cert_name, + 'common_name': common_name, + 'sans': sans, + }) + + @property + def _key(self): + return '.'.join((self._unit.relation.relation_id, + self.unit_name, + self.common_name)) + + def resolve_unit_name(self, unit): + """Return name of unit associated with this request. + + unit_name should be provided in the relation data to ensure + compatability with cross-model relations. If the unit name + is absent then fall back to unit_name attribute of the + unit associated with this request. + + :param unit: Unit to extract name from + :type unit: charms.reactive.endpoints.RelatedUnit + :returns: Name of unit + :rtype: str + """ + unit_name = unit.received_raw['unit_name'] + if not unit_name: + unit_name = unit.unit_name + return unit_name + + @property + def unit_name(self): + """Name of this unit. + + :returns: Name of unit + :rtype: str + """ + return self.resolve_unit_name(unit=self._unit).replace('/', '_') + + @property + def application_name(self): + """Name of the application which the request came from. + + :returns: Name of application + :rtype: str + """ + return self.resolve_unit_name(unit=self._unit).split('/')[0] + + @property + def cert_type(self): + """ + Type of certificate, 'server' or 'client', being requested. + """ + return self._cert_type + + @property + def cert_name(self): + return self['certificate_name'] + + @property + def common_name(self): + return self['common_name'] + + @property + def sans(self): + return self['sans'] + + @property + def _publish_key(self): + if self.cert_type == 'server': + return '{}.processed_requests'.format(self.unit_name) + elif self.cert_type == 'client': + return '{}.processed_client_requests'.format(self.unit_name) + raise ValueError('Unknown cert_type: {}'.format(self.cert_type)) + + @property + def _server_cert_key(self): + return '{}.server.cert'.format(self.unit_name) + + @property + def _server_key_key(self): + return '{}.server.key'.format(self.unit_name) + + @property + def _is_top_level_server_cert(self): + return (self.cert_type == 'server' and + self.common_name == self._unit.received_raw['common_name']) + + @property + def cert(self): + """ + The cert published for this request, if any. + """ + cert, key = None, None + if self._is_top_level_server_cert: + tpr = self._unit.relation.to_publish_raw + cert = tpr[self._server_cert_key] + key = tpr[self._server_key_key] + else: + tp = self._unit.relation.to_publish + certs_data = tp.get(self._publish_key, {}) + cert_data = certs_data.get(self.common_name, {}) + cert = cert_data.get('cert') + key = cert_data.get('key') + if cert and key: + return Certificate(self.cert_type, self.common_name, cert, key) + return None + + @property + def is_handled(self): + has_cert = self.cert is not None + same_sans = not is_data_changed(self._key, + sorted(set(self.sans or []))) + return has_cert and same_sans + + def set_cert(self, cert, key): + rel = self._unit.relation + if self._is_top_level_server_cert: + # backwards compatibility; if this is the cert that was requested + # as a single server cert, set it in the response as the single + # server cert + rel.to_publish_raw.update({ + self._server_cert_key: cert, + self._server_key_key: key, + }) + else: + data = rel.to_publish.get(self._publish_key, {}) + data[self.common_name] = { + 'cert': cert, + 'key': key, + } + rel.to_publish[self._publish_key] = data + if not rel.endpoint.new_server_requests: + clear_flag(rel.endpoint.expand_name('{endpoint_name}.server' + '.cert.requested')) + if not rel.endpoint.new_requests: + clear_flag(rel.endpoint.expand_name('{endpoint_name}.' + 'certs.requested')) + data_changed(self._key, sorted(set(self.sans or []))) + + +class ApplicationCertificateRequest(CertificateRequest): + """ + A request for an application consistent certificate. + + This is a request for a certificate that works for all units of an + application. All sans and cns are added together to produce one + certificate and the same certificate and key are sent to all the + units of an application. Only one ApplicationCertificateRequest + is needed per application. + """ + + @property + def _key(self): + """Key to identify this cert. + + :returns: cert key + :rtype: str + """ + return '{}.{}'.format(self._unit.relation.relation_id, 'app_cert') + + @property + def cert(self): + """ + The cert published for this request, if any. + + :returns: Certificate + :rtype: Certificate or None + """ + cert, key = None, None + tp = self._unit.relation.to_publish + certs_data = tp.get(self._publish_key, {}) + cert_data = certs_data.get('app_data', {}) + cert = cert_data.get('cert') + key = cert_data.get('key') + if cert and key: + return Certificate(self.cert_type, self.common_name, cert, key) + return None + + @property + def is_handled(self): + """Whether the certificate has been handled. + + :returns: If the cert has been handled + :rtype: bool + """ + has_cert = self.cert is not None + same_sans = not is_data_changed(self._key, + sorted(set(self.sans or []))) + return has_cert and same_sans + + @property + def sans(self): + """Generate a list of all sans from all units of application + + Examine all units of the application and compile a list of + all sans. CNs are treated as addition san entries. + + :returns: List of sans + :rtype: List[str] + """ + _sans = [] + for unit in self._unit.relation.units: + reqs = unit.received['application_cert_requests'] or {} + for cn, req in reqs.items(): + _sans.append(cn) + _sans.extend(req['sans']) + return sorted(list(set(_sans))) + + @property + def _request_key(self): + """Key used to request cert + + :returns: Key used to request cert + :rtype: str + """ + return 'application_cert_requests' + + def derive_publish_key(self, unit=None): + """Derive the application cert publish key for a unit. + + :param unit: Unit to extract name from + :type unit: charms.reactive.endpoints.RelatedUnit + :returns: publish key + :rtype: str + """ + if not unit: + unit = self._unit + unit_name = self.resolve_unit_name(unit).replace('/', '_') + return '{}.processed_application_requests'.format(unit_name) + + @property + def _publish_key(self): + """Key used to publish cert + + :returns: Key used to publish cert + :rtype: str + """ + return self.derive_publish_key(unit=self._unit) + + def set_cert(self, cert, key): + """Send the cert and key to all units of the application + + :param cert: TLS Certificate + :type cert: str + :param key: TLS Private Key + :type cert: str + """ + rel = self._unit.relation + for unit in self._unit.relation.units: + pub_key = self.derive_publish_key(unit=unit) + data = rel.to_publish.get( + pub_key, + {}) + data['app_data'] = { + 'cert': cert, + 'key': key, + } + rel.to_publish[pub_key] = data + if not rel.endpoint.new_application_requests: + clear_flag(rel.endpoint.expand_name( + '{endpoint_name}.application.certs.requested')) + data_changed(self._key, sorted(set(self.sans or []))) + + +class Certificate(dict): + """ + Represents a created certificate and key. + + The ``cert_type``, ``common_name``, ``cert``, and ``key`` values can + be accessed either as properties or as the contents of the dict. + """ + def __init__(self, cert_type, common_name, cert, key): + super().__init__({ + 'cert_type': cert_type, + 'common_name': common_name, + 'cert': cert, + 'key': key, + }) + + @property + def cert_type(self): + return self['cert_type'] + + @property + def common_name(self): + return self['common_name'] + + @property + def cert(self): + return self['cert'] + + @property + def key(self): + return self['key'] diff --git a/kubernetes-worker/hooks/relations/vsphere-integration/.gitignore b/kubernetes-worker/hooks/relations/vsphere-integration/.gitignore new file mode 100644 index 0000000..5f9f2c5 --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/.gitignore @@ -0,0 +1,3 @@ +.tox +__pycache__ +*.pyc diff --git a/kubernetes-worker/hooks/relations/vsphere-integration/LICENSE b/kubernetes-worker/hooks/relations/vsphere-integration/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/LICENSE @@ -0,0 +1,202 @@ + + 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/kubernetes-worker/hooks/relations/vsphere-integration/README.md b/kubernetes-worker/hooks/relations/vsphere-integration/README.md new file mode 100644 index 0000000..28ff438 --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/README.md @@ -0,0 +1,28 @@ +# Overview + +This layer encapsulates the `vsphere-integration` interface communication +protocol and provides an API for charms on either side of relations using this +interface. + +## Usage + +In your charm's `layer.yaml`, ensure that `interface:vsphere-integration` is +included in the `includes` section: + +```yaml +includes: ['layer:basic', 'interface:vsphere-integration'] +``` + +And in your charm's `metadata.yaml`, ensure that a relation endpoint is defined +using the `vsphere-integration` interface protocol: + +```yaml +requires: + vsphere: + interface: vsphere-integration +``` + +For documentation on how to use the API for this interface, see: + +* [Requires API documentation](docs/requires.md) +* [Provides API documentation](docs/provides.md) (this will only be used by the vsphere-integrator charm) diff --git a/kubernetes-worker/hooks/relations/vsphere-integration/__init__.py b/kubernetes-worker/hooks/relations/vsphere-integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/hooks/relations/vsphere-integration/copyright b/kubernetes-worker/hooks/relations/vsphere-integration/copyright new file mode 100644 index 0000000..a91bdf1 --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/copyright @@ -0,0 +1,16 @@ +Format: http://dep.debian.net/deps/dep5/ + +Files: * +Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved. +License: Apache License 2.0 + 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/kubernetes-worker/hooks/relations/vsphere-integration/docs/provides.md b/kubernetes-worker/hooks/relations/vsphere-integration/docs/provides.md new file mode 100644 index 0000000..796b7e6 --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/docs/provides.md @@ -0,0 +1,74 @@ +

provides

+ + +This is the provides side of the interface layer, for use only by the +vSphere integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for vSphere integration + features. The vSphere integration charm should then iterate over each + request, perform whatever actions are necessary to satisfy those requests, + and then mark them as complete. + +

VsphereIntegrationProvides

+ +```python +VsphereIntegrationProvides(self, endpoint_name, relation_ids=None) +``` + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag +from charms import layer + +@when('endpoint.vsphere.requests-pending') +def handle_requests(): + vsphere = endpoint_from_flag('endpoint.vsphere.requests-pending') + for request in vsphere.requests: + request.set_credentials(layer.vsphere.get_user_credentials()) + vsphere.mark_completed() +``` + +

requests

+ + +A list of the new or updated `IntegrationRequests` that +have been made. + +

mark_completed

+ +```python +VsphereIntegrationProvides.mark_completed(self) +``` + +Mark all requests as completed and remove the `requests-pending` flag. + +

IntegrationRequest

+ +```python +IntegrationRequest(self, unit) +``` + +A request for integration from a single remote unit. + +

has_credentials

+ + +Whether or not credentials have been set via `set_credentials`. + +

is_changed

+ + +Whether this request has changed since the last time it was +marked completed (if ever). + +

set_credentials

+ +```python +IntegrationRequest.set_credentials(self, vsphere_ip, user, password, datacenter, datastore) +``` + +Set the credentials for this request. diff --git a/kubernetes-worker/hooks/relations/vsphere-integration/docs/requires.md b/kubernetes-worker/hooks/relations/vsphere-integration/docs/requires.md new file mode 100644 index 0000000..0ce10a9 --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/docs/requires.md @@ -0,0 +1,56 @@ +

requires

+ + +This is the requires side of the interface layer, for use in charms that wish +to request integration with vSphere native features. The integration will be +provided by the vSphere integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of vSphere +specific API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific vSphere features. This flag is automatically removed + if the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the vSphere instance on which the charm is + running. This flag is automatically removed if new integration features are + requested. It should not be removed by the charm. + +

VsphereIntegrationRequires

+ +```python +VsphereIntegrationRequires(self, endpoint_name, relation_ids=None) +``` + +Interface to request integration access. + +Note that due to resource limits and permissions granularity, policies are +limited to being applied at the charm level. That means that, if any +permissions are requested (i.e., any of the enable methods are called), +what is granted will be the sum of those ever requested by any instance of +the charm on this cloud. + +Labels, on the other hand, will be instance specific. + +Example usage: + +```python +from charms.reactive import when, endpoint_from_flag + +@when('endpoint.vsphere.ready') +def vsphere_integration_ready(): + vsphere = endpoint_from_flag('endpoint.vsphere.joined') + update_config_enable_vsphere(vsphere.vsphere_ip, + vsphere.user, + vsphere.password, + vsphere.datacenter, + vsphere.datastore) +``` + +

is_ready

+ + +Whether or not the request for this instance has been completed. diff --git a/kubernetes-worker/hooks/relations/vsphere-integration/interface.yaml b/kubernetes-worker/hooks/relations/vsphere-integration/interface.yaml new file mode 100644 index 0000000..c4c0c07 --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/interface.yaml @@ -0,0 +1,4 @@ +name: vsphere-integration +summary: Interface for connecting to the VMware vSphere integrator charm. +version: 1 +maintainer: Kevin Monroe diff --git a/kubernetes-worker/hooks/relations/vsphere-integration/make_docs b/kubernetes-worker/hooks/relations/vsphere-integration/make_docs new file mode 100644 index 0000000..04cf35b --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/make_docs @@ -0,0 +1,20 @@ +#!.tox/py3/bin/python + +import sys +from shutil import rmtree +from unittest.mock import patch + +import pydocmd.__main__ + + +with patch('charmhelpers.core.hookenv.metadata') as metadata: + metadata.return_value = { + 'requires': {'vsphere': {'interface': 'vsphere'}}, + 'provides': {'vsphere': {'interface': 'vsphere'}}, + } + sys.path.insert(0, '.') + print(sys.argv) + if len(sys.argv) == 1: + sys.argv.extend(['build']) + pydocmd.__main__.main() + rmtree('_build') diff --git a/kubernetes-worker/hooks/relations/vsphere-integration/provides.py b/kubernetes-worker/hooks/relations/vsphere-integration/provides.py new file mode 100644 index 0000000..c3db1d8 --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/provides.py @@ -0,0 +1,132 @@ +""" +This is the provides side of the interface layer, for use only by the +vSphere integration charm itself. + +The flags that are set by the provides side of this interface are: + +* **`endpoint.{endpoint_name}.requested`** This flag is set when there is + a new or updated request by a remote unit for vSphere integration + features. The vSphere integration charm should then iterate over each + request, perform whatever actions are necessary to satisfy those requests, + and then mark them as complete. +""" + +from operator import attrgetter + +from charms.reactive import Endpoint +from charms.reactive import when +from charms.reactive import toggle_flag, clear_flag + + +class VsphereIntegrationProvides(Endpoint): + """ + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + from charms import layer + + @when('endpoint.vsphere.requests-pending') + def handle_requests(): + vsphere = endpoint_from_flag('endpoint.vsphere.requests-pending') + for request in vsphere.requests: + request.set_credentials(layer.vsphere.get_vsphere_credentials()) + request.set_config(layer.vsphere.get_vsphere_config()) + vsphere.mark_completed() + ``` + """ + + @when('endpoint.{endpoint_name}.changed') + def check_requests(self): + toggle_flag(self.expand_name('requests-pending'), + len(self.new_requests) > 0) + clear_flag(self.expand_name('changed')) + + @property + def all_requests(self): + """ + A list of all the #IntegrationRequests that have been made. + """ + return [IntegrationRequest(unit) for unit in self.all_joined_units] + + @property + def new_requests(self): + """ + A list of the new or updated #IntegrationRequests that have been made. + """ + is_changed = attrgetter('is_changed') + return list(filter(is_changed, self.all_requests)) + + def mark_completed(self): + """ + Remove the `requests-pending` flag. + """ + clear_flag(self.expand_name('requests-pending')) + + +class IntegrationRequest: + """ + A request for integration from a single remote unit. + """ + def __init__(self, unit): + self._unit = unit + + @property + def _to_publish(self): + return self._unit.relation.to_publish + + @property + def has_credentials(self): + """ + Whether or not `set_credentials` has been called. + """ + return {'vsphere_ip', 'user', + 'password', 'datacenter'}.issubset(self._to_publish) + + @property + def has_config(self): + """ + Whether or not `set_config` has been called. + """ + return {'datastore', 'folder', + 'respool_path'}.issubset(self._to_publish) + + @property + def is_changed(self): + """ + Whether this request has changed since the last time it was + marked completed (if ever). + """ + return not (self.has_credentials and self.has_config) + + @property + def unit_name(self): + return self._unit.unit_name + + def set_credentials(self, + vsphere_ip, + user, + password, + datacenter): + """ + Set the vsphere credentials for this request. + """ + self._to_publish.update({ + 'vsphere_ip': vsphere_ip, + 'user': user, + 'password': password, + 'datacenter': datacenter, + }) + + def set_config(self, + datastore, + folder, + respool_path): + """ + Set the non-credential vsphere config for this request. + """ + self._to_publish.update({ + 'datastore': datastore, + 'folder': folder, + 'respool_path': respool_path, + }) diff --git a/kubernetes-worker/hooks/relations/vsphere-integration/pydocmd.yml b/kubernetes-worker/hooks/relations/vsphere-integration/pydocmd.yml new file mode 100644 index 0000000..e1d5d4a --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/pydocmd.yml @@ -0,0 +1,16 @@ +site_name: 'VMware vSphere Integration Interface' + +generate: + - requires.md: + - requires + - requires.VsphereIntegrationRequires+ + - provides.md: + - provides + - provides.VsphereIntegrationProvides+ + - provides.IntegrationRequest+ + +pages: + - Requires: requires.md + - Provides: provides.md + +gens_dir: docs diff --git a/kubernetes-worker/hooks/relations/vsphere-integration/requires.py b/kubernetes-worker/hooks/relations/vsphere-integration/requires.py new file mode 100644 index 0000000..d8b9cdb --- /dev/null +++ b/kubernetes-worker/hooks/relations/vsphere-integration/requires.py @@ -0,0 +1,141 @@ +""" +This is the requires side of the interface layer, for use in charms that wish +to request integration with vSphere native features. The integration will be +provided by the vSphere integration charm, which allows the requiring charm +to not require cloud credentials itself and not have a lot of vSphere +specific API code. + +The flags that are set by the requires side of this interface are: + +* **`endpoint.{endpoint_name}.joined`** This flag is set when the relation + has been joined, and the charm should then use the methods documented below + to request specific vSphere features. This flag is automatically removed + if the relation is broken. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready`** This flag is set once the requested + features have been enabled for the vSphere instance on which the charm is + running. This flag is automatically removed if new integration features are + requested. It should not be removed by the charm. + +* **`endpoint.{endpoint_name}.ready.changed`** This flag is set if the data + changes after the ready flag was set. This flag should be removed by the + charm once handled. +""" + + +from charms.reactive import Endpoint +from charms.reactive import when, when_not +from charms.reactive import clear_flag, is_flag_set, set_flag, toggle_flag +from charms.reactive import data_changed + + +class VsphereIntegrationRequires(Endpoint): + """ + Interface to request integration access. + + Note that due to resource limits and permissions granularity, policies are + limited to being applied at the charm level. That means that, if any + permissions are requested (i.e., any of the enable methods are called), + what is granted will be the sum of those ever requested by any instance of + the charm on this cloud. + + Labels, on the other hand, will be instance specific. + + Example usage: + + ```python + from charms.reactive import when, endpoint_from_flag + + @when('endpoint.vsphere.ready') + def vsphere_integration_ready(): + vsphere = endpoint_from_flag('endpoint.vsphere.joined') + update_config_enable_vsphere(vsphere.vsphere_ip, + vsphere.user, + vsphere.password, + vsphere.datacenter, + vsphere.datastore, + vsphere.folder, + vsphere.respool_path) + ``` + """ + + @property + def _received(self): + """ + Helper to streamline access to received data. + """ + return self.all_joined_units.received + + @when('endpoint.{endpoint_name}.changed') + def check_ready(self): + """ + Manage flags to signal when the endpoint is ready as well as noting + if changes have been made since it became ready. + """ + was_ready = is_flag_set(self.expand_name('ready')) + toggle_flag(self.expand_name('ready'), self.is_ready) + if self.is_ready and was_ready and self.is_changed: + set_flag(self.expand_name('ready.changed')) + clear_flag(self.expand_name('changed')) + + @when_not('endpoint.{endpoint_name}.joined') + def remove_ready(self): + clear_flag(self.expand_name('ready')) + + @property + def is_ready(self): + """ + Whether or not the request for this instance has been completed. + """ + return all(field is not None for field in [ + self.vsphere_ip, + self.user, + self.password, + self.datacenter, + self.datastore, + self.folder, + self.respool_path, + ]) + + @property + def is_changed(self): + """ + Whether or not the request for this instance has changed. + """ + return data_changed(self.expand_name('all-data'), [ + self.vsphere_ip, + self.user, + self.password, + self.datacenter, + self.datastore, + self.folder, + self.respool_path, + ]) + + @property + def vsphere_ip(self): + return self._received['vsphere_ip'] + + @property + def user(self): + return self._received['user'] + + @property + def password(self): + return self._received['password'] + + @property + def datacenter(self): + return self._received['datacenter'] + + @property + def datastore(self): + return self._received['datastore'] + + @property + def folder(self): + return self._received['folder'] + + @property + def respool_path(self): + return self._received['respool_path'] diff --git a/kubernetes-worker/hooks/start b/kubernetes-worker/hooks/start new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/start @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/stop b/kubernetes-worker/hooks/stop new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/stop @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/update-status b/kubernetes-worker/hooks/update-status new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/update-status @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/upgrade-charm b/kubernetes-worker/hooks/upgrade-charm new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/upgrade-charm @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/vsphere-relation-broken b/kubernetes-worker/hooks/vsphere-relation-broken new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/vsphere-relation-broken @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/vsphere-relation-changed b/kubernetes-worker/hooks/vsphere-relation-changed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/vsphere-relation-changed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/vsphere-relation-created b/kubernetes-worker/hooks/vsphere-relation-created new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/vsphere-relation-created @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/vsphere-relation-departed b/kubernetes-worker/hooks/vsphere-relation-departed new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/vsphere-relation-departed @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/hooks/vsphere-relation-joined b/kubernetes-worker/hooks/vsphere-relation-joined new file mode 100755 index 0000000..9858c6b --- /dev/null +++ b/kubernetes-worker/hooks/vsphere-relation-joined @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +# Load modules from $JUJU_CHARM_DIR/lib +import sys +sys.path.append('lib') + +from charms.layer import basic # noqa +basic.bootstrap_charm_deps() + +from charmhelpers.core import hookenv # noqa +hookenv.atstart(basic.init_config_states) +hookenv.atexit(basic.clear_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 # noqa +main() diff --git a/kubernetes-worker/icon.svg b/kubernetes-worker/icon.svg new file mode 100644 index 0000000..dca16ee --- /dev/null +++ b/kubernetes-worker/icon.svg @@ -0,0 +1,362 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/kubernetes-worker/layer.yaml b/kubernetes-worker/layer.yaml new file mode 100644 index 0000000..3945048 --- /dev/null +++ b/kubernetes-worker/layer.yaml @@ -0,0 +1,66 @@ +"includes": +- "layer:options" +- "layer:basic" +- "interface:nrpe-external-master" +- "layer:debug" +- "interface:tls-certificates" +- "layer:cis-benchmark" +- "layer:coordinator" +- "layer:kubernetes-common" +- "interface:container-runtime" +- "layer:snap" +- "layer:leadership" +- "layer:metrics" +- "layer:nagios" +- "layer:tls-client" +- "layer:cdk-service-kicker" +- "layer:kubernetes-master-worker-base" +- "interface:http" +- "interface:kubernetes-cni" +- "interface:kube-control" +- "interface:aws-integration" +- "interface:gcp-integration" +- "interface:openstack-integration" +- "interface:vsphere-integration" +- "interface:azure-integration" +- "interface:mount" +"exclude": [".travis.yml", "tests", "tox.ini", "test-requirements.txt", "unit_tests", + ".tox", "__pycache__", "Makefile"] +"options": + "coordinator": + # Absolute path to the charmhelpers.coordinator.BaseCoordinator to use. + "class": "charms.coordinator.SimpleCoordinator" + # Layer log level (debug, info, warning, error, critical) + "log_level": "info" + "basic": + "packages": + - "cifs-utils" + - "ceph-common" + - "nfs-common" + - "socat" + "python_packages": [] + "use_venv": !!bool "true" + "include_system_packages": !!bool "false" + "tls-client": + "ca_certificate_path": "/root/cdk/ca.crt" + "server_certificate_path": "" + "server_key_path": "" + "client_certificate_path": "" + "client_key_path": "" + "cdk-service-kicker": + "services": + - "snap.kubelet.daemon" + - "snap.kube-proxy.daemon" + "debug": {} + "snap": {} + "leadership": {} + "nagios": {} + "cis-benchmark": {} + "kubernetes-common": {} + "kubernetes-master-worker-base": {} + "kubernetes-worker": {} +"repo": "https://github.com/kubernetes/kubernetes.git" +"config": + "deletes": + - "install_from_upstream" +"is": "kubernetes-worker" diff --git a/kubernetes-worker/lib/charms/coordinator.py b/kubernetes-worker/lib/charms/coordinator.py new file mode 100644 index 0000000..b954b92 --- /dev/null +++ b/kubernetes-worker/lib/charms/coordinator.py @@ -0,0 +1,144 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Coordinator Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import importlib + +from charmhelpers.coordinator import BaseCoordinator +from charmhelpers.core import hookenv +from charms import reactive +import charms.layer + + +__all__ = ['coordinator', 'acquire'] + + +def acquire(lock): + """ + Sets either the coordinator.granted.{lockname} or + coordinator.requested.{lockname} state. + + Returns True if the lock could be immediately granted. + + If locks cannot be granted immediately, they will be granted + in a future hook and the coordinator.granted.{lockname} state set. + """ + global coordinator + if coordinator.acquire(lock): + s = 'coordinator.granted.{}'.format(lock) + if not reactive.is_state(s): + log('Granted {} lock'.format(lock), hookenv.DEBUG) + reactive.set_state('coordinator.granted.{}'.format(lock)) + return True + else: + log('Requested {} lock'.format(lock), hookenv.DEBUG) + reactive.set_state('coordinator.requested.{}'.format(lock)) + return False + + +options = charms.layer.options('coordinator') + + +def log(msg, level=hookenv.INFO): + lmap = {hookenv.DEBUG: 1, + hookenv.INFO: 2, + hookenv.WARNING: 3, + hookenv.ERROR: 4, + hookenv.CRITICAL: 5} + if lmap[level] >= lmap[options.get('log_level', 'DEBUG').upper()]: + hookenv.log('Coordinator: {}'.format(msg), level) + + +class SimpleCoordinator(BaseCoordinator): + '''A simple BaseCoordinator that is suitable for almost all cases. + + Only one unit at a time will be granted locks. All requests by that + unit will be granted. So only one unit may run tasks guarded by a lock, + and the lock name is irrelevant. + ''' + def default_grant(self, lock, unit, granted, queue): + '''Grant locks to only one unit at a time, regardless of the lock name. + + This lets us keep separate locks like join and restart, + while ensuring the operations do not occur on different nodes + at the same time. + ''' + existing_grants = {k: v for k, v in self.grants.items() if v} + + # Return True if this unit has already been granted any lock. + if existing_grants.get(unit): + self.msg('Granting {} to {} (existing grants)'.format(lock, unit), + hookenv.INFO) + return True + + # Return False if another unit has been granted any lock. + if existing_grants: + self.msg('Not granting {} to {} (locks held by {})' + ''.format(lock, unit, ','.join(existing_grants.keys())), + hookenv.INFO) + return False + + # Otherwise, return True if the unit is first in the queue for + # this named lock. + if queue[0] == unit: + self.msg('Granting {} to {} (first in queue)' + ''.format(lock, unit), hookenv.INFO) + return True + else: + self.msg('Not granting {} to {} (not first in queue)' + ''.format(lock, unit), hookenv.INFO) + return False + + def msg(self, msg, level=hookenv.DEBUG): + '''Emit a message.''' + log(msg, level) + + def _save_state(self): + # If the leader aquired a lock, and now released it, + # there may be outstanding requests in the queue from other + # units. We need to grant them now, as we have no guarantee + # of another hook running on the leader for some time (until + # update-status). + self.handle() + super(SimpleCoordinator, self)._save_state() + + +def _instantiate(): + default_name = 'charms.coordinator.SimpleCoordinator' + full_name = options.get('class', default_name) + components = full_name.split('.') + module = '.'.join(components[:-1]) + name = components[-1] + + if not module: + module = 'charms.coordinator' + + class_ = getattr(importlib.import_module(module), name) + + assert issubclass(class_, BaseCoordinator), \ + '{} is not a BaseCoordinator subclass'.format(full_name) + + try: + # The Coordinator layer defines its own peer relation, as it + # can't piggy back on an existing peer relation that may not + # exist. + return class_(peer_relation_name='coordinator') + finally: + log('Using {} coordinator'.format(full_name), hookenv.DEBUG) + + +# Instantiate the BaseCoordinator singleton, which installs +# its charmhelpers.core.atstart() hooks. +coordinator = _instantiate() diff --git a/kubernetes-worker/lib/charms/layer/__init__.py b/kubernetes-worker/lib/charms/layer/__init__.py new file mode 100644 index 0000000..a8e0c64 --- /dev/null +++ b/kubernetes-worker/lib/charms/layer/__init__.py @@ -0,0 +1,60 @@ +import sys +from importlib import import_module +from pathlib import Path + + +def import_layer_libs(): + """ + Ensure that all layer libraries are imported. + + This makes it possible to do the following: + + from charms import layer + + layer.foo.do_foo_thing() + + Note: This function must be called after bootstrap. + """ + for module_file in Path('lib/charms/layer').glob('*'): + module_name = module_file.stem + if module_name in ('__init__', 'basic', 'execd') or not ( + module_file.suffix == '.py' or module_file.is_dir() + ): + continue + import_module('charms.layer.{}'.format(module_name)) + + +# Terrible hack to support the old terrible interface. +# Try to get people to call layer.options.get() instead so +# that we can remove this garbage. +# Cribbed from https://stackoverfLow.com/a/48100440/4941864 +class OptionsBackwardsCompatibilityHack(sys.modules[__name__].__class__): + def __call__(self, section=None, layer_file=None): + if layer_file is None: + return self.get(section=section) + else: + return self.get(section=section, + layer_file=Path(layer_file)) + + +def patch_options_interface(): + from charms.layer import options + if sys.version_info.minor >= 5: + options.__class__ = OptionsBackwardsCompatibilityHack + else: + # Py 3.4 doesn't support changing the __class__, so we have to do it + # another way. The last line is needed because we already have a + # reference that doesn't get updated with sys.modules. + name = options.__name__ + hack = OptionsBackwardsCompatibilityHack(name) + hack.get = options.get + sys.modules[name] = hack + sys.modules[__name__].options = hack + + +try: + patch_options_interface() +except ImportError: + # This may fail if pyyaml hasn't been installed yet. But in that + # case, the bootstrap logic will try it again once it has. + pass diff --git a/kubernetes-worker/lib/charms/layer/basic.py b/kubernetes-worker/lib/charms/layer/basic.py new file mode 100644 index 0000000..bbdd074 --- /dev/null +++ b/kubernetes-worker/lib/charms/layer/basic.py @@ -0,0 +1,501 @@ +import os +import sys +import re +import shutil +from distutils.version import LooseVersion +from pkg_resources import Requirement +from glob import glob +from subprocess import check_call, check_output, CalledProcessError +from time import sleep + +from charms import layer +from charms.layer.execd import execd_preinstall + + +def _get_subprocess_env(): + env = os.environ.copy() + env['LANG'] = env.get('LANG', 'C.UTF-8') + return env + + +def get_series(): + """ + Return series for a few known OS:es. + Tested as of 2019 november: + * centos6, centos7, rhel6. + * bionic + """ + series = "" + + # Looking for content in /etc/os-release + # works for ubuntu + some centos + if os.path.isfile('/etc/os-release'): + d = {} + with open('/etc/os-release', 'r') as rel: + for l in rel: + if not re.match(r'^\s*$', l): + k, v = l.split('=') + d[k.strip()] = v.strip().replace('"', '') + series = "{ID}{VERSION_ID}".format(**d) + + # Looking for content in /etc/redhat-release + # works for redhat enterprise systems + elif os.path.isfile('/etc/redhat-release'): + with open('/etc/redhat-release', 'r') as redhatlsb: + # CentOS Linux release 7.7.1908 (Core) + line = redhatlsb.readline() + release = int(line.split("release")[1].split()[0][0]) + series = "centos" + str(release) + + # Looking for content in /etc/lsb-release + # works for ubuntu + elif os.path.isfile('/etc/lsb-release'): + d = {} + with open('/etc/lsb-release', 'r') as lsb: + for l in lsb: + k, v = l.split('=') + d[k.strip()] = v.strip() + series = d['DISTRIB_CODENAME'] + + # This is what happens if we cant figure out the OS. + else: + series = "unknown" + return series + + +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 + + series = get_series() + + # OMG?! is build-essentials needed? + ubuntu_packages = ['python3-pip', + 'python3-setuptools', + 'python3-yaml', + 'python3-dev', + 'python3-wheel', + 'build-essential'] + + # I'm not going to "yum group info "Development Tools" + # omitting above madness + centos_packages = ['python3-pip', + 'python3-setuptools', + 'python3-devel', + 'python3-wheel'] + + packages_needed = [] + if 'centos' in series: + packages_needed = centos_packages + else: + packages_needed = ubuntu_packages + + 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') + hook_name = os.path.basename(sys.argv[0]) + is_bootstrapped = os.path.exists('wheelhouse/.bootstrapped') + is_charm_upgrade = hook_name == 'upgrade-charm' + is_series_upgrade = hook_name == 'post-series-upgrade' + is_post_upgrade = os.path.exists('wheelhouse/.upgraded') + is_upgrade = (not is_post_upgrade and + (is_charm_upgrade or is_series_upgrade)) + if is_bootstrapped and not is_upgrade: + # older subordinates might have downgraded charm-env, so we should + # restore it if necessary + install_or_update_charm_env() + activate_venv() + # the .upgrade file prevents us from getting stuck in a loop + # when re-execing to activate the venv; at this point, we've + # activated the venv, so it's safe to clear it + if is_post_upgrade: + os.unlink('wheelhouse/.upgraded') + return + if os.path.exists(venv): + try: + # focal installs or upgrades prior to PR 160 could leave the venv + # in a broken state which would prevent subsequent charm upgrades + _load_installed_versions(vpip) + except CalledProcessError: + is_broken_venv = True + else: + is_broken_venv = False + if is_upgrade or is_broken_venv: + # All upgrades should do a full clear of the venv, rather than + # just updating it, to bring in updates to Python itself + shutil.rmtree(venv) + if is_upgrade: + if os.path.exists('wheelhouse/.bootstrapped'): + os.unlink('wheelhouse/.bootstrapped') + # bootstrap wheelhouse + if os.path.exists('wheelhouse'): + pre_eoan = series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty', + 'ubuntu16.04', 'xenial', + 'ubuntu18.04', 'bionic') + pydistutils_lines = [ + "[easy_install]\n", + "find_links = file://{}/wheelhouse/\n".format(charm_dir), + "no_index=True\n", + "index_url=\n", # deliberately nothing here; disables it. + ] + if pre_eoan: + pydistutils_lines.append("allow_hosts = ''\n") + 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(pydistutils_lines) + if 'centos' in series: + yum_install(packages_needed) + else: + apt_install(packages_needed) + from charms.layer import options + cfg = options.get('basic') + # include packages defined in layer.yaml + if 'centos' in series: + yum_install(cfg.get('packages', [])) + else: + 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 = get_series() + if series in ('ubuntu12.04', 'precise', + 'ubuntu14.04', 'trusty'): + apt_install(['python-virtualenv']) + elif 'centos' in series: + yum_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, env=_get_subprocess_env()) + 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') + pre_install_pkgs = ['pip', 'setuptools', 'setuptools-scm'] + # we bundle these packages to work around bugs in older versions (such + # as https://github.com/pypa/pip/issues/56), but if the system already + # provided a newer version, downgrading it can cause other problems + _update_if_newer(pip, pre_install_pkgs) + # install the rest of the wheelhouse deps (extract the pkg names into + # a set so that we can ignore the pre-install packages and let pip + # choose the best version in case there are multiple from layer + # conflicts) + _versions = _load_wheelhouse_versions() + _pkgs = _versions.keys() - set(pre_install_pkgs) + # add back the versions such that each package in pkgs is + # ==. + # This ensures that pip 20.3.4+ will install the packages from the + # wheelhouse without (erroneously) flagging an error. + pkgs = _add_back_versions(_pkgs, _versions) + reinstall_flag = '--force-reinstall' + if not cfg.get('use_venv', True) and pre_eoan: + reinstall_flag = '--ignore-installed' + check_call([pip, 'install', '-U', reinstall_flag, '--no-index', + '--no-cache-dir', '-f', 'wheelhouse'] + list(pkgs), + env=_get_subprocess_env()) + # re-enable installation from pypi + os.remove('/root/.pydistutils.cfg') + + # install pyyaml for centos7, since, unlike the ubuntu image, the + # default image for centos doesn't include pyyaml; see the discussion: + # https://discourse.jujucharms.com/t/charms-for-centos-lets-begin + if 'centos' in series: + check_call([pip, 'install', '-U', 'pyyaml'], + env=_get_subprocess_env()) + + # install python packages from layer options + if cfg.get('python_packages'): + check_call([pip, 'install', '-U'] + cfg.get('python_packages'), + env=_get_subprocess_env()) + 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') + # setup wrappers to ensure envs are used for scripts + install_or_update_charm_env() + for wrapper in ('charms.reactive', 'charms.reactive.sh', + 'chlp', 'layer_option'): + src = os.path.join('/usr/local/sbin', 'charm-env') + dst = os.path.join('/usr/local/sbin', wrapper) + if not os.path.exists(dst): + os.symlink(src, dst) + if cfg.get('use_venv'): + shutil.copy2('bin/layer_option', vbin) + else: + shutil.copy2('bin/layer_option', '/usr/local/bin/') + # re-link the charm copy to the wrapper in case charms + # call bin/layer_option directly (as was the old pattern) + os.remove('bin/layer_option') + os.symlink('/usr/local/sbin/layer_option', 'bin/layer_option') + # flag us as having already bootstrapped so we don't do it again + open('wheelhouse/.bootstrapped', 'w').close() + if is_upgrade: + # flag us as having already upgraded so we don't do it again + open('wheelhouse/.upgraded', '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 _load_installed_versions(pip): + pip_freeze = check_output([pip, 'freeze']).decode('utf8') + versions = {} + for pkg_ver in pip_freeze.splitlines(): + try: + req = Requirement.parse(pkg_ver) + except ValueError: + continue + versions.update({ + req.project_name: LooseVersion(ver) + for op, ver in req.specs if op == '==' + }) + return versions + + +def _load_wheelhouse_versions(): + versions = {} + for wheel in glob('wheelhouse/*'): + pkg, ver = os.path.basename(wheel).rsplit('-', 1) + # nb: LooseVersion ignores the file extension + versions[pkg.replace('_', '-')] = LooseVersion(ver) + return versions + + +def _add_back_versions(pkgs, versions): + """Add back the version strings to each of the packages. + + The versions are LooseVersion() from _load_wheelhouse_versions(). This + function strips the ".zip" or ".tar.gz" from the end of the version string + and adds it back to the package in the form of == + + If a package name is not a key in the versions dictionary, then it is + returned in the list unchanged. + + :param pkgs: A list of package names + :type pkgs: List[str] + :param versions: A map of package to LooseVersion + :type versions: Dict[str, LooseVersion] + :returns: A list of (maybe) versioned packages + :rtype: List[str] + """ + def _strip_ext(s): + """Strip an extension (if it exists) from the string + + :param s: the string to strip an extension off if it exists + :type s: str + :returns: string without an extension of .zip or .tar.gz + :rtype: str + """ + for ending in [".zip", ".tar.gz"]: + if s.endswith(ending): + return s[:-len(ending)] + return s + + def _maybe_add_version(pkg): + """Maybe add back the version number to a package if it exists. + + Adds the version number, if the package exists in the lexically + captured `versions` dictionary, in the form ==. Strips + the extension if it exists. + + :param pkg: the package name to (maybe) add the version number to. + :type pkg: str + """ + try: + return "{}=={}".format(pkg, _strip_ext(str(versions[pkg]))) + except KeyError: + pass + return pkg + + return [_maybe_add_version(pkg) for pkg in pkgs] + + +def _update_if_newer(pip, pkgs): + installed = _load_installed_versions(pip) + wheelhouse = _load_wheelhouse_versions() + for pkg in pkgs: + if pkg not in installed or wheelhouse[pkg] > installed[pkg]: + check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', + pkg], env=_get_subprocess_env()) + + +def install_or_update_charm_env(): + # On Trusty python3-pkg-resources is not installed + try: + from pkg_resources import parse_version + except ImportError: + apt_install(['python3-pkg-resources']) + from pkg_resources import parse_version + + try: + installed_version = parse_version( + check_output(['/usr/local/sbin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + installed_version = parse_version('0.0.0') + try: + bundled_version = parse_version( + check_output(['bin/charm-env', + '--version']).decode('utf8')) + except (CalledProcessError, FileNotFoundError): + bundled_version = parse_version('0.0.0') + if installed_version < bundled_version: + shutil.copy2('bin/charm-env', '/usr/local/sbin/') + + +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. + """ + from charms.layer import options + venv = os.path.abspath('../.venv') + vbin = os.path.join(venv, 'bin') + vpy = os.path.join(vbin, 'python') + use_venv = options.get('basic', 'use_venv') + if use_venv and '.venv' not in sys.executable: + # activate the venv + os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) + reload_interpreter(vpy) + layer.patch_options_interface() + layer.import_layer_libs() + + +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 = _get_subprocess_env() + + 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 + try: + # sometimes apt-get update needs to be run + check_call(['apt-get', 'update'], env=env) + except CalledProcessError: + # sometimes it's a dpkg lock issue + pass + sleep(5) + else: + break + + +def yum_install(packages): + """ Installs packages with yum. + This function largely mimics the apt_install function for consistency. + """ + if packages: + env = os.environ.copy() + cmd = ['yum', '-y', 'install'] + for attempt in range(3): + try: + check_call(cmd + packages, env=env) + except CalledProcessError: + if attempt == 2: + raise + try: + check_call(['yum', 'update'], env=env) + except CalledProcessError: + pass + sleep(5) + else: + break + else: + pass + + +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]) + + +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/kubernetes-worker/lib/charms/layer/execd.py b/kubernetes-worker/lib/charms/layer/execd.py new file mode 100644 index 0000000..438d9a1 --- /dev/null +++ b/kubernetes-worker/lib/charms/layer/execd.py @@ -0,0 +1,114 @@ +# 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 . + +# This module may only import from the Python standard library. +import os +import sys +import subprocess +import time + +''' +execd/preinstall + +Read the layer-basic docs for more info on how to use this feature. +https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#exec-d-support +''' + + +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<>> `get_version('kubelet') + (1, 6, 0) + + """ + cmd = "{} --version".format(bin_name).split() + version_string = subprocess.check_output(cmd).decode("utf-8") + return tuple(int(q) for q in re.findall("[0-9]+", version_string)[:3]) + + +def retry(times, delay_secs): + """Decorator for retrying a method call. + + Args: + times: How many times should we retry before giving up + delay_secs: Delay in secs + + Returns: A callable that would return the last call outcome + """ + + def retry_decorator(func): + """Decorator to wrap the function provided. + + Args: + func: Provided function should return either True od False + + Returns: A callable that would return the last call outcome + + """ + + def _wrapped(*args, **kwargs): + res = func(*args, **kwargs) + attempt = 0 + while not res and attempt < times: + sleep(delay_secs) + res = func(*args, **kwargs) + if res: + break + attempt += 1 + return res + + return _wrapped + + return retry_decorator + + +def calculate_resource_checksum(resource): + """Calculate a checksum for a resource""" + md5 = hashlib.md5() + path = hookenv.resource_get(resource) + if path: + with open(path, "rb") as f: + data = f.read() + md5.update(data) + return md5.hexdigest() + + +def get_resource_checksum_db_key(checksum_prefix, resource): + """Convert a resource name to a resource checksum database key.""" + return checksum_prefix + resource + + +def migrate_resource_checksums(checksum_prefix, snap_resources): + """Migrate resource checksums from the old schema to the new one""" + for resource in snap_resources: + new_key = get_resource_checksum_db_key(checksum_prefix, resource) + if not db.get(new_key): + path = hookenv.resource_get(resource) + if path: + # old key from charms.reactive.helpers.any_file_changed + old_key = "reactive.files_changed." + path + old_checksum = db.get(old_key) + db.set(new_key, old_checksum) + else: + # No resource is attached. Previously, this meant no checksum + # would be calculated and stored. But now we calculate it as if + # it is a 0-byte resource, so let's go ahead and do that. + zero_checksum = hashlib.md5().hexdigest() + db.set(new_key, zero_checksum) + + +def check_resources_for_upgrade_needed(checksum_prefix, snap_resources): + hookenv.status_set("maintenance", "Checking resources") + for resource in snap_resources: + key = get_resource_checksum_db_key(checksum_prefix, resource) + old_checksum = db.get(key) + new_checksum = calculate_resource_checksum(resource) + if new_checksum != old_checksum: + return True + return False + + +def calculate_and_store_resource_checksums(checksum_prefix, snap_resources): + for resource in snap_resources: + key = get_resource_checksum_db_key(checksum_prefix, resource) + checksum = calculate_resource_checksum(resource) + db.set(key, checksum) + + +def get_ingress_address(endpoint_name, ignore_addresses=None): + try: + network_info = hookenv.network_get(endpoint_name) + except NotImplementedError: + network_info = {} + + if not network_info or "ingress-addresses" not in network_info: + # if they don't have ingress-addresses they are running a juju that + # doesn't support spaces, so just return the private address + return hookenv.unit_get("private-address") + + addresses = network_info["ingress-addresses"] + + if ignore_addresses: + hookenv.log("ingress-addresses before filtering: {}".format(addresses)) + iter_filter = filter(lambda item: item not in ignore_addresses, addresses) + addresses = list(iter_filter) + hookenv.log("ingress-addresses after filtering: {}".format(addresses)) + + # Need to prefer non-fan IP addresses due to various issues, e.g. + # https://bugs.launchpad.net/charm-gcp-integrator/+bug/1822997 + # Fan typically likes to use IPs in the 240.0.0.0/4 block, so we'll + # prioritize those last. Not technically correct, but good enough. + try: + sort_key = lambda a: int(a.partition(".")[0]) >= 240 # noqa: E731 + addresses = sorted(addresses, key=sort_key) + except Exception: + hookenv.log(traceback.format_exc()) + + return addresses[0] + + +def get_ingress_address6(endpoint_name): + try: + network_info = hookenv.network_get(endpoint_name) + except NotImplementedError: + network_info = {} + + if not network_info or "ingress-addresses" not in network_info: + return None + + addresses = network_info["ingress-addresses"] + + for addr in addresses: + ip_addr = ipaddress.ip_interface(addr).ip + if ip_addr.version == 6: + return str(ip_addr) + else: + return None + + +def service_restart(service_name): + hookenv.status_set("maintenance", "Restarting {0} service".format(service_name)) + host.service_restart(service_name) + + +def service_start(service_name): + hookenv.log("Starting {0} service.".format(service_name)) + host.service_stop(service_name) + + +def service_stop(service_name): + hookenv.log("Stopping {0} service.".format(service_name)) + host.service_stop(service_name) + + +def arch(): + """Return the package architecture as a string. Raise an exception if the + architecture is not supported by kubernetes.""" + # Get the package architecture for this system. + architecture = check_output(["dpkg", "--print-architecture"]).rstrip() + # Convert the binary result into a string. + architecture = architecture.decode("utf-8") + return architecture + + +def get_service_ip(service, namespace="kube-system", errors_fatal=True): + try: + output = kubectl( + "get", "service", "--namespace", namespace, service, "--output", "json" + ) + except CalledProcessError: + if errors_fatal: + raise + else: + return None + else: + svc = json.loads(output.decode()) + return svc["spec"]["clusterIP"] + + +def kubectl(*args): + """Run a kubectl cli command with a config file. Returns stdout and throws + an error if the command fails.""" + command = ["kubectl", "--kubeconfig=" + kubeclientconfig_path] + list(args) + hookenv.log("Executing {}".format(command)) + return check_output(command) + + +def kubectl_success(*args): + """Runs kubectl with the given args. Returns True if successful, False if + not.""" + try: + kubectl(*args) + return True + except CalledProcessError: + return False + + +def kubectl_manifest(operation, manifest): + """Wrap the kubectl creation command when using filepath resources + :param operation - one of get, create, delete, replace + :param manifest - filepath to the manifest + """ + # Deletions are a special case + if operation == "delete": + # Ensure we immediately remove requested resources with --now + return kubectl_success(operation, "-f", manifest, "--now") + else: + # Guard against an error re-creating the same manifest multiple times + if operation == "create": + # If we already have the definition, its probably safe to assume + # creation was true. + if kubectl_success("get", "-f", manifest): + hookenv.log("Skipping definition for {}".format(manifest)) + return True + # Execute the requested command that did not match any of the special + # cases above + return kubectl_success(operation, "-f", manifest) + + +def get_node_name(): + kubelet_extra_args = parse_extra_args("kubelet-extra-args") + cloud_provider = kubelet_extra_args.get("cloud-provider", "") + if is_state("endpoint.aws.ready"): + cloud_provider = "aws" + elif is_state("endpoint.gcp.ready"): + cloud_provider = "gce" + elif is_state("endpoint.openstack.ready"): + cloud_provider = "openstack" + elif is_state("endpoint.vsphere.ready"): + cloud_provider = "vsphere" + elif is_state("endpoint.azure.ready"): + cloud_provider = "azure" + if cloud_provider == "aws": + return getfqdn().lower() + else: + return gethostname().lower() + + +def create_kubeconfig( + kubeconfig, + server, + ca, + key=None, + certificate=None, + user="ubuntu", + context="juju-context", + cluster="juju-cluster", + password=None, + token=None, + keystone=False, + aws_iam_cluster_id=None, +): + """Create a configuration for Kubernetes based on path using the supplied + arguments for values of the Kubernetes server, CA, key, certificate, user + context and cluster.""" + if not key and not certificate and not password and not token: + raise ValueError("Missing authentication mechanism.") + elif key and not certificate: + raise ValueError("Missing certificate.") + elif not key and certificate: + raise ValueError("Missing key.") + elif token and password: + # token and password are mutually exclusive. Error early if both are + # present. The developer has requested an impossible situation. + # see: kubectl config set-credentials --help + raise ValueError("Token and Password are mutually exclusive.") + + old_kubeconfig = Path(kubeconfig) + new_kubeconfig = Path(str(kubeconfig) + ".new") + + # Create the config file with the address of the master server. + cmd = ( + "kubectl config --kubeconfig={0} set-cluster {1} " + "--server={2} --certificate-authority={3} --embed-certs=true" + ) + check_call(split(cmd.format(new_kubeconfig, cluster, server, ca))) + # Delete old users + cmd = "kubectl config --kubeconfig={0} unset users" + check_call(split(cmd.format(new_kubeconfig))) + # Create the credentials using the client flags. + cmd = "kubectl config --kubeconfig={0} " "set-credentials {1} ".format( + new_kubeconfig, user + ) + + if key and certificate: + cmd = ( + "{0} --client-key={1} --client-certificate={2} " + "--embed-certs=true".format(cmd, key, certificate) + ) + if password: + cmd = "{0} --username={1} --password={2}".format(cmd, user, password) + # This is mutually exclusive from password. They will not work together. + if token: + cmd = "{0} --token={1}".format(cmd, token) + check_call(split(cmd)) + # Create a default context with the cluster. + cmd = "kubectl config --kubeconfig={0} set-context {1} " "--cluster={2} --user={3}" + check_call(split(cmd.format(new_kubeconfig, context, cluster, user))) + # Make the config use this new context. + cmd = "kubectl config --kubeconfig={0} use-context {1}" + check_call(split(cmd.format(new_kubeconfig, context))) + if keystone: + # create keystone user + cmd = "kubectl config --kubeconfig={0} " "set-credentials keystone-user".format( + new_kubeconfig + ) + check_call(split(cmd)) + # create keystone context + cmd = ( + "kubectl config --kubeconfig={0} " + "set-context --cluster={1} " + "--user=keystone-user keystone".format(new_kubeconfig, cluster) + ) + check_call(split(cmd)) + # use keystone context + cmd = "kubectl config --kubeconfig={0} " "use-context keystone".format( + new_kubeconfig + ) + check_call(split(cmd)) + # manually add exec command until kubectl can do it for us + with open(new_kubeconfig, "r") as f: + content = f.read() + content = content.replace( + """- name: keystone-user + user: {}""", + """- name: keystone-user + user: + exec: + command: "/snap/bin/client-keystone-auth" + apiVersion: "client.authentication.k8s.io/v1beta1" +""", + ) + with open(new_kubeconfig, "w") as f: + f.write(content) + if aws_iam_cluster_id: + # create aws-iam context + cmd = ( + "kubectl config --kubeconfig={0} " + "set-context --cluster={1} " + "--user=aws-iam-user aws-iam-authenticator" + ) + check_call(split(cmd.format(new_kubeconfig, cluster))) + + # append a user for aws-iam + cmd = ( + "kubectl --kubeconfig={0} config set-credentials " + "aws-iam-user --exec-command=aws-iam-authenticator " + '--exec-arg="token" --exec-arg="-i" --exec-arg="{1}" ' + '--exec-arg="-r" --exec-arg="<>" ' + "--exec-api-version=client.authentication.k8s.io/v1alpha1" + ) + check_call(split(cmd.format(new_kubeconfig, aws_iam_cluster_id))) + + # not going to use aws-iam context by default since we don't have + # the desired arn. This will make the config not usable if copied. + + # cmd = 'kubectl config --kubeconfig={0} ' \ + # 'use-context aws-iam-authenticator'.format(new_kubeconfig) + # check_call(split(cmd)) + if old_kubeconfig.exists(): + changed = new_kubeconfig.read_text() != old_kubeconfig.read_text() + else: + changed = True + if changed: + new_kubeconfig.rename(old_kubeconfig) + + +def parse_extra_args(config_key): + elements = hookenv.config().get(config_key, "").split() + args = {} + + for element in elements: + if "=" in element: + key, _, value = element.partition("=") + args[key] = value + else: + args[element] = "true" + + return args + + +def configure_kubernetes_service(key, service, base_args, extra_args_key): + db = unitdata.kv() + + prev_args_key = key + service + prev_snap_args = db.get(prev_args_key) or {} + + extra_args = parse_extra_args(extra_args_key) + + args = {} + args.update(base_args) + args.update(extra_args) + + # CIS benchmark action may inject kv config to pass failing tests. Merge + # these after the func args as they should take precedence. + cis_args_key = "cis-" + service + cis_args = db.get(cis_args_key) or {} + args.update(cis_args) + + # Remove any args with 'None' values (all k8s args are 'k=v') and + # construct an arg string for use by 'snap set'. + args = {k: v for k, v in args.items() if v is not None} + args = ['--%s="%s"' % arg for arg in args.items()] + args = " ".join(args) + + snap_opts = {} + for arg in prev_snap_args: + # remove previous args by setting to null + snap_opts[arg] = "null" + snap_opts["args"] = args + snap_opts = ["%s=%s" % opt for opt in snap_opts.items()] + + cmd = ["snap", "set", service] + snap_opts + check_call(cmd) + + # Now that we've started doing snap configuration through the "args" + # option, we should never need to clear previous args again. + db.set(prev_args_key, {}) + + +def _snap_common_path(component): + return Path("/var/snap/{}/common".format(component)) + + +def cloud_config_path(component): + return _snap_common_path(component) / "cloud-config.conf" + + +def _gcp_creds_path(component): + return _snap_common_path(component) / "gcp-creds.json" + + +def _daemon_env_path(component): + return _snap_common_path(component) / "environment" + + +def _cloud_endpoint_ca_path(component): + return _snap_common_path(component) / "cloud-endpoint-ca.crt" + + +def encryption_config_path(): + apiserver_snap_common_path = _snap_common_path("kube-apiserver") + encryption_conf_dir = apiserver_snap_common_path / "encryption" + return encryption_conf_dir / "encryption_config.yaml" + + +def write_gcp_snap_config(component): + # gcp requires additional credentials setup + gcp = endpoint_from_flag("endpoint.gcp.ready") + creds_path = _gcp_creds_path(component) + with creds_path.open("w") as fp: + os.fchmod(fp.fileno(), 0o600) + fp.write(gcp.credentials) + + # create a cloud-config file that sets token-url to nil to make the + # services use the creds env var instead of the metadata server, as + # well as making the cluster multizone + comp_cloud_config_path = cloud_config_path(component) + comp_cloud_config_path.write_text( + "[Global]\n" "token-url = nil\n" "multizone = true\n" + ) + + daemon_env_path = _daemon_env_path(component) + if daemon_env_path.exists(): + daemon_env = daemon_env_path.read_text() + if not daemon_env.endswith("\n"): + daemon_env += "\n" + else: + daemon_env = "" + if gcp_creds_env_key not in daemon_env: + daemon_env += "{}={}\n".format(gcp_creds_env_key, creds_path) + daemon_env_path.parent.mkdir(parents=True, exist_ok=True) + daemon_env_path.write_text(daemon_env) + + +def generate_openstack_cloud_config(): + # openstack requires additional credentials setup + openstack = endpoint_from_flag("endpoint.openstack.ready") + + lines = [ + "[Global]", + "auth-url = {}".format(openstack.auth_url), + "region = {}".format(openstack.region), + "username = {}".format(openstack.username), + "password = {}".format(openstack.password), + "tenant-name = {}".format(openstack.project_name), + "domain-name = {}".format(openstack.user_domain_name), + "tenant-domain-name = {}".format(openstack.project_domain_name), + ] + if openstack.endpoint_tls_ca: + lines.append("ca-file = /etc/config/endpoint-ca.cert") + + lines.extend( + [ + "", + "[LoadBalancer]", + ] + ) + + if openstack.has_octavia in (True, None): + # Newer integrator charm will detect whether underlying OpenStack has + # Octavia enabled so we can set this intelligently. If we're still + # related to an older integrator, though, default to assuming Octavia + # is available. + lines.append("use-octavia = true") + else: + lines.append("use-octavia = false") + lines.append("lb-provider = haproxy") + if openstack.subnet_id: + lines.append("subnet-id = {}".format(openstack.subnet_id)) + if openstack.floating_network_id: + lines.append("floating-network-id = {}".format(openstack.floating_network_id)) + if openstack.lb_method: + lines.append("lb-method = {}".format(openstack.lb_method)) + if openstack.manage_security_groups: + lines.append( + "manage-security-groups = {}".format(openstack.manage_security_groups) + ) + if any( + [openstack.bs_version, openstack.trust_device_path, openstack.ignore_volume_az] + ): + lines.append("") + lines.append("[BlockStorage]") + if openstack.bs_version is not None: + lines.append("bs-version = {}".format(openstack.bs_version)) + if openstack.trust_device_path is not None: + lines.append("trust-device-path = {}".format(openstack.trust_device_path)) + if openstack.ignore_volume_az is not None: + lines.append("ignore-volume-az = {}".format(openstack.ignore_volume_az)) + return "\n".join(lines) + "\n" + + +def write_azure_snap_config(component): + azure = endpoint_from_flag("endpoint.azure.ready") + comp_cloud_config_path = cloud_config_path(component) + comp_cloud_config_path.write_text( + json.dumps( + { + "useInstanceMetadata": True, + "useManagedIdentityExtension": azure.managed_identity, + "subscriptionId": azure.subscription_id, + "resourceGroup": azure.resource_group, + "location": azure.resource_group_location, + "vnetName": azure.vnet_name, + "vnetResourceGroup": azure.vnet_resource_group, + "subnetName": azure.subnet_name, + "securityGroupName": azure.security_group_name, + "loadBalancerSku": "standard", + "securityGroupResourceGroup": azure.security_group_resource_group, + "aadClientId": azure.aad_client_id, + "aadClientSecret": azure.aad_client_secret, + "tenantId": azure.tenant_id, + } + ) + ) + + +def configure_kube_proxy( + configure_prefix, api_servers, cluster_cidr, bind_address=None +): + kube_proxy_opts = {} + kube_proxy_opts["cluster-cidr"] = cluster_cidr + kube_proxy_opts["kubeconfig"] = kubeproxyconfig_path + kube_proxy_opts["logtostderr"] = "true" + kube_proxy_opts["v"] = "0" + num_apis = len(api_servers) + kube_proxy_opts["master"] = api_servers[get_unit_number() % num_apis] + kube_proxy_opts["hostname-override"] = get_node_name() + if bind_address: + kube_proxy_opts["bind-address"] = bind_address + elif is_ipv6(cluster_cidr): + kube_proxy_opts["bind-address"] = "::" + + if host.is_container(): + kube_proxy_opts["conntrack-max-per-core"] = "0" + + if is_dual_stack(cluster_cidr): + kube_proxy_opts["feature-gates"] = "IPv6DualStack=true" + + configure_kubernetes_service( + configure_prefix, "kube-proxy", kube_proxy_opts, "proxy-extra-args" + ) + + +def get_unit_number(): + return int(hookenv.local_unit().split("/")[1]) + + +def cluster_cidr(): + """Return the cluster CIDR provided by the CNI""" + cni = endpoint_from_flag("cni.available") + if not cni: + return None + config = hookenv.config() + if "default-cni" in config: + # master + default_cni = config["default-cni"] + else: + # worker + kube_control = endpoint_from_flag("kube-control.dns.available") + if not kube_control: + return None + default_cni = kube_control.get_default_cni() + return cni.get_config(default=default_cni)["cidr"] + + +def is_dual_stack(cidrs): + """Detect IPv4/IPv6 dual stack from CIDRs""" + return {net.version for net in get_networks(cidrs)} == {4, 6} + + +def is_ipv4(cidrs): + """Detect IPv6 from CIDRs""" + return get_ipv4_network(cidrs) is not None + + +def is_ipv6(cidrs): + """Detect IPv6 from CIDRs""" + return get_ipv6_network(cidrs) is not None + + +def is_ipv6_preferred(cidrs): + """Detect if IPv6 is preffered from CIDRs""" + return get_networks(cidrs)[0].version == 6 + + +def get_networks(cidrs): + """Convert a comma-separated list of CIDRs to a list of networks.""" + if not cidrs: + return [] + return [ipaddress.ip_interface(cidr).network for cidr in cidrs.split(",")] + + +def get_ipv4_network(cidrs): + """Get the IPv4 network from the given CIDRs or None""" + return {net.version: net for net in get_networks(cidrs)}.get(4) + + +def get_ipv6_network(cidrs): + """Get the IPv6 network from the given CIDRs or None""" + return {net.version: net for net in get_networks(cidrs)}.get(6) + + +def enable_ipv6_forwarding(): + """Enable net.ipv6.conf.all.forwarding in sysctl if it is not already.""" + check_call(["sysctl", "net.ipv6.conf.all.forwarding=1"]) + + +def get_bind_addrs(ipv4=True, ipv6=True): + """Get all global-scoped addresses that we might bind to.""" + try: + output = check_output(["ip", "-br", "addr", "show", "scope", "global"]) + except CalledProcessError: + # stderr will have any details, and go to the log + hookenv.log("Unable to determine global addresses", hookenv.ERROR) + return [] + + ignore_interfaces = ("lxdbr", "flannel", "cni", "virbr", "docker") + accept_versions = set() + if ipv4: + accept_versions.add(4) + if ipv6: + accept_versions.add(6) + + addrs = [] + for line in output.decode("utf8").splitlines(): + intf, state, *intf_addrs = line.split() + if state != "UP" or any( + intf.startswith(prefix) for prefix in ignore_interfaces + ): + continue + for addr in intf_addrs: + ip_addr = ipaddress.ip_interface(addr).ip + if ip_addr.version in accept_versions: + addrs.append(str(ip_addr)) + return addrs + + +class InvalidVMwareHost(Exception): + pass + + +def _get_vmware_uuid(): + serial_id_file = "/sys/class/dmi/id/product_serial" + # The serial id from VMWare VMs comes in following format: + # VMware-42 28 13 f5 d4 20 71 61-5d b0 7b 96 44 0c cf 54 + try: + with open(serial_id_file, "r") as f: + serial_string = f.read().strip() + if "VMware-" not in serial_string: + hookenv.log( + "Unable to find VMware ID in " + "product_serial: {}".format(serial_string) + ) + raise InvalidVMwareHost + serial_string = ( + serial_string.split("VMware-")[1].replace(" ", "").replace("-", "") + ) + uuid = "%s-%s-%s-%s-%s" % ( + serial_string[0:8], + serial_string[8:12], + serial_string[12:16], + serial_string[16:20], + serial_string[20:32], + ) + except IOError as err: + hookenv.log("Unable to read UUID from sysfs: {}".format(err)) + uuid = "UNKNOWN" + + return uuid + + +def token_generator(length=32): + """Generate a random token for use in account tokens. + + param: length - the length of the token to generate + """ + alpha = string.ascii_letters + string.digits + token = "".join(random.SystemRandom().choice(alpha) for _ in range(length)) + return token + + +def get_secret_names(): + """Return a dict of 'username: secret_id' for Charmed Kubernetes users.""" + try: + output = kubectl( + "get", + "secrets", + "-n", + AUTH_SECRET_NS, + "--field-selector", + "type={}".format(AUTH_SECRET_TYPE), + "-o", + "json", + ).decode("UTF-8") + except (CalledProcessError, FileNotFoundError): + # The api server may not be up, or we may be trying to run kubelet before + # the snap is installed. Send back an empty dict. + hookenv.log("Unable to get existing secrets", level=hookenv.WARNING) + return {} + + secrets = json.loads(output) + secret_names = {} + if "items" in secrets: + for secret in secrets["items"]: + try: + secret_id = secret["metadata"]["name"] + username_b64 = secret["data"]["username"].encode("UTF-8") + except (KeyError, TypeError): + # CK secrets will have populated 'data', but not all secrets do + continue + secret_names[b64decode(username_b64).decode("UTF-8")] = secret_id + return secret_names + + +def generate_rfc1123(length=10): + """Generate a random string compliant with RFC 1123. + + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names + + param: length - the length of the string to generate + """ + length = 253 if length > 253 else length + valid_chars = string.ascii_lowercase + string.digits + rand_str = "".join(random.SystemRandom().choice(valid_chars) for _ in range(length)) + return rand_str + + +def create_secret(token, username, user, groups=None): + secrets = get_secret_names() + if username in secrets: + # Use existing secret ID if one exists for our username + secret_id = secrets[username] + else: + # secret IDs must be unique and rfc1123 compliant + sani_name = re.sub("[^0-9a-z.-]+", "-", user.lower()) + secret_id = "auth-{}-{}".format(sani_name, generate_rfc1123(10)) + + # The authenticator expects tokens to be in the form user::token + token_delim = "::" + if token_delim not in token: + token = "{}::{}".format(user, token) + + context = { + "type": AUTH_SECRET_TYPE, + "secret_name": secret_id, + "secret_namespace": AUTH_SECRET_NS, + "user": b64encode(user.encode("UTF-8")).decode("utf-8"), + "username": b64encode(username.encode("UTF-8")).decode("utf-8"), + "password": b64encode(token.encode("UTF-8")).decode("utf-8"), + "groups": b64encode(groups.encode("UTF-8")).decode("utf-8") if groups else "", + } + with tempfile.NamedTemporaryFile() as tmp_manifest: + render("cdk.auth-webhook-secret.yaml", tmp_manifest.name, context=context) + + if kubectl_manifest("apply", tmp_manifest.name): + hookenv.log("Created secret for {}".format(username)) + return True + else: + hookenv.log("WARN: Unable to create secret for {}".format(username)) + return False + + +def get_secret_password(username): + """Get the password for the given user from the secret that CK created.""" + try: + output = kubectl( + "get", + "secrets", + "-n", + AUTH_SECRET_NS, + "--field-selector", + "type={}".format(AUTH_SECRET_TYPE), + "-o", + "json", + ).decode("UTF-8") + except CalledProcessError: + # NB: apiserver probably isn't up. This can happen on boostrap or upgrade + # while trying to build kubeconfig files. If we need the 'admin' token during + # this time, pull it directly out of the kubeconfig file if possible. + token = None + if username == "admin": + admin_kubeconfig = Path("/root/.kube/config") + if admin_kubeconfig.exists(): + data = yaml.safe_load(admin_kubeconfig.read_text()) + try: + token = data["users"][0]["user"]["token"] + except (KeyError, IndexError, TypeError): + pass + return token + except FileNotFoundError: + # New deployments may ask for a token before the kubectl snap is installed. + # Give them nothing! + return None + + secrets = json.loads(output) + if "items" in secrets: + for secret in secrets["items"]: + try: + data_b64 = secret["data"] + password_b64 = data_b64["password"].encode("UTF-8") + username_b64 = data_b64["username"].encode("UTF-8") + except (KeyError, TypeError): + # CK authn secrets will have populated 'data', but not all secrets do + continue + + password = b64decode(password_b64).decode("UTF-8") + secret_user = b64decode(username_b64).decode("UTF-8") + if username == secret_user: + return password + return None diff --git a/kubernetes-worker/lib/charms/layer/nagios.py b/kubernetes-worker/lib/charms/layer/nagios.py new file mode 100644 index 0000000..f6ad998 --- /dev/null +++ b/kubernetes-worker/lib/charms/layer/nagios.py @@ -0,0 +1,60 @@ +from pathlib import Path + +NAGIOS_PLUGINS_DIR = '/usr/lib/nagios/plugins' + + +def install_nagios_plugin_from_text(text, plugin_name): + """ Install a nagios plugin. + + Args: + text: Plugin source code (str) + plugin_name: Name of the plugin in nagios + + Returns: Full path to installed plugin + """ + dest_path = Path(NAGIOS_PLUGINS_DIR) / plugin_name + if dest_path.exists(): + # we could complain here, test the files are the same contents, or + # just bail. Idempotency is a big deal in Juju, so I'd like to be + # ok with being called with the same file multiple times, but we + # certainly want to catch the case where multiple layers are using + # the same filename for their nagios checks. + dest = dest_path.read_text() + if dest == text: + # same file + return dest_path + # different file contents! + # maybe someone changed options or something so we need to write + # it again + + dest_path.write_text(text) + dest_path.chmod(0o755) + + return dest_path + + +def install_nagios_plugin_from_file(source_file_path, plugin_name): + """ Install a nagios plugin. + + Args: + source_file_path: Path to plugin source file + plugin_name: Name of the plugin in nagios + + Returns: Full path to installed plugin + """ + + return install_nagios_plugin_from_text(Path(source_file_path).read_text(), + plugin_name) + + +def remove_nagios_plugin(plugin_name): + """ Remove a nagios plugin. + + Args: + plugin_name: Name of the plugin in nagios + + Returns: None + """ + dest_path = Path(NAGIOS_PLUGINS_DIR) / plugin_name + if dest_path.exists(): + dest_path.unlink() diff --git a/kubernetes-worker/lib/charms/layer/options.py b/kubernetes-worker/lib/charms/layer/options.py new file mode 100644 index 0000000..d3f273f --- /dev/null +++ b/kubernetes-worker/lib/charms/layer/options.py @@ -0,0 +1,26 @@ +import os +from pathlib import Path + +import yaml + + +_CHARM_PATH = Path(os.environ.get('JUJU_CHARM_DIR', '.')) +_DEFAULT_FILE = _CHARM_PATH / 'layer.yaml' +_CACHE = {} + + +def get(section=None, option=None, layer_file=_DEFAULT_FILE): + if option and not section: + raise ValueError('Cannot specify option without section') + + layer_file = (_CHARM_PATH / layer_file).resolve() + if layer_file not in _CACHE: + with layer_file.open() as fp: + _CACHE[layer_file] = yaml.safe_load(fp.read()) + + data = _CACHE[layer_file].get('options', {}) + if section: + data = data.get(section, {}) + if option: + data = data.get(option) + return data diff --git a/kubernetes-worker/lib/charms/layer/snap.py b/kubernetes-worker/lib/charms/layer/snap.py new file mode 100644 index 0000000..06cc4b1 --- /dev/null +++ b/kubernetes-worker/lib/charms/layer/snap.py @@ -0,0 +1,466 @@ +# Copyright 2016-2019 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 + +import tenacity +import yaml + +from charmhelpers.core import hookenv +from charms import layer +from charms import reactive +from charms.reactive.helpers import any_file_changed, data_changed +from datetime import datetime, timedelta + + +def get_installed_flag(snapname): + return "snap.installed.{}".format(snapname) + + +def get_refresh_available_flag(snapname): + return "snap.refresh-available.{}".format(snapname) + + +def get_local_flag(snapname): + return "snap.local.{}".format(snapname) + + +def get_disabled_flag(snapname): + return "snap.disabled.{}".format(snapname) + + +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} flag. + + If the snap.installed.{snapname} flag is already set then the refresh() + function is called. + """ + installed_flag = get_installed_flag(snapname) + local_flag = get_local_flag(snapname) + if reactive.is_flag_set(installed_flag): + 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) + reactive.set_flag(local_flag) + else: + _install_store(snapname, **kw) + reactive.set_flag(installed_flag) + + # Installing any snap will first ensure that 'core' is installed. Set an + # appropriate flag for consumers that want to get/set core options. + core_installed = get_installed_flag("core") + if not reactive.is_flag_set(core_installed): + reactive.set_flag(core_installed) + + +def is_installed(snapname): + return reactive.is_flag_set(get_installed_flag(snapname)) + + +def is_local(snapname): + return reactive.is_flag_set(get_local_flag(snapname)) + + +def get_installed_snaps(): + """Return a list of snaps which are installed by this layer.""" + flag_prefix = "snap.installed." + return [flag[len(flag_prefix) :] for flag in reactive.get_flags() if flag.startswith(flag_prefix)] + + +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. + local_flag = get_local_flag(snapname) + if hookenv.has_juju_version("2.0"): + res_path = _resource_get(snapname) + if res_path is False: + _refresh_store(snapname, **kw) + reactive.clear_flag(local_flag) + else: + _install_local(res_path, **kw) + reactive.set_flag(local_flag) + else: + _refresh_store(snapname, **kw) + reactive.clear_flag(local_flag) + + +def remove(snapname): + hookenv.log("Removing snap {}".format(snapname)) + subprocess.check_call(["snap", "remove", snapname]) + reactive.clear_flag(get_installed_flag(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]) + + +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 disable(snapname): + """Disables a snap in the system + + Sets the snap.disabled.{snapname} flag + + This method doesn't affect any snap flag if requested snap does not + exist + """ + hookenv.log("Disabling {} snap".format(snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot disable {} snap because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + + subprocess.check_call(["snap", "disable", snapname]) + reactive.set_flag(get_disabled_flag(snapname)) + + +def enable(snapname): + """Enables a snap in the system + + Clears the snap.disabled.{snapname} flag + + This method doesn't affect any snap flag if requeted snap does not + exist + """ + hookenv.log("Enabling {} snap".format(snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot enable {} snap because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + + subprocess.check_call(["snap", "enable", snapname]) + reactive.clear_flag(get_disabled_flag(snapname)) + + +def restart(snapname): + """Restarts a snap in the system + + This method doesn't affect any snap flag if requested snap does not + exist + """ + hookenv.log("Restarting {} snap".format(snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot restart {} snap because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + + subprocess.check_call(["snap", "restart", snapname]) + + +def set(snapname, key, value): + """Changes configuration options in a snap + + This method will fail if snapname is not an installed snap + """ + hookenv.log("Set config {}={} for snap {}".format(key, value, snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot set {} snap config because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + + subprocess.check_call(["snap", "set", snapname, "{}={}".format(key, value)]) + + +def set_refresh_timer(timer=""): + """Set the system refresh.timer option (snapd 2.31+) + + This method sets how often snapd will refresh installed snaps. Call with + an empty timer string to use the system default (currently 4x per day). + Use 'max' to schedule refreshes as far into the future as possible + (currently 1 month). Also accepts custom timer strings as defined in the + refresh.timer section here: + https://forum.snapcraft.io/t/system-options/87 + + This method does not validate custom strings and will lead to a + CalledProcessError if an invalid string is given. + + :param: timer: empty string (default), 'max', or custom string + """ + if timer == "max": + # A month from yesterday is the farthest we should delay to safely stay + # under the 1 month max. Translate that to a valid refresh.timer value. + # Examples: + # - Today is Friday the 13th, set the refresh timer to + # 'thu2' (Thursday the 12th is the 2nd thursday of the month). + # - Today is Tuesday the 1st, set the refresh timer to + # 'mon5' (Monday the [28..31] is the 5th monday of the month). + yesterday = datetime.now() - timedelta(1) + dow = yesterday.strftime("%a").lower() + # increment after int division because we want occurrence 1-5, not 0-4. + occurrence = yesterday.day // 7 + 1 + timer = "{}{}".format(dow, occurrence) + + # NB: 'system' became synonymous with 'core' in 2.32.5, but we use 'core' + # here to ensure max compatibility. + set(snapname="core", key="refresh.timer", value=timer) + subprocess.check_call(["systemctl", "restart", "snapd.service"]) + + +def get(snapname, key): + """Gets configuration options for a snap + + This method returns the stripped output from the snap get command. + This method will fail if snapname is not an installed snap. + """ + hookenv.log("Get config {} for snap {}".format(key, snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot get {} snap config because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + + return subprocess.check_output(["snap", "get", snapname, key]).strip() + + +def get_installed_version(snapname): + """Gets the installed version of a snapname. + This function will fail if snapname is not an installed snap. + """ + cmd = ["snap", "info", snapname] + hookenv.log("Get installed key for snap {}".format(snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot get {} snap installed version because it is not installed".format(snapname), + hookenv.WARNING, + ) + return + return subprocess.check_output(cmd).decode("utf-8", errors="replace").partition("installed:")[-1].split()[0] + + +def get_installed_channel(snapname): + """Gets the tracking (channel) of a snapname. + This function will fail if snapname is not an installed snap. + """ + cmd = ["snap", "info", snapname] + hookenv.log("Get channel for snap {}".format(snapname)) + if not reactive.is_flag_set(get_installed_flag(snapname)): + hookenv.log( + "Cannot get snap tracking (channel) because it is not installed", + hookenv.WARNING, + ) + return + try: + return subprocess.check_output(cmd).decode("utf-8", errors="replace").partition("tracking:")[-1].split()[0] + except Exception as e: + # If it fails to get the channel information(ex. installed via resource), return nothing. + hookenv.log( + "Cannot get snap tracking (channel): {}".format(e), + hookenv.WARNING, + ) + return + + +def _snap_args( + channel="stable", + devmode=False, + jailmode=False, + dangerous=False, + force_dangerous=False, + connect=None, + classic=False, + revision=None, +): + 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) + + +def _install_store(snapname, **kw): + """Install snap from store + + :param snapname: Name of snap to install + :type snapname: str + :param kw: Keyword arguments to pass on to ``snap install`` + :type kw: Dict[str, str] + :raises: subprocess.CalledProcessError + """ + cmd = ["snap", "install"] + cmd.extend(_snap_args(**kw)) + cmd.append(snapname) + hookenv.log("Installing {} from store".format(snapname)) + + # Use tenacity decorator for Trusty support (See LP Bug #1934163) + @tenacity.retry( + wait=tenacity.wait_fixed(10), # seconds + stop=tenacity.stop_after_attempt(3), + reraise=True, + ) + def _run_install(): + try: + out = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + hookenv.log( + 'Installation successful cmd="{}" output="{}"'.format(cmd, out), + level=hookenv.DEBUG, + ) + reactive.clear_flag(get_local_flag(snapname)) + except subprocess.CalledProcessError as cp: + hookenv.log( + 'Installation failed cmd="{}" returncode={} output="{}"'.format(cmd, cp.returncode, cp.output), + level=hookenv.ERROR, + ) + raise + + _run_install() + + +def _refresh_store(snapname, **kw): + if not data_changed("snap.opts.{}".format(snapname), kw): + return + + # --amend allows us to refresh from a local resource + cmd = ["snap", "refresh", "--amend"] + cmd.extend(_snap_args(**kw)) + cmd.append(snapname) + hookenv.log("Refreshing {} from store".format(snapname)) + out = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + print(out) + + +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 + + +def get_available_refreshes(): + """Return a list of snaps which have refreshes available.""" + try: + out = subprocess.check_output(["snap", "refresh", "--list"]).decode("utf8") + except subprocess.CalledProcessError: + # If snap refresh fails for whatever reason, we should just return no + # refreshes available - LP:1869630. + return [] + + if out == "All snaps up to date.": + return [] + else: + return [line.split()[0] for line in out.splitlines()[1:]] + + +def is_refresh_available(snapname): + """Check whether a new revision is available for the given snap.""" + return reactive.is_flag_set(get_refresh_available_flag(snapname)) + + +def _check_refresh_available(snapname): + return snapname in get_available_refreshes() + + +def create_cohort_snapshot(snapname): + """Create a new cohort key for the given snap. + + Cohort keys represent a snapshot of the revision of a snap at the time + the key was created. These keys can then be used on any machine to lock + the revision of the snap until a new cohort is joined (or the key expires, + after 90 days). This is used to maintain consistency of the revision of + the snap across units or applications, and to manage the refresh of the + snap in a controlled manner. + + Returns a cohort key. + """ + out = subprocess.check_output(["snap", "create-cohort", snapname]) + data = yaml.safe_load(out.decode("utf8")) + return data["cohorts"][snapname]["cohort-key"] + + +def join_cohort_snapshot(snapname, cohort_key): + """Refresh the snap into the given cohort. + + If the snap was previously in a cohort, this will update the revision + to that of the new cohort snapshot. Note that this does not change the + channel that the snap is in, only the revision within that channel. + """ + if is_local(snapname): + # joining a cohort can override a locally installed snap + hookenv.log("Skipping joining cohort for local snap: " "{}".format(snapname)) + return + subprocess.check_output(["snap", "refresh", snapname, "--cohort", cohort_key]) + # even though we just refreshed to the latest in the cohort, it's + # slightly possible that there's a newer rev available beyond the cohort + reactive.toggle_flag(get_refresh_available_flag(snapname), _check_refresh_available(snapname)) diff --git a/kubernetes-worker/lib/charms/layer/tls_client.py b/kubernetes-worker/lib/charms/layer/tls_client.py new file mode 100644 index 0000000..b2980dc --- /dev/null +++ b/kubernetes-worker/lib/charms/layer/tls_client.py @@ -0,0 +1,61 @@ +# Copyright 2016-2017 Canonical Ltd. +# +# This file is part of the tls-client 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. + +from charmhelpers.core.hookenv import log +from charmhelpers.core import unitdata + +from charms.reactive import remove_state +from charms.reactive import endpoint_from_flag + + +def reset_certificate_write_flag(cert_type): + """ + Reset the certificate written flag so notification will work on the next + write cert_type must be 'server', 'client', or 'ca' to indicate type of + certificate + """ + if cert_type not in ['server', 'client', 'ca']: + log('Unknown certificate type!') + else: + remove_state('tls_client.{0}.certificate.written'.format(cert_type)) + + +def request_server_cert(common_name, sans=None, crt_path=None, key_path=None): + tls = endpoint_from_flag('certificates.available') + tls.request_server_cert(common_name, sans) + if not crt_path and not key_path: + return + kv = unitdata.kv() + cert_paths = kv.get('layer.tls-client.cert-paths', {}) + cert_paths.setdefault('server', {})[common_name] = { + 'crt': str(crt_path), + 'key': str(key_path), + } + kv.set('layer.tls-client.cert-paths', cert_paths) + + +def request_client_cert(common_name, sans=None, crt_path=None, key_path=None): + tls = endpoint_from_flag('certificates.available') + tls.request_client_cert(common_name, sans) + if not crt_path and not key_path: + return + kv = unitdata.kv() + cert_paths = kv.get('layer.tls-client.cert-paths', {}) + cert_paths.setdefault('client', {})[common_name] = { + 'crt': str(crt_path), + 'key': str(key_path), + } + kv.set('layer.tls-client.cert-paths', cert_paths) diff --git a/kubernetes-worker/lib/charms/leadership.py b/kubernetes-worker/lib/charms/leadership.py new file mode 100644 index 0000000..d2a95fa --- /dev/null +++ b/kubernetes-worker/lib/charms/leadership.py @@ -0,0 +1,68 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Leadership Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.reactive import not_unless + + +__all__ = ['leader_get', 'leader_set'] + + +@not_unless('leadership.is_leader') +def leader_set(*args, **kw): + '''Change leadership settings, per charmhelpers.core.hookenv.leader_set. + + Settings may either be passed in as a single dictionary, or using + keyword arguments. All values must be strings. + + The leadership.set.{key} reactive state will be set while the + leadership hook environment setting remains set. + + Changed leadership settings will set the leadership.changed.{key} + and leadership.changed states. These states will remain set until + the following hook. + + These state changes take effect immediately on the leader, and + in future hooks run on non-leaders. In this way both leaders and + non-leaders can share handlers, waiting on these states. + ''' + if args: + if len(args) > 1: + raise TypeError('leader_set() takes 1 positional argument but ' + '{} were given'.format(len(args))) + else: + settings = dict(args[0]) + else: + settings = {} + settings.update(kw) + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + + for key, value in settings.items(): + if value != previous.get(key): + reactive.set_state('leadership.changed.{}'.format(key)) + reactive.set_state('leadership.changed') + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + hookenv.leader_set(settings) + unitdata.kv().update(settings, prefix='leadership.settings.') + + +def leader_get(attribute=None): + '''Return leadership settings, per charmhelpers.core.hookenv.leader_get.''' + return hookenv.leader_get(attribute) diff --git a/kubernetes-worker/lib/debug_script.py b/kubernetes-worker/lib/debug_script.py new file mode 100644 index 0000000..e156924 --- /dev/null +++ b/kubernetes-worker/lib/debug_script.py @@ -0,0 +1,8 @@ +import os + +dir = os.environ["DEBUG_SCRIPT_DIR"] + + +def open_file(path, *args, **kwargs): + """ Open a file within the debug script dir """ + return open(os.path.join(dir, path), *args, **kwargs) diff --git a/kubernetes-worker/lxd-profile.yaml b/kubernetes-worker/lxd-profile.yaml new file mode 100644 index 0000000..6b4babc --- /dev/null +++ b/kubernetes-worker/lxd-profile.yaml @@ -0,0 +1,16 @@ +name: juju-default-k8s-deployment-0 +config: + linux.kernel_modules: ip_tables,ip6_tables,netlink_diag,nf_nat,overlay + raw.lxc: | + lxc.apparmor.profile=unconfined + lxc.mount.auto=proc:rw sys:rw + lxc.cgroup.devices.allow=a + lxc.cap.drop= + security.nesting: true + security.privileged: true +description: "" +devices: + aadisable: + path: /dev/kmsg + source: /dev/kmsg + type: unix-char diff --git a/kubernetes-worker/metadata.yaml b/kubernetes-worker/metadata.yaml new file mode 100644 index 0000000..636e59f --- /dev/null +++ b/kubernetes-worker/metadata.yaml @@ -0,0 +1,102 @@ +"name": "kubernetes-worker" +"summary": "The workload bearing units of a kubernetes cluster" +"maintainers": +- "Tim Van Steenburgh " +- "George Kraft " +- "Rye Terrell " +- "Konstantinos Tsakalozos " +- "Charles Butler " +- "Matthew Bruzek " +- "Mike Wilson " +- "Joe Borg " +"description": | + Kubernetes is an open-source platform for deploying, scaling, and operations + of application containers across a cluster of hosts. Kubernetes is portable + in that it works with public, private, and hybrid clouds. Extensible through + a pluggable infrastructure. Self healing in that it will automatically + restart and place containers on healthy nodes if a node ever goes away. +"tags": +- "misc" +"series": +- "focal" +- "bionic" +- "xenial" +"requires": + "certificates": + "interface": "tls-certificates" + "kube-api-endpoint": + # kube-api-endpoint is not recommended as the API endpoints will be provided + # via the kube-control relation. However, it can be used to override those + # endpoints if you need to inject a reverse proxy between the master and workers. + "interface": "http" + "kube-control": + "interface": "kube-control" + "aws": + "interface": "aws-integration" + "gcp": + "interface": "gcp-integration" + "openstack": + "interface": "openstack-integration" + "vsphere": + "interface": "vsphere-integration" + "azure": + "interface": "azure-integration" + "nfs": + "interface": "mount" +"provides": + "nrpe-external-master": + "interface": "nrpe-external-master" + "scope": "container" + "container-runtime": + "interface": "container-runtime" + "scope": "container" + "cni": + "interface": "kubernetes-cni" + "scope": "container" + "ingress-proxy": + "interface": "http" +"peers": + "coordinator": + "interface": "coordinator" +"resources": + "cni-amd64": + "type": "file" + "filename": "cni.tgz" + "description": "CNI plugins for amd64" + "cni-arm64": + "type": "file" + "filename": "cni.tgz" + "description": "CNI plugins for arm64" + "cni-s390x": + "type": "file" + "filename": "cni.tgz" + "description": "CNI plugins for s390x" + "core": + "type": "file" + "filename": "core.snap" + "description": | + core snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. + "kubectl": + "type": "file" + "filename": "kubectl.snap" + "description": | + kubectl snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. + "kubelet": + "type": "file" + "filename": "kubelet.snap" + "description": | + kubelet snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. + "kube-proxy": + "type": "file" + "filename": "kube-proxy.snap" + "description": | + kube-proxy snap + [DEPRECATED] in favor of using a Snap Store Proxy. + See https://ubuntu.com/kubernetes/docs/proxies for more details. +"subordinate": !!bool "false" diff --git a/kubernetes-worker/metrics.yaml b/kubernetes-worker/metrics.yaml new file mode 100644 index 0000000..0fcb3c1 --- /dev/null +++ b/kubernetes-worker/metrics.yaml @@ -0,0 +1,2 @@ +metrics: + juju-units: {} diff --git a/kubernetes-worker/reactive/__init__.py b/kubernetes-worker/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kubernetes-worker/reactive/cdk_service_kicker.py b/kubernetes-worker/reactive/cdk_service_kicker.py new file mode 100644 index 0000000..f7fd33a --- /dev/null +++ b/kubernetes-worker/reactive/cdk_service_kicker.py @@ -0,0 +1,32 @@ +import os +import subprocess +from charms import layer +from charms.reactive import hook, when_not, remove_state, set_state +from charmhelpers.core.templating import render + + +@hook('upgrade-charm') +def upgrade_charm(): + remove_state('cdk-service-kicker.installed') + + +@when_not('cdk-service-kicker.installed') +def install_cdk_service_kicker(): + ''' Installs the cdk-service-kicker service. Workaround for + https://github.com/juju-solutions/bundle-canonical-kubernetes/issues/357 + ''' + source = 'cdk-service-kicker' + dest = '/usr/bin/cdk-service-kicker' + services = layer.options('cdk-service-kicker').get('services') + context = {'services': ' '.join(services)} + render(source, dest, context) + os.chmod('/usr/bin/cdk-service-kicker', 0o775) + + source = 'cdk-service-kicker.service' + dest = '/etc/systemd/system/cdk-service-kicker.service' + context = {} + render(source, dest, context) + command = ['systemctl', 'enable', 'cdk-service-kicker'] + subprocess.check_call(command) + + set_state('cdk-service-kicker.installed') diff --git a/kubernetes-worker/reactive/coordinator.py b/kubernetes-worker/reactive/coordinator.py new file mode 100644 index 0000000..474a95d --- /dev/null +++ b/kubernetes-worker/reactive/coordinator.py @@ -0,0 +1,71 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Coordinator Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charms.coordinator import coordinator, log +import charms.reactive + + +def initialize_coordinator_state(): + ''' + The coordinator.granted.{lockname} state will be set and the + coordinator.requested.{lockname} state removed for every lock + granted to the currently running hook. + + The coordinator.requested.{lockname} state will remain set for locks + not yet granted + ''' + log('Initializing coordinator layer') + + requested = set(coordinator.requests.get(hookenv.local_unit(), {}).keys()) + previously_requested = set(state.split('.', 2)[2] + for state in charms.reactive.bus.get_states() + if state.startswith('coordinator.requested.')) + + granted = set(coordinator.grants.get(hookenv.local_unit(), {}).keys()) + previously_granted = set(state.split('.', 2)[2] + for state in charms.reactive.bus.get_states() + if state.startswith('coordinator.granted.')) + + # Set reactive state for requested locks. + for lock in requested: + log('Requested {} lock'.format(lock), hookenv.DEBUG) + charms.reactive.set_state('coordinator.requested.{}'.format(lock)) + + # Set reactive state for locks that have been granted. + for lock in granted: + log('Granted {} lock'.format(lock), hookenv.DEBUG) + charms.reactive.set_state('coordinator.granted.{}'.format(lock)) + + # Remove reactive state for locks that have been released. + for lock in (previously_granted - granted): + log('Dropped {} lock'.format(lock), hookenv.DEBUG) + charms.reactive.remove_state('coordinator.granted.{}'.format(lock)) + + # Remove requested state for locks no longer requested and not granted. + for lock in (previously_requested - requested - granted): + log('Request for {} lock was dropped'.format(lock), hookenv.DEBUG) + charms.reactive.remove_state('coordinator.requested.{}'.format(lock)) + + +# 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(charms.reactive, '_coordinator_registered'): + hookenv.atstart(initialize_coordinator_state) + charms.reactive._coordinator_registered = True diff --git a/kubernetes-worker/reactive/kubernetes_master_worker_base.py b/kubernetes-worker/reactive/kubernetes_master_worker_base.py new file mode 100644 index 0000000..0bb1de4 --- /dev/null +++ b/kubernetes-worker/reactive/kubernetes_master_worker_base.py @@ -0,0 +1,88 @@ +from charms.layer import snap +from charms.leadership import ( + leader_get, + leader_set +) +from charms.reactive import ( + when, + when_not, + when_any, + data_changed +) + +from charmhelpers.core import hookenv +from charmhelpers.core.host import is_container +from charmhelpers.core.sysctl import create as create_sysctl + + +@when_any('kubernetes-master.snaps.installed', + 'kubernetes-worker.snaps.installed') +@when('snap.refresh.set') +@when('leadership.is_leader') +def process_snapd_timer(): + """ + Set the snapd refresh timer on the leader so all cluster members + (present and future) will refresh near the same time. + + :return: None + """ + # Get the current snapd refresh timer; we know layer-snap has set this + # when the 'snap.refresh.set' flag is present. + timer = snap.get( + snapname='core', key='refresh.timer').decode('utf-8').strip() + if not timer: + # The core snap timer is empty. This likely means a subordinate timer + # reset ours. Try to set it back to a previously leader-set value, + # falling back to config if needed. Luckily, this should only happen + # during subordinate install, so this should remain stable afterward. + timer = leader_get('snapd_refresh') or hookenv.config('snapd_refresh') + snap.set_refresh_timer(timer) + + # Ensure we have the timer known by snapd (it may differ from config). + timer = snap.get( + snapname='core', key='refresh.timer').decode('utf-8').strip() + + # The first time through, data_changed will be true. Subsequent calls + # should only update leader data if something changed. + if data_changed('snapd_refresh', timer): + hookenv.log('setting leader snapd_refresh timer to: {}'.format(timer)) + leader_set({'snapd_refresh': timer}) + + +@when_any('kubernetes-master.snaps.installed', + 'kubernetes-worker.snaps.installed') +@when('snap.refresh.set') +@when('leadership.changed.snapd_refresh') +@when_not('leadership.is_leader') +def set_snapd_timer(): + """ + Set the snapd refresh.timer on non-leader cluster members. + + :return: None + """ + # NB: This method should only be run when 'snap.refresh.set' is present. + # Layer-snap will always set a core refresh.timer, which may not be the + # same as our leader. Gating with 'snap.refresh.set' ensures layer-snap + # has finished and we are free to set our config to the leader's timer. + timer = leader_get('snapd_refresh') or '' # None will error + hookenv.log('setting snapd_refresh timer to: {}'.format(timer)) + snap.set_refresh_timer(timer) + + +@when('config.changed.sysctl') +def write_sysctl(): + """ + :return: None + """ + sysctl_settings = hookenv.config('sysctl') + if sysctl_settings and not is_container(): + create_sysctl( + sysctl_settings, + '/etc/sysctl.d/50-kubernetes-charm.conf', + # Some keys in the config may not exist in /proc/sys/net/. + # For example, the conntrack module may not be loaded when + # using lxd drivers insteam of kvm. In these cases, we + # simply ignore the missing keys, rather than making time + # consuming calls out to the filesystem to check for their + # existence. + ignore=True) diff --git a/kubernetes-worker/reactive/kubernetes_worker.py b/kubernetes-worker/reactive/kubernetes_worker.py new file mode 100644 index 0000000..eaa86f3 --- /dev/null +++ b/kubernetes-worker/reactive/kubernetes_worker.py @@ -0,0 +1,1584 @@ +#!/usr/bin/env python + +# Copyright 2015 The Kubernetes Authors. +# +# 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 re +import shutil +import subprocess +import time +import traceback +import yaml + +from base64 import b64encode +from subprocess import check_call, check_output +from subprocess import CalledProcessError +from socket import gethostname + +import charms.coordinator +from charms import layer +from charms.layer import snap +from charms.reactive import hook +from charms.reactive import endpoint_from_flag +from charms.reactive import endpoint_from_name +from charms.reactive import remove_state, clear_flag +from charms.reactive import set_state, set_flag +from charms.reactive import is_state, is_flag_set, any_flags_set +from charms.reactive import when, when_any, when_not, when_none +from charms.reactive import data_changed, is_data_changed +from charms.templating.jinja2 import render + +from charmhelpers.core import hookenv, unitdata +from charmhelpers.core.host import fstab_add, is_container +from charmhelpers.core.host import service_stop, service_restart +from charmhelpers.core.host import service_pause, service_resume +from charmhelpers.contrib.charmsupport import nrpe + +from charms.layer import kubernetes_common + +from charms.layer.kubernetes_common import kubeclientconfig_path +from charms.layer.kubernetes_common import migrate_resource_checksums +from charms.layer.kubernetes_common import check_resources_for_upgrade_needed +from charms.layer.kubernetes_common import calculate_and_store_resource_checksums # noqa +from charms.layer.kubernetes_common import create_kubeconfig +from charms.layer.kubernetes_common import kubectl +from charms.layer.kubernetes_common import arch, get_node_name +from charms.layer.kubernetes_common import configure_kubernetes_service +from charms.layer.kubernetes_common import parse_extra_args +from charms.layer.kubernetes_common import cloud_config_path +from charms.layer.kubernetes_common import write_gcp_snap_config +from charms.layer.kubernetes_common import write_azure_snap_config +from charms.layer.kubernetes_common import kubeproxyconfig_path +from charms.layer.kubernetes_common import configure_kube_proxy +from charms.layer.kubernetes_common import get_version +from charms.layer.kubernetes_common import ca_crt_path +from charms.layer.kubernetes_common import server_crt_path +from charms.layer.kubernetes_common import server_key_path +from charms.layer.kubernetes_common import client_crt_path +from charms.layer.kubernetes_common import client_key_path +from charms.layer.kubernetes_common import get_unit_number +from charms.layer.kubernetes_common import _get_vmware_uuid + +from charms.layer.nagios import install_nagios_plugin_from_text +from charms.layer.nagios import remove_nagios_plugin + +# Override the default nagios shortname regex to allow periods, which we +# need because our bin names contain them (e.g. 'snap.foo.daemon'). The +# default regex in charmhelpers doesn't allow periods, but nagios itself does. +nrpe.Check.shortname_re = r'[\.A-Za-z0-9-_]+$' +nrpe_kubeconfig_path = '/var/lib/nagios/.kube/config' + +kubeconfig_path = '/root/cdk/kubeconfig' +gcp_creds_env_key = 'GOOGLE_APPLICATION_CREDENTIALS' +snap_resources = ['kubectl', 'kubelet', 'kube-proxy'] +worker_services = ('kubelet', 'kube-proxy') +checksum_prefix = 'kubernetes-worker.resource-checksums.' +configure_prefix = 'kubernetes-worker.prev_args.' +cpu_manager_state = "/var/lib/kubelet/cpu_manager_state" + +cohort_snaps = ['kubectl', 'kubelet', 'kube-proxy'] + +os.environ['PATH'] += os.pathsep + os.path.join(os.sep, 'snap', 'bin') +db = unitdata.kv() + + +@hook('upgrade-charm') +def upgrade_charm(): + # migrate to new flags + if is_state('kubernetes-worker.restarted-for-cloud'): + remove_state('kubernetes-worker.restarted-for-cloud') + set_state('kubernetes-worker.cloud.ready') + if is_state('kubernetes-worker.cloud-request-sent'): + # minor change, just for consistency + remove_state('kubernetes-worker.cloud-request-sent') + set_state('kubernetes-worker.cloud.request-sent') + + set_state('config.changed.install_from_upstream') + hookenv.atexit(remove_state, 'config.changed.install_from_upstream') + + cleanup_pre_snap_services() + migrate_resource_checksums(checksum_prefix, snap_resources) + if check_resources_for_upgrade_needed(checksum_prefix, snap_resources): + set_upgrade_needed() + + # Remove the RC for nginx ingress if it exists + if hookenv.config().get('ingress'): + set_state('kubernetes-worker.remove-old-ingress') + + # Remove gpu.enabled state so we can reconfigure gpu-related kubelet flags, + # since they can differ between k8s versions + if is_state('kubernetes-worker.gpu.enabled'): + remove_state('kubernetes-worker.gpu.enabled') + try: + disable_gpu() + except ApplyNodeLabelFailed: + # Removing node label failed. Probably the master is unavailable. + # Proceed with the upgrade in hope GPUs will still be there. + hookenv.log('Failed to remove GPU labels. Proceed with upgrade.') + + if hookenv.config('ingress'): + set_state('kubernetes-worker.ingress.enabled') + else: + remove_state('kubernetes-worker.ingress.enabled') + + # force certs to be updated + if all(is_state(flag) for flag in ('certificates.available', + 'kube-control.connected', + 'cni.available', + 'kube-control.dns.available')): + send_data() + + if is_state('kubernetes-worker.registry.configured'): + set_state('kubernetes-master-worker-base.registry.configured') + remove_state('kubernetes-worker.registry.configured') + + # need to clear cni.available state if it's no longer accurate + if is_state('cni.available'): + cni = endpoint_from_flag('cni.available') + if not cni.config_available(): + hookenv.log('cni.config_available() is False, clearing' + + ' cni.available flag') + remove_state('cni.available') + + # need to bump the kube-control relation in case + # kube-control.default_cni.available is not set when it should be + if is_state('kube-control.connected'): + kube_control = endpoint_from_flag('kube-control.connected') + kube_control.manage_flags() + + shutil.rmtree('/root/cdk/kubelet/dynamic-config', ignore_errors=True) + + remove_state('kubernetes-worker.cni-plugins.installed') + remove_state('kubernetes-worker.config.created') + remove_state('kubernetes-worker.ingress.available') + remove_state('worker.auth.bootstrapped') + remove_state('nfs.configured') + set_state('kubernetes-worker.restart-needed') + + +@hook('pre-series-upgrade') +def pre_series_upgrade(): + # NB: We use --force here because unmanaged pods are going to die anyway + # when the node is shut down, and it's better to let drain cleanly + # terminate them. We use --delete-local-data because the dashboard, at + # least, uses local data (emptyDir); but local data is documented as being + # ephemeral anyway, so we can assume it should be ok. + kubectl('drain', get_node_name(), '--ignore-daemonsets', '--force', + '--delete-local-data') + service_pause('snap.kubelet.daemon') + service_pause('snap.kube-proxy.daemon') + + +@hook('post-series-upgrade') +def post_series_upgrade(): + service_resume('snap.kubelet.daemon') + service_resume('snap.kube-proxy.daemon') + kubectl('uncordon', get_node_name()) + + +@when('kubernetes-worker.remove-old-ingress') +def remove_old_ingress(): + try: + kubectl('delete', 'rc', 'nginx-ingress-controller', + '--ignore-not-found') + + # these moved into a different namespace for 1.12 + kubectl('delete', 'rc', 'default-http-backend', + '--ignore-not-found') + kubectl('delete', 'svc', 'default-http-backend', + '--ignore-not-found') + kubectl('delete', 'ds', 'nginx-ingress-{}-controller'.format( + hookenv.service_name()), '--ignore-not-found') + kubectl('delete', 'serviceaccount', + 'nginx-ingress-{}-serviceaccount'.format( + hookenv.service_name()), '--ignore-not-found') + kubectl('delete', 'clusterrolebinding', + 'nginx-ingress-clusterrole-nisa-{}-binding'.format( + hookenv.service_name()), '--ignore-not-found') + kubectl('delete', 'configmap', + 'nginx-load-balancer-{}-conf'.format( + hookenv.service_name()), '--ignore-not-found') + except CalledProcessError: + # try again next time + return + + remove_state('kubernetes-worker.remove-old-ingress') + + +def set_upgrade_needed(): + set_state('kubernetes-worker.snaps.upgrade-needed') + config = hookenv.config() + previous_channel = config.previous('channel') + require_manual = config.get('require-manual-upgrade') + if previous_channel is None or not require_manual: + set_state('kubernetes-worker.snaps.upgrade-specified') + + +def cleanup_pre_snap_services(): + # remove old states + remove_state('kubernetes-worker.components.installed') + + # disable old services + services = ['kubelet', 'kube-proxy'] + for service in services: + hookenv.log('Stopping {0} service.'.format(service)) + service_stop(service) + + # cleanup old files + files = [ + "/lib/systemd/system/kubelet.service", + "/lib/systemd/system/kube-proxy.service", + "/etc/default/kube-default", + "/etc/default/kubelet", + "/etc/default/kube-proxy", + "/usr/local/bin/kubectl", + "/usr/local/bin/kubelet", + "/usr/local/bin/kube-proxy", + "/etc/kubernetes" + ] + for file in files: + if os.path.isdir(file): + hookenv.log("Removing directory: " + file) + shutil.rmtree(file) + elif os.path.isfile(file): + hookenv.log("Removing file: " + file) + os.remove(file) + + +@when('config.changed.channel') +def channel_changed(): + set_upgrade_needed() + + +@when('kubernetes-worker.snaps.upgrade-specified') +def install_snaps(): + channel = hookenv.config('channel') + hookenv.status_set('maintenance', 'Installing core snap') + snap.install('core') + hookenv.status_set('maintenance', 'Installing kubectl snap') + snap.install('kubectl', channel=channel, classic=True) + hookenv.status_set('maintenance', 'Installing kubelet snap') + snap.install('kubelet', channel=channel, classic=True) + hookenv.status_set('maintenance', 'Installing kube-proxy snap') + snap.install('kube-proxy', channel=channel, classic=True) + calculate_and_store_resource_checksums(checksum_prefix, snap_resources) + set_state('kubernetes-worker.snaps.installed') + set_state('kubernetes-worker.restart-needed') + remove_state('kubernetes-worker.snaps.upgrade-needed') + remove_state('kubernetes-worker.snaps.upgrade-specified') + + +@when('kubernetes-worker.snaps.installed', + 'kube-control.cohort_keys.available') +@when_none('coordinator.granted.cohort', + 'coordinator.requested.cohort') +def safely_join_cohort(): + '''Coordinate the rollout of snap refreshes. + + When cohort keys change, grab a lock so that only 1 unit in the + application joins the new cohort at a time. This allows us to roll out + snap refreshes without risking all units going down at once. + ''' + kube_control = endpoint_from_flag('kube-control.cohort_keys.available') + + cohort_keys = kube_control.cohort_keys + if is_data_changed('master-cohorts', cohort_keys): + clear_flag('kubernetes-worker.cohorts.joined') + charms.coordinator.acquire('cohort') + + +@when('kubernetes-worker.snaps.installed', + 'kube-control.cohort_keys.available', + 'coordinator.granted.cohort') +@when_not('kubernetes-worker.cohorts.joined') +def join_or_update_cohorts(): + '''Join or update a cohort snapshot. + + All units of this application (leader and followers) need to refresh their + installed snaps to the current cohort snapshot. + ''' + kube_control = endpoint_from_flag('kube-control.cohort_keys.available') + cohort_keys = kube_control.cohort_keys + for snapname in cohort_snaps: + hookenv.status_set('maintenance', 'Joining cohort for {}.'.format(snapname)) + cohort_key = cohort_keys[snapname] + for delay in (5, 30, 60): + try: + snap.join_cohort_snapshot(snapname, cohort_key) + hookenv.log('Joined cohort for {}'.format(snapname)) + break + except subprocess.CalledProcessError: + hookenv.log('Error joining cohort for {}'.format(snapname), + level=hookenv.ERROR) + hookenv.status_set('maintenance', + 'Error joining cohort for {} (see logs), ' + 'will retry.'.format(snapname)) + time.sleep(delay) + else: + set_flag('kubernetes-worker.cohorts.failed') + return + # Update our cache of the cohort keys, now that they're successfully applied. + data_changed('master-cohorts', cohort_keys) + set_flag('kubernetes-worker.cohorts.joined') + clear_flag('kubernetes-worker.cohorts.failed') + + +@when_none('coordinator.granted.cohort', + 'coordinator.requested.cohort') +@when('kubernetes-worker.cohorts.failed') +def reaquire_coordinator_lock(): + # We can't do this in the same hook that the cohort join failed, + # because if we request the lock when we already have it, it's + # treated as a no-op and then dropped at the end of the hook. + charms.coordinator.acquire('cohort') + + +@hook('stop') +def shutdown(): + ''' When this unit is destroyed: + - delete the current node + - stop the worker services + ''' + try: + if os.path.isfile(kubeconfig_path): + kubectl('delete', 'node', get_node_name()) + except CalledProcessError: + hookenv.log('Failed to unregister node.') + service_stop('snap.kubelet.daemon') + service_stop('snap.kube-proxy.daemon') + + +@when('endpoint.container-runtime.available') +@when_not('kubernetes-worker.cni-plugins.installed') +def install_cni_plugins(): + ''' Unpack the cni-plugins resource ''' + # Get the resource via resource_get + try: + resource_name = 'cni-{}'.format(arch()) + archive = hookenv.resource_get(resource_name) + except Exception: + message = 'Error fetching the cni resource.' + hookenv.log(message) + hookenv.status_set('blocked', message) + return + + if not archive: + hookenv.log('Missing cni resource.') + hookenv.status_set('blocked', 'Missing cni resource.') + return + + # Handle null resource publication, we check if filesize < 1mb + filesize = os.stat(archive).st_size + if filesize < 1000000: + hookenv.status_set('blocked', 'Incomplete cni resource.') + return + + hookenv.status_set('maintenance', 'Unpacking cni resource.') + + unpack_path = '/opt/cni/bin' + os.makedirs(unpack_path, exist_ok=True) + cmd = ['tar', 'xfvz', archive, '-C', unpack_path] + hookenv.log(cmd) + check_call(cmd) + + # Used by the "registry" action. The action is run on a single worker, but + # the registry pod can end up on any worker, so we need this directory on + # all the workers. + os.makedirs('/srv/registry', exist_ok=True) + + set_state('kubernetes-worker.cni-plugins.installed') + + +@when('kubernetes-worker.snaps.installed') +def set_app_version(): + ''' Declare the application version to juju ''' + cmd = ['kubelet', '--version'] + version = check_output(cmd) + hookenv.application_version_set(version.split(b' v')[-1].rstrip()) + + +@hookenv.atexit +def charm_status(): + '''Update the status message with the current status of kubelet.''' + container_runtime_connected = \ + is_state('endpoint.container-runtime.joined') + vsphere_joined = is_state('endpoint.vsphere.joined') + azure_joined = is_state('endpoint.azure.joined') + cloud_blocked = is_state('kubernetes-worker.cloud.blocked') + + if is_state('upgrade.series.in-progress'): + hookenv.status_set('blocked', + 'Series upgrade in progress') + return + if not is_flag_set('certificates.available'): + hookenv.status_set('blocked', 'Missing relation to certificate authority.') + return + if not container_runtime_connected: + hookenv.status_set('blocked', + 'Connect a container runtime.') + return + if vsphere_joined and cloud_blocked: + hookenv.status_set('blocked', + 'vSphere integration requires K8s 1.12 or greater') + return + if azure_joined and cloud_blocked: + hookenv.status_set('blocked', + 'Azure integration requires K8s 1.11 or greater') + return + if is_state('kubernetes-worker.cloud.pending'): + hookenv.status_set('waiting', 'Waiting for cloud integration') + return + if is_state('kubernetes-worker.cohorts.failed'): + hookenv.status_set('waiting', + 'Failed to join snap cohorts (see logs), will retry.') + if missing_kube_control(): + # the check calls status_set + return + if not any_flags_set('kube-control.api_endpoints.available', + 'kube-api-endpoint.available'): + hookenv.status_set('waiting', 'Waiting for cluster endpoint.') + return + if not get_kube_api_servers(): + hookenv.status_set('waiting', 'Unable to determine cluster endpoint.') + return + if not is_state('kube-control.auth.available'): + hookenv.status_set('waiting', 'Waiting for cluster credentials.') + return + if not is_state('kube-control.dns.available'): + # During deployment the worker has to start kubelet without cluster dns + # configured. If this is the first unit online in a service pool + # waiting to self host the dns pod, and configure itself to query the + # dns service declared in the kube-system namespace + hookenv.status_set('waiting', 'Waiting for cluster DNS.') + return + if is_state('kubernetes-worker.snaps.upgrade-specified'): + hookenv.status_set('waiting', 'Upgrade pending') + return + if is_state('kubernetes-worker.snaps.upgrade-needed'): + hookenv.status_set('blocked', + 'Needs manual upgrade, run the upgrade action') + return + if is_state('kubernetes-worker.snaps.installed'): + update_kubelet_status() + return + else: + pass # will have been set by snap layer or other handler + + +def deprecated_extra_args(): + '''Returns a list of tuples (config_key, arg) for args that have been set + via extra-args, but are deprecated. + + This works by parsing help output, which can be brittle. Be cautious when + calling this. + ''' + deprecated_args = [] + services = [ + # service config_key + ('kubelet', 'kubelet-extra-args'), + ('kube-proxy', 'proxy-extra-args') + ] + for service, config_key in services: + # Parse help output into a format we can check easily + cmd = [service, '-h'] + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + sections = re.split(r'\n\s*--', output.decode('utf-8'))[1:] + partitioned_sections = [section.partition(' ') for section in sections] + arg_help = {part[0]: part[2] for part in partitioned_sections} + + # Check extra-args against the help output + extra_args = parse_extra_args(config_key) + for arg in extra_args: + if arg not in arg_help: + # This is most likely a problem, though it could also be + # intentional use of a hidden arg. Let's just log a warning. + hookenv.log( + '%s: %s is missing from help output' % (config_key, arg), + level='WARNING' + ) + elif 'DEPRECATED:' in arg_help[arg]: + deprecated_args.append((config_key, arg)) + return deprecated_args + + +def update_kubelet_status(): + ''' There are different states that the kubelet can be in, where we are + waiting for dns, waiting for cluster turnup, or ready to serve + applications.''' + # deprecated_extra_args is brittle, be cautious + deprecated_args = [] + try: + deprecated_args = deprecated_extra_args() + except Exception: + # this isn't vital, log it and move on + traceback.print_exc() + if deprecated_args: + messages = ['%s: %s is deprecated' % arg for arg in deprecated_args] + for message in messages: + hookenv.log(message, level='WARNING') + status = messages[0] + if len(messages) > 1: + other_count = len(messages) - 1 + status += " (+%d others, see juju debug-log)" % other_count + hookenv.status_set('blocked', status) + return + + services = [ + 'kubelet', + 'kube-proxy' + ] + failing_services = [] + for service in services: + daemon = 'snap.{}.daemon'.format(service) + if not _systemctl_is_active(daemon): + failing_services.append(service) + if failing_services: + msg = 'Waiting for {} to start.'.format(','.join(failing_services)) + hookenv.status_set('waiting', msg) + return + + hookenv.status_set('active', 'Kubernetes worker running.') + + +def get_node_ip(): + '''Determines the preferred NodeIP value for this node.''' + cluster_cidr = kubernetes_common.cluster_cidr() + if not cluster_cidr: + return None + if kubernetes_common.is_ipv6_preferred(cluster_cidr): + return kubernetes_common.get_ingress_address6('kube-control') + else: + return kubernetes_common.get_ingress_address('kube-control') + + +@when('certificates.available', 'kube-control.connected', + 'cni.available', 'kube-control.dns.available') +def send_data(): + '''Send the data that is required to create a server certificate for + this server.''' + # Use the public ip of this unit as the Common Name for the certificate. + common_name = hookenv.unit_public_ip() + + ingress_ip = get_node_ip() + bind_addrs = kubernetes_common.get_bind_addrs() + + # Create SANs that the tls layer will add to the server cert. + sans = [ + hookenv.unit_public_ip(), + ingress_ip, + gethostname() + ] + bind_addrs + + # Request a server cert with this information. + layer.tls_client.request_server_cert(common_name, sorted(set(sans)), + crt_path=server_crt_path, + key_path=server_key_path) + + # Request a client cert for kubelet. + layer.tls_client.request_client_cert('system:kubelet', + crt_path=client_crt_path, + key_path=client_key_path) + + +@when('kube-control.dns.available', 'cni.available', + 'endpoint.container-runtime.available') +@when_any('kube-control.api_endpoints.available', + 'kube-api-endpoint.available') +def watch_for_changes(): + ''' Watch for configuration changes and signal if we need to restart the + worker services ''' + kube_control = endpoint_from_flag('kube-control.dns.available') + container_runtime = \ + endpoint_from_flag('endpoint.container-runtime.available') + + servers = get_kube_api_servers() + dns = kube_control.get_dns() + cluster_cidr = kubernetes_common.cluster_cidr() + container_runtime_name = \ + container_runtime.get_runtime() + container_runtime_socket = \ + container_runtime.get_socket() + container_runtime_nvidia = \ + container_runtime.get_nvidia_enabled() + + if container_runtime_nvidia: + set_state('nvidia.ready') + else: + remove_state('nvidia.ready') + + if (data_changed('kube-api-servers', servers) or + data_changed('kube-dns', dns) or + data_changed('cluster-cidr', cluster_cidr) or + data_changed('container-runtime', container_runtime_name) or + data_changed('container-socket', container_runtime_socket)): + set_state('kubernetes-worker.restart-needed') + + +@when('kubernetes-worker.snaps.installed', + 'tls_client.ca.saved', 'tls_client.certs.saved', + 'kube-control.dns.available', 'kube-control.auth.available', + 'cni.available', 'kubernetes-worker.restart-needed', + 'worker.auth.bootstrapped', 'endpoint.container-runtime.available', + 'kube-control.default_cni.available') +@when_not('kubernetes-worker.cloud.pending', + 'kubernetes-worker.cloud.blocked', + 'upgrade.series.in-progress') +@when_any('kube-control.api_endpoints.available', + 'kube-api-endpoint.available') +def start_worker(): + ''' Start kubelet using the provided API and DNS info.''' + # Note that the DNS server doesn't necessarily exist at this point. We know + # what its IP will eventually be, though, so we can go ahead and configure + # kubelet with that info. This ensures that early pods are configured with + # the correct DNS even though the server isn't ready yet. + kube_control = endpoint_from_flag('kube-control.dns.available') + + servers = get_kube_api_servers() + dns = kube_control.get_dns() + ingress_ip = get_node_ip() + cluster_cidr = kubernetes_common.cluster_cidr() + + if cluster_cidr is None: + hookenv.log('Waiting for cluster cidr.') + return + + if not servers: + hookenv.log("Waiting for API server URL") + return + + if kubernetes_common.is_ipv6(cluster_cidr): + kubernetes_common.enable_ipv6_forwarding() + + creds = db.get('credentials') + data_changed('kube-control.creds', creds) + + create_config(servers[get_unit_number() % len(servers)], creds) + configure_default_cni() + configure_kubelet(dns, ingress_ip) + configure_kube_proxy(configure_prefix, servers, + cluster_cidr) + set_state('kubernetes-worker.config.created') + restart_unit_services() + update_kubelet_status() + set_state('kubernetes-worker.label-config-required') + set_state('nrpe-external-master.reconfigure') + remove_state('kubernetes-worker.restart-needed') + + +@when('cni.connected') +@when_not('cni.configured') +def configure_cni(cni): + ''' Set worker configuration on the CNI relation. This lets the CNI + subordinate know that we're the worker so it can respond accordingly. ''' + cni.set_config(is_master=False) + + +@when('config.changed.labels') +def handle_labels_changed(): + set_state('kubernetes-worker.label-config-required') + + +@when('kubernetes-worker.label-config-required', + 'kubernetes-worker.config.created') +def apply_node_labels(): + ''' Parse the labels configuration option and apply the labels to the + node. ''' + # Get the user's configured labels. + config = hookenv.config() + user_labels = {} + for item in config.get('labels').split(' '): + if '=' in item: + key, val = item.split('=') + user_labels[key] = val + else: + hookenv.log('Skipping malformed option: {}.'.format(item)) + # Collect the current label state. + current_labels = db.get('current_labels') or {} + + try: + # Remove any labels that the user has removed from the config. + for key in list(current_labels.keys()): + if key not in user_labels: + remove_label(key) + del current_labels[key] + db.set('current_labels', current_labels) + + # Add any new labels. + for key, val in user_labels.items(): + set_label(key, val) + current_labels[key] = val + db.set('current_labels', current_labels) + + # Set the juju-application label. + set_label('juju-application', hookenv.service_name()) + + # Set the juju.io/cloud label. + if is_state('endpoint.aws.ready'): + set_label('juju.io/cloud', 'ec2') + elif is_state('endpoint.gcp.ready'): + set_label('juju.io/cloud', 'gce') + elif is_state('endpoint.openstack.ready'): + set_label('juju.io/cloud', 'openstack') + elif is_state('endpoint.vsphere.ready'): + set_label('juju.io/cloud', 'vsphere') + elif is_state('endpoint.azure.ready'): + set_label('juju.io/cloud', 'azure') + else: + remove_label('juju.io/cloud') + except ApplyNodeLabelFailed as e: + hookenv.log(str(e)) + return + + # Label configuration complete. + remove_state('kubernetes-worker.label-config-required') + + +@when_any('config.changed.kubelet-extra-args', + 'config.changed.proxy-extra-args', + 'config.changed.kubelet-extra-config') +def config_changed_requires_restart(): + # LP bug #1826833, always delete the state file when extra config changes + # since CPU manager doesn’t support offlining and onlining of CPUs at runtime. + if os.path.isfile(cpu_manager_state): + hookenv.log("Removing file: " + cpu_manager_state) + os.remove(cpu_manager_state) + set_state('kubernetes-worker.restart-needed') + + +@when_any('tls_client.certs.changed', + 'tls_client.ca.written') +def restart_for_certs(): + set_state('kubernetes-worker.restart-needed') + remove_state('tls_client.certs.changed') + remove_state('tls_client.ca.written') + + +def create_config(server, creds): + '''Create a kubernetes configuration for the worker unit.''' + # Create kubernetes configuration in the default location for ubuntu. + create_kubeconfig('/home/ubuntu/.kube/config', server, ca_crt_path, + token=creds['client_token'], user='ubuntu') + # Make the config dir readable by the ubuntu users so juju scp works. + cmd = ['chown', '-R', 'ubuntu:ubuntu', '/home/ubuntu/.kube'] + check_call(cmd) + # Create kubernetes configuration in the default location for root. + create_kubeconfig(kubeclientconfig_path, server, ca_crt_path, + token=creds['client_token'], user='root') + # Create kubernetes configuration for kubelet, and kube-proxy services. + create_kubeconfig(kubeconfig_path, server, ca_crt_path, + token=creds['kubelet_token'], user='kubelet') + create_kubeconfig(kubeproxyconfig_path, server, ca_crt_path, + token=creds['proxy_token'], user='kube-proxy') + cni = endpoint_from_name('cni') + if cni: + cni.notify_kubeconfig_changed() + + +def merge_kubelet_extra_config(config, extra_config): + ''' Updates config to include the contents of extra_config. This is done + recursively to allow deeply nested dictionaries to be merged. + + This is destructive: it modifies the config dict that is passed in. + ''' + for k, extra_config_value in extra_config.items(): + if isinstance(extra_config_value, dict): + config_value = config.setdefault(k, {}) + merge_kubelet_extra_config(config_value, extra_config_value) + else: + config[k] = extra_config_value + + +def configure_kubelet(dns, ingress_ip): + kubelet_opts = {} + kubelet_opts['kubeconfig'] = kubeconfig_path + kubelet_opts['network-plugin'] = 'cni' + kubelet_opts['v'] = '0' + kubelet_opts['logtostderr'] = 'true' + kubelet_opts['node-ip'] = ingress_ip + + container_runtime = \ + endpoint_from_flag('endpoint.container-runtime.available') + + kubelet_opts['container-runtime'] = container_runtime.get_runtime() + if kubelet_opts['container-runtime'] == 'remote': + kubelet_opts['container-runtime-endpoint'] = container_runtime.get_socket() + + kubelet_cloud_config_path = cloud_config_path('kubelet') + if is_state('endpoint.aws.ready'): + kubelet_opts['cloud-provider'] = 'aws' + elif is_state('endpoint.gcp.ready'): + kubelet_opts['cloud-provider'] = 'gce' + kubelet_opts['cloud-config'] = str(kubelet_cloud_config_path) + elif is_state('endpoint.openstack.ready'): + kubelet_opts['cloud-provider'] = 'external' + elif is_state('endpoint.vsphere.joined'): + # vsphere just needs to be joined on the worker (vs 'ready') + kubelet_opts['cloud-provider'] = 'vsphere' + # NB: vsphere maps node product-id to its uuid (no config file needed). + uuid = _get_vmware_uuid() + kubelet_opts['provider-id'] = 'vsphere://{}'.format(uuid) + elif is_state('endpoint.azure.ready'): + azure = endpoint_from_flag('endpoint.azure.ready') + kubelet_opts['cloud-provider'] = 'azure' + kubelet_opts['cloud-config'] = str(kubelet_cloud_config_path) + kubelet_opts['provider-id'] = azure.vm_id + + # Put together the KubeletConfiguration data + kubelet_config = { + 'apiVersion': 'kubelet.config.k8s.io/v1beta1', + 'kind': 'KubeletConfiguration', + 'address': '0.0.0.0', + 'authentication': { + 'anonymous': { + 'enabled': False + }, + 'x509': { + 'clientCAFile': str(ca_crt_path) + } + }, + # NB: authz webhook config tells the kubelet to ask the api server + # if a request is authorized; it is not related to the authn + # webhook config of the k8s master services. + 'authorization': { + 'mode': 'Webhook' + }, + 'clusterDomain': dns['domain'], + 'failSwapOn': False, + 'port': 10250, + 'protectKernelDefaults': True, + 'readOnlyPort': 0, + 'tlsCertFile': str(server_crt_path), + 'tlsPrivateKeyFile': str(server_key_path) + } + if dns['enable-kube-dns']: + kubelet_config['clusterDNS'] = [dns['sdn-ip']] + + # Handle feature gates + feature_gates = {} + if get_version('kubelet') >= (1, 19): + # NB: required for CIS compliance + feature_gates['RotateKubeletServerCertificate'] = True + if is_state('kubernetes-worker.gpu.enabled'): + feature_gates['DevicePlugins'] = True + if feature_gates: + kubelet_config['featureGates'] = feature_gates + if kubernetes_common.is_dual_stack(kubernetes_common.cluster_cidr()): + feature_gates = kubelet_config.setdefault('featureGates', {}) + feature_gates['IPv6DualStack'] = True + + # Workaround for DNS on bionic + # https://github.com/juju-solutions/bundle-canonical-kubernetes/issues/655 + resolv_path = os.path.realpath('/etc/resolv.conf') + if resolv_path == '/run/systemd/resolve/stub-resolv.conf': + kubelet_config['resolvConf'] = '/run/systemd/resolve/resolv.conf' + + # Add kubelet-extra-config. This needs to happen last so that it + # overrides any config provided by the charm. + kubelet_extra_config = hookenv.config('kubelet-extra-config') + kubelet_extra_config = yaml.safe_load(kubelet_extra_config) + merge_kubelet_extra_config(kubelet_config, kubelet_extra_config) + + # Render the file and configure Kubelet to use it + os.makedirs('/root/cdk/kubelet', exist_ok=True) + with open('/root/cdk/kubelet/config.yaml', 'w') as f: + f.write('# Generated by kubernetes-worker charm, do not edit\n') + yaml.dump(kubelet_config, f) + kubelet_opts['config'] = '/root/cdk/kubelet/config.yaml' + + # If present, ensure kubelet gets the pause container from the configured + # registry. When not present, kubelet uses a default image location + # (currently k8s.gcr.io/pause:3.4.1). + registry_location = get_registry_location() + if registry_location: + kubelet_opts['pod-infra-container-image'] = \ + '{}/pause:3.4.1'.format(registry_location) + + workaround_lxd_kernel_params() + + configure_kubernetes_service(configure_prefix, 'kubelet', kubelet_opts, + 'kubelet-extra-args') + + +@when('config.changed.ingress') +def toggle_ingress_state(): + ''' Ingress is a toggled state. Remove ingress.available if set when + toggled ''' + if hookenv.config('ingress'): + set_state('kubernetes-worker.ingress.enabled') + else: + remove_state('kubernetes-worker.ingress.enabled') + + +@when_any('config.changed.default-backend-image', + 'config.changed.ingress-ssl-chain-completion', + 'config.changed.nginx-image', + 'config.changed.ingress-ssl-passthrough', + 'config.changed.ingress-default-ssl-certificate', + 'config.changed.ingress-default-ssl-key') +def reconfigure_ingress(): + remove_state('kubernetes-worker.ingress.available') + + +@when('kubernetes-worker.config.created', 'kubernetes-worker.ingress.enabled') +@when_not('kubernetes-worker.ingress.available') +def render_and_launch_ingress(): + ''' Launch the Kubernetes ingress controller & default backend (404) ''' + config = hookenv.config() + + # need to test this in case we get in + # here from a config change to the image + if not config.get('ingress'): + return + + context = {} + context['arch'] = arch() + addon_path = '/root/cdk/addons/{}' + context['juju_application'] = hookenv.service_name() + + # If present, workers will get the ingress containers from the configured + # registry. Otherwise, we'll set an appropriate upstream image registry. + registry_location = get_registry_location() + + context['defaultbackend_image'] = config.get('default-backend-image') + if (context['defaultbackend_image'] == "" or + context['defaultbackend_image'] == "auto"): + if registry_location: + backend_registry = registry_location + else: + backend_registry = 'k8s.gcr.io' + if context['arch'] == 's390x': + context['defaultbackend_image'] = \ + "{}/defaultbackend-s390x:1.4".format(backend_registry) + elif context['arch'] == 'ppc64el': + context['defaultbackend_image'] = \ + "{}/defaultbackend-ppc64le:1.5".format(backend_registry) + else: + context['defaultbackend_image'] = \ + "{}/defaultbackend-{}:1.5".format(backend_registry, context['arch']) + + # Render the ingress daemon set controller manifest + context['ssl_chain_completion'] = config.get( + 'ingress-ssl-chain-completion') + context['enable_ssl_passthrough'] = config.get( + 'ingress-ssl-passthrough') + context['default_ssl_certificate_option'] = None + if config.get('ingress-default-ssl-certificate') and config.get( + 'ingress-default-ssl-key'): + context['default_ssl_certificate'] = b64encode( + config.get('ingress-default-ssl-certificate').encode( + 'utf-8')).decode('utf-8') + context['default_ssl_key'] = b64encode( + config.get('ingress-default-ssl-key').encode('utf-8')).decode( + 'utf-8') + default_certificate_option = ( + '- --default-ssl-certificate=' + '$(POD_NAMESPACE)/default-ssl-certificate') + context['default_ssl_certificate_option'] = default_certificate_option + context['ingress_image'] = config.get('nginx-image') + if context['ingress_image'] == "" or context['ingress_image'] == "auto": + if context['arch'] == 'ppc64el': + # multi-arch image doesn't include ppc64le, have to use an older version + context['ingress_uid'] = '33' + context['ingress_image'] = '/'.join([ + registry_location or 'quay.io', + 'kubernetes-ingress-controller/nginx-ingress-controller-ppc64le:0.20.0', + ]) + else: + context['ingress_uid'] = '101' + context['ingress_image'] = '/'.join([ + registry_location or 'us.gcr.io', + 'k8s-artifacts-prod/ingress-nginx/controller:v1.0.0-beta.3', + ]) + + kubelet_version = get_version('kubelet') + if kubelet_version < (1, 9): + context['daemonset_api_version'] = 'extensions/v1beta1' + context['deployment_api_version'] = 'extensions/v1beta1' + elif kubelet_version < (1, 16): + context['daemonset_api_version'] = 'apps/v1beta2' + context['deployment_api_version'] = 'extensions/v1beta1' + else: + context['daemonset_api_version'] = 'apps/v1' + context['deployment_api_version'] = 'apps/v1' + context['use_forwarded_headers'] = "true" if config.get( + "ingress-use-forwarded-headers") else "false" + + manifest = addon_path.format('ingress-daemon-set.yaml') + render('ingress-daemon-set.yaml', manifest, context) + hookenv.log('Creating the ingress daemon set.') + try: + kubectl('apply', '-f', manifest) + except CalledProcessError as e: + hookenv.log(e) + hookenv.log('Failed to create ingress controller. Will attempt again next update.') # noqa + hookenv.close_port(80) + hookenv.close_port(443) + return + + # Render the default http backend (404) deployment manifest + # needs to happen after ingress-daemon-set since that sets up the namespace + manifest = addon_path.format('default-http-backend.yaml') + render('default-http-backend.yaml', manifest, context) + hookenv.log('Creating the default http backend.') + try: + kubectl('apply', '-f', manifest) + except CalledProcessError as e: + hookenv.log(e) + hookenv.log('Failed to create default-http-backend. Will attempt again next update.') # noqa + hookenv.close_port(80) + hookenv.close_port(443) + return + + set_state('kubernetes-worker.ingress.available') + hookenv.open_port(80) + hookenv.open_port(443) + + +@when('kubernetes-worker.config.created', + 'kubernetes-worker.ingress.available') +@when_not('kubernetes-worker.ingress.enabled') +def disable_ingress(): + hookenv.log('Deleting the http backend and ingress.') + hookenv.close_port(80) + hookenv.close_port(443) + try: + kubectl('delete', '--ignore-not-found', '-f', + '/root/cdk/addons/default-http-backend.yaml') + kubectl('delete', '--ignore-not-found', '-f', + '/root/cdk/addons/ingress-daemon-set.yaml') + except CalledProcessError: + traceback.print_exc() + hookenv.log('Failed to disable ingress, waiting to retry') + return + remove_state('kubernetes-worker.ingress.available') + + +def restart_unit_services(): + '''Restart worker services.''' + hookenv.log('Restarting kubelet and kube-proxy.') + services = ['kube-proxy', 'kubelet'] + for service in services: + service_restart('snap.%s.daemon' % service) + + +def get_kube_api_servers(): + '''Return the list of kubernetes API endpoint URLs.''' + kube_control = endpoint_from_name("kube-control") + kube_api = endpoint_from_name("kube-api-endpoint") + # prefer kube-api-endpoints + if kube_api.services(): + return [ + 'https://{0}:{1}'.format(unit['hostname'], unit['port']) + for service in kube_api.services() + for unit in service['hosts'] + ] + if hasattr(kube_control, "get_api_endpoints"): + return kube_control.get_api_endpoints() + hookenv.log("Unable to determine API server URLs from either kube-control " + "or kube-api-endpoint relation", hookenv.ERROR) + return [] + + +@when('kubernetes-worker.config.created') +@when('nrpe-external-master.available') +@when('kube-control.auth.available') +@when_any('config.changed.nagios_context', + 'config.changed.nagios_servicegroups', + 'nrpe-external-master.reconfigure') +@when_any('kube-control.api_endpoints.available', + 'kube-api-endpoint.available') +def update_nrpe_config(): + services = ['snap.{}.daemon'.format(s) for s in worker_services] + data = render('nagios_plugin.py', context={'node_name': get_node_name()}) + plugin_path = install_nagios_plugin_from_text(data, + 'check_k8s_worker.py') + hostname = nrpe.get_nagios_hostname() + current_unit = nrpe.get_nagios_unit_name() + nrpe_setup = nrpe.NRPE(hostname=hostname) + nrpe_setup.add_check("node", + "Node registered with API Server", + str(plugin_path)) + nrpe.add_init_service_checks(nrpe_setup, services, current_unit) + nrpe_setup.write() + + creds = db.get('credentials') + servers = get_kube_api_servers() + if creds and servers: + server = servers[get_unit_number() % len(servers)] + create_kubeconfig(nrpe_kubeconfig_path, server, ca_crt_path, + token=creds['client_token'], user='nagios') + # Make sure Nagios dirs are the correct permissions. + cmd = ['chown', '-R', 'nagios:nagios'] + for p in ['/var/lib/nagios/', os.path.dirname(nrpe_kubeconfig_path)]: + if os.path.exists(p): + check_call(cmd + [p]) + + remove_state('nrpe-external-master.reconfigure') + set_state('nrpe-external-master.initial-config') + # request CPU governor check from nrpe relation to be performance + rel_settings = { + 'requested_cpu_governor': 'performance', + } + for rid in hookenv.relation_ids('nrpe-external-master'): + hookenv.relation_set(relation_id=rid, relation_settings=rel_settings) + + +@when_not('nrpe-external-master.available') +@when('nrpe-external-master.initial-config') +def remove_nrpe_config(): + remove_state('nrpe-external-master.initial-config') + remove_nagios_plugin('check_k8s_worker.py') + + # The current nrpe-external-master interface doesn't handle a lot of logic, + # use the charm-helpers code for now. + hostname = nrpe.get_nagios_hostname() + nrpe_setup = nrpe.NRPE(hostname=hostname) + + for service in worker_services: + nrpe_setup.remove_check(shortname=service) + nrpe_setup.remove_check(shortname='node') + + +@when('nvidia.ready') +@when('kubernetes-worker.config.created') +@when_not('kubernetes-worker.gpu.enabled') +def enable_gpu(): + """Enable GPU usage on this node. + + """ + hookenv.log('Enabling gpu mode') + try: + # Not sure why this is necessary, but if you don't run this, k8s will + # think that the node has 0 gpus (as shown by the output of + # `kubectl get nodes -o yaml` + check_call(['nvidia-smi']) + except CalledProcessError as cpe: + hookenv.log('Unable to communicate with the NVIDIA driver.') + hookenv.log(cpe) + return + + set_label('gpu', 'true') + set_label('cuda', 'true') + + set_state('kubernetes-worker.gpu.enabled') + set_state('kubernetes-worker.restart-needed') + + +@when('kubernetes-worker.gpu.enabled') +@when_not('nvidia.ready') +@when_not('kubernetes-worker.restart-needed') +def nvidia_departed(): + """Cuda departed.""" + disable_gpu() + remove_state('kubernetes-worker.gpu.enabled') + set_state('kubernetes-worker.restart-needed') + + +def disable_gpu(): + """Disable GPU usage on this node. + + """ + hookenv.log('Disabling gpu mode') + + # Remove node labels + remove_label('gpu') + remove_label('cuda') + + +@when('kubernetes-worker.gpu.enabled') +@when('kube-control.connected') +def notify_master_gpu_enabled(kube_control): + """Notify kubernetes-master that we're gpu-enabled. + + """ + kube_control.set_gpu(True) + + +@when_not('kubernetes-worker.gpu.enabled') +@when('kube-control.connected') +def notify_master_gpu_not_enabled(kube_control): + """Notify kubernetes-master that we're not gpu-enabled. + + """ + kube_control.set_gpu(False) + + +@when('kube-control.connected') +def request_kubelet_and_proxy_credentials(kube_control): + """ Request kubelet node authorization with a well formed kubelet user. + This also implies that we are requesting kube-proxy auth. """ + + # The kube-cotrol interface is created to support RBAC. + # At this point we might as well do the right thing and return the hostname + # even if it will only be used when we enable RBAC + nodeuser = 'system:node:{}'.format(get_node_name().lower()) + kube_control.set_auth_request(nodeuser) + + +@when('kube-control.connected') +def catch_change_in_creds(kube_control): + """Request a service restart in case credential updates were detected.""" + nodeuser = 'system:node:{}'.format(get_node_name().lower()) + creds = kube_control.get_auth_credentials(nodeuser) + if creds and creds['user'] == nodeuser: + # We need to cache the credentials here because if the + # master changes (master leader dies and replaced by a new one) + # the new master will have no recollection of our certs. + db.set('credentials', creds) + set_state('worker.auth.bootstrapped') + if data_changed('kube-control.creds', creds): + set_state('kubernetes-worker.restart-needed') + + +def missing_kube_control(): + """Inform the operator they need to add the kube-control relation. + + If deploying via bundle this won't happen, but if operator is upgrading a + a charm in a deployment that pre-dates the kube-control relation, it'll be + missing. + + Called from charm_status. + """ + try: + goal_state = hookenv.goal_state() + except NotImplementedError: + goal_state = {} + + if 'kube-control' in goal_state.get('relations', {}): + if not is_flag_set("kube-control.connected"): + hookenv.status_set( + 'waiting', + 'Waiting for kubernetes-master to become ready') + return True + else: + hookenv.status_set( + 'blocked', + 'Relate {}:kube-control kubernetes-master:kube-control'.format( + hookenv.service_name())) + return True + return False + + +def _systemctl_is_active(application): + ''' Poll systemctl to determine if the application is running ''' + cmd = ['systemctl', 'is-active', application] + try: + raw = check_output(cmd) + return b'active' in raw + except Exception: + return False + + +class ApplyNodeLabelFailed(Exception): + pass + + +def persistent_call(cmd, retry_message): + deadline = time.time() + 180 + while time.time() < deadline: + code = subprocess.call(cmd) + if code == 0: + return True + hookenv.log(retry_message) + time.sleep(1) + else: + return False + + +def set_label(label, value): + nodename = get_node_name() + cmd = 'kubectl --kubeconfig={0} label node {1} {2}={3} --overwrite' + cmd = cmd.format(kubeconfig_path, nodename, label, value) + cmd = cmd.split() + retry = 'Failed to apply label %s=%s. Will retry.' % (label, value) + if not persistent_call(cmd, retry): + raise ApplyNodeLabelFailed(retry) + + +def remove_label(label): + nodename = get_node_name() + cmd = 'kubectl --kubeconfig={0} label node {1} {2}-' + cmd = cmd.format(kubeconfig_path, nodename, label) + cmd = cmd.split() + retry = 'Failed to remove label {0}. Will retry.'.format(label) + if not persistent_call(cmd, retry): + raise ApplyNodeLabelFailed(retry) + + +@when_any('endpoint.aws.joined', + 'endpoint.gcp.joined', + 'endpoint.openstack.joined', + 'endpoint.vsphere.joined', + 'endpoint.azure.joined') +@when_not('kubernetes-worker.cloud.ready') +def set_cloud_pending(): + k8s_version = get_version('kubelet') + k8s_1_11 = k8s_version >= (1, 11) + k8s_1_12 = k8s_version >= (1, 12) + vsphere_joined = is_state('endpoint.vsphere.joined') + azure_joined = is_state('endpoint.azure.joined') + if (vsphere_joined and not k8s_1_12) or (azure_joined and not k8s_1_11): + set_state('kubernetes-worker.cloud.blocked') + else: + remove_state('kubernetes-worker.cloud.blocked') + set_state('kubernetes-worker.cloud.pending') + + +@when_any('endpoint.aws.joined', + 'endpoint.gcp.joined', + 'endpoint.azure.joined') +@when('kube-control.cluster_tag.available') +@when_not('kubernetes-worker.cloud.request-sent') +def request_integration(): + hookenv.status_set('maintenance', 'requesting cloud integration') + kube_control = endpoint_from_flag('kube-control.cluster_tag.available') + cluster_tag = kube_control.get_cluster_tag() + if is_state('endpoint.aws.joined'): + cloud = endpoint_from_flag('endpoint.aws.joined') + cloud.tag_instance({ + 'kubernetes.io/cluster/{}'.format(cluster_tag): 'owned', + }) + cloud.tag_instance_security_group({ + 'kubernetes.io/cluster/{}'.format(cluster_tag): 'owned', + }) + cloud.tag_instance_subnet({ + 'kubernetes.io/cluster/{}'.format(cluster_tag): 'owned', + }) + cloud.enable_object_storage_management(['kubernetes-*']) + elif is_state('endpoint.gcp.joined'): + cloud = endpoint_from_flag('endpoint.gcp.joined') + cloud.label_instance({ + 'k8s-io-cluster-name': cluster_tag, + }) + cloud.enable_object_storage_management() + elif is_state('endpoint.azure.joined'): + cloud = endpoint_from_flag('endpoint.azure.joined') + cloud.tag_instance({ + 'k8s-io-cluster-name': cluster_tag, + }) + cloud.enable_object_storage_management() + cloud.enable_instance_inspection() + cloud.enable_dns_management() + set_state('kubernetes-worker.cloud.request-sent') + hookenv.status_set('waiting', 'Waiting for cloud integration') + + +@when_none('endpoint.aws.joined', + 'endpoint.gcp.joined', + 'endpoint.openstack.joined', + 'endpoint.vsphere.joined', + 'endpoint.azure.joined') +@when_any('kubernetes-worker.cloud.pending', + 'kubernetes-worker.cloud.request-sent', + 'kubernetes-worker.cloud.blocked', + 'kubernetes-worker.cloud.ready') +def clear_cloud_flags(): + remove_state('kubernetes-worker.cloud.pending') + remove_state('kubernetes-worker.cloud.request-sent') + remove_state('kubernetes-worker.cloud.blocked') + remove_state('kubernetes-worker.cloud.ready') + set_state('kubernetes-worker.restart-needed') # force restart + + +@when_any('endpoint.aws.ready', + 'endpoint.gcp.ready', + 'endpoint.openstack.ready', + 'endpoint.vsphere.ready', + 'endpoint.azure.ready') +@when_not('kubernetes-worker.cloud.blocked', + 'kubernetes-worker.cloud.ready') +def cloud_ready(): + remove_state('kubernetes-worker.cloud.pending') + if is_state('endpoint.gcp.ready'): + write_gcp_snap_config('kubelet') + elif is_state('endpoint.azure.ready'): + write_azure_snap_config('kubelet') + set_state('kubernetes-worker.cloud.ready') + set_state('kubernetes-worker.restart-needed') # force restart + + +def get_first_mount(mount_relation): + mount_relation_list = mount_relation.mounts() + if mount_relation_list and len(mount_relation_list) > 0: + # mount relation list is a list of the mount layer relations + # for now we just use the first one that is nfs + for mount in mount_relation_list: + # for now we just check the first mount and use that. + # the nfs charm only supports one for now. + if ('mounts' in mount and + mount['mounts'][0]['fstype'] == 'nfs'): + return mount['mounts'][0] + return None + + +@when('nfs.available') +def nfs_state_control(mount): + ''' Determine if we should remove the state that controls the re-render + and execution of the nfs-relation-changed event because there + are changes in the relationship data, and we should re-render any + configs ''' + + mount_data = get_first_mount(mount) + if mount_data: + nfs_relation_data = { + 'options': mount_data['options'], + 'host': mount_data['hostname'], + 'mountpoint': mount_data['mountpoint'], + 'fstype': mount_data['fstype'] + } + + # Re-execute the rendering if the data has changed. + if data_changed('nfs-config', nfs_relation_data): + hookenv.log('reconfiguring nfs') + remove_state('nfs.configured') + + +@when('nfs.available') +@when_not('nfs.configured') +def nfs_storage(mount): + '''NFS on kubernetes requires nfs config rendered into a deployment of + the nfs client provisioner. That will handle the persistent volume claims + with no persistent volume to back them.''' + + mount_data = get_first_mount(mount) + if not mount_data: + return + + # If present, use the configured registry to define the nfs image location. + registry_location = get_registry_location() + if registry_location: + mount_data['registry'] = registry_location + + addon_path = '/root/cdk/addons/{}' + # Render the NFS deployment + manifest = addon_path.format('nfs-provisioner.yaml') + render('nfs-provisioner.yaml', manifest, mount_data) + hookenv.log('Creating the nfs provisioner.') + try: + kubectl('apply', '-f', manifest) + except CalledProcessError as e: + hookenv.log(e) + hookenv.log('Failed to create nfs provisioner. Will attempt again next update.') # noqa + return + + set_state('nfs.configured') + + +@when('kube-control.registry_location.available') +def update_registry_location(): + """Handle changes to the container image registry. + + Monitor the image registry location. If it changes, manage flags to ensure + our image-related handlers will be invoked with an accurate registry. + """ + registry_location = get_registry_location() + + if registry_location: + runtime = endpoint_from_flag('endpoint.container-runtime.available') + if runtime: + # Construct and send the sandbox image (pause container) to our runtime + uri = '{}/pause:3.4.1'.format(registry_location) + runtime.set_config( + sandbox_image=uri + ) + + if data_changed('registry-location', registry_location): + remove_state('kubernetes-worker.config.created') + remove_state('kubernetes-worker.ingress.available') + remove_state('nfs.configured') + set_state('kubernetes-worker.restart-needed') + + +def get_registry_location(): + """Get the image registry from the kube-control relation. + + If an image-registry has been configured on the k8s-master, it will be set + set on the kube-control relation. This function returns that value stripped + of any trailing slash. If the relation or registry location are missing, + this returns an empty string. + """ + kube_control = endpoint_from_flag( + 'kube-control.registry_location.available') + if kube_control: + rel_registry = kube_control.get_registry_location() + registry = rel_registry.rstrip('/') if rel_registry else "" + else: + registry = "" + + return registry + + +def configure_default_cni(): + """Set the default CNI configuration to be used by CNI clients + (kubelet, containerd). + + CNI clients choose whichever CNI config in /etc/cni/net.d/ is + alphabetically first, so we accomplish this by creating a file named + /etc/cni/net.d/05-default.conflist, which is alphabetically earlier than + typical CNI config names, e.g. 10-flannel.conflist and 10-calico.conflist + + The created 05-default.conflist file is a symlink to whichever CNI config + is actually going to be used. + """ + # Clean up current default + cni_conf_dir = '/etc/cni/net.d' + for filename in os.listdir(cni_conf_dir): + if filename.startswith('05-default.'): + os.remove(cni_conf_dir + '/' + filename) + + # Set new default + kube_control = endpoint_from_flag('kube-control.default_cni.available') + default_cni = kube_control.get_default_cni() + cni = endpoint_from_flag('cni.available') + cni_conf = cni.get_config(default=default_cni) + source = cni_conf['cni-conf-file'] + dest = cni_conf_dir + '/' + '05-default.' + source.split('.')[-1] + os.symlink(source, dest) + + +@when('ingress-proxy.available') +def configure_ingress_proxy(ingress_proxy): + ingress_proxy.configure(port='80') + + +def workaround_lxd_kernel_params(): + ''' + Workaround for kubelet not starting in LXD when kernel params are not set + to the desired values. + ''' + if is_container(): + hookenv.log('LXD detected, faking kernel params via bind mounts') + root_dir = '/root/cdk/lxd-kernel-params' + os.makedirs(root_dir, exist_ok=True) + # Kernel params taken from: + # https://github.com/kubernetes/kubernetes/blob/v1.22.0/pkg/kubelet/cm/container_manager_linux.go#L421-L426 + # https://github.com/kubernetes/kubernetes/blob/v1.22.0/pkg/util/sysctl/sysctl.go#L30-L64 + params = { + 'vm.overcommit_memory': 1, + 'vm.panic_on_oom': 0, + 'kernel.panic': 10, + 'kernel.panic_on_oops': 1, + 'kernel.keys.root_maxkeys': 1000000, + 'kernel.keys.root_maxbytes': 1000000 * 25 + } + for param, param_value in params.items(): + fake_param_path = root_dir + '/' + param + with open(fake_param_path, 'w') as f: + f.write(str(param_value)) + real_param_path = '/proc/sys/' + param.replace('.', '/') + fstab_add(fake_param_path, real_param_path, 'none', 'bind') + subprocess.check_call(['mount', '-a']) + else: + hookenv.log('LXD not detected, not faking kernel params') diff --git a/kubernetes-worker/reactive/leadership.py b/kubernetes-worker/reactive/leadership.py new file mode 100644 index 0000000..29c6f3a --- /dev/null +++ b/kubernetes-worker/reactive/leadership.py @@ -0,0 +1,68 @@ +# Copyright 2015-2016 Canonical Ltd. +# +# This file is part of the Leadership Layer for Juju. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranties of +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR +# PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from charmhelpers.core import hookenv +from charmhelpers.core import unitdata + +from charms import reactive +from charms.leadership import leader_get, leader_set + + +__all__ = ['leader_get', 'leader_set'] # Backwards compatibility + + +def initialize_leadership_state(): + '''Initialize leadership.* states from the hook environment. + + Invoked by hookenv.atstart() so states are available in + @hook decorated handlers. + ''' + is_leader = hookenv.is_leader() + if is_leader: + hookenv.log('Initializing Leadership Layer (is leader)') + else: + hookenv.log('Initializing Leadership Layer (is follower)') + + reactive.helpers.toggle_state('leadership.is_leader', is_leader) + + previous = unitdata.kv().getrange('leadership.settings.', strip=True) + current = hookenv.leader_get() + + # Handle deletions. + for key in set(previous.keys()) - set(current.keys()): + current[key] = None + + any_changed = False + for key, value in current.items(): + reactive.helpers.toggle_state('leadership.changed.{}'.format(key), + value != previous.get(key)) + if value != previous.get(key): + any_changed = True + reactive.helpers.toggle_state('leadership.set.{}'.format(key), + value is not None) + reactive.helpers.toggle_state('leadership.changed', any_changed) + + unitdata.kv().update(current, prefix='leadership.settings.') + + +# 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, '_leadership_registered'): + hookenv.atstart(initialize_leadership_state) + reactive._leadership_registered = True diff --git a/kubernetes-worker/reactive/snap.py b/kubernetes-worker/reactive/snap.py new file mode 100644 index 0000000..1fda7b7 --- /dev/null +++ b/kubernetes-worker/reactive/snap.py @@ -0,0 +1,349 @@ +# Copyright 2016-2019 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. +""" +from collections import OrderedDict +from distutils.version import LooseVersion +import os.path +from os import uname +import shutil +import subprocess +from textwrap import dedent +import time +from urllib.request import urlretrieve + +from charmhelpers.core import hookenv, host +from charmhelpers.core.hookenv import ERROR +from charmhelpers.core.host import write_file +from charms import layer +from charms import reactive +from charms.layer import snap +from charms.reactive import register_trigger, when, when_not, toggle_flag +from charms.reactive.helpers import data_changed + + +class UnsatisfiedMinimumVersionError(Exception): + def __init__(self, desired, actual): + super().__init__() + self.desired = desired + self.actual = actual + + def __str__(self): + return "Could not install snapd >= {0.desired}, got {0.actual}".format(self) + + +class InvalidBundleError(Exception): + pass + + +def sorted_snap_opts(): + opts = layer.options("snap") + opts = sorted(opts.items(), key=lambda item: item[0] != "core") + opts = OrderedDict(opts) + return opts + + +def install(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + opts = sorted_snap_opts() + # supported-architectures is EXPERIMENTAL and undocumented. + # It probably should live in the base layer, blocking the charm + # during bootstrap if the arch is unsupported. + arch = uname().machine + for snapname, snap_opts in opts.items(): + supported_archs = snap_opts.pop("supported-architectures", None) + if supported_archs and arch not in supported_archs: + # Note that this does *not* error. The charm will need to + # cope with the snaps it requested never getting installed, + # likely by doing its own check on supported-architectures. + hookenv.log( + "Snap {} not supported on {!r} architecture" "".format(snapname, arch), + ERROR, + ) + continue + installed_flag = "snap.installed.{}".format(snapname) + if not reactive.is_flag_set(installed_flag): + snap.install(snapname, **snap_opts) + if data_changed("snap.install.opts", opts): + snap.connect_all() + + +def check_refresh_available(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + available_refreshes = snap.get_available_refreshes() + for snapname in snap.get_installed_snaps(): + toggle_flag(snap.get_refresh_available_flag(snapname), snapname in available_refreshes) + + +def refresh(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + opts = sorted_snap_opts() + # supported-architectures is EXPERIMENTAL and undocumented. + # It probably should live in the base layer, blocking the charm + # during bootstrap if the arch is unsupported. + arch = uname()[4] + check_refresh_available() + for snapname, snap_opts in opts.items(): + supported_archs = snap_opts.pop("supported-architectures", None) + if supported_archs and arch not in supported_archs: + continue + snap.refresh(snapname, **snap_opts) + snap.connect_all() + + +@reactive.hook("upgrade-charm") +def upgrade_charm(): + refresh() + + +def get_series(): + return subprocess.check_output(["lsb_release", "-sc"], universal_newlines=True).strip() + + +def snapd_supported(): + # snaps are not supported in trusty lxc containers. + if get_series() == "trusty" and host.is_container(): + return False + return True # For all other cases, assume true. + + +def kernel_supported(): + kernel_version = uname().release + + if LooseVersion(kernel_version) < LooseVersion("4.4"): + hookenv.log( + "Snaps do not work on kernel {}, a reboot " + "into a supported kernel (>4.4) is required" + "".format(kernel_version) + ) + return False + return True + + +def ensure_snapd(): + if not snapd_supported(): + hookenv.log("Snaps do not work in this environment", hookenv.ERROR) + raise Exception("Snaps do not work in this environment") + + # 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"): + os.environ["DEBIAN_FRONTEND"] = "noninteractive" + cmd = ["apt-get", "install", "-y", "snapd"] + # LP:1699986: Force install of systemd on Trusty. + if get_series() == "trusty": + cmd.append("systemd") + 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 host.is_container() and not shutil.which("squashfuse"): + os.environ["DEBIAN_FRONTEND"] = "noninteractive" + cmd = ["apt-get", "install", "-y", "squashfuse", "fuse"] + subprocess.check_call(cmd, universal_newlines=True) + + +def proxy_settings(): + proxy_vars = ("http_proxy", "https_proxy") + proxy_env = {key: value for key, value in os.environ.items() if key in proxy_vars} + + snap_proxy = hookenv.config().get("snap_proxy") + if snap_proxy: + proxy_env["http_proxy"] = snap_proxy + proxy_env["https_proxy"] = snap_proxy + return proxy_env + + +def update_snap_proxy(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + # 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 = proxy_settings() + + override_dir = "/etc/systemd/system/snapd.service.d" + path = os.path.join(override_dir, "snap_layer_proxy.conf") + if not proxy and not os.path.exists(path): + return # No proxy asked for and proxy never configured. + + # It seems we cannot rely on this directory existing, so manually + # create it. + if not os.path.exists(override_dir): + host.mkdir(override_dir, perms=0o755) + + if not data_changed("snap.proxy", proxy): + return # Short circuit avoids unnecessary restarts. + + 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] + """ + ) + for proxy_key, proxy_value in proxy.items(): + content += "Environment={}={}\n".format(proxy_key, proxy_value) + 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" + + +def _get_snapd_version(): + stdout = subprocess.check_output(["snap", "version"], stdin=subprocess.DEVNULL, universal_newlines=True) + version_info = dict(line.split(None, 1) for line in stdout.splitlines()) + return LooseVersion(version_info["snapd"]) + + +PREFERENCES = """\ +Package: * +Pin: release a={}-proposed +Pin-Priority: 400 +""" + + +def ensure_snapd_min_version(min_version): + snapd_version = _get_snapd_version() + if snapd_version < LooseVersion(min_version): + from charmhelpers.fetch import add_source, apt_update, apt_install + + # Temporary until LP:1735344 lands + add_source("distro-proposed", fail_invalid=True) + distro = get_series() + # disable proposed by default, needs to explicit + write_file( + "/etc/apt/preferences.d/proposed", + PREFERENCES.format(distro), + ) + apt_update() + # explicitly install snapd from proposed + apt_install("snapd/{}-proposed".format(distro)) + snapd_version = _get_snapd_version() + if snapd_version < LooseVersion(min_version): + hookenv.log("Failed to install snapd >= {}".format(min_version), ERROR) + raise UnsatisfiedMinimumVersionError(min_version, snapd_version) + + +def download_assertion_bundle(proxy_url): + """Download proxy assertion bundle and store id""" + assertions_url = "{}/v2/auth/store/assertions".format(proxy_url) + local_bundle, headers = urlretrieve(assertions_url) + store_id = headers["X-Assertion-Store-Id"] + return local_bundle, store_id + + +def configure_snap_store_proxy(): + # Do nothing if we don't have kernel support yet + if not kernel_supported(): + return + + if not reactive.is_flag_set("config.changed.snap_proxy_url"): + return + config = hookenv.config() + if "snap_proxy_url" not in config: + # The deprecated snap_proxy_url config items have been removed + # from config.yaml. If the charm author hasn't added them back + # explicitly, there is nothing to do. Juju is maintaining these + # settings as model configuration. + return + snap_store_proxy_url = config.get("snap_proxy_url") + if not snap_store_proxy_url and not config.previous("snap_proxy_url"): + # Proxy url is not set, and was not set previous hook. Do nothing, + # to avoid overwriting the Juju maintained setting. + return + ensure_snapd_min_version("2.30") + if snap_store_proxy_url: + bundle, store_id = download_assertion_bundle(snap_store_proxy_url) + try: + subprocess.check_output( + ["snap", "ack", bundle], + stdin=subprocess.DEVNULL, + universal_newlines=True, + ) + except subprocess.CalledProcessError as e: + raise InvalidBundleError("snapd could not ack the proxy assertion: " + e.output) + else: + store_id = "" + + try: + subprocess.check_output( + ["snap", "set", "core", "proxy.store={}".format(store_id)], + stdin=subprocess.DEVNULL, + universal_newlines=True, + ) + except subprocess.CalledProcessError as e: + raise InvalidBundleError("Proxy ID from header did not match store assertion: " + e.output) + + +register_trigger(when="config.changed.snapd_refresh", clear_flag="snap.refresh.set") + + +@when_not("snap.refresh.set") +@when("snap.installed.core") +def change_snapd_refresh(): + """Set the system refresh.timer option""" + ensure_snapd_min_version("2.31") + timer = hookenv.config()["snapd_refresh"] + was_set = reactive.is_flag_set("snap.refresh.was-set") + if timer or was_set: + snap.set_refresh_timer(timer) + reactive.toggle_flag("snap.refresh.was-set", timer) + reactive.set_flag("snap.refresh.set") + + +# Bootstrap. We don't use standard reactive handlers to ensure that +# everything is bootstrapped before any charm handlers are run. +hookenv.atstart(hookenv.log, "Initializing Snap Layer") +hookenv.atstart(ensure_snapd) +hookenv.atstart(ensure_path) +hookenv.atstart(update_snap_proxy) +hookenv.atstart(configure_snap_store_proxy) +hookenv.atstart(install) diff --git a/kubernetes-worker/reactive/tls_client.py b/kubernetes-worker/reactive/tls_client.py new file mode 100644 index 0000000..afa2228 --- /dev/null +++ b/kubernetes-worker/reactive/tls_client.py @@ -0,0 +1,208 @@ +import os + +from pathlib import Path +from subprocess import check_call + +from charms import layer +from charms.reactive import hook +from charms.reactive import set_state, remove_state +from charms.reactive import when +from charms.reactive import set_flag, clear_flag +from charms.reactive import endpoint_from_flag +from charms.reactive.helpers import data_changed + +from charmhelpers.core import hookenv, unitdata +from charmhelpers.core.hookenv import log + + +@when('certificates.ca.available') +def store_ca(tls): + '''Read the certificate authority from the relation object and install + the ca on this system.''' + # Get the CA from the relationship object. + certificate_authority = tls.get_ca() + if certificate_authority: + layer_options = layer.options('tls-client') + ca_path = layer_options.get('ca_certificate_path') + changed = data_changed('certificate_authority', certificate_authority) + if ca_path: + if changed or not os.path.exists(ca_path): + log('Writing CA certificate to {0}'.format(ca_path)) + # ensure we have a newline at the end of the certificate. + # some things will blow up without one. + # See https://bugs.launchpad.net/charm-kubernetes-master/+bug/1828034 + if not certificate_authority.endswith('\n'): + certificate_authority += '\n' + _write_file(ca_path, certificate_authority) + set_state('tls_client.ca.written') + set_state('tls_client.ca.saved') + if changed: + # Update /etc/ssl/certs and generate ca-certificates.crt + install_ca(certificate_authority) + + +@when('certificates.server.cert.available') +def store_server(tls): + '''Read the server certificate and server key from the relation object + and save them to the certificate directory..''' + server_cert, server_key = tls.get_server_cert() + chain = tls.get_chain() + if chain: + server_cert = server_cert + '\n' + chain + if server_cert and server_key: + layer_options = layer.options('tls-client') + cert_path = layer_options.get('server_certificate_path') + key_path = layer_options.get('server_key_path') + cert_changed = data_changed('server_certificate', server_cert) + key_changed = data_changed('server_key', server_key) + if cert_path: + if cert_changed or not os.path.exists(cert_path): + log('Writing server certificate to {0}'.format(cert_path)) + _write_file(cert_path, server_cert) + set_state('tls_client.server.certificate.written') + set_state('tls_client.server.certificate.saved') + if key_path: + if key_changed or not os.path.exists(key_path): + log('Writing server key to {0}'.format(key_path)) + _write_file(key_path, server_key) + set_state('tls_client.server.key.saved') + + +@when('certificates.client.cert.available') +def store_client(tls): + '''Read the client certificate and client key from the relation object + and copy them to the certificate directory.''' + client_cert, client_key = tls.get_client_cert() + chain = tls.get_chain() + if chain: + client_cert = client_cert + '\n' + chain + if client_cert and client_key: + layer_options = layer.options('tls-client') + cert_path = layer_options.get('client_certificate_path') + key_path = layer_options.get('client_key_path') + cert_changed = data_changed('client_certificate', client_cert) + key_changed = data_changed('client_key', client_key) + if cert_path: + if cert_changed or not os.path.exists(cert_path): + log('Writing client certificate to {0}'.format(cert_path)) + _write_file(cert_path, client_cert) + set_state('tls_client.client.certificate.written') + set_state('tls_client.client.certificate.saved') + if key_path: + if key_changed or not os.path.exists(key_path): + log('Writing client key to {0}'.format(key_path)) + _write_file(key_path, client_key) + set_state('tls_client.client.key.saved') + + +@when('certificates.certs.changed') +def update_certs(): + tls = endpoint_from_flag('certificates.certs.changed') + certs_paths = unitdata.kv().get('layer.tls-client.cert-paths', {}) + all_ready = True + any_changed = False + maps = { + 'server': tls.server_certs_map, + 'client': tls.client_certs_map, + } + + if maps.get('client') == {}: + log( + 'No client certs found using maps. Checking for global \ + client certificates.', + 'WARNING' + ) + # Check for global certs, + # Backwards compatibility https://bugs.launchpad.net/charm-kubernetes-master/+bug/1825819 + cert_pair = tls.get_client_cert() + if cert_pair is not None: + for client_name in certs_paths.get('client', {}).keys(): + maps.get('client').update({ + client_name: cert_pair + }) + + chain = tls.get_chain() + for cert_type in ('server', 'client'): + for common_name, paths in certs_paths.get(cert_type, {}).items(): + cert_pair = maps[cert_type].get(common_name) + if not cert_pair: + all_ready = False + continue + if not data_changed('layer.tls-client.' + '{}.{}'.format(cert_type, common_name), cert_pair): + continue + + cert = None + key = None + if type(cert_pair) is not tuple: + if paths['crt']: + cert = cert_pair.cert + if paths['key']: + key = cert_pair.key + else: + cert, key = cert_pair + + if cert: + if chain: + cert = cert + '\n' + chain + _ensure_directory(paths['crt']) + Path(paths['crt']).write_text(cert) + + if key: + _ensure_directory(paths['key']) + Path(paths['key']).write_text(key) + + any_changed = True + # clear flags first to ensure they are re-triggered if left set + clear_flag('tls_client.{}.certs.changed'.format(cert_type)) + clear_flag('tls_client.{}.cert.{}.changed'.format(cert_type, + common_name)) + set_flag('tls_client.{}.certs.changed'.format(cert_type)) + set_flag('tls_client.{}.cert.{}.changed'.format(cert_type, + common_name)) + if all_ready: + set_flag('tls_client.certs.saved') + if any_changed: + clear_flag('tls_client.certs.changed') + set_flag('tls_client.certs.changed') + clear_flag('certificates.certs.changed') + + +def install_ca(certificate_authority): + '''Install a certificiate authority on the system by calling the + update-ca-certificates command.''' + if certificate_authority: + name = hookenv.service_name() + # Create a path to install CAs on Debian systems. + ca_path = '/usr/local/share/ca-certificates/{0}.crt'.format(name) + log('Writing CA certificate to {0}'.format(ca_path)) + _write_file(ca_path, certificate_authority) + # Update the trusted CAs on this system (a time expensive operation). + check_call(['update-ca-certificates']) + log('Generated ca-certificates.crt for {0}'.format(name)) + set_state('tls_client.ca_installed') + + +@hook('upgrade-charm') +def remove_states(): + remove_state('tls_client.ca.saved') + remove_state('tls_client.server.certificate.saved') + remove_state('tls_client.server.key.saved') + remove_state('tls_client.client.certificate.saved') + remove_state('tls_client.client.key.saved') + + +def _ensure_directory(path): + '''Ensure the parent directory exists creating directories if necessary.''' + directory = os.path.dirname(path) + if not os.path.isdir(directory): + os.makedirs(directory) + os.chmod(directory, 0o770) + + +def _write_file(path, content): + '''Write the path to a file.''' + _ensure_directory(path) + with open(path, 'w') as stream: + stream.write(content) + os.chmod(path, 0o440) diff --git a/kubernetes-worker/requirements.txt b/kubernetes-worker/requirements.txt new file mode 100644 index 0000000..55543d9 --- /dev/null +++ b/kubernetes-worker/requirements.txt @@ -0,0 +1,3 @@ +mock +flake8 +pytest diff --git a/kubernetes-worker/revision b/kubernetes-worker/revision new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/kubernetes-worker/revision @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/kubernetes-worker/setup.py b/kubernetes-worker/setup.py new file mode 100755 index 0000000..b30bff5 --- /dev/null +++ b/kubernetes-worker/setup.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +import os +from setuptools import setup + +here = os.path.abspath(os.path.dirname(__file__)) + +with open(os.path.join(here, "README.md")) as f: + README = f.read() + +setup( + name="layer_snap", + version="1.0.0", + description="layer_snap", + long_description=README, + license="Apache License 2.0", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + ], + url="https://git.launchpad.net/layer-snap", + package_dir={"": "lib"}, + packages=["charms/layer"], + include_package_data=True, + zip_safe=False, + install_requires=["charmhelpers", "charms.reactive"], +) diff --git a/kubernetes-worker/templates/cdk-service-kicker b/kubernetes-worker/templates/cdk-service-kicker new file mode 100644 index 0000000..26d3740 --- /dev/null +++ b/kubernetes-worker/templates/cdk-service-kicker @@ -0,0 +1,34 @@ +#!/bin/sh +set -eu + +# This service runs on boot to work around issues relating to LXD and snapd. + +# Workaround for https://github.com/conjure-up/conjure-up/issues/1448 +if [ -f '/proc/1/environ' ] && grep -q '^container=lxc' /proc/1/environ; then + echo "lxc detected, applying snapd apparmor profiles" + (set +e + apparmor_parser /var/lib/snapd/apparmor/profiles/* + echo "apparmor_parser: exit status $?" + ) +else + echo "lxc not detected, skipping snapd apparmor profiles" +fi + +# Workaround for https://github.com/juju-solutions/bundle-canonical-kubernetes/issues/357 +services="{{services}}" + +deadline="$(expr "$(date +%s)" + 600)" + +while [ "$(date +%s)" -lt "$deadline" ]; do + for service in $services; do + echo "$service: checking" + if ! systemctl is-active "$service"; then + echo "$service: not active, restarting" + systemctl restart "$service" || true + fi + done + + sleep 10 +done + +echo "deadline has passed, exiting gracefully" diff --git a/kubernetes-worker/templates/cdk-service-kicker.service b/kubernetes-worker/templates/cdk-service-kicker.service new file mode 100644 index 0000000..5c2105e --- /dev/null +++ b/kubernetes-worker/templates/cdk-service-kicker.service @@ -0,0 +1,10 @@ +[Unit] +Description=cdk-service-kicker + +[Service] +ExecStart=/usr/bin/cdk-service-kicker +Restart=on-failure +Type=simple + +[Install] +WantedBy=multi-user.target diff --git a/kubernetes-worker/templates/cdk.auth-webhook-secret.yaml b/kubernetes-worker/templates/cdk.auth-webhook-secret.yaml new file mode 100644 index 0000000..a12c402 --- /dev/null +++ b/kubernetes-worker/templates/cdk.auth-webhook-secret.yaml @@ -0,0 +1,13 @@ +# Manifest for CK secrets that auth-webhook expects +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ secret_name }} + namespace: {{ secret_namespace }} +type: {{ type }} +data: + uid: {{ user }} + username: {{ username }} + password: {{ password }} + groups: '{{ groups }}' diff --git a/kubernetes-worker/templates/default-http-backend.yaml b/kubernetes-worker/templates/default-http-backend.yaml new file mode 100644 index 0000000..4f1969f --- /dev/null +++ b/kubernetes-worker/templates/default-http-backend.yaml @@ -0,0 +1,62 @@ +apiVersion: {{ deployment_api_version }} +kind: Deployment +metadata: + name: default-http-backend-{{ juju_application }} + labels: + app.kubernetes.io/name: default-http-backend-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" + cdk-restart-on-ca-change: "true" + namespace: ingress-nginx-{{ juju_application }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: default-http-backend-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + template: + metadata: + labels: + app.kubernetes.io/name: default-http-backend-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + spec: + terminationGracePeriodSeconds: 60 + containers: + - name: default-http-backend-{{ juju_application }} + # Any image is permissible as long as: + # 1. It serves a 404 page at / + # 2. It serves 200 on a /healthz endpoint + image: {{ defaultbackend_image }} + livenessProbe: + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + initialDelaySeconds: 30 + timeoutSeconds: 5 + ports: + - containerPort: 8080 + resources: + limits: + cpu: 10m + memory: 20Mi + requests: + cpu: 10m + memory: 20Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: default-http-backend-{{ juju_application }} + namespace: ingress-nginx-{{ juju_application }} + labels: + app.kubernetes.io/name: default-http-backend-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" +spec: + ports: + - port: 80 + targetPort: 8080 + selector: + app.kubernetes.io/name: default-http-backend-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} diff --git a/kubernetes-worker/templates/ingress-daemon-set.yaml b/kubernetes-worker/templates/ingress-daemon-set.yaml new file mode 100644 index 0000000..72d0fcd --- /dev/null +++ b/kubernetes-worker/templates/ingress-daemon-set.yaml @@ -0,0 +1,356 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ingress-nginx-{{ juju_application }} + labels: + cdk-{{ juju_application }}-ingress: "true" + +{%- if default_ssl_certificate_option %} +--- +kind: Secret +apiVersion: v1 +type: Opaque +metadata: + name: default-ssl-certificate + namespace: ingress-nginx-{{ juju_application }} + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" +data: + tls.crt: {{ default_ssl_certificate }} + tls.key: {{ default_ssl_key }} +{%- endif %} + +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: nginx-configuration + namespace: ingress-nginx-{{ juju_application }} + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" +data: + use-forwarded-headers: "{{ use_forwarded_headers }}" + +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: tcp-services + namespace: ingress-nginx-{{ juju_application }} + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" + +--- +kind: ConfigMap +apiVersion: v1 +metadata: + name: udp-services + namespace: ingress-nginx-{{ juju_application }} + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nginx-ingress-serviceaccount-{{ juju_application }} + namespace: ingress-nginx-{{ juju_application }} + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: nginx-ingress-clusterrole-{{ juju_application }} + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" +rules: + - apiGroups: + - "" + resources: + - configmaps + - endpoints + - nodes + - pods + - secrets + verbs: + - list + - watch + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - apiGroups: + - extensions + - "networking.k8s.io" # # k8s 1.14+ + resources: + - ingresses + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - apiGroups: + - extensions + - "networking.k8s.io" # k8s 1.14+ + resources: + - ingresses/status + verbs: + - update + - apiGroups: + - "networking.k8s.io" # k8s 1.14+ + resources: + - ingressclasses + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: nginx-ingress-role-{{ juju_application }} + namespace: ingress-nginx-{{ juju_application }} + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" +rules: + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - apiGroups: + - "" + resources: + - configmaps + - pods + - secrets + - endpoints + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - apiGroups: + - extensions + - "networking.k8s.io" # k8s 1.14+ + resources: + - ingresses + verbs: + - get + - list + - watch + - apiGroups: + - extensions + - "networking.k8s.io" # k8s 1.14+ + resources: + - ingresses/status + verbs: + - update + - apiGroups: + - "networking.k8s.io" # k8s 1.14+ + resources: + - ingressclasses + verbs: + - get + - list + - watch + - apiGroups: + - "" + resourceNames: + - ingress-controller-leader + resources: + - configmaps + verbs: + - get + - update + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: nginx-ingress-role-nisa-binding-{{ juju_application }} + namespace: ingress-nginx-{{ juju_application }} + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: nginx-ingress-role-{{ juju_application }} +subjects: + - kind: ServiceAccount + name: nginx-ingress-serviceaccount-{{ juju_application }} + namespace: ingress-nginx-{{ juju_application }} + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: nginx-ingress-clusterrole-nisa-binding-{{ juju_application }} + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: nginx-ingress-clusterrole-{{ juju_application }} +subjects: + - kind: ServiceAccount + name: nginx-ingress-serviceaccount-{{ juju_application }} + namespace: ingress-nginx-{{ juju_application }} + +--- +apiVersion: {{ daemonset_api_version }} +kind: DaemonSet +metadata: + name: nginx-ingress-controller-{{ juju_application }} + namespace: ingress-nginx-{{ juju_application }} + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + juju-application: nginx-ingress-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" + cdk-restart-on-ca-change: "true" +spec: + selector: + matchLabels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + template: + metadata: + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + annotations: + prometheus.io/port: "10254" + prometheus.io/scrape: "true" + spec: + serviceAccountName: nginx-ingress-serviceaccount-{{ juju_application }} + nodeSelector: + juju-application: {{ juju_application }} + terminationGracePeriodSeconds: 60 + # hostPort doesn't work with CNI, so we have to use hostNetwork instead + # see https://github.com/kubernetes/kubernetes/issues/23920 + hostNetwork: true + containers: + - name: nginx-ingress-controller{{ juju_application }} + image: {{ ingress_image }} + args: + - /nginx-ingress-controller + - --configmap=$(POD_NAMESPACE)/nginx-configuration + - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services + - --udp-services-configmap=$(POD_NAMESPACE)/udp-services + - --annotations-prefix=nginx.ingress.kubernetes.io + - --enable-ssl-chain-completion={{ ssl_chain_completion }} + - --enable-ssl-passthrough={{ enable_ssl_passthrough }} +{%- if default_ssl_certificate_option %} + {{ default_ssl_certificate_option }} +{%- endif %} + securityContext: + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + runAsUser: {{ ingress_uid }} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + ports: + - name: http + containerPort: 80 + - name: https + containerPort: 443 + livenessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + +--- +apiVersion: networking.k8s.io/v1 +kind: IngressClass +metadata: + labels: + app.kubernetes.io/name: ingress-nginx-{{ juju_application }} + app.kubernetes.io/part-of: ingress-nginx-{{ juju_application }} + cdk-{{ juju_application }}-ingress: "true" + name: nginx-ingress-controller + annotations: + ingressclass.kubernetes.io/is-default-class: "true" +spec: + controller: k8s.io/ingress-nginx diff --git a/kubernetes-worker/templates/microbot-example.yaml b/kubernetes-worker/templates/microbot-example.yaml new file mode 100644 index 0000000..c89be53 --- /dev/null +++ b/kubernetes-worker/templates/microbot-example.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + creationTimestamp: null + labels: + app: microbot + name: microbot +spec: + replicas: {{ replicas }} + selector: + matchLabels: + app: microbot + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + app: microbot + spec: + containers: + - image: {{ registry|default("docker.io") }}/cdkbot/microbot-{{ arch }}:latest + imagePullPolicy: "" + name: microbot + ports: + - containerPort: 80 + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + timeoutSeconds: 30 + resources: {} + restartPolicy: Always + serviceAccountName: "" +status: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: microbot + labels: + app: microbot +spec: + ports: + - port: 80 + protocol: TCP + targetPort: 80 + selector: + app: microbot +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: microbot-ingress +spec: + rules: + - host: microbot.{{ public_address }}.nip.io + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: microbot + port: + number: 80 diff --git a/kubernetes-worker/templates/nagios_plugin.py b/kubernetes-worker/templates/nagios_plugin.py new file mode 100644 index 0000000..3e4a9b4 --- /dev/null +++ b/kubernetes-worker/templates/nagios_plugin.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2019 Canonical Ltd. + +import nagios_plugin3 +import yaml +from subprocess import check_output, CalledProcessError, PIPE + +snap_resources = ['kubectl', 'kubelet', 'kube-proxy'] + + +def check_snaps_installed(): + """Confirm the snaps are installed, raise an error if not""" + for snap_name in snap_resources: + cmd = ['snap', 'list', snap_name] + try: + check_output(cmd).decode('UTF-8') + except Exception: + msg = '{} snap is not installed'.format(snap_name) + raise nagios_plugin3.CriticalError(msg) + + +def check_node(node): + # Note: Keep the Ready check first since all checks will fail when not Ready + checks = [{'name': 'Ready', + 'expected': 'True', + 'type': 'error', + 'error': 'Node Not Ready'}, + {'name': 'MemoryPressure', + 'expected': 'False', + 'type': 'warn', + 'error': 'Memory Pressure'}, + {'name': 'DiskPressure', + 'expected': 'False', + 'type': 'warn', + 'error': 'Disk Pressure'}, + {'name': 'PIDPressure', + 'expected': 'False', + 'type': 'warn', + 'error': 'PID Pressure'}, + ] + msg = [] + error = False + for check in checks: + # find the status that matches + for s in node['status']['conditions']: + if s['type'] == check['name']: + # does it match expectations? If not, toss it on the list + # of errors so we don't show the first issue, but all. + if s['status'].lower() != check['expected'].lower(): + msg.append(check['error']) + if check['type'] == 'error': + error = True + break + else: + err_msg = 'Unable to find status for {}'.format(check['error']) + raise nagios_plugin3.CriticalError(err_msg) + + if msg: + if error: + raise nagios_plugin3.CriticalError(msg) + else: + raise nagios_plugin3.WarnError(msg) + + +def verify_node_registered_and_ready(): + node = None + try: + cmd = [ + "/snap/bin/kubectl", "--kubeconfig", "/var/lib/nagios/.kube/config", + "get", "no", "{{node_name}}", "-o=yaml" + ] + node = yaml.safe_load(check_output(cmd, stderr=PIPE)) + except CalledProcessError as e: + err = e.stderr.decode('UTF-8') + if "not found" in err: + raise nagios_plugin3.CriticalError("Unable to find " + "node registered on API server") + if not node: + raise nagios_plugin3.CriticalError("Unable to run kubectl " + "and parse output") + return check_node(node) + + +def main(): + nagios_plugin3.try_check(check_snaps_installed) + nagios_plugin3.try_check(verify_node_registered_and_ready) + print("OK - No memory, disk, or PID pressure. Registered with API server") + + +if __name__ == "__main__": + main() diff --git a/kubernetes-worker/templates/nfs-provisioner.yaml b/kubernetes-worker/templates/nfs-provisioner.yaml new file mode 100644 index 0000000..0f0c85c --- /dev/null +++ b/kubernetes-worker/templates/nfs-provisioner.yaml @@ -0,0 +1,103 @@ +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: default + annotations: + storageclass.kubernetes.io/is-default-class: "true" +provisioner: fuseim.pri/ifs +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: nfs-client-provisioner + labels: + app: nfs-client-provisioner +spec: + replicas: 1 + selector: + matchLabels: + app: nfs-client-provisioner + strategy: + type: Recreate + template: + metadata: + labels: + app: nfs-client-provisioner + spec: + serviceAccountName: nfs-client-provisioner + containers: + - name: nfs-client-provisioner + image: {{registry|default('quay.io')}}/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11 + volumeMounts: + - name: nfs-client-root + mountPath: /persistentvolumes + env: + - name: PROVISIONER_NAME + value: fuseim.pri/ifs + - name: NFS_SERVER + value: {{ hostname }} + - name: NFS_PATH + value: {{ mountpoint }} + volumes: + - name: nfs-client-root + nfs: + server: {{ hostname }} + path: {{ mountpoint }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nfs-client-provisioner +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: nfs-client-provisioner-runner +rules: + - apiGroups: [""] + resources: ["persistentvolumes"] + verbs: ["get", "list", "watch", "create", "delete"] + - apiGroups: [""] + resources: ["persistentvolumeclaims"] + verbs: ["get", "list", "watch", "update"] + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["events"] + verbs: ["create", "update", "patch"] +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: run-nfs-client-provisioner +subjects: + - kind: ServiceAccount + name: nfs-client-provisioner + namespace: default +roleRef: + kind: ClusterRole + name: nfs-client-provisioner-runner + apiGroup: rbac.authorization.k8s.io +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: leader-locking-nfs-client-provisioner +rules: + - apiGroups: [""] + resources: ["endpoints"] + verbs: ["get", "list", "watch", "create", "update", "patch"] +--- +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: leader-locking-nfs-client-provisioner +subjects: + - kind: ServiceAccount + name: nfs-client-provisioner + namespace: default +roleRef: + kind: Role + name: leader-locking-nfs-client-provisioner + apiGroup: rbac.authorization.k8s.io diff --git a/kubernetes-worker/tests/functional/conftest.py b/kubernetes-worker/tests/functional/conftest.py new file mode 100644 index 0000000..a92e249 --- /dev/null +++ b/kubernetes-worker/tests/functional/conftest.py @@ -0,0 +1,4 @@ +import charms.unit_test + + +charms.unit_test.patch_reactive() diff --git a/kubernetes-worker/tests/functional/test_k8s_common.py b/kubernetes-worker/tests/functional/test_k8s_common.py new file mode 100644 index 0000000..4b867e6 --- /dev/null +++ b/kubernetes-worker/tests/functional/test_k8s_common.py @@ -0,0 +1,90 @@ +from functools import partial + +import pytest +from unittest import mock +from charms.layer import kubernetes_common + + +class TestCreateKubeConfig: + @pytest.fixture(autouse=True) + def _files(self, tmp_path): + self.cfg_file = tmp_path / "config" + self.ca_file = tmp_path / "ca.crt" + self.ca_file.write_text("foo") + self.ckc = partial( + kubernetes_common.create_kubeconfig, + self.cfg_file, + "server", + self.ca_file, + ) + + def test_guard_clauses(self): + with pytest.raises(ValueError): + self.ckc() + assert not self.cfg_file.exists() + with pytest.raises(ValueError): + self.ckc(token="token", password="password") + assert not self.cfg_file.exists() + with pytest.raises(ValueError): + self.ckc(key="key") + assert not self.cfg_file.exists() + + def test_file_creation(self): + self.ckc(password="password") + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert cfg_data_1 + + def test_idempotency(self): + self.ckc(password="password") + cfg_data_1 = self.cfg_file.read_text() + self.ckc(password="password") + cfg_data_2 = self.cfg_file.read_text() + # Verify that calling w/ the same data keeps the same file contents. + assert cfg_data_2 == cfg_data_1 + + def test_efficient_updates(self): + self.ckc(password="old_password") + cfg_stat_1 = self.cfg_file.stat() + self.ckc(password="old_password") + cfg_stat_2 = self.cfg_file.stat() + self.ckc(password="new_password") + cfg_stat_3 = self.cfg_file.stat() + # Verify that calling with the same data doesn't + # modify the file at all, but that new data does + assert cfg_stat_1.st_mtime == cfg_stat_2.st_mtime < cfg_stat_3.st_mtime + + def test_aws_iam(self): + self.ckc(password="password", aws_iam_cluster_id="aws-cluster") + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert "aws-cluster" in cfg_data_1 + + def test_keystone(self): + self.ckc(password="password", keystone=True) + assert self.cfg_file.exists() + cfg_data_1 = self.cfg_file.read_text() + assert "keystone-user" in cfg_data_1 + assert "exec" in cfg_data_1 + + def test_atomic_updates(self): + self.ckc(password="old_password") + with self.cfg_file.open("rt") as f: + # Perform a write in the middle of reading + self.ckc(password="new_password") + # Read data from existing FH after new data was written + cfg_data_1 = f.read() + # Read updated data + cfg_data_2 = self.cfg_file.read_text() + # Verify that the in-progress read didn't get any of the new data + assert cfg_data_1 != cfg_data_2 + assert "old_password" in cfg_data_1 + assert "new_password" in cfg_data_2 + + @mock.patch("charmhelpers.core.hookenv.network_get", autospec=True) + def test_get_ingress_address(self, network_get): + network_get.return_value = {"ingress-addresses": ["1.2.3.4", "5.6.7.8"]} + ingress = kubernetes_common.get_ingress_address("endpoint-name") + assert ingress == "1.2.3.4" + ingress = kubernetes_common.get_ingress_address("endpoint-name", ["1.2.3.4"]) + assert ingress == "5.6.7.8" diff --git a/kubernetes-worker/tests/unit/conftest.py b/kubernetes-worker/tests/unit/conftest.py new file mode 100644 index 0000000..a92e249 --- /dev/null +++ b/kubernetes-worker/tests/unit/conftest.py @@ -0,0 +1,4 @@ +import charms.unit_test + + +charms.unit_test.patch_reactive() diff --git a/kubernetes-worker/tests/unit/test_k8s_common.py b/kubernetes-worker/tests/unit/test_k8s_common.py new file mode 100644 index 0000000..0dcad31 --- /dev/null +++ b/kubernetes-worker/tests/unit/test_k8s_common.py @@ -0,0 +1,122 @@ +import json +import string +from subprocess import CalledProcessError +from unittest.mock import Mock + +from charms.layer import kubernetes_common as kc + + +def test_token_generator(): + alphanum = string.ascii_letters + string.digits + token = kc.token_generator(10) + assert len(token) == 10 + unknown_chars = set(token) - set(alphanum) + assert not unknown_chars + + +def test_get_secret_names(monkeypatch): + monkeypatch.setattr(kc, "kubectl", Mock()) + kc.kubectl.side_effect = [ + CalledProcessError(1, "none"), + FileNotFoundError, + "{}".encode("utf8"), + json.dumps( + { + "items": [ + { + "metadata": {"name": "secret-id"}, + "data": {"username": "dXNlcg=="}, + }, + ], + } + ).encode("utf8"), + ] + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {} + assert kc.get_secret_names() == {"user": "secret-id"} + + +def test_generate_rfc1123(): + alphanum = string.ascii_letters + string.digits + token = kc.generate_rfc1123(1000) + assert len(token) == 253 + unknown_chars = set(token) - set(alphanum) + assert not unknown_chars + + +def test_create_secret(monkeypatch): + monkeypatch.setattr(kc, "render", Mock()) + monkeypatch.setattr(kc, "kubectl_manifest", Mock()) + monkeypatch.setattr(kc, "get_secret_names", Mock()) + monkeypatch.setattr(kc, "generate_rfc1123", Mock()) + kc.kubectl_manifest.side_effect = [True, False] + kc.get_secret_names.side_effect = [{"username": "secret-id"}, {}] + kc.generate_rfc1123.return_value = "foo" + assert kc.create_secret("token", "username", "user", "groups") + assert kc.render.call_args[1]["context"] == { + "groups": "Z3JvdXBz", + "password": "dXNlcjo6dG9rZW4=", + "secret_name": "secret-id", + "secret_namespace": "kube-system", + "type": "juju.is/token-auth", + "user": "dXNlcg==", + "username": "dXNlcm5hbWU=", + } + assert not kc.create_secret("token", "username", "user", "groups") + assert kc.render.call_args[1]["context"] == { + "groups": "Z3JvdXBz", + "password": "dXNlcjo6dG9rZW4=", + "secret_name": "auth-user-foo", + "secret_namespace": "kube-system", + "type": "juju.is/token-auth", + "user": "dXNlcg==", + "username": "dXNlcm5hbWU=", + } + + +def test_get_secret_password(monkeypatch): + monkeypatch.setattr(kc, "kubectl", Mock()) + monkeypatch.setattr(kc, "Path", Mock()) + monkeypatch.setattr(kc, "yaml", Mock()) + kc.kubectl.side_effect = [ + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + CalledProcessError(1, "none"), + FileNotFoundError, + json.dumps({}).encode("utf8"), + json.dumps({"items": []}).encode("utf8"), + json.dumps({"items": []}).encode("utf8"), + json.dumps({"items": [{}]}).encode("utf8"), + json.dumps({"items": [{"data": {}}]}).encode("utf8"), + json.dumps( + {"items": [{"data": {"username": "Ym9i", "password": "c2VjcmV0"}}]} + ).encode("utf8"), + json.dumps( + {"items": [{"data": {"username": "dXNlcm5hbWU=", "password": "c2VjcmV0"}}]} + ).encode("utf8"), + ] + kc.yaml.safe_load.side_effect = [ + {}, + {"users": None}, + {"users": []}, + {"users": [{"user": {}}]}, + {"users": [{"user": {"token": "secret"}}]}, + ] + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") is None + assert kc.get_secret_password("admin") == "secret" + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") is None + assert kc.get_secret_password("username") == "secret" diff --git a/kubernetes-worker/tox.ini b/kubernetes-worker/tox.ini new file mode 100644 index 0000000..a6671d4 --- /dev/null +++ b/kubernetes-worker/tox.ini @@ -0,0 +1,35 @@ +[flake8] +max-line-length = 88 + +[tox] +skipsdist = True +envlist = lint,unit,functional + +[testenv] +setenv = + PYTHONPATH={toxinidir}:{toxinidir}/lib + PYTHONBREAKPOINT=ipdb.set_trace + +[testenv:lint] +deps = + black + flake8 +commands = + flake8 {toxinidir}/lib {toxinidir}/tests + black --check {toxinidir}/lib {toxinidir}/tests + +[testenv:unit] +deps = + pyyaml + pytest + charms.unit_test + ipdb +commands = pytest -vv --tb native -s {posargs} {toxinidir}/tests/unit + +[testenv:functional] +deps = + pyyaml + pytest + charms.unit_test + ipdb +commands = pytest -vv --tb native -s {posargs} {toxinidir}/tests/functional diff --git a/kubernetes-worker/version b/kubernetes-worker/version new file mode 100644 index 0000000..20817dd --- /dev/null +++ b/kubernetes-worker/version @@ -0,0 +1 @@ +ccfa68be \ No newline at end of file diff --git a/kubernetes-worker/wheelhouse.txt b/kubernetes-worker/wheelhouse.txt new file mode 100644 index 0000000..4d586af --- /dev/null +++ b/kubernetes-worker/wheelhouse.txt @@ -0,0 +1,27 @@ +# layer:basic +# pip is pinned to <19.0 to avoid https://github.com/pypa/pip/issues/6164 +# even with installing setuptools before upgrading pip ends up with pip seeing +# the older setuptools at the system level if include_system_packages is true +pip>=18.1,<19.0 +# pin Jinja2, PyYAML and MarkupSafe to the last versions supporting python 3.5 +# for trusty +Jinja2<=2.10.1 +PyYAML<=5.2 +MarkupSafe<2.0.0 +setuptools<42 +setuptools-scm<=1.17.0 +charmhelpers>=0.4.0,<1.0.0 +charms.reactive>=0.1.0,<2.0.0 +wheel<0.34 +# pin netaddr to avoid pulling importlib-resources +netaddr<=0.7.19 + +# layer:snap +# Newer versions of tenacity rely on `typing` which is in stdlib in +# python3.5 but not python3.4. We want to continue to support +# python3.4 (Trusty) +tenacity<5.0.4 + +# kubernetes-worker +charms.templating.jinja2>=0.0.1,<2.0.0 + diff --git a/kubernetes-worker/wheelhouse/Jinja2-2.10.1.tar.gz b/kubernetes-worker/wheelhouse/Jinja2-2.10.1.tar.gz new file mode 100644 index 0000000..ffd1054 Binary files /dev/null and b/kubernetes-worker/wheelhouse/Jinja2-2.10.1.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/MarkupSafe-1.1.1.tar.gz b/kubernetes-worker/wheelhouse/MarkupSafe-1.1.1.tar.gz new file mode 100644 index 0000000..a6dad8e Binary files /dev/null and b/kubernetes-worker/wheelhouse/MarkupSafe-1.1.1.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/PyYAML-5.2.tar.gz b/kubernetes-worker/wheelhouse/PyYAML-5.2.tar.gz new file mode 100644 index 0000000..666d12a Binary files /dev/null and b/kubernetes-worker/wheelhouse/PyYAML-5.2.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/Tempita-0.5.2.tar.gz b/kubernetes-worker/wheelhouse/Tempita-0.5.2.tar.gz new file mode 100644 index 0000000..755befc Binary files /dev/null and b/kubernetes-worker/wheelhouse/Tempita-0.5.2.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/charmhelpers-0.20.23.tar.gz b/kubernetes-worker/wheelhouse/charmhelpers-0.20.23.tar.gz new file mode 100644 index 0000000..8fbc8ec Binary files /dev/null and b/kubernetes-worker/wheelhouse/charmhelpers-0.20.23.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/charms.reactive-1.4.1.tar.gz b/kubernetes-worker/wheelhouse/charms.reactive-1.4.1.tar.gz new file mode 100644 index 0000000..03bc1fe Binary files /dev/null and b/kubernetes-worker/wheelhouse/charms.reactive-1.4.1.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/charms.templating.jinja2-1.0.2.tar.gz b/kubernetes-worker/wheelhouse/charms.templating.jinja2-1.0.2.tar.gz new file mode 100644 index 0000000..5c03a81 Binary files /dev/null and b/kubernetes-worker/wheelhouse/charms.templating.jinja2-1.0.2.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/netaddr-0.7.19.tar.gz b/kubernetes-worker/wheelhouse/netaddr-0.7.19.tar.gz new file mode 100644 index 0000000..cc31d9d Binary files /dev/null and b/kubernetes-worker/wheelhouse/netaddr-0.7.19.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/pbr-5.6.0.tar.gz b/kubernetes-worker/wheelhouse/pbr-5.6.0.tar.gz new file mode 100644 index 0000000..0d5c965 Binary files /dev/null and b/kubernetes-worker/wheelhouse/pbr-5.6.0.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/pip-18.1.tar.gz b/kubernetes-worker/wheelhouse/pip-18.1.tar.gz new file mode 100644 index 0000000..a18192d Binary files /dev/null and b/kubernetes-worker/wheelhouse/pip-18.1.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/pyaml-21.10.1.tar.gz b/kubernetes-worker/wheelhouse/pyaml-21.10.1.tar.gz new file mode 100644 index 0000000..b19aad3 Binary files /dev/null and b/kubernetes-worker/wheelhouse/pyaml-21.10.1.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/setuptools-41.6.0.zip b/kubernetes-worker/wheelhouse/setuptools-41.6.0.zip new file mode 100644 index 0000000..3345759 Binary files /dev/null and b/kubernetes-worker/wheelhouse/setuptools-41.6.0.zip differ diff --git a/kubernetes-worker/wheelhouse/setuptools_scm-1.17.0.tar.gz b/kubernetes-worker/wheelhouse/setuptools_scm-1.17.0.tar.gz new file mode 100644 index 0000000..43b16c7 Binary files /dev/null and b/kubernetes-worker/wheelhouse/setuptools_scm-1.17.0.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/six-1.16.0.tar.gz b/kubernetes-worker/wheelhouse/six-1.16.0.tar.gz new file mode 100644 index 0000000..5bf3a27 Binary files /dev/null and b/kubernetes-worker/wheelhouse/six-1.16.0.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/tenacity-5.0.3.tar.gz b/kubernetes-worker/wheelhouse/tenacity-5.0.3.tar.gz new file mode 100644 index 0000000..c7d05ba Binary files /dev/null and b/kubernetes-worker/wheelhouse/tenacity-5.0.3.tar.gz differ diff --git a/kubernetes-worker/wheelhouse/wheel-0.33.6.tar.gz b/kubernetes-worker/wheelhouse/wheel-0.33.6.tar.gz new file mode 100644 index 0000000..c922c4e Binary files /dev/null and b/kubernetes-worker/wheelhouse/wheel-0.33.6.tar.gz differ