~containers/kubeapi-load-balancer

Owner: mbruzek
Status: Needs Review
Vote: +0 (+2 needed for approval)

CPP?: No
OIL?: No

This is our attempt at getting the kubernetes charms promulgated. Please review this charm for policy and best practices.

Thanks.


Tests

Substrate Status Results Last Updated
aws RETRY 19 days ago
gce RETRY 19 days ago
lxc RETRY 19 days ago

Add Comment

Login to comment/vote on this review.


Policy Checklist

Description Unreviewed Pass Fail

General

Must verify that any software installed or utilized is verified as coming from the intended source. kos.tsakalozos
  • Any software installed from the Ubuntu or CentOS default archives satisfies this due to the apt and yum sources including cryptographic signing information.
  • Third party repositories must be listed as a configuration option that can be overridden by the user and not hard coded in the charm itself.
  • Launchpad PPAs are acceptable as the add-apt-repository command retrieves the keys securely.
  • Other third party repositories are acceptable if the signing key is embedded in the charm.
Must provide a means to protect users from known security vulnerabilities in a way consistent with best practices as defined by either operating system policies or upstream documentation. kos.tsakalozos
Basically, this means there must be instructions on how to apply updates if you use software not from distribution channels.
Must have hooks that are idempotent. kos.tsakalozos
Should be built using charm layers. kos.tsakalozos
Should use Juju Resources to deliver required payloads. kos.tsakalozos

Testing and Quality

charm proof must pass without errors or warnings. kos.tsakalozos
Must include passing unit, functional, or integration tests. kos.tsakalozos
Tests must exercise all relations. kos.tsakalozos
Tests must exercise config. kos.tsakalozos
set-config, unset-config, and re-set must be tested as a minimum
Must not use anything infrastructure-provider specific (i.e. querying EC2 metadata service). kos.tsakalozos
Must be self contained unless the charm is a proxy for an existing cloud service, e.g. ec2-elb charm.
Must not use symlinks. kos.tsakalozos
Bundles must only use promulgated charms, they cannot reference charms in personal namespaces. kos.tsakalozos
Must call Juju hook tools (relation-*, unit-*, config-*, etc) without a hard coded path. kos.tsakalozos
Should include a tests.yaml for all integration tests. kos.tsakalozos

Metadata

Must include a full description of what the software does. kos.tsakalozos
Must include a maintainer email address for a team or individual who will be responsive to contact. kos.tsakalozos
Must include a license. Call the file 'copyright' and make sure all files' licenses are specified clearly. kos.tsakalozos
Must be under a Free license. kos.tsakalozos
Must have a well documented and valid README.md. kos.tsakalozos
Must describe the service. kos.tsakalozos
Must describe how it interacts with other services, if applicable. kos.tsakalozos
Must document the interfaces. kos.tsakalozos
Must show how to deploy the charm. kos.tsakalozos
Must define external dependencies, if applicable. kos.tsakalozos
Should link to a recommend production usage bundle and recommended configuration if this differs from the default.
Should reference and link to upstream documentation and best practices.

Security

Must not run any network services using default passwords. kos.tsakalozos
Must verify and validate any external payload kos.tsakalozos
  • Known and understood packaging systems that verify packages like apt, pip, and yum are ok.
  • wget | sh style is not ok.
Should make use of whatever Mandatory Access Control system is provided by the distribution. kos.tsakalozos
Should avoid running services as root. kos.tsakalozos

All changes | Changes since last revision

Source Diff

Files changed 90

Inline diff comments 0

No comments yet.

Back to file index

AUTHORS

1
--- 
2
+++ AUTHORS
3
@@ -0,0 +1,2 @@
4
+Adam Stokes <adam.stokes@ubuntu.com>
5
+Marco Ceppi <marco@ceppi.net>
Back to file index

Makefile

 1
--- 
 2
+++ Makefile
 3
@@ -0,0 +1,24 @@
 4
+#!/usr/bin/make
 5
+
 6
+all: lint unit_test
 7
+
 8
+
 9
+.PHONY: clean
10
+clean:
11
+	@rm -rf .tox
12
+
13
+.PHONY: apt_prereqs
14
+apt_prereqs:
15
+	@# Need tox, but don't install the apt version unless we have to (don't want to conflict with pip)
16
+	@which tox >/dev/null || (sudo apt-get install -y python-pip && sudo pip install tox)
17
+
18
+.PHONY: lint
19
+lint: apt_prereqs
20
+	@tox --notest
21
+	@PATH=.tox/py34/bin:.tox/py35/bin flake8 $(wildcard hooks reactive lib unit_tests tests)
22
+	@charm proof
23
+
24
+.PHONY: unit_test
25
+unit_test: apt_prereqs
26
+	@echo Starting tests...
27
+	tox
Back to file index

README.md

1
--- 
2
+++ README.md
3
@@ -0,0 +1,5 @@
4
+# kubeapi-load-balancer
5
+
6
+Simple NGINX reverse proxy to lend a hand in HA kubernetes-master deployments.
7
+
8
+
Back to file index

bin/layer_option

 1
--- 
 2
+++ bin/layer_option
 3
@@ -0,0 +1,24 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+import sys
 7
+sys.path.append('lib')
 8
+
 9
+import argparse
10
+from charms.layer import options
11
+
12
+
13
+parser = argparse.ArgumentParser(description='Access layer options.')
14
+parser.add_argument('section',
15
+                    help='the section, or layer, the option is from')
16
+parser.add_argument('option',
17
+                    help='the option to access')
18
+
19
+args = parser.parse_args()
20
+value = options(args.section).get(args.option, '')
21
+if isinstance(value, bool):
22
+    sys.exit(0 if value else 1)
23
+elif isinstance(value, list):
24
+    for val in value:
25
+        print(val)
26
+else:
27
+    print(value)
Back to file index

config.yaml

 1
--- 
 2
+++ config.yaml
 3
@@ -0,0 +1,51 @@
 4
+"options":
 5
+  "nagios_context":
 6
+    "default": "juju"
 7
+    "type": "string"
 8
+    "description": |
 9
+      Used by the nrpe subordinate charms.
10
+      A string that will be prepended to instance name to set the host name
11
+      in nagios. So for instance the hostname would be something like:
12
+          juju-myservice-0
13
+      If you're running multiple environments with the same services in them
14
+      this allows you to differentiate between them.
15
+  "nagios_servicegroups":
16
+    "default": ""
17
+    "type": "string"
18
+    "description": |
19
+      A comma-separated list of nagios servicegroups.
20
+      If left empty, the nagios_context will be used as the servicegroup
21
+  "extra_packages":
22
+    "description": "Space separated list of extra deb packages to install.\n"
23
+    "type": "string"
24
+    "default": ""
25
+  "package_status":
26
+    "default": "install"
27
+    "type": "string"
28
+    "description": "The status of service-affecting packages will be set to this value\
29
+      \ in the dpkg database. Valid values are \"install\" and \"hold\".\n"
30
+  "install_sources":
31
+    "description": "List of extra apt sources, per charm-helpers standard format (a\
32
+      \ yaml list of strings encoded as a string). Each source may be either a line\
33
+      \ that can be added directly to sources.list(5), or in the form ppa:<user>/<ppa-name>\
34
+      \ for adding Personal Package Archives, or a distribution component to enable.\n"
35
+    "type": "string"
36
+    "default": ""
37
+  "install_keys":
38
+    "description": "List of signing keys for install_sources package sources, per\
39
+      \ charmhelpers standard format (a yaml list of strings encoded as a string).\
40
+      \ The keys should be the full ASCII armoured GPG public keys. While GPG key\
41
+      \ ids are also supported and looked up on a keyserver, operators should be aware\
42
+      \ that this mechanism is insecure. null can be used if a standard package signing\
43
+      \ key is used that will already be installed on the machine, and for PPA sources\
44
+      \ where the package signing key is securely retrieved from Launchpad.\n"
45
+    "type": "string"
46
+    "default": ""
47
+  "port":
48
+    "type": "int"
49
+    "default": !!int "443"
50
+    "description": "The port to run the loadbalancer"
51
+  "host":
52
+    "type": "string"
53
+    "default": "127.0.0.1"
54
+    "description": "listen address"
Back to file index

copyright

 1
--- 
 2
+++ copyright
 3
@@ -0,0 +1,13 @@
 4
+Copyright 2016 The Kubernetes Authors.
 5
+
 6
+ Licensed under the Apache License, Version 2.0 (the "License");
 7
+ you may not use this file except in compliance with the License.
 8
+ You may obtain a copy of the License at
 9
+
10
+     http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+ Unless required by applicable law or agreed to in writing, software
13
+ distributed under the License is distributed on an "AS IS" BASIS,
14
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ See the License for the specific language governing permissions and
16
+ limitations under the License.
Back to file index

hooks/apiserver-relation-broken

 1
--- 
 2
+++ hooks/apiserver-relation-broken
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/apiserver-relation-changed

 1
--- 
 2
+++ hooks/apiserver-relation-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/apiserver-relation-departed

 1
--- 
 2
+++ hooks/apiserver-relation-departed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/apiserver-relation-joined

 1
--- 
 2
+++ hooks/apiserver-relation-joined
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/certificates-relation-broken

 1
--- 
 2
+++ hooks/certificates-relation-broken
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/certificates-relation-changed

 1
--- 
 2
+++ hooks/certificates-relation-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/certificates-relation-departed

 1
--- 
 2
+++ hooks/certificates-relation-departed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/certificates-relation-joined

 1
--- 
 2
+++ hooks/certificates-relation-joined
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/collect-metrics

 1
--- 
 2
+++ hooks/collect-metrics
 3
@@ -0,0 +1,38 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+import yaml
11
+import os
12
+from subprocess import check_output, check_call
13
+
14
+
15
+def build_command(doc):
16
+    values = {}
17
+    metrics = doc.get("metrics", {})
18
+    for metric, mdoc in metrics.items():
19
+        cmd = mdoc.get("command")
20
+        if cmd:
21
+            value = check_output(cmd, shell=True, universal_newlines=True)
22
+            value = value.strip()
23
+            if value:
24
+                values[metric] = value
25
+
26
+    if not values:
27
+        return None
28
+    command = ["add-metric"]
29
+    for metric, value in values.items():
30
+        command.append("%s=%s" % (metric, value))
31
+    return command
32
+
33
+
34
+if __name__ == '__main__':
35
+    charm_dir = os.path.dirname(os.path.abspath(os.path.join(__file__, "..")))
36
+    metrics_yaml = os.path.join(charm_dir, "metrics.yaml")
37
+    with open(metrics_yaml) as f:
38
+        doc = yaml.load(f)
39
+        command = build_command(doc)
40
+        if command:
41
+            check_call(command)
Back to file index

hooks/config-changed

 1
--- 
 2
+++ hooks/config-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/hook.template

 1
--- 
 2
+++ hooks/hook.template
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/install

 1
--- 
 2
+++ hooks/install
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/leader-elected

 1
--- 
 2
+++ hooks/leader-elected
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/leader-settings-changed

 1
--- 
 2
+++ hooks/leader-settings-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/loadbalancer-relation-broken

 1
--- 
 2
+++ hooks/loadbalancer-relation-broken
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/loadbalancer-relation-changed

 1
--- 
 2
+++ hooks/loadbalancer-relation-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/loadbalancer-relation-departed

 1
--- 
 2
+++ hooks/loadbalancer-relation-departed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/loadbalancer-relation-joined

 1
--- 
 2
+++ hooks/loadbalancer-relation-joined
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/nrpe-external-master-relation-broken

 1
--- 
 2
+++ hooks/nrpe-external-master-relation-broken
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/nrpe-external-master-relation-changed

 1
--- 
 2
+++ hooks/nrpe-external-master-relation-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/nrpe-external-master-relation-departed

 1
--- 
 2
+++ hooks/nrpe-external-master-relation-departed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/nrpe-external-master-relation-joined

 1
--- 
 2
+++ hooks/nrpe-external-master-relation-joined
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/relations/http/README.md

 1
--- 
 2
+++ hooks/relations/http/README.md
 3
@@ -0,0 +1,68 @@
 4
+# Overview
 5
+
 6
+This interface layer implements the basic form of the `http` interface protocol,
 7
+which is used for things such as reverse-proxies, load-balanced servers, REST
 8
+service discovery, et cetera.
 9
+
10
+# Usage
11
+
12
+## Provides
13
+
14
+By providing the `http` interface, your charm is providing an HTTP server that
15
+can be load-balanced, reverse-proxied, used as a REST endpoint, etc.
16
+
17
+Your charm need only provide the port on which it is serving its content, as
18
+soon as the `{relation_name}.available` state is set:
19
+
20
+```python
21
+@when('website.available')
22
+def configure_website(website):
23
+    website.configure(port=hookenv.config('port'))
24
+```
25
+
26
+## Requires
27
+
28
+By requiring the `http` interface, your charm is consuming one or more HTTP
29
+servers, as a REST endpoint, to load-balance a set of servers, etc.
30
+
31
+Your charm should respond to the `{relation_name}.available` state, which
32
+indicates that there is at least one HTTP server connected.
33
+
34
+The `services()` method returns a list of available HTTP services and their
35
+associated hosts and ports.
36
+
37
+The return value is a list of dicts of the following form:
38
+
39
+```python
40
+[
41
+    {
42
+        'service_name': name_of_service,
43
+        'hosts': [
44
+            {
45
+                'hostname': address_of_host,
46
+                'port': port_for_host,
47
+            },
48
+            # ...
49
+        ],
50
+    },
51
+    # ...
52
+]
53
+```
54
+
55
+A trivial example of handling this interface would be:
56
+
57
+```python
58
+from charms.reactive.helpers import data_changed
59
+
60
+@when('reverseproxy.available')
61
+def update_reverse_proxy_config(reverseproxy):
62
+    services = reverseproxy.services()
63
+    if not data_changed('reverseproxy.services', services):
64
+        return
65
+    for service in services:
66
+        for host in service['hosts']:
67
+            hookenv.log('{} has a unit {}:{}'.format(
68
+                services['service_name'],
69
+                host['hostname'],
70
+                host['port']))
71
+```
Back to file index

hooks/relations/http/interface.yaml

1
--- 
2
+++ hooks/relations/http/interface.yaml
3
@@ -0,0 +1,4 @@
4
+name: http
5
+summary: Basic HTTP interface
6
+version: 1
7
+repo: https://git.launchpad.net/~bcsaller/charms/+source/http
Back to file index

hooks/relations/http/provides.py

 1
--- 
 2
+++ hooks/relations/http/provides.py
 3
@@ -0,0 +1,28 @@
 4
+from charmhelpers.core import hookenv
 5
+from charms.reactive import hook
 6
+from charms.reactive import RelationBase
 7
+from charms.reactive import scopes
 8
+
 9
+
10
+class HttpProvides(RelationBase):
11
+    scope = scopes.GLOBAL
12
+
13
+    @hook('{provides:http}-relation-{joined,changed}')
14
+    def changed(self):
15
+        self.set_state('{relation_name}.available')
16
+
17
+    @hook('{provides:http}-relation-{broken,departed}')
18
+    def broken(self):
19
+        self.remove_state('{relation_name}.available')
20
+
21
+    def configure(self, port, private_address=None, hostname=None):
22
+        if not hostname:
23
+            hostname = hookenv.unit_get('private-address')
24
+        if not private_address:
25
+            private_address = hookenv.unit_get('private-address')
26
+        relation_info = {
27
+            'hostname': hostname,
28
+            'private-address': private_address,
29
+            'port': port,
30
+        }
31
+        self.set_remote(**relation_info)
Back to file index

hooks/relations/http/requires.py

 1
--- 
 2
+++ hooks/relations/http/requires.py
 3
@@ -0,0 +1,58 @@
 4
+from charms.reactive import hook
 5
+from charms.reactive import RelationBase
 6
+from charms.reactive import scopes
 7
+
 8
+
 9
+class HttpRequires(RelationBase):
10
+    scope = scopes.UNIT
11
+
12
+    @hook('{requires:http}-relation-{joined,changed}')
13
+    def changed(self):
14
+        conv = self.conversation()
15
+        if conv.get_remote('port'):
16
+            # this unit's conversation has a port, so
17
+            # it is part of the set of available units
18
+            conv.set_state('{relation_name}.available')
19
+
20
+    @hook('{requires:http}-relation-{departed,broken}')
21
+    def broken(self):
22
+        conv = self.conversation()
23
+        conv.remove_state('{relation_name}.available')
24
+
25
+    def services(self):
26
+        """
27
+        Returns a list of available HTTP services and their associated hosts
28
+        and ports.
29
+
30
+        The return value is a list of dicts of the following form::
31
+
32
+            [
33
+                {
34
+                    'service_name': name_of_service,
35
+                    'hosts': [
36
+                        {
37
+                            'hostname': address_of_host,
38
+                            'port': port_for_host,
39
+                        },
40
+                        # ...
41
+                    ],
42
+                },
43
+                # ...
44
+            ]
45
+        """
46
+        services = {}
47
+        for conv in self.conversations():
48
+            service_name = conv.scope.split('/')[0]
49
+            service = services.setdefault(service_name, {
50
+                'service_name': service_name,
51
+                'hosts': [],
52
+            })
53
+            host = conv.get_remote('hostname') or \
54
+                conv.get_remote('private-address')
55
+            port = conv.get_remote('port')
56
+            if host and port:
57
+                service['hosts'].append({
58
+                    'hostname': host,
59
+                    'port': port,
60
+                })
61
+        return [s for s in services.values() if s['hosts']]
Back to file index

hooks/relations/nrpe-external-master/README.md

 1
--- 
 2
+++ hooks/relations/nrpe-external-master/README.md
 3
@@ -0,0 +1,65 @@
 4
+# nrpe-external-master interface
 5
+
 6
+Use this interface to register nagios checks in your charm layers.
 7
+
 8
+## Purpose
 9
+
10
+This interface is designed to interoperate with the
11
+[nrpe-external-master](https://jujucharms.com/nrpe-external-master) subordinate charm.
12
+
13
+## How to use in your layers
14
+
15
+The event handler for `nrpe-external-master.available` is called with an object
16
+through which you can register your own custom nagios checks, when a relation
17
+is established with `nrpe-external-master:nrpe-external-master`.
18
+
19
+This object provides a method,
20
+
21
+_add_check_(args, name=_check_name_, description=_description_, context=_context_, unit=_unit_)
22
+
23
+which is called to register a nagios plugin check for your service.
24
+
25
+All arguments are required.
26
+
27
+*args* is a list of nagios plugin command line arguments, starting with the path to the plugin executable.
28
+
29
+*name* is the name of the check registered in nagios
30
+
31
+*description* is some text that describes what the check is for and what it does
32
+
33
+*context* is the nagios context name, something that identifies your application
34
+
35
+*unit* is `hookenv.local_unit()`
36
+
37
+The nrpe subordinate installs `check_http`, so you can use it like this:
38
+
39
+```
40
+@when('nrpe-external-master.available')
41
+def setup_nagios(nagios):
42
+    config = hookenv.config()
43
+    unit_name = hookenv.local_unit()
44
+    nagios.add_check(['/usr/lib/nagios/plugins/check_http',
45
+            '-I', '127.0.0.1', '-p', str(config['port']),
46
+            '-e', " 200 OK", '-u', '/publickey'],
47
+        name="check_http",
48
+        description="Verify my awesome service is responding",
49
+        context=config["nagios_context"],
50
+        unit=unit_name,
51
+    )
52
+```
53
+
54
+Consult the nagios documentation for more information on [how to write your own
55
+plugins](https://assets.nagios.com/downloads/nagioscore/docs/nagioscore/4/en/pluginapi.html)
56
+or [find one](https://www.nagios.org/projects/nagios-plugins/) that does what you need.
57
+
58
+## Example deployment
59
+
60
+```
61
+$ juju deploy your-awesome-charm
62
+$ juju deploy nrpe-external-master --config site-nagios.yaml
63
+$ juju add-relation your-awesome-charm nrpe-external-master
64
+```
65
+
66
+where `site-nagios.yaml` has the necessary configuration settings for the
67
+subordinate to connect to nagios.
68
+
Back to file index

hooks/relations/nrpe-external-master/interface.yaml

1
--- 
2
+++ hooks/relations/nrpe-external-master/interface.yaml
3
@@ -0,0 +1,3 @@
4
+name: nrpe-external-master
5
+summary: Nagios interface
6
+version: 1
Back to file index

hooks/relations/nrpe-external-master/provides.py

 1
--- 
 2
+++ hooks/relations/nrpe-external-master/provides.py
 3
@@ -0,0 +1,62 @@
 4
+import datetime
 5
+
 6
+from charms.reactive import hook
 7
+from charms.reactive import RelationBase
 8
+from charms.reactive import scopes
 9
+
10
+
11
+class NrpeExternalMasterProvides(RelationBase):
12
+    scope = scopes.GLOBAL
13
+
14
+    @hook('{provides:nrpe-external-master}-relation-{joined,changed}')
15
+    def changed_nrpe(self):
16
+        self.set_state('{relation_name}.available')
17
+
18
+    @hook('{provides:nrpe-external-master}-relation-{broken,departed}')
19
+    def broken_nrpe(self):
20
+        self.remove_state('{relation_name}.available')
21
+
22
+    def add_check(self, args, name=None, description=None, context=None,
23
+                  servicegroups=None, unit=None):
24
+        unit = unit.replace('/', '-')
25
+        check_tmpl = """
26
+#---------------------------------------------------
27
+# This file is Juju managed
28
+#---------------------------------------------------
29
+command[%(check_name)s]=%(check_args)s
30
+"""
31
+        service_tmpl = """
32
+#---------------------------------------------------
33
+# This file is Juju managed
34
+#---------------------------------------------------
35
+define service {
36
+    use                             active-service
37
+    host_name                       %(context)s-%(unit_name)s
38
+    service_description             %(description)s
39
+    check_command                   check_nrpe!%(check_name)s
40
+    servicegroups                   %(servicegroups)s
41
+}
42
+"""
43
+        check_filename = "/etc/nagios/nrpe.d/%s.cfg" % (name)
44
+        with open(check_filename, "w") as fh:
45
+            fh.write(check_tmpl % {
46
+                'check_args': ' '.join(args),
47
+                'check_name': name,
48
+            })
49
+        service_filename = "/var/lib/nagios/export/service__%s_%s.cfg" % (
50
+                           unit, name)
51
+        with open(service_filename, "w") as fh:
52
+            fh.write(service_tmpl % {
53
+                'servicegroups': servicegroups or context,
54
+                'context': context,
55
+                'description': description,
56
+                'check_name': name,
57
+                'unit_name': unit,
58
+            })
59
+
60
+    def updated(self):
61
+        relation_info = {
62
+            'timestamp': datetime.datetime.now().isoformat(),
63
+        }
64
+        self.set_remote(**relation_info)
65
+        self.remove_state('{relation_name}.available')
Back to file index

hooks/relations/public-address/README.md

 1
--- 
 2
+++ hooks/relations/public-address/README.md
 3
@@ -0,0 +1,59 @@
 4
+# Overview
 5
+
 6
+This interface layer implements a public address protocol useful for load 
 7
+balancers and their subordinates. The load balancers (providers) set their 
 8
+own public address and port, which is then available to the subordinates 
 9
+(requirers).
10
+
11
+# Usage
12
+
13
+## Provides
14
+
15
+By providing the `public-address` interface, your charm is providing an HTTP 
16
+server that can load-balance for another HTTP based service.
17
+
18
+Your charm need only provide the address and port on which it is serving its 
19
+content, as soon as the `{relation_name}.available` state is set:
20
+
21
+```python
22
+from charmhelpers.core import hookenv
23
+@when('website.available')
24
+def configure_website(website):
25
+    website.set_address_port(hookenv.unit_get('public-address'), hookenv.config('port'))
26
+```
27
+
28
+## Requires
29
+
30
+By requiring the `public-address` interface, your charm is consuming one or 
31
+more HTTP servers, to load-balance a set of servers, etc.
32
+
33
+Your charm should respond to the `{relation_name}.available` state, which
34
+indicates that there is at least one HTTP server connected.
35
+
36
+The `get_addresses_ports()` method returns a list of available addresses and
37
+ports.
38
+
39
+The return value is a list of dicts of the following form:
40
+
41
+```python
42
+[
43
+    {
44
+        'public-address': address_of_host,
45
+        'port': port_for_host,
46
+    },
47
+    # ...
48
+]
49
+```
50
+
51
+A trivial example of handling this interface would be:
52
+
53
+```python
54
+from charmhelpers.core import hookenv
55
+@when('loadbalancer.available')
56
+def update_reverse_proxy_config(loadbalancer):
57
+    hosts = loadbalancer.get_addresses_ports()
58
+    for host in hosts:
59
+        hookenv.log('The loadbalancer for this unit is {}:{}'.format(
60
+                host['public-address'],
61
+                host['port']))
62
+```
Back to file index

hooks/relations/public-address/interface.yaml

1
--- 
2
+++ hooks/relations/public-address/interface.yaml
3
@@ -0,0 +1,4 @@
4
+name: public-address
5
+summary: A basic interface to provide the public address for load balancers.
6
+version: 1
7
+repo: https://githb.com/juju-solutions/interface-public-address.git
Back to file index

hooks/relations/public-address/provides.py

 1
--- 
 2
+++ hooks/relations/public-address/provides.py
 3
@@ -0,0 +1,23 @@
 4
+from charms.reactive import hook
 5
+from charms.reactive import RelationBase
 6
+from charms.reactive import scopes
 7
+
 8
+
 9
+class PublicAddressProvides(RelationBase):
10
+    '''This class provides a public address and port to other units.'''
11
+    scope = scopes.UNIT
12
+
13
+    @hook('{provides:public-address}-relation-{joined,changed}')
14
+    def changed(self):
15
+        self.set_state('{relation_name}.available')
16
+
17
+    @hook('{provides:public-address}-relation-{broken,departed}')
18
+    def broken(self):
19
+        self.remove_state('{relation_name}.available')
20
+
21
+    def set_address_port(self, address, port):
22
+        # Iterate over all conversations of this type to send data to everyone.
23
+        for conversation in self.conversations():
24
+            client = {'public-address': address, 'port': port}
25
+            # Send the address and port to every unit using the conversation.
26
+            conversation.set_remote(data=client)
Back to file index

hooks/relations/public-address/requires.py

 1
--- 
 2
+++ hooks/relations/public-address/requires.py
 3
@@ -0,0 +1,43 @@
 4
+from charms.reactive import hook
 5
+from charms.reactive import RelationBase
 6
+from charms.reactive import scopes
 7
+
 8
+
 9
+class PublicAddressRequires(RelationBase):
10
+    '''The class that requires a public address to another unit.'''
11
+    scope = scopes.UNIT
12
+
13
+    @hook('{requires:public-address}-relation-{joined,changed}')
14
+    def changed(self):
15
+        conv = self.conversation()
16
+        conv.set_state('{relation_name}.connected')
17
+        if conv.get_remote('port') and conv.get_remote('public-address'):
18
+            # this unit's conversation has a port, and public-address so
19
+            # it is part of the set of available units
20
+            conv.set_state('{relation_name}.available')
21
+
22
+    @hook('{requires:public-address}-relation-{departed,broken}')
23
+    def broken(self):
24
+        conversation = self.conversation()
25
+        conversation.remove_state('{relation_name}.available')
26
+
27
+    def get_addresses_ports(self):
28
+        '''Returns a list of available HTTP providers and their associated
29
+        public addresses and ports.
30
+
31
+        The return value is a list of dicts of the following form::
32
+            [
33
+                {
34
+                    'public-address': address_of_host,
35
+                    'port': port_for_host,
36
+                },
37
+                # ...
38
+            ]
39
+        '''
40
+        hosts = []
41
+        for conversation in self.conversations():
42
+            address = conversation.get_remote('public-address')
43
+            port = conversation.get_remote('port')
44
+            if address and port:
45
+                hosts.append({'public-address': address, 'port': port})
46
+        return hosts
Back to file index

hooks/relations/tls-certificates/README.md

 1
--- 
 2
+++ hooks/relations/tls-certificates/README.md
 3
@@ -0,0 +1,80 @@
 4
+# tls-certificates
 5
+
 6
+This is a [Juju](https://jujucharms.com) interface layer that handles the
 7
+transport layer security (TLS) for charms. Using relations between charms.  
 8
+Meaning the charms that use this layer can communicate securely
 9
+with each other based on TLS certificates.
10
+
11
+To get started please read the
12
+[Introduction to PKI](https://github.com/OpenVPN/easy-rsa/blob/master/doc/Intro-To-PKI.md)
13
+which defines some PKI terms, concepts and processes used in this document.
14
+
15
+> **NOTE**: It is important to point out that this interface does not do the 
16
+actual work of issuing certificates. The interface layer only handles the 
17
+communication between the peers and the charm layer must react to the states 
18
+correctly for this interface to work.  
19
+
20
+The [layer-tls](https://github.com/mbruzek/layer-tls) charm layer was created
21
+to implement this using the [easy-rsa](https://github.com/OpenVPN/easy-rsa)
22
+project.  This interface could be implemented with other PKI technology tools
23
+(such as openssl commands) in other charm layers.
24
+
25
+# States
26
+
27
+The interface layer emits several reactive states that a charm layer can respond
28
+to:
29
+
30
+## {relation_name}.available
31
+This is the start state that is generated when the relation is joined.
32
+A charm layer responding to this state should get the common name, a list of 
33
+Subject Alt Names, and the certificate_name call 
34
+`request_server_cert(common_name, sans, certificate_name)` on the relation 
35
+object.
36
+
37
+## {relation_name}.ca.available
38
+The Certificate Authority is available on the relation object when the 
39
+"{relation_name}.ca.available" state is set. The charm layer can retrieve the
40
+CA by calling `get_ca()` method on the relationship object.
41
+
42
+```python
43
+from charms.reactive import when
44
+@when('certificates.ca.available')
45
+def store_ca(tls):
46
+    certificate_authority = tls.get_ca()
47
+```
48
+
49
+## {relation_name}.server.cert.available
50
+Once the server certificate is set on the relation the interface layer will
51
+emit the "{relation_name}.server.cert.available" state, indicating that the 
52
+server certificate is available from the relationship object.  The charm layer 
53
+can retrieve the certificate and use it in the code by calling the
54
+`get_server_cert()` method on the relationship object.
55
+
56
+```python
57
+from charms.reactive import when
58
+@when('certificates.server.cert.available')
59
+def get_server(tls):
60
+    server_cert, server_key = tls.get_server_cert()
61
+```
62
+
63
+## {relation_name}.client.cert.available
64
+Once the client certificate is set on the relation the interface layer will
65
+emit the "{relation_name}.client.cert.available" state, indicated that the
66
+server certificates is available from the relationship object.  The charm layer
67
+can retrieve the certificate and use it in the code by calling the
68
+`get_client_cert()` method on the relationship object.
69
+
70
+```python
71
+from charms.reactive import when
72
+@when('certificates.client.cert.available')
73
+def store_client(tls):
74
+    client_cert, client_key = tls.get_client_cert()
75
+```
76
+
77
+# Contact Information
78
+
79
+Interface author: Matt Bruzek &lt;Matthew.Bruzek@canonical.com&gt; 
80
+
81
+Contributor: Charles Butler &lt;Charles.Butler@canonical.com&gt; 
82
+
83
+Contributor: Cory Johns &lt;Cory.Johns@canonical.com&gt; 
Back to file index

hooks/relations/tls-certificates/interface.yaml

1
--- 
2
+++ hooks/relations/tls-certificates/interface.yaml
3
@@ -0,0 +1,6 @@
4
+name: tls-certificates
5
+summary: |
6
+  A Transport Layer Security (TLS) charm layer that uses requires and provides
7
+  to exchange certifcates.
8
+version: 1
9
+repo: https://github.com/juju-solutions/interface-tls-certificates
Back to file index

hooks/relations/tls-certificates/provides.py

 1
--- 
 2
+++ hooks/relations/tls-certificates/provides.py
 3
@@ -0,0 +1,82 @@
 4
+import json
 5
+
 6
+from charms.reactive import hook
 7
+from charms.reactive import scopes
 8
+from charms.reactive import RelationBase
 9
+
10
+
11
+class TlsProvides(RelationBase):
12
+    '''The class that provides a TLS interface other units.'''
13
+    scope = scopes.UNIT
14
+
15
+    @hook('{provides:tls-certificates}-relation-joined')
16
+    def joined(self):
17
+        '''When a unit joins, set the available state.'''
18
+        # Get the conversation scoped to the unit name.
19
+        conversation = self.conversation()
20
+        conversation.set_state('{relation_name}.available')
21
+
22
+    @hook('{provides:tls-certificates}-relation-changed')
23
+    def changed(self):
24
+        '''When a unit relation changes, check for a server certificate request
25
+        and set the server.cert.requested state.'''
26
+        conversation = self.conversation()
27
+        cn = conversation.get_remote('common_name')
28
+        sans = conversation.get_remote('sans')
29
+        name = conversation.get_remote('certificate_name')
30
+        # When the relation has all three values set the server.cert.requested.
31
+        if cn and sans and name:
32
+            conversation.set_state('{relation_name}.server.cert.requested')
33
+
34
+    @hook('{provides:tls-certificates}-relation-{broken,departed}')
35
+    def broken_or_departed(self):
36
+        '''Remove the available state from the unit as we are leaving.'''
37
+        conversation = self.conversation()
38
+        conversation.remove_state('{relation_name}.available')
39
+
40
+    def set_ca(self, certificate_authority):
41
+        '''Set the CA on all the conversations in the relation data.'''
42
+        # Iterate over all conversations of this type.
43
+        for conversation in self.conversations():
44
+            # All the clients get the same CA, so send it to them.
45
+            conversation.set_remote(data={'ca': certificate_authority})
46
+
47
+    def set_client_cert(self, cert, key):
48
+        '''Set the client cert and key on the relation data.'''
49
+        # Iterate over all conversations of this type.
50
+        for conversation in self.conversations():
51
+            client = {}
52
+            client['client.cert'] = cert
53
+            client['client.key'] = key
54
+            # Send the client cert and key to the unit using the conversation.
55
+            conversation.set_remote(data=client)
56
+
57
+    def set_server_cert(self, scope, cert, key):
58
+        '''Set the server cert and key on the relation data.'''
59
+        # Get the coversation scoped to the unit.
60
+        conversation = self.conversation(scope)
61
+        server = {}
62
+        # The scope is the unit name, replace the slash with underscore.
63
+        name = scope.replace('/', '_')
64
+        # Prefix the key with name so each unit can get a unique cert and key.
65
+        server['{0}.server.cert'.format(name)] = cert
66
+        server['{0}.server.key'.format(name)] = key
67
+        # Send the server cert and key to the unit using the conversation.
68
+        conversation.set_remote(data=server)
69
+        # Remove the server.cert.requested state as it is no longer needed.
70
+        conversation.remove_state('{relation_name}.server.cert.requested')
71
+
72
+    def get_server_requests(self):
73
+        '''One provider can have many requests to generate server certificates.
74
+        Return a map of all server request objects indexed by the scope
75
+        which is essentially unit name.'''
76
+        request_map = {}
77
+        for conversation in self.conversations():
78
+            scope = conversation.scope
79
+            request = {}
80
+            request['common_name'] = conversation.get_remote('common_name')
81
+            request['sans'] = json.loads(conversation.get_remote('sans'))
82
+            request['certificate_name'] = conversation.get_remote('certificate_name')  # noqa
83
+            # Create a map indexed by scope.
84
+            request_map[scope] = request
85
+        return request_map
Back to file index

hooks/relations/tls-certificates/requires.py

 1
--- 
 2
+++ hooks/relations/tls-certificates/requires.py
 3
@@ -0,0 +1,76 @@
 4
+import json
 5
+
 6
+from charmhelpers.core import hookenv
 7
+
 8
+from charms.reactive import hook
 9
+from charms.reactive import scopes
10
+from charms.reactive import RelationBase
11
+
12
+
13
+class TlsRequires(RelationBase):
14
+    '''The class that requires a TLS relationship to another unit.'''
15
+    # Use the gloabal scope for requires relation.
16
+    scope = scopes.GLOBAL
17
+
18
+    @hook('{requires:tls-certificates}-relation-joined')
19
+    def joined(self):
20
+        '''When joining with a TLS provider request a certificate..'''
21
+        # Get the conversation scoped to the unit.
22
+        conversation = self.conversation()
23
+        conversation.set_state('{relation_name}.available')
24
+
25
+    @hook('{requires:tls-certificates}-relation-changed')
26
+    def changed(self):
27
+        '''Only the leader should change the state to sign the request. '''
28
+        # Get the global scoped conversation.
29
+        conversation = self.conversation()
30
+        # When the conversation has a CA set notify that the ca is available.
31
+        if conversation.get_remote('ca'):
32
+            conversation.set_state('{relation_name}.ca.available')
33
+        # When the client.cert has a value notify that the client is available.
34
+        if conversation.get_remote('client.cert'):
35
+            conversation.set_state('{relation_name}.client.cert.available')
36
+        # Get the name of the unit this code is running on.
37
+        name = hookenv.local_unit().replace('/', '_')
38
+        # Prefix the key with the name so each unit is notified cert available.
39
+        if conversation.get_remote('{0}.server.cert'.format(name)):
40
+            conversation.set_state('{relation_name}.server.cert.available')
41
+
42
+    @hook('{provides:tls-certificates}-relation-{broken,departed}')
43
+    def broken_or_departed(self):
44
+        '''Remove the states that were set.'''
45
+        conversation = self.conversation()
46
+        conversation.remove_state('{relation_name}.available')
47
+
48
+    def get_ca(self):
49
+        '''Return the certificate authority from the relation object.'''
50
+        # Get the global scoped conversation.
51
+        conversation = self.conversation()
52
+        # Find the certificate authority by key, and return the value.
53
+        return conversation.get_remote('ca')
54
+
55
+    def get_client_cert(self):
56
+        '''Return the client certificate and key from the relation object.'''
57
+        conversation = self.conversation()
58
+        client_cert = conversation.get_remote('client.cert')
59
+        client_key = conversation.get_remote('client.key')
60
+        return client_cert, client_key
61
+
62
+    def get_server_cert(self):
63
+        '''Return the server certificate and key from the relation objects.'''
64
+        conversation = self.conversation()
65
+        # Get the name of the unit this code is running on.
66
+        name = hookenv.local_unit().replace('/', '_')
67
+        # Prefix the keys with name so each unit can get unique certs and keys.
68
+        server_cert = conversation.get_remote('{0}.server.cert'.format(name))
69
+        server_key = conversation.get_remote('{0}.server.key'.format(name))
70
+        return server_cert, server_key
71
+
72
+    def request_server_cert(self, cn, sans, cert_name):
73
+        '''Set the CN, list of sans, and certifiicate name on the relation to
74
+        request a server certificate.'''
75
+        conversation = self.conversation()
76
+        # A server certificate requires a CN, sans, and a certificate name.
77
+        conversation.set_remote('common_name', cn)
78
+        conversation.set_remote('sans', json.dumps(sans))
79
+        conversation.set_remote('certificate_name', cert_name)
Back to file index

hooks/start

 1
--- 
 2
+++ hooks/start
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/stop

 1
--- 
 2
+++ hooks/stop
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/update-status

 1
--- 
 2
+++ hooks/update-status
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/upgrade-charm

 1
--- 
 2
+++ hooks/upgrade-charm
 3
@@ -0,0 +1,28 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import os
 8
+import sys
 9
+sys.path.append('lib')
10
+
11
+# This is an upgrade-charm context, make sure we install latest deps
12
+if not os.path.exists('wheelhouse/.upgrade'):
13
+    open('wheelhouse/.upgrade', 'w').close()
14
+    if os.path.exists('wheelhouse/.bootstrapped'):
15
+        os.unlink('wheelhouse/.bootstrapped')
16
+else:
17
+    os.unlink('wheelhouse/.upgrade')
18
+
19
+from charms.layer import basic
20
+basic.bootstrap_charm_deps()
21
+basic.init_config_states()
22
+
23
+
24
+# This will load and run the appropriate @hook and other decorated
25
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
26
+# and $JUJU_CHARM_DIR/hooks/relations.
27
+#
28
+# See https://jujucharms.com/docs/stable/authors-charm-building
29
+# for more information on this pattern.
30
+from charms.reactive import main
31
+main()
Back to file index

hooks/website-relation-broken

 1
--- 
 2
+++ hooks/website-relation-broken
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/website-relation-changed

 1
--- 
 2
+++ hooks/website-relation-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/website-relation-departed

 1
--- 
 2
+++ hooks/website-relation-departed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

hooks/website-relation-joined

 1
--- 
 2
+++ hooks/website-relation-joined
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $JUJU_CHARM_DIR/lib
 7
+import sys
 8
+sys.path.append('lib')
 9
+
10
+from charms.layer import basic
11
+basic.bootstrap_charm_deps()
12
+basic.init_config_states()
13
+
14
+
15
+# This will load and run the appropriate @hook and other decorated
16
+# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive,
17
+# and $JUJU_CHARM_DIR/hooks/relations.
18
+#
19
+# See https://jujucharms.com/docs/stable/authors-charm-building
20
+# for more information on this pattern.
21
+from charms.reactive import main
22
+main()
Back to file index

icon.svg

  1
--- 
  2
+++ icon.svg
  3
@@ -0,0 +1,412 @@
  4
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
  5
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
  6
+
  7
+<svg
  8
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
  9
+   xmlns:cc="http://creativecommons.org/ns#"
 10
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 11
+   xmlns:svg="http://www.w3.org/2000/svg"
 12
+   xmlns="http://www.w3.org/2000/svg"
 13
+   xmlns:xlink="http://www.w3.org/1999/xlink"
 14
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
 15
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
 16
+   width="96"
 17
+   height="96"
 18
+   id="svg6517"
 19
+   version="1.1"
 20
+   inkscape:version="0.91 r13725"
 21
+   sodipodi:docname="kubapi-load-balancer_circle.svg"
 22
+   viewBox="0 0 96 96">
 23
+  <defs
 24
+     id="defs6519">
 25
+    <linearGradient
 26
+       id="Background">
 27
+      <stop
 28
+         id="stop4178"
 29
+         offset="0"
 30
+         style="stop-color:#22779e;stop-opacity:1" />
 31
+      <stop
 32
+         id="stop4180"
 33
+         offset="1"
 34
+         style="stop-color:#2991c0;stop-opacity:1" />
 35
+    </linearGradient>
 36
+    <filter
 37
+       style="color-interpolation-filters:sRGB"
 38
+       inkscape:label="Inner Shadow"
 39
+       id="filter1121">
 40
+      <feFlood
 41
+         flood-opacity="0.59999999999999998"
 42
+         flood-color="rgb(0,0,0)"
 43
+         result="flood"
 44
+         id="feFlood1123" />
 45
+      <feComposite
 46
+         in="flood"
 47
+         in2="SourceGraphic"
 48
+         operator="out"
 49
+         result="composite1"
 50
+         id="feComposite1125" />
 51
+      <feGaussianBlur
 52
+         in="composite1"
 53
+         stdDeviation="1"
 54
+         result="blur"
 55
+         id="feGaussianBlur1127" />
 56
+      <feOffset
 57
+         dx="0"
 58
+         dy="2"
 59
+         result="offset"
 60
+         id="feOffset1129" />
 61
+      <feComposite
 62
+         in="offset"
 63
+         in2="SourceGraphic"
 64
+         operator="atop"
 65
+         result="composite2"
 66
+         id="feComposite1131" />
 67
+    </filter>
 68
+    <filter
 69
+       style="color-interpolation-filters:sRGB"
 70
+       inkscape:label="Drop Shadow"
 71
+       id="filter950">
 72
+      <feFlood
 73
+         flood-opacity="0.25"
 74
+         flood-color="rgb(0,0,0)"
 75
+         result="flood"
 76
+         id="feFlood952" />
 77
+      <feComposite
 78
+         in="flood"
 79
+         in2="SourceGraphic"
 80
+         operator="in"
 81
+         result="composite1"
 82
+         id="feComposite954" />
 83
+      <feGaussianBlur
 84
+         in="composite1"
 85
+         stdDeviation="1"
 86
+         result="blur"
 87
+         id="feGaussianBlur956" />
 88
+      <feOffset
 89
+         dx="0"
 90
+         dy="1"
 91
+         result="offset"
 92
+         id="feOffset958" />
 93
+      <feComposite
 94
+         in="SourceGraphic"
 95
+         in2="offset"
 96
+         operator="over"
 97
+         result="composite2"
 98
+         id="feComposite960" />
 99
+    </filter>
100
+    <clipPath
101
+       clipPathUnits="userSpaceOnUse"
102
+       id="clipPath873">
103
+      <g
104
+         transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"
105
+         id="g875"
106
+         inkscape:label="Layer 1"
107
+         style="display:inline;fill:#ff00ff;fill-opacity:1;stroke:none">
108
+        <path
109
+           style="display:inline;fill:#ff00ff;fill-opacity:1;stroke:none"
110
+           d="M 46.702703,898.22775 H 97.297297 C 138.16216,898.22775 144,904.06497 144,944.92583 v 50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 H 46.702703 C 5.8378378,1042.3622 0,1036.525 0,995.66429 v -50.73846 c 0,-40.86086 5.8378378,-46.69808 46.702703,-46.69808 z"
111
+           id="path877"
112
+           inkscape:connector-curvature="0"
113
+           sodipodi:nodetypes="sssssssss" />
114
+      </g>
115
+    </clipPath>
116
+    <style
117
+       id="style867"
118
+       type="text/css"><![CDATA[
119
+    .fil0 {fill:#1F1A17}
120
+   ]]></style>
121
+    <clipPath
122
+       id="clipPath16">
123
+      <path
124
+         id="path18"
125
+         d="M -9,-9 H 605 V 222 H -9 Z"
126
+         inkscape:connector-curvature="0" />
127
+    </clipPath>
128
+    <clipPath
129
+       id="clipPath116">
130
+      <path
131
+         id="path118"
132
+         d="m 91.7368,146.3253 -9.7039,-1.577 -8.8548,-3.8814 -7.5206,-4.7308 -7.1566,-8.7335 -4.0431,-4.282 -3.9093,-1.4409 -1.034,2.5271 1.8079,2.6096 0.4062,3.6802 1.211,-0.0488 1.3232,-1.2069 -0.3569,3.7488 -1.4667,0.9839 0.0445,1.4286 -3.4744,-1.9655 -3.1462,-3.712 -0.6559,-3.3176 1.3453,-2.6567 1.2549,-4.5133 2.5521,-1.2084 2.6847,0.1318 2.5455,1.4791 -1.698,-8.6122 1.698,-9.5825 -1.8692,-4.4246 -6.1223,-6.5965 1.0885,-3.941 2.9002,-4.5669 5.4688,-3.8486 2.9007,-0.3969 3.225,-0.1094 -2.012,-8.2601 7.3993,-3.0326 9.2188,-1.2129 3.1535,2.0619 0.2427,5.5797 3.5178,5.8224 0.2426,4.6094 8.4909,-0.6066 7.8843,0.7279 -7.8843,-4.7307 1.3343,-5.701 4.9731,-7.763 4.8521,-2.0622 3.8814,1.5769 1.577,3.1538 8.1269,6.1861 1.5769,-1.3343 12.7363,-0.485 2.5473,2.0619 0.2426,3.6391 -0.849,1.5767 -0.6066,9.8251 -4.2454,8.4909 0.7276,3.7605 2.5475,-1.3343 7.1566,-6.6716 3.5175,-0.2424 3.8815,1.5769 3.8818,2.9109 1.9406,6.3077 11.4021,-0.7277 6.914,2.6686 5.5797,5.2157 4.0028,7.5206 0.9706,8.8546 -0.8493,10.3105 -2.1832,9.2185 -2.1836,2.9112 -3.0322,0.9706 -5.3373,-5.8224 -4.8518,-1.6982 -4.2455,7.0353 -4.2454,3.8815 -2.3049,1.4556 -9.2185,7.6419 -7.3993,4.0028 -7.3993,0.6066 -8.6119,-1.4556 -7.5206,-2.7899 -5.2158,-4.2454 -4.1241,-4.9734 -4.2454,-1.2129"
133
+         inkscape:connector-curvature="0" />
134
+    </clipPath>
135
+    <clipPath
136
+       id="clipPath128">
137
+      <path
138
+         id="path130"
139
+         d="m 91.7368,146.3253 -9.7039,-1.577 -8.8548,-3.8814 -7.5206,-4.7308 -7.1566,-8.7335 -4.0431,-4.282 -3.9093,-1.4409 -1.034,2.5271 1.8079,2.6096 0.4062,3.6802 1.211,-0.0488 1.3232,-1.2069 -0.3569,3.7488 -1.4667,0.9839 0.0445,1.4286 -3.4744,-1.9655 -3.1462,-3.712 -0.6559,-3.3176 1.3453,-2.6567 1.2549,-4.5133 2.5521,-1.2084 2.6847,0.1318 2.5455,1.4791 -1.698,-8.6122 1.698,-9.5825 -1.8692,-4.4246 -6.1223,-6.5965 1.0885,-3.941 2.9002,-4.5669 5.4688,-3.8486 2.9007,-0.3969 3.225,-0.1094 -2.012,-8.2601 7.3993,-3.0326 9.2188,-1.2129 3.1535,2.0619 0.2427,5.5797 3.5178,5.8224 0.2426,4.6094 8.4909,-0.6066 7.8843,0.7279 -7.8843,-4.7307 1.3343,-5.701 4.9731,-7.763 4.8521,-2.0622 3.8814,1.5769 1.577,3.1538 8.1269,6.1861 1.5769,-1.3343 12.7363,-0.485 2.5473,2.0619 0.2426,3.6391 -0.849,1.5767 -0.6066,9.8251 -4.2454,8.4909 0.7276,3.7605 2.5475,-1.3343 7.1566,-6.6716 3.5175,-0.2424 3.8815,1.5769 3.8818,2.9109 1.9406,6.3077 11.4021,-0.7277 6.914,2.6686 5.5797,5.2157 4.0028,7.5206 0.9706,8.8546 -0.8493,10.3105 -2.1832,9.2185 -2.1836,2.9112 -3.0322,0.9706 -5.3373,-5.8224 -4.8518,-1.6982 -4.2455,7.0353 -4.2454,3.8815 -2.3049,1.4556 -9.2185,7.6419 -7.3993,4.0028 -7.3993,0.6066 -8.6119,-1.4556 -7.5206,-2.7899 -5.2158,-4.2454 -4.1241,-4.9734 -4.2454,-1.2129"
140
+         inkscape:connector-curvature="0" />
141
+    </clipPath>
142
+    <linearGradient
143
+       id="linearGradient3850"
144
+       inkscape:collect="always">
145
+      <stop
146
+         id="stop3852"
147
+         offset="0"
148
+         style="stop-color:#000000;stop-opacity:1;" />
149
+      <stop
150
+         id="stop3854"
151
+         offset="1"
152
+         style="stop-color:#000000;stop-opacity:0;" />
153
+    </linearGradient>
154
+    <clipPath
155
+       clipPathUnits="userSpaceOnUse"
156
+       id="clipPath3095">
157
+      <path
158
+         d="M 976.648,389.551 H 134.246 V 1229.55 H 976.648 V 389.551"
159
+         id="path3097"
160
+         inkscape:connector-curvature="0" />
161
+    </clipPath>
162
+    <clipPath
163
+       clipPathUnits="userSpaceOnUse"
164
+       id="clipPath3195">
165
+      <path
166
+         d="m 611.836,756.738 -106.34,105.207 c -8.473,8.289 -13.617,20.102 -13.598,33.379 L 598.301,790.207 c -0.031,-13.418 5.094,-25.031 13.535,-33.469"
167
+         id="path3197"
168
+         inkscape:connector-curvature="0" />
169
+    </clipPath>
170
+    <clipPath
171
+       clipPathUnits="userSpaceOnUse"
172
+       id="clipPath3235">
173
+      <path
174
+         d="m 1095.64,1501.81 c 35.46,-35.07 70.89,-70.11 106.35,-105.17 4.4,-4.38 7.11,-10.53 7.11,-17.55 l -106.37,105.21 c 0,7 -2.71,13.11 -7.09,17.51"
175
+         id="path3237"
176
+         inkscape:connector-curvature="0" />
177
+    </clipPath>
178
+    <clipPath
179
+       id="clipPath4591"
180
+       clipPathUnits="userSpaceOnUse">
181
+      <path
182
+         inkscape:connector-curvature="0"
183
+         d="m 1106.6009,730.43734 -0.036,21.648 c -0.01,3.50825 -2.8675,6.61375 -6.4037,6.92525 l -83.6503,7.33162 c -3.5205,0.30763 -6.3812,-2.29987 -6.3671,-5.8145 l 0.036,-21.6475 20.1171,-1.76662 -0.011,4.63775 c 0,1.83937 1.4844,3.19925 3.3262,3.0395 l 49.5274,-4.33975 c 1.8425,-0.166 3.3425,-1.78125 3.3538,-3.626 l 0.01,-4.63025 20.1,-1.7575"
184
+         style="fill:#ff00ff;fill-opacity:1;fill-rule:nonzero;stroke:none"
185
+         id="path4593" />
186
+    </clipPath>
187
+    <radialGradient
188
+       gradientUnits="userSpaceOnUse"
189
+       gradientTransform="matrix(-1.4333926,-2.2742838,1.1731823,-0.73941125,-174.08025,98.374394)"
190
+       r="20.40658"
191
+       fy="93.399292"
192
+       fx="-26.508606"
193
+       cy="93.399292"
194
+       cx="-26.508606"
195
+       id="radialGradient3856"
196
+       xlink:href="#linearGradient3850"
197
+       inkscape:collect="always" />
198
+    <linearGradient
199
+       gradientTransform="translate(-318.48033,212.32022)"
200
+       gradientUnits="userSpaceOnUse"
201
+       y2="993.19702"
202
+       x2="-51.879555"
203
+       y1="593.11615"
204
+       x1="348.20132"
205
+       id="linearGradient3895"
206
+       xlink:href="#linearGradient3850"
207
+       inkscape:collect="always" />
208
+    <clipPath
209
+       id="clipPath3906"
210
+       clipPathUnits="userSpaceOnUse">
211
+      <rect
212
+         transform="scale(1,-1)"
213
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.8;fill:#ff00ff;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
214
+         id="rect3908"
215
+         width="1019.1371"
216
+         height="1019.1371"
217
+         x="357.9816"
218
+         y="-1725.8152" />
219
+    </clipPath>
220
+  </defs>
221
+  <sodipodi:namedview
222
+     id="base"
223
+     pagecolor="#ffffff"
224
+     bordercolor="#666666"
225
+     borderopacity="1.0"
226
+     inkscape:pageopacity="0.0"
227
+     inkscape:pageshadow="2"
228
+     inkscape:zoom="7.9580781"
229
+     inkscape:cx="-61.002332"
230
+     inkscape:cy="48.450019"
231
+     inkscape:document-units="px"
232
+     inkscape:current-layer="layer1"
233
+     showgrid="false"
234
+     fit-margin-top="0"
235
+     fit-margin-left="0"
236
+     fit-margin-right="0"
237
+     fit-margin-bottom="0"
238
+     inkscape:window-width="1920"
239
+     inkscape:window-height="1029"
240
+     inkscape:window-x="0"
241
+     inkscape:window-y="24"
242
+     inkscape:window-maximized="1"
243
+     showborder="true"
244
+     showguides="false"
245
+     inkscape:guide-bbox="true"
246
+     inkscape:showpageshadow="false"
247
+     inkscape:snap-global="false"
248
+     inkscape:snap-bbox="true"
249
+     inkscape:bbox-paths="true"
250
+     inkscape:bbox-nodes="true"
251
+     inkscape:snap-bbox-edge-midpoints="true"
252
+     inkscape:snap-bbox-midpoints="true"
253
+     inkscape:object-paths="true"
254
+     inkscape:snap-intersection-paths="true"
255
+     inkscape:object-nodes="true"
256
+     inkscape:snap-smooth-nodes="true"
257
+     inkscape:snap-midpoints="true"
258
+     inkscape:snap-object-midpoints="true"
259
+     inkscape:snap-center="true"
260
+     inkscape:snap-nodes="true"
261
+     inkscape:snap-others="true"
262
+     inkscape:snap-page="true">
263
+    <inkscape:grid
264
+       type="xygrid"
265
+       id="grid821" />
266
+    <sodipodi:guide
267
+       orientation="1,0"
268
+       position="16,48"
269
+       id="guide823"
270
+       inkscape:locked="false" />
271
+    <sodipodi:guide
272
+       orientation="0,1"
273
+       position="64,80"
274
+       id="guide825"
275
+       inkscape:locked="false" />
276
+    <sodipodi:guide
277
+       orientation="1,0"
278
+       position="80,40"
279
+       id="guide827"
280
+       inkscape:locked="false" />
281
+    <sodipodi:guide
282
+       orientation="0,1"
283
+       position="64,16"
284
+       id="guide829"
285
+       inkscape:locked="false" />
286
+  </sodipodi:namedview>
287
+  <metadata
288
+     id="metadata6522">
289
+    <rdf:RDF>
290
+      <cc:Work
291
+         rdf:about="">
292
+        <dc:format>image/svg+xml</dc:format>
293
+        <dc:type
294
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
295
+        <dc:title />
296
+      </cc:Work>
297
+    </rdf:RDF>
298
+  </metadata>
299
+  <g
300
+     inkscape:label="BACKGROUND"
301
+     inkscape:groupmode="layer"
302
+     id="layer1"
303
+     transform="translate(268,-635.29076)"
304
+     style="display:inline">
305
+    <path
306
+       style="display:inline;fill:#ffffff;fill-opacity:1;stroke:none"
307
+       d="M 48 0 A 48 48 0 0 0 0 48 A 48 48 0 0 0 48 96 A 48 48 0 0 0 96 48 A 48 48 0 0 0 48 0 z "
308
+       id="path6455"
309
+       transform="translate(-268,635.29076)" />
310
+    <path
311
+       inkscape:connector-curvature="0"
312
+       style="display:inline;fill:#326de6;fill-opacity:1;stroke:none"
313
+       d="m -220,635.29076 a 48,48 0 0 0 -48,48 48,48 0 0 0 48,48 48,48 0 0 0 48,-48 48,48 0 0 0 -48,-48 z"
314
+       id="path6455-3" />
315
+    <path
316
+       inkscape:connector-curvature="0"
317
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#326de6;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
318
+       d="m -257.13275,693.64544 a 5.0524169,5.01107 0 0 0 0.28787,0.39638 l 18.28736,22.73877 a 5.0524169,5.01107 0 0 0 3.95007,1.88616 l 29.32654,-0.003 a 5.0524169,5.01107 0 0 0 3.94943,-1.88675 l 18.28255,-22.74294 a 5.0524169,5.01107 0 0 0 0.97485,-4.2391 l -6.52857,-28.3566 a 5.0524169,5.01107 0 0 0 -2.73381,-3.39906 l -26.4238,-12.61752 a 5.0524169,5.01107 0 0 0 -4.38381,4.3e-4 l -26.42114,12.62305 a 5.0524169,5.01107 0 0 0 -2.73296,3.39983 l -6.52262,28.35798 a 5.0524169,5.01107 0 0 0 0.68804,3.84268 z"
319
+       id="path4809" />
320
+    <path
321
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
322
+       d="M 47.976562,17.478516 C 47.148902,17.491446 46.488127,18.172324 46.5,19 l -1,27.669922 -10.861328,24.701172 c -0.91351,1.842103 1.912154,3.147502 2.722656,1.257812 L 48,53.08 58.638672,72.628906 c 0.810502,1.88969 3.636166,0.584291 2.722656,-1.257812 L 50.5,46.671875 49.5,19 c 0.01214,-0.846036 -0.677418,-1.534706 -1.523438,-1.521484 z"
323
+       transform="translate(-268,635.29076)"
324
+       id="path4207"
325
+       inkscape:connector-curvature="0"
326
+       sodipodi:nodetypes="scccccccccs" />
327
+    <path
328
+       sodipodi:type="star"
329
+       style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
330
+       id="path4218"
331
+       sodipodi:sides="3"
332
+       sodipodi:cx="-232"
333
+       sodipodi:cy="706.29077"
334
+       sodipodi:r1="5.8309517"
335
+       sodipodi:r2="2.9154758"
336
+       sodipodi:arg1="0.52359878"
337
+       sodipodi:arg2="1.5707963"
338
+       inkscape:flatsided="true"
339
+       inkscape:rounded="0"
340
+       inkscape:randomized="0"
341
+       d="m -226.95025,709.20625 -10.0995,0 5.04975,-8.74643 z"
342
+       inkscape:transform-center-y="0.28435141"
343
+       transform="matrix(-1.1408434,-0.54609465,0.54609465,-1.1408434,-881.23628,1383.8624)"
344
+       inkscape:transform-center-x="-1.5920962" />
345
+    <path
346
+       inkscape:transform-center-x="1.5920987"
347
+       transform="matrix(1.1408434,-0.54609465,-0.54609465,-1.1408434,441.40253,1383.8624)"
348
+       inkscape:transform-center-y="0.28435141"
349
+       d="m -226.95025,709.20625 -10.0995,0 5.04975,-8.74643 z"
350
+       inkscape:randomized="0"
351
+       inkscape:rounded="0"
352
+       inkscape:flatsided="true"
353
+       sodipodi:arg2="1.5707963"
354
+       sodipodi:arg1="0.52359878"
355
+       sodipodi:r2="2.9154758"
356
+       sodipodi:r1="5.8309517"
357
+       sodipodi:cy="706.29077"
358
+       sodipodi:cx="-232"
359
+       sodipodi:sides="3"
360
+       id="path4225"
361
+       style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
362
+       sodipodi:type="star" />
363
+    <path
364
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
365
+       d="m -220.61945,681.04476 -19.90428,4.99988 -1.00407,-4.6236 -9.45532,8.58951 12.16601,3.8932 -0.95421,-4.40019 15.62258,-2.55254 6.59459,-1.95609 z"
366
+       id="path4227"
367
+       inkscape:connector-curvature="0"
368
+       sodipodi:nodetypes="ccccccccc" />
369
+    <path
370
+       sodipodi:nodetypes="ccccccccc"
371
+       inkscape:connector-curvature="0"
372
+       id="path4234"
373
+       d="m -219.4808,681.04476 19.90428,4.99988 1.00407,-4.6236 9.45532,8.58951 -12.16601,3.8932 0.95421,-4.40019 -15.62258,-2.55254 -6.59459,-1.95609 z"
374
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
375
+  </g>
376
+  <g
377
+     inkscape:groupmode="layer"
378
+     id="layer3"
379
+     inkscape:label="PLACE YOUR PICTOGRAM HERE"
380
+     style="display:inline">
381
+    <g
382
+       id="g4185" />
383
+  </g>
384
+  <style
385
+     id="style4217"
386
+     type="text/css">
387
+	.st0{fill:#419EDA;}
388
+</style>
389
+  <style
390
+     id="style4285"
391
+     type="text/css">
392
+	.st0{clip-path:url(#SVGID_2_);fill:#EFBF1B;}
393
+	.st1{clip-path:url(#SVGID_2_);fill:#40BEB0;}
394
+	.st2{clip-path:url(#SVGID_2_);fill:#0AA5DE;}
395
+	.st3{clip-path:url(#SVGID_2_);fill:#231F20;}
396
+	.st4{fill:#D7A229;}
397
+	.st5{fill:#009B8F;}
398
+</style>
399
+  <style
400
+     id="style4240"
401
+     type="text/css">
402
+	.st0{fill:#E8478B;}
403
+	.st1{fill:#40BEB0;}
404
+	.st2{fill:#37A595;}
405
+	.st3{fill:#231F20;}
406
+</style>
407
+  <style
408
+     id="style4812"
409
+     type="text/css">
410
+	.st0{fill:#0AA5DE;}
411
+	.st1{fill:#40BEB0;}
412
+	.st2{opacity:0.26;fill:#353535;}
413
+	.st3{fill:#231F20;}
414
+</style>
415
+</svg>
Back to file index

layer.yaml

 1
--- 
 2
+++ layer.yaml
 3
@@ -0,0 +1,32 @@
 4
+"options":
 5
+  "apt":
 6
+    "packages":
 7
+    - "nginx-full"
 8
+    "version_package": ""
 9
+    "full_version": !!bool "false"
10
+  "tls-client":
11
+    "ca_certificate_path": "/srv/kubernetes/ca.crt"
12
+    "server_certificate_path": "/srv/kubernetes/server.crt"
13
+    "server_key_path": "/srv/kubernetes/server.key"
14
+    "client_certificate_path": "/srv/kubernetes/client.crt"
15
+    "client_key_path": "/srv/kubernetes/client.key"
16
+  "kubeapi-load-balancer": {}
17
+  "nagios": {}
18
+  "nginx": {}
19
+  "basic":
20
+    "use_venv": !!bool "false"
21
+    "packages": []
22
+    "include_system_packages": !!bool "false"
23
+"repo": "https://github.com/kubernetes/kubernetes.git"
24
+"includes":
25
+- "layer:basic"
26
+- "interface:nrpe-external-master"
27
+- "layer:apt"
28
+- "interface:http"
29
+- "interface:tls-certificates"
30
+- "layer:metrics"
31
+- "layer:nagios"
32
+- "layer:nginx"
33
+- "layer:tls-client"
34
+- "interface:public-address"
35
+"is": "kubeapi-load-balancer"
Back to file index

lib/charms/__init__.py

1
--- 
2
+++ lib/charms/__init__.py
3
@@ -0,0 +1,2 @@
4
+from pkgutil import extend_path
5
+__path__ = extend_path(__path__, __name__)
Back to file index

lib/charms/apt.py

  1
--- 
  2
+++ lib/charms/apt.py
  3
@@ -0,0 +1,217 @@
  4
+# Copyright 2015-2016 Canonical Ltd.
  5
+#
  6
+# This file is part of the Apt layer for Juju.
  7
+#
  8
+# This program is free software: you can redistribute it and/or modify
  9
+# it under the terms of the GNU General Public License version 3, as
 10
+# published by the Free Software Foundation.
 11
+#
 12
+# This program is distributed in the hope that it will be useful, but
 13
+# WITHOUT ANY WARRANTY; without even the implied warranties of
 14
+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 15
+# PURPOSE.  See the GNU General Public License for more details.
 16
+#
 17
+# You should have received a copy of the GNU General Public License
 18
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 19
+
 20
+'''
 21
+charms.reactive helpers for dealing with deb packages.
 22
+
 23
+Add apt package sources using add_source(). Queue deb packages for
 24
+installation with install(). Configure and work with your software
 25
+once the apt.installed.{packagename} state is set.
 26
+'''
 27
+import itertools
 28
+import re
 29
+import subprocess
 30
+
 31
+from charmhelpers import fetch
 32
+from charmhelpers.core import hookenv, unitdata
 33
+from charms import layer, reactive
 34
+
 35
+
 36
+__all__ = ['add_source', 'update', 'queue_install', 'install_queued',
 37
+           'installed', 'purge', 'ensure_package_status']
 38
+
 39
+
 40
+def add_source(source, key=None):
 41
+    '''Add an apt source.
 42
+
 43
+    Sets the apt.needs_update state.
 44
+
 45
+    A source may be either a line that can be added directly to
 46
+    sources.list(5), or in the form ppa:<user>/<ppa-name> for adding
 47
+    Personal Package Archives, or a distribution component to enable.
 48
+
 49
+    The package signing key should be an ASCII armoured GPG key. While
 50
+    GPG key ids are also supported, the retrieval mechanism is insecure.
 51
+    There is no need to specify the package signing key for PPAs or for
 52
+    the main Ubuntu archives.
 53
+    '''
 54
+    # Maybe we should remember which sources have been added already
 55
+    # so we don't waste time re-adding them. Is this time significant?
 56
+    fetch.add_source(source, key)
 57
+    reactive.set_state('apt.needs_update')
 58
+
 59
+
 60
+def queue_install(packages, options=None):
 61
+    """Queue one or more deb packages for install.
 62
+
 63
+    The `apt.installed.{name}` state is set once the package is installed.
 64
+
 65
+    If a package has already been installed it will not be reinstalled.
 66
+
 67
+    If a package has already been queued it will not be requeued, and
 68
+    the install options will not be changed.
 69
+
 70
+    Sets the apt.queued_installs state.
 71
+    """
 72
+    if isinstance(packages, str):
 73
+        packages = [packages]
 74
+    # Filter installed packages.
 75
+    store = unitdata.kv()
 76
+    queued_packages = store.getrange('apt.install_queue.', strip=True)
 77
+    packages = {package: options for package in packages
 78
+                if not (package in queued_packages or
 79
+                        reactive.helpers.is_state('apt.installed.' + package))}
 80
+    if packages:
 81
+        unitdata.kv().update(packages, prefix='apt.install_queue.')
 82
+        reactive.set_state('apt.queued_installs')
 83
+
 84
+
 85
+def installed():
 86
+    '''Return the set of deb packages completed install'''
 87
+    return set(state.split('.', 2)[2] for state in reactive.bus.get_states()
 88
+               if state.startswith('apt.installed.'))
 89
+
 90
+
 91
+def purge(packages):
 92
+    """Purge one or more deb packages from the system"""
 93
+    fetch.apt_purge(packages, fatal=True)
 94
+    store = unitdata.kv()
 95
+    store.unsetrange(packages, prefix='apt.install_queue.')
 96
+    for package in packages:
 97
+        reactive.remove_state('apt.installed.{}'.format(package))
 98
+
 99
+
100
+def update():
101
+    """Update the apt cache.
102
+
103
+    Removes the apt.needs_update state.
104
+    """
105
+    status_set(None, 'Updating apt cache')
106
+    fetch.apt_update(fatal=True)  # Friends don't let friends set fatal=False
107
+    reactive.remove_state('apt.needs_update')
108
+
109
+
110
+def install_queued():
111
+    '''Installs queued deb packages.
112
+
113
+    Removes the apt.queued_installs state and sets the apt.installed state.
114
+
115
+    On failure, sets the unit's workload state to 'blocked' and returns
116
+    False. Package installs remain queued.
117
+
118
+    On success, sets the apt.installed.{packagename} state for each
119
+    installed package and returns True.
120
+    '''
121
+    store = unitdata.kv()
122
+    queue = sorted((options, package)
123
+                   for package, options in store.getrange('apt.install_queue.',
124
+                                                          strip=True).items())
125
+
126
+    installed = set()
127
+    for options, batch in itertools.groupby(queue, lambda x: x[0]):
128
+        packages = [b[1] for b in batch]
129
+        try:
130
+            status_set(None, 'Installing {}'.format(','.join(packages)))
131
+            fetch.apt_install(packages, options, fatal=True)
132
+            store.unsetrange(packages, prefix='apt.install_queue.')
133
+            installed.update(packages)
134
+        except subprocess.CalledProcessError:
135
+            status_set('blocked',
136
+                       'Unable to install packages {}'
137
+                       .format(','.join(packages)))
138
+            return False  # Without setting reactive state.
139
+
140
+    for package in installed:
141
+        reactive.set_state('apt.installed.{}'.format(package))
142
+    reactive.remove_state('apt.queued_installs')
143
+
144
+    reset_application_version()
145
+
146
+    return True
147
+
148
+
149
+def get_package_version(package, full_version=False):
150
+    '''Return the version of an installed package.
151
+
152
+    If `full_version` is True, returns the full Debian package version.
153
+    Otherwise, returns the shorter 'upstream' version number.
154
+    '''
155
+    # Don't use fetch.get_upstream_version, as it depends on python-apt
156
+    # and not available if the basic layer's use_site_packages option is off.
157
+    cmd = ['dpkg-query', '--show', r'--showformat=${Version}\n', package]
158
+    full = subprocess.check_output(cmd, universal_newlines=True).strip()
159
+    if not full_version:
160
+        # Attempt to strip off Debian style metadata from the end of the
161
+        # version number.
162
+        m = re.search('^([\d.a-z]+)', full, re.I)
163
+        if m is not None:
164
+            return m.group(1)
165
+    return full
166
+
167
+
168
+def reset_application_version():
169
+    '''Set the Juju application version, per settings in layer.yaml'''
170
+    # Reset the application version. We call this after installing
171
+    # packages to initialize the version. We also call this every
172
+    # hook, incase the version has changed (eg. Landscape upgraded
173
+    # the package).
174
+    opts = layer.options().get('apt', {})
175
+    pkg = opts.get('version_package')
176
+    if pkg and pkg in installed():
177
+        ver = get_package_version(pkg, opts.get('full_version', False))
178
+        hookenv.application_version_set(ver)
179
+
180
+
181
+def ensure_package_status():
182
+    '''Hold or unhold packages per the package_status configuration option.
183
+
184
+    All packages installed using this module and handlers are affected.
185
+
186
+    An mechanism may be added in the future to override this for a
187
+    subset of installed packages.
188
+    '''
189
+    packages = installed()
190
+    if not packages:
191
+        return
192
+    config = hookenv.config()
193
+    package_status = config.get('package_status') or ''
194
+    changed = reactive.helpers.data_changed('apt.package_status',
195
+                                            (package_status, sorted(packages)))
196
+    if changed:
197
+        if package_status == 'hold':
198
+            hookenv.log('Holding packages {}'.format(','.join(packages)))
199
+            fetch.apt_hold(packages)
200
+        else:
201
+            hookenv.log('Unholding packages {}'.format(','.join(packages)))
202
+            fetch.apt_unhold(packages)
203
+    reactive.remove_state('apt.needs_hold')
204
+
205
+
206
+def status_set(state, message):
207
+    '''Set the unit's workload status.
208
+
209
+    Set state == None to keep the same state and just change the message.
210
+    '''
211
+    if state is None:
212
+        state = hookenv.status_get()[0]
213
+        if state == 'unknown':
214
+            state = 'maintenance'  # Guess
215
+    if state in ('error', 'blocked'):
216
+        lvl = hookenv.WARNING
217
+    else:
218
+        lvl = hookenv.INFO
219
+    hookenv.status_set(state, message)
220
+    hookenv.log('{}: {}'.format(state, message), lvl)
Back to file index

lib/charms/layer/__init__.py

 1
--- 
 2
+++ lib/charms/layer/__init__.py
 3
@@ -0,0 +1,21 @@
 4
+import os
 5
+
 6
+
 7
+class LayerOptions(dict):
 8
+    def __init__(self, layer_file, section=None):
 9
+        import yaml  # defer, might not be available until bootstrap
10
+        with open(layer_file) as f:
11
+            layer = yaml.safe_load(f.read())
12
+        opts = layer.get('options', {})
13
+        if section and section in opts:
14
+            super(LayerOptions, self).__init__(opts.get(section))
15
+        else:
16
+            super(LayerOptions, self).__init__(opts)
17
+
18
+
19
+def options(section=None, layer_file=None):
20
+    if not layer_file:
21
+        base_dir = os.environ.get('JUJU_CHARM_DIR', os.getcwd())
22
+        layer_file = os.path.join(base_dir, 'layer.yaml')
23
+
24
+    return LayerOptions(layer_file, section)
Back to file index

lib/charms/layer/basic.py

  1
--- 
  2
+++ lib/charms/layer/basic.py
  3
@@ -0,0 +1,205 @@
  4
+import os
  5
+import sys
  6
+import shutil
  7
+from glob import glob
  8
+from subprocess import check_call, CalledProcessError
  9
+from time import sleep
 10
+
 11
+from charms.layer.execd import execd_preinstall
 12
+
 13
+
 14
+def lsb_release():
 15
+    """Return /etc/lsb-release in a dict"""
 16
+    d = {}
 17
+    with open('/etc/lsb-release', 'r') as lsb:
 18
+        for l in lsb:
 19
+            k, v = l.split('=')
 20
+            d[k.strip()] = v.strip()
 21
+    return d
 22
+
 23
+
 24
+def bootstrap_charm_deps():
 25
+    """
 26
+    Set up the base charm dependencies so that the reactive system can run.
 27
+    """
 28
+    # execd must happen first, before any attempt to install packages or
 29
+    # access the network, because sites use this hook to do bespoke
 30
+    # configuration and install secrets so the rest of this bootstrap
 31
+    # and the charm itself can actually succeed. This call does nothing
 32
+    # unless the operator has created and populated $JUJU_CHARM_DIR/exec.d.
 33
+    execd_preinstall()
 34
+    # ensure that $JUJU_CHARM_DIR/bin is on the path, for helper scripts
 35
+    charm_dir = os.environ['JUJU_CHARM_DIR']
 36
+    os.environ['PATH'] += ':%s' % os.path.join(charm_dir, 'bin')
 37
+    venv = os.path.abspath('../.venv')
 38
+    vbin = os.path.join(venv, 'bin')
 39
+    vpip = os.path.join(vbin, 'pip')
 40
+    vpy = os.path.join(vbin, 'python')
 41
+    if os.path.exists('wheelhouse/.bootstrapped'):
 42
+        activate_venv()
 43
+        return
 44
+    # bootstrap wheelhouse
 45
+    if os.path.exists('wheelhouse'):
 46
+        with open('/root/.pydistutils.cfg', 'w') as fp:
 47
+            # make sure that easy_install also only uses the wheelhouse
 48
+            # (see https://github.com/pypa/pip/issues/410)
 49
+            fp.writelines([
 50
+                "[easy_install]\n",
 51
+                "allow_hosts = ''\n",
 52
+                "find_links = file://{}/wheelhouse/\n".format(charm_dir),
 53
+            ])
 54
+        apt_install([
 55
+            'python3-pip',
 56
+            'python3-setuptools',
 57
+            'python3-yaml',
 58
+            'python3-dev',
 59
+        ])
 60
+        from charms import layer
 61
+        cfg = layer.options('basic')
 62
+        # include packages defined in layer.yaml
 63
+        apt_install(cfg.get('packages', []))
 64
+        # if we're using a venv, set it up
 65
+        if cfg.get('use_venv'):
 66
+            if not os.path.exists(venv):
 67
+                series = lsb_release()['DISTRIB_CODENAME']
 68
+                if series in ('precise', 'trusty'):
 69
+                    apt_install(['python-virtualenv'])
 70
+                else:
 71
+                    apt_install(['virtualenv'])
 72
+                cmd = ['virtualenv', '-ppython3', '--never-download', venv]
 73
+                if cfg.get('include_system_packages'):
 74
+                    cmd.append('--system-site-packages')
 75
+                check_call(cmd)
 76
+            os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
 77
+            pip = vpip
 78
+        else:
 79
+            pip = 'pip3'
 80
+            # save a copy of system pip to prevent `pip3 install -U pip`
 81
+            # from changing it
 82
+            if os.path.exists('/usr/bin/pip'):
 83
+                shutil.copy2('/usr/bin/pip', '/usr/bin/pip.save')
 84
+        # need newer pip, to fix spurious Double Requirement error:
 85
+        # https://github.com/pypa/pip/issues/56
 86
+        check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse',
 87
+                    'pip'])
 88
+        # install the rest of the wheelhouse deps
 89
+        check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse'] +
 90
+                   glob('wheelhouse/*'))
 91
+        if not cfg.get('use_venv'):
 92
+            # restore system pip to prevent `pip3 install -U pip`
 93
+            # from changing it
 94
+            if os.path.exists('/usr/bin/pip.save'):
 95
+                shutil.copy2('/usr/bin/pip.save', '/usr/bin/pip')
 96
+                os.remove('/usr/bin/pip.save')
 97
+        os.remove('/root/.pydistutils.cfg')
 98
+        # flag us as having already bootstrapped so we don't do it again
 99
+        open('wheelhouse/.bootstrapped', 'w').close()
100
+        # Ensure that the newly bootstrapped libs are available.
101
+        # Note: this only seems to be an issue with namespace packages.
102
+        # Non-namespace-package libs (e.g., charmhelpers) are available
103
+        # without having to reload the interpreter. :/
104
+        reload_interpreter(vpy if cfg.get('use_venv') else sys.argv[0])
105
+
106
+
107
+def activate_venv():
108
+    """
109
+    Activate the venv if enabled in ``layer.yaml``.
110
+
111
+    This is handled automatically for normal hooks, but actions might
112
+    need to invoke this manually, using something like:
113
+
114
+        # Load modules from $JUJU_CHARM_DIR/lib
115
+        import sys
116
+        sys.path.append('lib')
117
+
118
+        from charms.layer.basic import activate_venv
119
+        activate_venv()
120
+
121
+    This will ensure that modules installed in the charm's
122
+    virtual environment are available to the action.
123
+    """
124
+    venv = os.path.abspath('../.venv')
125
+    vbin = os.path.join(venv, 'bin')
126
+    vpy = os.path.join(vbin, 'python')
127
+    from charms import layer
128
+    cfg = layer.options('basic')
129
+    if cfg.get('use_venv') and '.venv' not in sys.executable:
130
+        # activate the venv
131
+        os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
132
+        reload_interpreter(vpy)
133
+
134
+
135
+def reload_interpreter(python):
136
+    """
137
+    Reload the python interpreter to ensure that all deps are available.
138
+
139
+    Newly installed modules in namespace packages sometimes seemt to
140
+    not be picked up by Python 3.
141
+    """
142
+    os.execve(python, [python] + list(sys.argv), os.environ)
143
+
144
+
145
+def apt_install(packages):
146
+    """
147
+    Install apt packages.
148
+
149
+    This ensures a consistent set of options that are often missed but
150
+    should really be set.
151
+    """
152
+    if isinstance(packages, (str, bytes)):
153
+        packages = [packages]
154
+
155
+    env = os.environ.copy()
156
+
157
+    if 'DEBIAN_FRONTEND' not in env:
158
+        env['DEBIAN_FRONTEND'] = 'noninteractive'
159
+
160
+    cmd = ['apt-get',
161
+           '--option=Dpkg::Options::=--force-confold',
162
+           '--assume-yes',
163
+           'install']
164
+    for attempt in range(3):
165
+        try:
166
+            check_call(cmd + packages, env=env)
167
+        except CalledProcessError:
168
+            if attempt == 2:  # third attempt
169
+                raise
170
+            sleep(5)
171
+        else:
172
+            break
173
+
174
+
175
+def init_config_states():
176
+    import yaml
177
+    from charmhelpers.core import hookenv
178
+    from charms.reactive import set_state
179
+    from charms.reactive import toggle_state
180
+    config = hookenv.config()
181
+    config_defaults = {}
182
+    config_defs = {}
183
+    config_yaml = os.path.join(hookenv.charm_dir(), 'config.yaml')
184
+    if os.path.exists(config_yaml):
185
+        with open(config_yaml) as fp:
186
+            config_defs = yaml.safe_load(fp).get('options', {})
187
+            config_defaults = {key: value.get('default')
188
+                               for key, value in config_defs.items()}
189
+    for opt in config_defs.keys():
190
+        if config.changed(opt):
191
+            set_state('config.changed')
192
+            set_state('config.changed.{}'.format(opt))
193
+        toggle_state('config.set.{}'.format(opt), config.get(opt))
194
+        toggle_state('config.default.{}'.format(opt),
195
+                     config.get(opt) == config_defaults[opt])
196
+    hookenv.atexit(clear_config_states)
197
+
198
+
199
+def clear_config_states():
200
+    from charmhelpers.core import hookenv, unitdata
201
+    from charms.reactive import remove_state
202
+    config = hookenv.config()
203
+    remove_state('config.changed')
204
+    for opt in config.keys():
205
+        remove_state('config.changed.{}'.format(opt))
206
+        remove_state('config.set.{}'.format(opt))
207
+        remove_state('config.default.{}'.format(opt))
208
+    unitdata.kv().flush()
Back to file index

lib/charms/layer/execd.py

  1
--- 
  2
+++ lib/charms/layer/execd.py
  3
@@ -0,0 +1,138 @@
  4
+# Copyright 2014-2016 Canonical Limited.
  5
+#
  6
+# This file is part of layer-basic, the reactive base layer for Juju.
  7
+#
  8
+# charm-helpers is free software: you can redistribute it and/or modify
  9
+# it under the terms of the GNU Lesser General Public License version 3 as
 10
+# published by the Free Software Foundation.
 11
+#
 12
+# charm-helpers is distributed in the hope that it will be useful,
 13
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
 14
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15
+# GNU Lesser General Public License for more details.
 16
+#
 17
+# You should have received a copy of the GNU Lesser General Public License
 18
+# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 19
+
 20
+# This module may only import from the Python standard library.
 21
+import os
 22
+import sys
 23
+import subprocess
 24
+import time
 25
+
 26
+'''
 27
+execd/preinstall
 28
+
 29
+It is often necessary to configure and reconfigure machines
 30
+after provisioning, but before attempting to run the charm.
 31
+Common examples are specialized network configuration, enabling
 32
+of custom hardware, non-standard disk partitioning and filesystems,
 33
+adding secrets and keys required for using a secured network.
 34
+
 35
+The reactive framework's base layer invokes this mechanism as
 36
+early as possible, before any network access is made or dependencies
 37
+unpacked or non-standard modules imported (including the charms.reactive
 38
+framework itself).
 39
+
 40
+Operators needing to use this functionality may branch a charm and
 41
+create an exec.d directory in it. The exec.d directory in turn contains
 42
+one or more subdirectories, each of which contains an executable called
 43
+charm-pre-install and any other required resources. The charm-pre-install
 44
+executables are run, and if successful, state saved so they will not be
 45
+run again.
 46
+
 47
+    $JUJU_CHARM_DIR/exec.d/mynamespace/charm-pre-install
 48
+
 49
+An alternative to branching a charm is to compose a new charm that contains
 50
+the exec.d directory, using the original charm as a layer,
 51
+
 52
+A charm author could also abuse this mechanism to modify the charm
 53
+environment in unusual ways, but for most purposes it is saner to use
 54
+charmhelpers.core.hookenv.atstart().
 55
+'''
 56
+
 57
+
 58
+def default_execd_dir():
 59
+    return os.path.join(os.environ['JUJU_CHARM_DIR'], 'exec.d')
 60
+
 61
+
 62
+def execd_module_paths(execd_dir=None):
 63
+    """Generate a list of full paths to modules within execd_dir."""
 64
+    if not execd_dir:
 65
+        execd_dir = default_execd_dir()
 66
+
 67
+    if not os.path.exists(execd_dir):
 68
+        return
 69
+
 70
+    for subpath in os.listdir(execd_dir):
 71
+        module = os.path.join(execd_dir, subpath)
 72
+        if os.path.isdir(module):
 73
+            yield module
 74
+
 75
+
 76
+def execd_submodule_paths(command, execd_dir=None):
 77
+    """Generate a list of full paths to the specified command within exec_dir.
 78
+    """
 79
+    for module_path in execd_module_paths(execd_dir):
 80
+        path = os.path.join(module_path, command)
 81
+        if os.access(path, os.X_OK) and os.path.isfile(path):
 82
+            yield path
 83
+
 84
+
 85
+def execd_sentinel_path(submodule_path):
 86
+    module_path = os.path.dirname(submodule_path)
 87
+    execd_path = os.path.dirname(module_path)
 88
+    module_name = os.path.basename(module_path)
 89
+    submodule_name = os.path.basename(submodule_path)
 90
+    return os.path.join(execd_path,
 91
+                        '.{}_{}.done'.format(module_name, submodule_name))
 92
+
 93
+
 94
+def execd_run(command, execd_dir=None, stop_on_error=True, stderr=None):
 95
+    """Run command for each module within execd_dir which defines it."""
 96
+    if stderr is None:
 97
+        stderr = sys.stdout
 98
+    for submodule_path in execd_submodule_paths(command, execd_dir):
 99
+        # Only run each execd once. We cannot simply run them in the
100
+        # install hook, as potentially storage hooks are run before that.
101
+        # We cannot rely on them being idempotent.
102
+        sentinel = execd_sentinel_path(submodule_path)
103
+        if os.path.exists(sentinel):
104
+            continue
105
+
106
+        try:
107
+            subprocess.check_call([submodule_path], stderr=stderr,
108
+                                  universal_newlines=True)
109
+            with open(sentinel, 'w') as f:
110
+                f.write('{} ran successfully {}\n'.format(submodule_path,
111
+                                                          time.ctime()))
112
+                f.write('Removing this file will cause it to be run again\n')
113
+        except subprocess.CalledProcessError as e:
114
+            # Logs get the details. We can't use juju-log, as the
115
+            # output may be substantial and exceed command line
116
+            # length limits.
117
+            print("ERROR ({}) running {}".format(e.returncode, e.cmd),
118
+                  file=stderr)
119
+            print("STDOUT<<EOM", file=stderr)
120
+            print(e.output, file=stderr)
121
+            print("EOM", file=stderr)
122
+
123
+            # Unit workload status gets a shorter fail message.
124
+            short_path = os.path.relpath(submodule_path)
125
+            block_msg = "Error ({}) running {}".format(e.returncode,
126
+                                                       short_path)
127
+            try:
128
+                subprocess.check_call(['status-set', 'blocked', block_msg],
129
+                                      universal_newlines=True)
130
+                if stop_on_error:
131
+                    sys.exit(0)  # Leave unit in blocked state.
132
+            except Exception:
133
+                pass  # We care about the exec.d/* failure, not status-set.
134
+
135
+            if stop_on_error:
136
+                sys.exit(e.returncode or 1)  # Error state for pre-1.24 Juju
137
+
138
+
139
+def execd_preinstall(execd_dir=None):
140
+    """Run charm-pre-install for each module within execd_dir."""
141
+    execd_run('charm-pre-install', execd_dir=execd_dir)
Back to file index

lib/charms/layer/nginx.py

 1
--- 
 2
+++ lib/charms/layer/nginx.py
 3
@@ -0,0 +1,52 @@
 4
+from charmhelpers.core.templating import render
 5
+from charmhelpers.core import hookenv
 6
+from charmhelpers.core import host
 7
+
 8
+import toml
 9
+import os
10
+
11
+
12
+def load_site():
13
+    if not os.path.isfile('site.toml'):
14
+        return {}
15
+
16
+    with open('site.toml') as fp:
17
+        conf = toml.loads(fp.read())
18
+
19
+    return conf
20
+
21
+
22
+def get_app_path():
23
+    site = load_site()
24
+    if 'app_path' in site:
25
+        return site['app_path']
26
+    return '/srv/app'
27
+
28
+
29
+def configure_site(site, template, **kwargs):
30
+    """ configures vhost
31
+
32
+    Arguments:
33
+    site: Site name
34
+    template: template to process in templates/<template.conf>
35
+    **kwargs: additional dict items to append to template variables exposed
36
+              through the site.toml
37
+    """
38
+    hookenv.status_set('maintenance', 'Configuring site {}'.format(site))
39
+
40
+    config = hookenv.config()
41
+    context = load_site()
42
+    context['host'] = config['host']
43
+    context['port'] = config['port']
44
+    context.update(**kwargs)
45
+    conf_path = '/etc/nginx/sites-enabled/{}'.format(site)
46
+    if os.path.exists(conf_path):
47
+        os.remove(conf_path)
48
+    render(source=template,
49
+           target=conf_path,
50
+           context=context)
51
+    hookenv.log('Wrote vhost config {} to {}'.format(context, template),
52
+                'info')
53
+
54
+    host.service_reload('nginx')
55
+    hookenv.status_set('active', '')
Back to file index

lib/nginxlib.py

1
--- 
2
+++ lib/nginxlib.py
3
@@ -0,0 +1,4 @@
4
+from warnings import warn
5
+from charms.layer.nginx import *  # noqa
6
+
7
+warn('nginxlib is being deprecated, use charms.layer.nginx instead')
Back to file index

metadata.yaml

 1
--- 
 2
+++ metadata.yaml
 3
@@ -0,0 +1,24 @@
 4
+"name": "kubeapi-load-balancer"
 5
+"summary": "Nginx Load Balancer"
 6
+"maintainers":
 7
+- "Charles Butler <charles.butler@canonical.com>"
 8
+"description": |
 9
+  A round robin Nginx load balancer to distribute traffic for kubernetes apiservers.
10
+"tags":
11
+- "misc"
12
+"series":
13
+- "xenial"
14
+"requires":
15
+  "certificates":
16
+    "interface": "tls-certificates"
17
+  "apiserver":
18
+    "interface": "http"
19
+"provides":
20
+  "nrpe-external-master":
21
+    "interface": "nrpe-external-master"
22
+    "scope": "container"
23
+  "website":
24
+    "interface": "http"
25
+  "loadbalancer":
26
+    "interface": "public-address"
27
+"subordinate": !!bool "false"
Back to file index

metrics.yaml

1
--- 
2
+++ metrics.yaml
3
@@ -0,0 +1,2 @@
4
+metrics:
5
+  juju-units: {}
Back to file index

reactive/apt.py

  1
--- 
  2
+++ reactive/apt.py
  3
@@ -0,0 +1,132 @@
  4
+# Copyright 2015-2016 Canonical Ltd.
  5
+#
  6
+# This file is part of the Apt layer for Juju.
  7
+#
  8
+# This program is free software: you can redistribute it and/or modify
  9
+# it under the terms of the GNU General Public License version 3, as
 10
+# published by the Free Software Foundation.
 11
+#
 12
+# This program is distributed in the hope that it will be useful, but
 13
+# WITHOUT ANY WARRANTY; without even the implied warranties of
 14
+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 15
+# PURPOSE.  See the GNU General Public License for more details.
 16
+#
 17
+# You should have received a copy of the GNU General Public License
 18
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 19
+
 20
+'''
 21
+charms.reactive helpers for dealing with deb packages.
 22
+
 23
+Add apt package sources using add_source(). Queue deb packages for
 24
+installation with install(). Configure and work with your software
 25
+once the apt.installed.{packagename} state is set.
 26
+'''
 27
+import subprocess
 28
+
 29
+from charmhelpers import fetch
 30
+from charmhelpers.core import hookenv
 31
+from charmhelpers.core.hookenv import WARNING
 32
+from charms import layer
 33
+from charms import reactive
 34
+from charms.reactive import when, when_not
 35
+
 36
+import charms.apt
 37
+
 38
+
 39
+@when('apt.needs_update')
 40
+def update():
 41
+    charms.apt.update()
 42
+
 43
+
 44
+@when('apt.queued_installs')
 45
+@when_not('apt.needs_update')
 46
+def install_queued():
 47
+    charms.apt.install_queued()
 48
+
 49
+
 50
+@when_not('apt.queued_installs')
 51
+def ensure_package_status():
 52
+    charms.apt.ensure_package_status()
 53
+
 54
+
 55
+def filter_installed_packages(packages):
 56
+    # Don't use fetch.filter_installed_packages, as it depends on python-apt
 57
+    # and not available if the basic layer's use_site_packages option is off
 58
+    cmd = ['dpkg-query', '--show', r'--showformat=${Package}\n']
 59
+    installed = set(subprocess.check_output(cmd,
 60
+                                            universal_newlines=True).split())
 61
+    return set(packages) - installed
 62
+
 63
+
 64
+def clear_removed_package_states():
 65
+    """On hook startup, clear install states for removed packages."""
 66
+    removed = filter_installed_packages(charms.apt.installed())
 67
+    if removed:
 68
+        hookenv.log('{} missing packages ({})'.format(len(removed),
 69
+                                                      ','.join(removed)),
 70
+                    WARNING)
 71
+        for package in removed:
 72
+            reactive.remove_state('apt.installed.{}'.format(package))
 73
+
 74
+
 75
+def configure_sources():
 76
+    """Add user specified package sources from the service configuration.
 77
+
 78
+    See charmhelpers.fetch.configure_sources for details.
 79
+    """
 80
+    config = hookenv.config()
 81
+
 82
+    # We don't have enums, so we need to validate this ourselves.
 83
+    package_status = config.get('package_status') or ''
 84
+    if package_status not in ('hold', 'install'):
 85
+        charms.apt.status_set('blocked',
 86
+                              'Unknown package_status {}'
 87
+                              ''.format(package_status))
 88
+        # Die before further hooks are run. This isn't very nice, but
 89
+        # there is no other way to inform the operator that they have
 90
+        # invalid configuration.
 91
+        raise SystemExit(0)
 92
+
 93
+    sources = config.get('install_sources') or ''
 94
+    keys = config.get('install_keys') or ''
 95
+    if reactive.helpers.data_changed('apt.configure_sources', (sources, keys)):
 96
+        fetch.configure_sources(update=False,
 97
+                                sources_var='install_sources',
 98
+                                keys_var='install_keys')
 99
+        reactive.set_state('apt.needs_update')
100
+
101
+    # Clumsy 'config.get() or' per Bug #1641362
102
+    extra_packages = sorted((config.get('extra_packages') or '').split())
103
+    if extra_packages:
104
+        charms.apt.queue_install(extra_packages)
105
+
106
+
107
+def queue_layer_packages():
108
+    """Add packages listed in build-time layer options."""
109
+    # Both basic and apt layer. basic layer will have already installed
110
+    # its defined packages, but rescheduling it here gets the apt layer
111
+    # state set and they will pinned as any other apt layer installed
112
+    # package.
113
+    opts = layer.options()
114
+    for section in ['basic', 'apt']:
115
+        if section in opts and 'packages' in opts[section]:
116
+            charms.apt.queue_install(opts[section]['packages'])
117
+
118
+
119
+# Per https://github.com/juju-solutions/charms.reactive/issues/33,
120
+# this module may be imported multiple times so ensure the
121
+# initialization hook is only registered once. I have to piggy back
122
+# onto the namespace of a module imported before reactive discovery
123
+# to do this.
124
+if not hasattr(reactive, '_apt_registered'):
125
+    # We need to register this to run every hook, not just during install
126
+    # and config-changed, to protect against race conditions. If we don't
127
+    # do this, then the config in the hook environment may show updates
128
+    # to running hooks well before the config-changed hook has been invoked
129
+    # and the intialization provided an opertunity to be run.
130
+    hookenv.atstart(hookenv.log, 'Initializing Apt Layer')
131
+    hookenv.atstart(clear_removed_package_states)
132
+    hookenv.atstart(configure_sources)
133
+    hookenv.atstart(queue_layer_packages)
134
+    hookenv.atstart(charms.apt.reset_application_version)
135
+    reactive._apt_registered = True
Back to file index

reactive/load_balancer.py

  1
--- 
  2
+++ reactive/load_balancer.py
  3
@@ -0,0 +1,153 @@
  4
+#!/usr/bin/env python
  5
+
  6
+# Copyright 2015 The Kubernetes Authors.
  7
+#
  8
+# Licensed under the Apache License, Version 2.0 (the "License");
  9
+# you may not use this file except in compliance with the License.
 10
+# You may obtain a copy of the License at
 11
+#
 12
+#     http://www.apache.org/licenses/LICENSE-2.0
 13
+#
 14
+# Unless required by applicable law or agreed to in writing, software
 15
+# distributed under the License is distributed on an "AS IS" BASIS,
 16
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 17
+# See the License for the specific language governing permissions and
 18
+# limitations under the License.
 19
+
 20
+import os
 21
+import socket
 22
+import subprocess
 23
+
 24
+from charms import layer
 25
+from charms.reactive import when, when_any, when_not
 26
+from charms.reactive import set_state, remove_state
 27
+from charmhelpers.core import hookenv
 28
+from charmhelpers.contrib.charmsupport import nrpe
 29
+
 30
+from charms.layer import nginx
 31
+
 32
+from subprocess import Popen
 33
+from subprocess import PIPE
 34
+from subprocess import STDOUT
 35
+
 36
+
 37
+@when('certificates.available')
 38
+def request_server_certificates(tls):
 39
+    '''Send the data that is required to create a server certificate for
 40
+    this server.'''
 41
+    # Use the public ip of this unit as the Common Name for the certificate.
 42
+    common_name = hookenv.unit_public_ip()
 43
+    # Create SANs that the tls layer will add to the server cert.
 44
+    sans = [
 45
+        hookenv.unit_public_ip(),
 46
+        hookenv.unit_private_ip(),
 47
+        socket.gethostname(),
 48
+    ]
 49
+    # Create a path safe name by removing path characters from the unit name.
 50
+    certificate_name = hookenv.local_unit().replace('/', '_')
 51
+    # Request a server cert with this information.
 52
+    tls.request_server_cert(common_name, sans, certificate_name)
 53
+
 54
+
 55
+@when('nginx.available', 'apiserver.available',
 56
+      'certificates.server.cert.available')
 57
+def install_load_balancer(apiserver, tls):
 58
+    ''' Create the default vhost template for load balancing '''
 59
+    # Get the tls paths from the layer data.
 60
+    layer_options = layer.options('tls-client')
 61
+    server_cert_path = layer_options.get('server_certificate_path')
 62
+    cert_exists = server_cert_path and os.path.isfile(server_cert_path)
 63
+    server_key_path = layer_options.get('server_key_path')
 64
+    key_exists = server_key_path and os.path.isfile(server_key_path)
 65
+    # Do both the the key and certificate exist?
 66
+    if cert_exists and key_exists:
 67
+        # At this point the cert and key exist, and they are owned by root.
 68
+        chown = ['chown', 'www-data:www-data', server_cert_path]
 69
+        # Change the owner to www-data so the nginx process can read the cert.
 70
+        subprocess.call(chown)
 71
+        chown = ['chown', 'www-data:www-data', server_key_path]
 72
+        # Change the owner to www-data so the nginx process can read the key.
 73
+        subprocess.call(chown)
 74
+
 75
+        hookenv.open_port(hookenv.config('port'))
 76
+        services = apiserver.services()
 77
+        nginx.configure_site(
 78
+                'apilb',
 79
+                'apilb.conf',
 80
+                server_name='_',
 81
+                services=services,
 82
+                port=hookenv.config('port'),
 83
+                server_certificate=server_cert_path,
 84
+                server_key=server_key_path,
 85
+        )
 86
+        hookenv.status_set('active', 'Loadbalancer ready.')
 87
+
 88
+
 89
+@when('nginx.available')
 90
+def set_nginx_version():
 91
+    ''' Surface the currently deployed version of nginx to Juju '''
 92
+    cmd = 'nginx -v'
 93
+    p = Popen(cmd, shell=True,
 94
+              stdin=PIPE,
 95
+              stdout=PIPE,
 96
+              stderr=STDOUT,
 97
+              close_fds=True)
 98
+    raw = p.stdout.read()
 99
+    # The version comes back as:
100
+    # nginx version: nginx/1.10.0 (Ubuntu)
101
+    version = raw.split(b'/')[-1].split(b' ')[0]
102
+    hookenv.application_version_set(version.rstrip())
103
+
104
+
105
+@when('website.available')
106
+def provide_application_details(website):
107
+    ''' re-use the nginx layer website relation to relay the hostname/port
108
+    to any consuming kubernetes-workers, or other units that require the
109
+    kubernetes API '''
110
+    website.configure(port=hookenv.config('port'))
111
+
112
+
113
+@when('loadbalancer.available')
114
+def provide_loadbalancing(loadbalancer):
115
+    '''Send the public address and port to the public-address interface, so
116
+    the subordinates can get the public address of this loadbalancer.'''
117
+    loadbalancer.set_address_port(hookenv.unit_get('public-address'),
118
+                                  hookenv.config('port'))
119
+
120
+
121
+@when('nrpe-external-master.available')
122
+@when_not('nrpe-external-master.initial-config')
123
+def initial_nrpe_config(nagios=None):
124
+    set_state('nrpe-external-master.initial-config')
125
+    update_nrpe_config(nagios)
126
+
127
+
128
+@when('nginx.available')
129
+@when('nrpe-external-master.available')
130
+@when_any('config.changed.nagios_context',
131
+          'config.changed.nagios_servicegroups')
132
+def update_nrpe_config(unused=None):
133
+    services = ('nginx',)
134
+
135
+    hostname = nrpe.get_nagios_hostname()
136
+    current_unit = nrpe.get_nagios_unit_name()
137
+    nrpe_setup = nrpe.NRPE(hostname=hostname)
138
+    nrpe.add_init_service_checks(nrpe_setup, services, current_unit)
139
+    nrpe_setup.write()
140
+
141
+
142
+@when_not('nrpe-external-master.available')
143
+@when('nrpe-external-master.initial-config')
144
+def remove_nrpe_config(nagios=None):
145
+    remove_state('nrpe-external-master.initial-config')
146
+
147
+    # List of systemd services for which the checks will be removed
148
+    services = ('nginx',)
149
+
150
+    # The current nrpe-external-master interface doesn't handle a lot of logic,
151
+    # use the charm-helpers code for now.
152
+    hostname = nrpe.get_nagios_hostname()
153
+    nrpe_setup = nrpe.NRPE(hostname=hostname)
154
+
155
+    for service in services:
156
+        nrpe_setup.remove_check(shortname=service)
Back to file index

reactive/nginx.py

 1
--- 
 2
+++ reactive/nginx.py
 3
@@ -0,0 +1,34 @@
 4
+from charms.reactive import (
 5
+    set_state,
 6
+    when_not,
 7
+    when
 8
+)
 9
+
10
+from charmhelpers.core import hookenv
11
+import os
12
+
13
+config = hookenv.config()
14
+
15
+
16
+# handlers --------------------------------------------------------------------
17
+@when('apt.installed.nginx-full')
18
+@when_not('nginx.available')
19
+def nginx_ready():
20
+    if os.path.exists('/etc/nginx/sites-enabled/default'):
21
+        os.remove('/etc/nginx/sites-enabled/default')
22
+
23
+    hookenv.status_set('active', 'NGINX is ready')
24
+    set_state('nginx.available')
25
+
26
+
27
+# Example website.available reaction ------------------------------------------
28
+"""
29
+This example reaction for an application layer which consumes this nginx layer.
30
+If left here then this reaction may overwrite your top-level reaction depending
31
+on service names, ie., both nginx and ghost have the same reaction method,
32
+however, nginx will execute since it's a higher precedence.
33
+
34
+@when('nginx.available', 'website.available')
35
+def configure_website(website):
36
+    website.configure(port=config['port'])
37
+"""
Back to file index

reactive/tls_client.py

  1
--- 
  2
+++ reactive/tls_client.py
  3
@@ -0,0 +1,118 @@
  4
+import os
  5
+
  6
+from subprocess import check_call
  7
+
  8
+from charms import layer
  9
+from charms.reactive import hook
 10
+from charms.reactive import set_state, remove_state
 11
+from charms.reactive import when
 12
+from charms.reactive.helpers import data_changed
 13
+
 14
+from charmhelpers.core import hookenv
 15
+from charmhelpers.core.hookenv import log
 16
+
 17
+
 18
+@when('certificates.ca.available')
 19
+def store_ca(tls):
 20
+    '''Read the certificate authority from the relation object and install
 21
+    the ca on this system.'''
 22
+    # Get the CA from the relationship object.
 23
+    certificate_authority = tls.get_ca()
 24
+    if certificate_authority:
 25
+        layer_options = layer.options('tls-client')
 26
+        ca_path = layer_options.get('ca_certificate_path')
 27
+        changed = data_changed('certificate_authority', certificate_authority)
 28
+        if ca_path:
 29
+            if changed or not os.path.exists(ca_path):
 30
+                log('Writing CA certificate to {0}'.format(ca_path))
 31
+                _write_file(ca_path, certificate_authority)
 32
+            set_state('tls_client.ca.saved')
 33
+        if changed:
 34
+            # Update /etc/ssl/certs and generate ca-certificates.crt
 35
+            install_ca(certificate_authority)
 36
+
 37
+
 38
+@when('certificates.server.cert.available')
 39
+def store_server(tls):
 40
+    '''Read the server certificate and server key from the relation object
 41
+    and save them to the certificate directory..'''
 42
+    server_cert, server_key = tls.get_server_cert()
 43
+    if server_cert and server_key:
 44
+        layer_options = layer.options('tls-client')
 45
+        cert_path = layer_options.get('server_certificate_path')
 46
+        key_path = layer_options.get('server_key_path')
 47
+        cert_changed = data_changed('server_certificate', server_cert)
 48
+        key_changed = data_changed('server_key', server_key)
 49
+        if cert_path:
 50
+            if cert_changed or not os.path.exists(cert_path):
 51
+                log('Writing server certificate to {0}'.format(cert_path))
 52
+                _write_file(cert_path, server_cert)
 53
+            set_state('tls_client.server.certificate.saved')
 54
+        if key_path:
 55
+            if key_changed or not os.path.exists(key_path):
 56
+                log('Writing server key to {0}'.format(key_path))
 57
+                _write_file(key_path, server_key)
 58
+            set_state('tls_client.server.key.saved')
 59
+
 60
+
 61
+@when('certificates.client.cert.available')
 62
+def store_client(tls):
 63
+    '''Read the client certificate and client key from the relation object
 64
+    and copy them to the certificate directory.'''
 65
+    client_cert, client_key = tls.get_client_cert()
 66
+    if client_cert and client_key:
 67
+        layer_options = layer.options('tls-client')
 68
+        cert_path = layer_options.get('client_certificate_path')
 69
+        key_path = layer_options.get('client_key_path')
 70
+        cert_changed = data_changed('client_certificate', client_cert)
 71
+        key_changed = data_changed('client_key', client_key)
 72
+        if cert_path:
 73
+            if cert_changed or not os.path.exists(cert_path):
 74
+                log('Writing client certificate to {0}'.format(cert_path))
 75
+                _write_file(cert_path, client_cert)
 76
+            set_state('tls_client.client.certificate.saved')
 77
+        if key_path:
 78
+            if key_changed or not os.path.exists(key_path):
 79
+                log('Writing client key to {0}'.format(key_path))
 80
+                _write_file(key_path, client_key)
 81
+            set_state('tls_client.client.key.saved')
 82
+
 83
+
 84
+def install_ca(certificate_authority):
 85
+    '''Install a certificiate authority on the system by calling the
 86
+    update-ca-certificates command.'''
 87
+    if certificate_authority:
 88
+        name = hookenv.service_name()
 89
+        # Create a path to install CAs on Debian systems.
 90
+        ca_path = '/usr/local/share/ca-certificates/{0}.crt'.format(name)
 91
+        log('Writing CA certificate to {0}'.format(ca_path))
 92
+        _write_file(ca_path, certificate_authority)
 93
+        # Update the trusted CAs on this system (a time expensive operation).
 94
+        check_call(['update-ca-certificates'])
 95
+        log('Generated ca-certificates.crt for {0}'.format(name))
 96
+        set_state('tls_client.ca_installed')
 97
+
 98
+
 99
+@hook('upgrade-charm')
100
+def remove_states():
101
+    remove_state('tls_client.ca.saved')
102
+    remove_state('tls_client.server.certificate.saved')
103
+    remove_state('tls_client.server.key.saved')
104
+    remove_state('tls_client.client.certificate.saved')
105
+    remove_state('tls_client.client.key.saved')
106
+
107
+
108
+def _ensure_directory(path):
109
+    '''Ensure the parent directory exists creating directories if necessary.'''
110
+    directory = os.path.dirname(path)
111
+    if not os.path.isdir(directory):
112
+        os.makedirs(directory)
113
+    os.chmod(directory, 0o770)
114
+
115
+
116
+def _write_file(path, content):
117
+    '''Write the path to a file.'''
118
+    _ensure_directory(path)
119
+    with open(path, 'w') as stream:
120
+        stream.write(content)
121
+    os.chmod(path, 0o770)
Back to file index

requirements.txt

1
--- 
2
+++ requirements.txt
3
@@ -0,0 +1,2 @@
4
+flake8
5
+pytest
Back to file index

revision

1
--- 
2
+++ revision
3
@@ -0,0 +1 @@
4
+0
Back to file index

templates/apilb.conf

 1
--- 
 2
+++ templates/apilb.conf
 3
@@ -0,0 +1,44 @@
 4
+{% for app in services -%}
 5
+upstream target_service {
 6
+  {% for host in app['hosts'] -%}
 7
+  server {{ host['hostname'] }}:{{ host['port'] }};
 8
+  {% endfor %}
 9
+}
10
+{% endfor %}
11
+
12
+
13
+server {
14
+    listen 443 ssl http2;
15
+    server_name {{ server_name }};
16
+
17
+    access_log /var/log/nginx.access.log;
18
+    error_log /var/log/nginx.error.log;
19
+
20
+    ssl on;
21
+    ssl_session_cache builtin:1000 shared:SSL:10m;
22
+    ssl_certificate {{ server_certificate }};
23
+    ssl_certificate_key {{ server_key }};
24
+    ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
25
+    ssl_prefer_server_ciphers on;
26
+
27
+
28
+    location / {
29
+      proxy_buffering         off;
30
+      proxy_set_header        Host $host;
31
+      proxy_set_header        X-Real-IP $remote_addr;
32
+      proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
33
+      proxy_set_header        X-Forwarded-Proto $scheme;
34
+      proxy_set_header        X-Forwarded-Proto-Version $http2;
35
+      proxy_set_header        Upgrade $http_upgrade;
36
+      proxy_set_header        Connection $http_connection;
37
+      proxy_set_header        X-Stream-Protocol-Version $http_x_stream_protocol_version;
38
+
39
+      proxy_ssl_certificate   {{ server_certificate }};
40
+      proxy_ssl_certificate_key {{ server_key }};
41
+
42
+      add_header              X-Stream-Protocol-Version $upstream_http_x_stream_protocol_version;
43
+
44
+      proxy_pass              https://target_service;
45
+      proxy_read_timeout      90;
46
+    }
47
+}
Back to file index

templates/vhost.conf.ex

 1
--- 
 2
+++ templates/vhost.conf.ex
 3
@@ -0,0 +1,18 @@
 4
+server {
 5
+    listen 80;
 6
+
 7
+    server_name {{server_name}};
 8
+
 9
+    location / {
10
+      proxy_redirect off;
11
+      proxy_set_header   X-Real-IP            $remote_addr;
12
+      proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
13
+      proxy_set_header   X-Forwarded-Proto $scheme;
14
+      proxy_set_header   Host                   $http_host;
15
+      proxy_set_header   X-NginX-Proxy    true;
16
+      proxy_set_header   Connection "";
17
+      proxy_http_version 1.1;
18
+      proxy_pass http://{{host}}:{{port}};
19
+
20
+    }
21
+}
Back to file index

tox.ini

 1
--- 
 2
+++ tox.ini
 3
@@ -0,0 +1,12 @@
 4
+[tox]
 5
+skipsdist=True
 6
+envlist = py34, py35
 7
+skip_missing_interpreters = True
 8
+
 9
+[testenv]
10
+commands = py.test -v
11
+deps =
12
+    -r{toxinidir}/requirements.txt
13
+
14
+[flake8]
15
+exclude=docs