~containers/easyrsa

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
lxc RETRY 19 days ago
gce RETRY 19 days ago
aws 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. johnsca
  • 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. johnsca
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. johnsca
Should be built using charm layers. johnsca
Should use Juju Resources to deliver required payloads. johnsca

Testing and Quality

charm proof must pass without errors or warnings. johnsca
Must include passing unit, functional, or integration tests. johnsca
Tests must exercise all relations. johnsca
Tests must exercise config. johnsca
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). johnsca
Must be self contained unless the charm is a proxy for an existing cloud service, e.g. ec2-elb charm.
Must not use symlinks. johnsca
Bundles must only use promulgated charms, they cannot reference charms in personal namespaces. johnsca
Must call Juju hook tools (relation-*, unit-*, config-*, etc) without a hard coded path. johnsca
Should include a tests.yaml for all integration tests. johnsca

Metadata

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

Security

Must not run any network services using default passwords. johnsca
Must verify and validate any external payload johnsca
  • 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. johnsca
Should avoid running services as root. johnsca

All changes | Changes since last revision

Source Diff

Inline diff comments 0

No comments yet.

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,113 @@
  4
+# EasyRSA
  5
+
  6
+This charm delivers the EasyRSA application to act as a Certificate Authority
  7
+(CA) and creates certificates for related charms.
  8
+
  9
+EasyRSA is a command line utility to build and manage Public Key 
 10
+Infrastructure (PKI) Certificate Authority (CA).
 11
+
 12
+The purpose of a Public Key Infrastructure (PKI) is to facilitate the secure
 13
+electronic transfer of information.
 14
+
 15
+## Deployment
 16
+You can deploy an EasyRSA charm with Juju:
 17
+
 18
+```
 19
+juju deploy easyrsa
 20
+juju deploy tls-client
 21
+juju add-relation easyrsa tls-client
 22
+```
 23
+
 24
+## Using the easyrsa charm
 25
+
 26
+The easyrsa charm will become a Certificate Authority (CA) and generate a CA
 27
+certificate. Other charms need only to relate to easyrsa with a requires 
 28
+using the `tls-certificates` interface.
 29
+
 30
+To get a server certificate from easyrsa, the charm must include the 
 31
+`interface:tls-certificates` interface in the `layer.yaml` file. The charm must
 32
+also require the `tls` interface, in the `metadata.yaml`. The relation name may
 33
+be named what ever you wish, assume the relation is named "certificates" for 
 34
+these examples.
 35
+
 36
+### CA
 37
+
 38
+The interface will generate a CA certificate immediately. If another charm 
 39
+requires a CA certificate the code must react to the flag
 40
+`certificates.ca.available`. The relationship object has a method named 
 41
+`get_ca` which returns the CA certificate.
 42
+
 43
+```python
 44
+@when('certificates.ca.available')
 45
+def store_ca(tls):
 46
+    '''Read the certificate authority from the relation object and install it
 47
+    on this system.'''
 48
+    # Get the CA from the relationship object.
 49
+    ca_cert = tls.get_ca()
 50
+    write_file('/usr/local/share/ca-certificates/easyrsa.crt', ca_cert)
 51
+```
 52
+
 53
+### Client certificate and key
 54
+
 55
+The easyrsa charm generates a client certificate after the CA certificate is 
 56
+created. If another charm needs the CA the code must react to the flag
 57
+`certificates.client.cert.available`.  The relationship object has a method 
 58
+that returns the client cert and client key called `get_client_cert`.
 59
+
 60
+```python
 61
+@when('certificates.client.cert.available')
 62
+def store_client(tls):
 63
+    '''Read the client certificate from the relation object and install it on
 64
+    this system.'''
 65
+    client_cert, client_key = tls.get_client_cert()
 66
+    write_file('/home/ubuntu/client.crt', client_cert)
 67
+    write_file('/home/ubuntu/client.key', client_key)
 68
+```
 69
+
 70
+### Request a server certificate
 71
+
 72
+The interface will set `certificates.available` flag on a relation. The
 73
+reactive code should send three values on the relation to request a 
 74
+certificate. Call the `request_server_cert` method on the relationship object. 
 75
+The three values are: Common Name (CN), a list of Subject Alt Names (SANs), and
 76
+the file name of the certificate (the unit name with the  '/' replaced with an
 77
+underscore). For example a client charm would send:
 78
+
 79
+```python
 80
+@when('certificates.available')
 81
+def send_data(tls):
 82
+    # Use the public ip of this unit as the Common Name for the certificate.
 83
+    common_name = hookenv.unit_public_ip()
 84
+    # Get a list of Subject Alt Names for the certificate.
 85
+    sans = []
 86
+    sans.append(hookenv.unit_public_ip())
 87
+    sans.append(hookenv.unit_private_ip())
 88
+    sans.append(socket.gethostname())
 89
+    # Create a path safe name by removing path characters from the unit name.
 90
+    certificate_name = hookenv.local_unit().replace('/', '_')
 91
+    # Send the information on the relation object.
 92
+    tls.request_server_cert(common_name, sans, certificate_name)
 93
+```
 94
+
 95
+### Server certificate and key
 96
+
 97
+The easyrsa charm generates the server certificate and key after the request 
 98
+have been made. If another charm needs the server certificate the code must 
 99
+react to the flag `{relation_name}.server.cert.available`.  The relationship 
100
+object has a method that returns the server cert and server key called 
101
+`get_server_cert`.
102
+
103
+```python
104
+@when('certificates.server.cert.available')
105
+def store_server(tls):
106
+    '''Read the server certificate from the relation object and install it on
107
+    this system.'''
108
+    server_cert, server_key = tls.get_server_cert()
109
+    write_file('/home/ubuntu/server.cert', server_cert)
110
+    write_file('/home/ubuntu/server.key', server_key)
111
+```
112
+
113
+## Contact
114
+
115
+ * Author: Matthew Bruzek <Matthew.Bruzek@canonical.com>
116
+ * Contributor: Cory Johns <Cory.Johns@canonical.com>
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 @@
4
+"options": {}
Back to file index

copyright

 1
--- 
 2
+++ copyright
 3
@@ -0,0 +1,13 @@
 4
+Copyright 2016 Canonical Ltd.
 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/client-relation-broken

 1
--- 
 2
+++ hooks/client-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/client-relation-changed

 1
--- 
 2
+++ hooks/client-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/client-relation-departed

 1
--- 
 2
+++ hooks/client-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/client-relation-joined

 1
--- 
 2
+++ hooks/client-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/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/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 <Matthew.Bruzek@canonical.com> 
80
+
81
+Contributor: Charles Butler <Charles.Butler@canonical.com> 
82
+
83
+Contributor: Cory Johns <Cory.Johns@canonical.com> 
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

icon.svg

  1
--- 
  2
+++ icon.svg
  3
@@ -0,0 +1,352 @@
  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="easy-rsa_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="12.434497"
229
+     inkscape:cx="-0.26630172"
230
+     inkscape:cy="28.864782"
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="true"
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
+       inkscape:connector-curvature="0"
307
+       style="display:inline;fill:#f68122;fill-opacity:1;stroke:none"
308
+       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"
309
+       id="path6455-3" />
310
+    <path
311
+       style="fill:#ffffff;fill-opacity:1"
312
+       d="M 47.978516 16.003906 C 45.336066 16.006906 43.513133 16.199339 41.132812 16.724609 C 32.423442 18.646489 24.936344 24.086293 20.402344 31.789062 C 17.896104 36.046902 16.449092 40.764159 16.076172 45.912109 C 15.572402 52.866429 17.570058 60.169986 21.580078 66.035156 C 24.090508 69.706996 27.499414 72.960761 31.183594 75.207031 C 31.608694 75.466211 31.9763 75.679744 32 75.677734 C 32.0237 75.676734 32.283992 75.252458 32.576172 74.736328 C 32.868332 74.220178 33.441839 73.222118 33.849609 72.517578 C 35.050149 70.443348 38.152283 65.071185 38.939453 63.703125 C 39.220813 63.214175 39.447266 62.780201 39.447266 62.738281 C 39.447266 62.696381 39.104161 62.433697 38.681641 62.154297 C 37.642731 61.467247 37.156541 61.070078 36.181641 60.117188 C 35.196301 59.154077 34.611347 58.462274 33.898438 57.402344 C 30.079267 51.724234 30.098599 44.226584 33.943359 38.496094 C 34.790879 37.232894 36.182226 35.717377 37.410156 34.716797 C 39.078056 33.357717 41.497207 32.149282 43.685547 31.576172 C 45.310067 31.150722 46.121055 31.048828 48.015625 31.048828 C 49.910195 31.048828 50.723126 31.150722 52.347656 31.576172 C 54.540816 32.150542 56.914507 33.338297 58.623047 34.716797 C 59.238007 35.212967 60.627798 36.586158 61.142578 37.210938 C 62.680358 39.077297 63.871514 41.445989 64.458984 43.792969 C 64.876124 45.459479 64.949219 46.067089 64.949219 47.980469 C 64.949219 49.893839 64.876144 50.501459 64.458984 52.167969 C 63.871504 54.514959 62.680358 56.885573 61.142578 58.751953 C 60.596818 59.414313 59.212376 60.774213 58.572266 61.283203 C 58.323286 61.481193 57.774082 61.874877 57.351562 62.154297 C 56.929053 62.433697 56.585938 62.697501 56.585938 62.738281 C 56.585938 62.807181 59.903744 68.597765 62.027344 72.234375 C 62.878244 73.691515 63.952342 75.566652 63.976562 75.638672 C 64.015762 75.755272 64.29155 75.600349 65.6875 74.667969 C 67.77825 73.271489 68.799866 72.430129 70.603516 70.636719 C 73.341136 67.914619 75.381186 65.030044 76.947266 61.677734 C 79.063926 57.146974 80 52.958829 80 47.980469 C 80 44.584239 79.589845 41.762664 78.640625 38.652344 C 77.476365 34.837464 75.524986 31.132874 73.066406 28.052734 C 68.731286 22.621624 62.732846 18.716122 56.103516 17.013672 C 53.182686 16.263582 51.042246 15.999736 47.978516 16.003906 z M 48.054688 37.412109 C 45.979167 37.412109 43.862516 38.057009 42.097656 39.230469 C 41.212316 39.819129 39.860154 41.17716 39.271484 42.0625 C 36.859644 45.68986 36.859644 50.271077 39.271484 53.898438 C 39.860994 54.785048 41.217479 56.141962 42.105469 56.732422 C 42.485669 56.985222 43.174685 57.372017 43.640625 57.591797 C 44.173395 57.843117 44.472579 58.023328 44.449219 58.085938 C 44.428819 58.140738 44.216496 59.34094 43.972656 60.75 C 43.728826 62.15903 43.363642 64.251624 43.164062 65.396484 C 42.964493 66.541324 42.699519 68.066419 42.574219 68.792969 C 42.139479 71.313499 41.695765 73.890395 41.265625 76.359375 C 41.027825 77.724405 40.854932 78.867548 40.882812 78.898438 C 40.910712 78.929337 41.506497 79.096211 42.210938 79.269531 C 43.509598 79.589071 44.794388 79.796623 46.173828 79.914062 C 47.842558 80.056102 49.606782 79.996243 51.576172 79.720703 C 52.593602 79.578343 54.500249 79.154048 54.605469 79.048828 C 54.647569 79.006728 54.529276 78.059257 54.316406 76.748047 C 54.117246 75.521517 53.818303 73.692448 53.658203 72.679688 C 53.498103 71.666947 53.30265 70.422396 53.21875 69.916016 C 53.13485 69.409656 52.955659 68.313183 52.824219 67.476562 C 52.692769 66.639923 52.382676 64.693504 52.134766 63.152344 C 51.886846 61.611194 51.607082 59.829303 51.507812 59.195312 L 51.326172 58.042969 L 51.570312 57.955078 C 52.121643 57.757968 53.420936 57.086783 54.003906 56.695312 C 54.793496 56.165072 56.192917 54.760739 56.716797 53.980469 C 57.519937 52.784279 58.106897 51.378428 58.435547 49.861328 C 58.573867 49.222808 58.573867 46.738129 58.435547 46.099609 C 58.106897 44.582489 57.519937 43.172743 56.716797 41.976562 C 56.197817 41.203593 54.794444 39.798277 54.021484 39.279297 C 52.248404 38.088827 50.086997 37.412109 48.054688 37.412109 z "
313
+       transform="translate(-268,635.29076)"
314
+       id="path4248" />
315
+  </g>
316
+  <g
317
+     inkscape:groupmode="layer"
318
+     id="layer3"
319
+     inkscape:label="PLACE YOUR PICTOGRAM HERE"
320
+     style="display:inline">
321
+    <g
322
+       id="g4185" />
323
+  </g>
324
+  <style
325
+     id="style4217"
326
+     type="text/css">
327
+	.st0{fill:#419EDA;}
328
+</style>
329
+  <style
330
+     id="style4285"
331
+     type="text/css">
332
+	.st0{clip-path:url(#SVGID_2_);fill:#EFBF1B;}
333
+	.st1{clip-path:url(#SVGID_2_);fill:#40BEB0;}
334
+	.st2{clip-path:url(#SVGID_2_);fill:#0AA5DE;}
335
+	.st3{clip-path:url(#SVGID_2_);fill:#231F20;}
336
+	.st4{fill:#D7A229;}
337
+	.st5{fill:#009B8F;}
338
+</style>
339
+  <style
340
+     id="style4240"
341
+     type="text/css">
342
+	.st0{fill:#E8478B;}
343
+	.st1{fill:#40BEB0;}
344
+	.st2{fill:#37A595;}
345
+	.st3{fill:#231F20;}
346
+</style>
347
+  <style
348
+     id="style4812"
349
+     type="text/css">
350
+	.st0{fill:#0AA5DE;}
351
+	.st1{fill:#40BEB0;}
352
+	.st2{opacity:0.26;fill:#353535;}
353
+	.st3{fill:#231F20;}
354
+</style>
355
+</svg>
Back to file index

layer.yaml

 1
--- 
 2
+++ layer.yaml
 3
@@ -0,0 +1,17 @@
 4
+"options":
 5
+  "basic":
 6
+    "packages":
 7
+    - "openssl"
 8
+    "use_venv": !!bool "false"
 9
+    "include_system_packages": !!bool "false"
10
+  "leadership": {}
11
+  "easyrsa": {}
12
+"includes":
13
+- "layer:basic"
14
+- "layer:leadership"
15
+- "interface:tls-certificates"
16
+"repo": "http://github.com/juju-solutions/layer-easyrsa.git"
17
+"exclude":
18
+- "tests/10-deploy.py"
19
+- "tests/tests.yaml"
20
+"is": "easyrsa"
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/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/leadership.py

 1
--- 
 2
+++ lib/charms/leadership.py
 3
@@ -0,0 +1,58 @@
 4
+# Copyright 2015-2016 Canonical Ltd.
 5
+#
 6
+# This file is part of the Leadership 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
+from charmhelpers.core import hookenv
21
+from charmhelpers.core import unitdata
22
+
23
+from charms import reactive
24
+from charms.reactive import not_unless
25
+
26
+
27
+__all__ = ['leader_get', 'leader_set']
28
+
29
+
30
+@not_unless('leadership.is_leader')
31
+def leader_set(settings=None, **kw):
32
+    '''Change leadership settings, per charmhelpers.core.hookenv.leader_set.
33
+
34
+    The leadership.set.{key} reactive state will be set while the
35
+    leadership hook environment setting remains set.
36
+
37
+    Changed leadership settings will set the leadership.changed.{key}
38
+    and leadership.changed states. These states will remain set until
39
+    the following hook.
40
+
41
+    These state changes take effect immediately on the leader, and
42
+    in future hooks run on non-leaders. In this way both leaders and
43
+    non-leaders can share handlers, waiting on these states.
44
+    '''
45
+    settings = settings or {}
46
+    settings.update(kw)
47
+    previous = unitdata.kv().getrange('leadership.settings.', strip=True)
48
+
49
+    for key, value in settings.items():
50
+        if value != previous.get(key):
51
+            reactive.set_state('leadership.changed.{}'.format(key))
52
+            reactive.set_state('leadership.changed')
53
+        reactive.helpers.toggle_state('leadership.set.{}'.format(key),
54
+                                      value is not None)
55
+    hookenv.leader_set(settings)
56
+    unitdata.kv().update(settings, prefix='leadership.settings.')
57
+
58
+
59
+def leader_get(attribute=None):
60
+    '''Return leadership settings, per charmhelpers.core.hookenv.leader_get.'''
61
+    return hookenv.leader_get(attribute)
Back to file index

metadata.yaml

 1
--- 
 2
+++ metadata.yaml
 3
@@ -0,0 +1,24 @@
 4
+"name": "easyrsa"
 5
+"summary": "Delivers EasyRSA to create a Certificate Authority (CA)."
 6
+"maintainer": "Matthew Bruzek <matthew.bruzek@canonical.com>"
 7
+"description": |
 8
+  This charm delivers the EasyRSA application and through Juju events creates
 9
+  a Certificate Authority (CA), server certificates, and client certificates.
10
+"tags":
11
+- "tls"
12
+- "pki"
13
+- "ca"
14
+"series":
15
+- "xenial"
16
+- "trusty"
17
+"provides":
18
+  "client":
19
+    "interface": "tls-certificates"
20
+"resources":
21
+  "easyrsa":
22
+    "type": "file"
23
+    "filename": "easyrsa.tgz"
24
+    "description": "The release of the EasyRSA software you would like to use to create\n\
25
+      certificate authority (CA) and other Public Key Infrastructure (PKI). \nThis\
26
+      \ charm was written using v3.0.1, so earlier versions of EasyRSA may \nnot work.\
27
+      \ You can find the releases of EasyRSA at \nhttps://github.com/OpenVPN/easy-rsa/releases\n"
Back to file index

reactive/easyrsa.py

  1
--- 
  2
+++ reactive/easyrsa.py
  3
@@ -0,0 +1,345 @@
  4
+import os
  5
+import shutil
  6
+
  7
+from shlex import split
  8
+from subprocess import check_call
  9
+from subprocess import check_output
 10
+
 11
+from charms.reactive import hook
 12
+from charms.reactive import is_state
 13
+from charms.reactive import remove_state
 14
+from charms.reactive import set_state
 15
+from charms.reactive import when
 16
+from charms.reactive import when_not
 17
+
 18
+from charmhelpers.core import hookenv
 19
+from charmhelpers.core import unitdata
 20
+from charmhelpers.core.host import chdir
 21
+from charmhelpers.core.hookenv import resource_get
 22
+from charmhelpers.core.hookenv import status_set
 23
+
 24
+from charms.leadership import leader_set
 25
+from charms.leadership import leader_get
 26
+
 27
+
 28
+charm_directory = hookenv.charm_dir()
 29
+easyrsa_directory = os.path.join(charm_directory, 'EasyRSA')
 30
+
 31
+
 32
+@when_not('easyrsa.installed')
 33
+def install():
 34
+    '''Install the easy-rsa software that is used by this layer.'''
 35
+    easyrsa_resource = None
 36
+    try:
 37
+        # Try to get the resource from Juju.
 38
+        easyrsa_resource = resource_get('easyrsa')
 39
+    except Exception as e:
 40
+        message = 'An error occurred fetching the easyrsa resource.'
 41
+        hookenv.log(message)
 42
+        hookenv.log(e)
 43
+        hookenv.status_set('blocked', message)
 44
+        return
 45
+
 46
+    if not easyrsa_resource:
 47
+        hookenv.status_set('blocked', 'The easyrsa resource is missing.')
 48
+        return
 49
+
 50
+    # Get the filesize in bytes.
 51
+    filesize = os.stat(easyrsa_resource).st_size
 52
+    # When the filesize is less than 10 KB we do not have a real file.
 53
+    if filesize < 10240:
 54
+        hookenv.status_set('blocked', 'The easyrsa resource is not complete.')
 55
+        return
 56
+
 57
+    # Expand the archive in the charm directory creating an EasyRSA directory.
 58
+    untar = 'tar -xvzf {0} -C {1}'.format(easyrsa_resource, charm_directory)
 59
+    check_call(split(untar))
 60
+
 61
+    version = get_version(easyrsa_resource)
 62
+    # Save the version in the key/value store of the charm.
 63
+    unitdata.kv().set('easyrsa-version', version)
 64
+
 65
+    if os.path.islink(easyrsa_directory):
 66
+        check_call(split('rm -v {0}'.format(easyrsa_directory)))
 67
+
 68
+    # Link the EasyRSA version directory to a common name.
 69
+    link = 'ln -v -s {0}/EasyRSA-{1} {2}'.format(charm_directory,
 70
+                                                 version,
 71
+                                                 easyrsa_directory)
 72
+    check_call(split(link))
 73
+    # The charm pki directory contains backup of pki for upgrades.
 74
+    charm_pki_directory = os.path.join(charm_directory, 'pki')
 75
+    if os.path.isdir(charm_pki_directory):
 76
+        new_pki_directory = os.path.join(easyrsa_directory, 'pki')
 77
+        # Only copy the directory if the new_pki_directory does not exist.
 78
+        if not os.path.isdir(new_pki_directory):
 79
+            # Copy the pki to this new directory.
 80
+            shutil.copytree(charm_pki_directory, new_pki_directory,
 81
+                            symlinks=True)
 82
+        # We are done with the old charm pki directory, so delete contents.
 83
+        shutil.rmtree(charm_pki_directory)
 84
+    else:
 85
+        # Create new pki.
 86
+        with chdir(easyrsa_directory):
 87
+            check_call(split('./easyrsa --batch init-pki 2>&1'))
 88
+    set_state('easyrsa.installed')
 89
+
 90
+
 91
+@when('easyrsa.installed')
 92
+def set_easyrsa_version():
 93
+    '''Find the version of easyrsa and set that on the charm.'''
 94
+    version = unitdata.kv().get('easyrsa-version')
 95
+    hookenv.application_version_set(version)
 96
+
 97
+
 98
+@when('easyrsa.installed')
 99
+@when_not('easyrsa.configured')
100
+def configure_easyrsa():
101
+    '''A transitional state to allow modifications to configuration before
102
+    generating the certificates and working with PKI.'''
103
+    hookenv.log('Configuring OpenSSL to copy extensions.')
104
+    configure_copy_extensions()
105
+    hookenv.log('Configuring X509 server extensions with clientAuth.')
106
+    configure_client_authorization()
107
+    set_state('easyrsa.configured')
108
+
109
+
110
+def configure_copy_extensions():
111
+    '''Update the EasyRSA configuration with the capacity to copy the exensions
112
+    through to the resulting certificates. '''
113
+    # Create an absolute path to the file which will not be impacted by cwd.
114
+    openssl_file = os.path.join(easyrsa_directory, 'openssl-1.0.cnf')
115
+    # Update EasyRSA configuration with the capacity to copy CSR Requested
116
+    # Extensions through to the resulting certificate. This can be tricky,
117
+    # and the implications are not fully clear on this.
118
+    with open(openssl_file, 'r') as f:
119
+        conf = f.readlines()
120
+    # When the copy_extensions key is not in the configuration.
121
+    if 'copy_extensions = copy\n' not in conf:
122
+        for idx, line in enumerate(conf):
123
+            if '[ CA_default ]' in line:
124
+                # Insert a new line with the copy_extensions key set to copy.
125
+                conf.insert(idx + 1, "copy_extensions = copy\n")
126
+        with open(openssl_file, 'w+') as f:
127
+            f.writelines(conf)
128
+
129
+
130
+def configure_client_authorization():
131
+    '''easyrsa has a default OpenSSL configuration that does not support
132
+    client authentication. Append "clientAuth" to the server ssl certificate
133
+    configuration. This is not default, to enable this in your charm set the
134
+    reactive state 'tls.client.authorization.required'.
135
+    '''
136
+    # Use an absolute path so current directory does not affect the result.
137
+    openssl_config = os.path.join(easyrsa_directory, 'x509-types/server')
138
+    hookenv.log('Updating {0}'.format(openssl_config))
139
+
140
+    # Read the X509 server extension file in.
141
+    with open(openssl_config, 'r') as f:
142
+        server_extensions = f.readlines()
143
+
144
+    client_server = []
145
+    for line in server_extensions:
146
+        # Replace the extendedKeyUsage with clientAuth and serverAuth.
147
+        if 'extendedKeyUsage' in line:
148
+            line = line.replace('extendedKeyUsage = serverAuth',
149
+                                'extendedKeyUsage = clientAuth, serverAuth')
150
+        client_server.append(line)
151
+    # Write the configuration file back out.
152
+    with open(openssl_config, 'w+') as f:
153
+        f.writelines(client_server)
154
+
155
+
156
+@when('easyrsa.configured')
157
+@when('leadership.is_leader')
158
+@when_not('easyrsa.certificate.authority.available')
159
+def create_certificate_authority():
160
+    '''Return the CA and server certificates for this system. If the CA is
161
+    empty, generate a self signged certificate authority.'''
162
+    with chdir(easyrsa_directory):
163
+        # The Common Name (CN) for a certificate must be an IP or hostname.
164
+        cn = hookenv.unit_public_ip()
165
+        # Create a self signed CA with the CN, stored pki/ca.crt
166
+        build_ca = './easyrsa --batch "--req-cn={0}" build-ca nopass 2>&1'
167
+        # Build a self signed Certificate Authority.
168
+        check_call(split(build_ca.format(cn)))
169
+
170
+        ca_file = 'pki/ca.crt'
171
+        # Read the CA so it can be returned in leader data.
172
+        with open(ca_file, 'r') as stream:
173
+            certificate_authority = stream.read()
174
+
175
+        key_file = 'pki/private/ca.key'
176
+        # Read the private key so it can be set in leader data.
177
+        with open(key_file, 'r') as stream:
178
+            ca_key = stream.read()
179
+
180
+        # Set these values on the leadership data.
181
+        leader_set({'certificate_authority': certificate_authority})
182
+        leader_set({'certificate_authority_key': ca_key})
183
+        # Install the CA on this system as a trusted CA.
184
+        install_ca(certificate_authority)
185
+        # Create a client certificate for this CA.
186
+        client_cert, client_key = create_client_certificate()
187
+        # Set the client certificate and key on leadership data.
188
+        leader_set({'client_certificate': client_cert})
189
+        leader_set({'client_key': client_key})
190
+        status_set('active', 'Certificiate Authority available')
191
+    set_state('easyrsa.certificate.authority.available')
192
+
193
+
194
+@when('easyrsa.certificate.authority.available')
195
+def message():
196
+    '''Set a message to notify the user that this charm is ready.'''
197
+    if is_state('client.available'):
198
+        status_set('active', 'Certificate Authority connected.')
199
+    else:
200
+        status_set('active', 'Certificate Authority ready.')
201
+
202
+
203
+@when('client.available', 'easyrsa.certificate.authority.available')
204
+@when('leadership.is_leader')
205
+def send_ca(tls):
206
+    '''The client relationship has been established, read the CA and client
207
+    certificate from leadership data to set them on the relationship object.'''
208
+    certificate_authority = leader_get('certificate_authority')
209
+    tls.set_ca(certificate_authority)
210
+    # The client cert and key should be same for all connections.
211
+    client_cert = leader_get('client_certificate')
212
+    client_key = leader_get('client_key')
213
+    # Set the client certificate and key on the relationship object.
214
+    tls.set_client_cert(client_cert, client_key)
215
+
216
+
217
+@when('client.server.cert.requested', 'easyrsa.configured')
218
+def create_server_cert(tls):
219
+    '''Create server certificates with the request information from the
220
+    relation object.'''
221
+    # Get the map of unit names to requests.
222
+    requests = tls.get_server_requests()
223
+    # Iterate over all items in the map.
224
+    for unit_name, request in requests.items():
225
+        cn = request.get('common_name')
226
+        sans = request.get('sans')
227
+        name = request.get('certificate_name')
228
+        # Create the server certificate based on the information in request.
229
+        server_cert, server_key = create_server_certificate(cn, sans, name)
230
+        # Set the certificate and key for the unit on the relationship object.
231
+        tls.set_server_cert(unit_name, server_cert, server_key)
232
+
233
+
234
+@hook('upgrade-charm')
235
+def upgrade():
236
+    '''An upgrade has been triggered.'''
237
+    pki_directory = os.path.join(easyrsa_directory, 'pki')
238
+    if os.path.isdir(pki_directory):
239
+        charm_pki_directory = os.path.join(charm_directory, 'pki')
240
+        # When the charm pki directory exists, it is stale, remove it.
241
+        if os.path.isdir(charm_pki_directory):
242
+            shutil.rmtree(charm_pki_directory)
243
+        # Copy the EasyRSA/pki to the charm pki directory.
244
+        shutil.copytree(pki_directory, charm_pki_directory, symlinks=True)
245
+    remove_state('easyrsa.installed')
246
+    remove_state('easyrsa.configured')
247
+
248
+
249
+def create_server_certificate(cn, san_list, name='server'):
250
+    '''Return a newly created server certificate and server key from a
251
+    common name, list of Subject Alternate Names, and the certificate name.'''
252
+    server_cert = None
253
+    server_key = None
254
+    with chdir(easyrsa_directory):
255
+        # Create the path to the server certificate.
256
+        cert_file = 'pki/issued/{0}.crt'.format(name)
257
+        # Create the path to the server key.
258
+        key_file = 'pki/private/{0}.key'.format(name)
259
+        # Do not regenerate the server certificate if it already exists.
260
+        if not os.path.isfile(cert_file) and not os.path.isfile(key_file):
261
+            # Get a string compatible with easyrsa for the subject-alt-names.
262
+            sans = get_sans(san_list)
263
+            # Create a server certificate for the server based on the CN.
264
+            server = './easyrsa --batch --req-cn={0} --subject-alt-name={1} ' \
265
+                     'build-server-full {2} nopass 2>&1'.format(cn, sans, name)
266
+            check_call(split(server))
267
+        # Read the server certificate from the file system.
268
+        with open(cert_file, 'r') as stream:
269
+            server_cert = stream.read()
270
+        # Read the server key from the file system.
271
+        with open(key_file, 'r') as stream:
272
+            server_key = stream.read()
273
+    return server_cert, server_key
274
+
275
+
276
+def create_client_certificate(name='client'):
277
+    '''Return a newly created client certificate and client key, by name.'''
278
+    client_cert = None
279
+    client_key = None
280
+    with chdir(easyrsa_directory):
281
+        # Create a path to the client certificate.
282
+        cert_file = 'pki/issued/{0}.crt'.format(name)
283
+        # Create a path to the client key.
284
+        key_file = 'pki/private/{0}.key'.format(name)
285
+        # Do not regenerate the client certificate if it already exists.
286
+        if not os.path.isfile(cert_file) and not os.path.isfile(key_file):
287
+            # Create a client certificate and key.
288
+            client = './easyrsa build-client-full {0} nopass 2>&1'.format(name)
289
+            check_call(split(client))
290
+        # Read the client certificate from the file system.
291
+        with open(cert_file, 'r') as stream:
292
+            client_cert = stream.read()
293
+        # Read the client key from the file system.
294
+        with open(key_file, 'r') as stream:
295
+            client_key = stream.read()
296
+    return client_cert, client_key
297
+
298
+
299
+def install_ca(certificate_authority):
300
+    '''Install a certificiate authority on the system by calling the
301
+    update-ca-certificates command.'''
302
+    name = hookenv.service_name()
303
+    ca_file = '/usr/local/share/ca-certificates/{0}.crt'.format(name)
304
+    hookenv.log('Writing CA to {0}'.format(ca_file))
305
+    # Write the contents of certificate authority to the file.
306
+    with open(ca_file, 'w') as fp:
307
+        fp.write(certificate_authority)
308
+    # Update the trusted CAs on this system.
309
+    check_call(['update-ca-certificates'])
310
+    message = 'Generated ca-certificates.crt for {0}'.format(name)
311
+    hookenv.log(message)
312
+
313
+
314
+def get_sans(address_list=[]):
315
+    '''Return a string suitable for the easy-rsa subjectAltNames.'''
316
+    sans = []
317
+    for address in address_list:
318
+        if _is_ip(address):
319
+            sans.append('IP:{0}'.format(address))
320
+        else:
321
+            sans.append('DNS:{0}'.format(address))
322
+    return ','.join(sans)
323
+
324
+
325
+def get_version(path):
326
+    '''Return the version of EasyRSA by investigating the tar file.'''
327
+    # Create a command that lists the tar file.
328
+    cmd = 'tar -tf {0}'.format(path)
329
+    # Get the listing of the directories and files in the tar file.
330
+    output = check_output(split(cmd)).decode('utf-8')
331
+    # Get the first listing which is the directory.
332
+    directory = output.splitlines()[0]
333
+    # Remove the path separator from the string.
334
+    if '/' in directory:
335
+        directory = directory.replace('/', '')
336
+    # Get the version by splitting on the hypen.
337
+    return directory.split('-')[1]
338
+
339
+
340
+def _is_ip(address):
341
+    '''Return True if the address is an IP address, false otherwise.'''
342
+    import ipaddress
343
+    try:
344
+        # This method will raise a ValueError if argument is not an IP address.
345
+        ipaddress.ip_address(address)
346
+        return True
347
+    except ValueError:
348
+        return False
Back to file index

reactive/leadership.py

 1
--- 
 2
+++ reactive/leadership.py
 3
@@ -0,0 +1,68 @@
 4
+# Copyright 2015-2016 Canonical Ltd.
 5
+#
 6
+# This file is part of the Leadership 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
+from charmhelpers.core import hookenv
21
+from charmhelpers.core import unitdata
22
+
23
+from charms import reactive
24
+from charms.leadership import leader_get, leader_set
25
+
26
+
27
+__all__ = ['leader_get', 'leader_set']  # Backwards compatibility
28
+
29
+
30
+def initialize_leadership_state():
31
+    '''Initialize leadership.* states from the hook environment.
32
+
33
+    Invoked by hookenv.atstart() so states are available in
34
+    @hook decorated handlers.
35
+    '''
36
+    is_leader = hookenv.is_leader()
37
+    if is_leader:
38
+        hookenv.log('Initializing Leadership Layer (is leader)')
39
+    else:
40
+        hookenv.log('Initializing Leadership Layer (is follower)')
41
+
42
+    reactive.helpers.toggle_state('leadership.is_leader', is_leader)
43
+
44
+    previous = unitdata.kv().getrange('leadership.settings.', strip=True)
45
+    current = hookenv.leader_get()
46
+
47
+    # Handle deletions.
48
+    for key in set(previous.keys()) - set(current.keys()):
49
+        current[key] = None
50
+
51
+    any_changed = False
52
+    for key, value in current.items():
53
+        reactive.helpers.toggle_state('leadership.changed.{}'.format(key),
54
+                                      value != previous.get(key))
55
+        if value != previous.get(key):
56
+            any_changed = True
57
+        reactive.helpers.toggle_state('leadership.set.{}'.format(key),
58
+                                      value is not None)
59
+    reactive.helpers.toggle_state('leadership.changed', any_changed)
60
+
61
+    unitdata.kv().update(current, prefix='leadership.settings.')
62
+
63
+
64
+# Per https://github.com/juju-solutions/charms.reactive/issues/33,
65
+# this module may be imported multiple times so ensure the
66
+# initialization hook is only registered once. I have to piggy back
67
+# onto the namespace of a module imported before reactive discovery
68
+# to do this.
69
+if not hasattr(reactive, '_leadership_registered'):
70
+    hookenv.atstart(initialize_leadership_state)
71
+    reactive._leadership_registered = True
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

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