~containers/kubernetes-master

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

CPP?: No
OIL?: No

This is our attempt at making 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
lxc RETRY 19 days ago
gce 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. marcoceppi
  • 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. marcoceppi
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. marcoceppi
Should be built using charm layers. marcoceppi
Should use Juju Resources to deliver required payloads. marcoceppi

Testing and Quality

charm proof must pass without errors or warnings. marcoceppi
Must include passing unit, functional, or integration tests. marcoceppi
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). marcoceppi
Must be self contained unless the charm is a proxy for an existing cloud service, e.g. ec2-elb charm.
Must not use symlinks. marcoceppi
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. marcoceppi
Should include a tests.yaml for all integration tests.

Metadata

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

Security

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

All changes | Changes since last revision

Source Diff

Files changed 152

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,96 @@
 4
+# Kubernetes-master
 5
+
 6
+[Kubernetes](http://kubernetes.io/) is an open source system for managing 
 7
+application containers across a cluster of hosts. The Kubernetes project was
 8
+started by Google in 2014, combining the experience of running production 
 9
+workloads combined with best practices from the community.
10
+
11
+The Kubernetes project defines some new terms that may be unfamiliar to users
12
+or operators. For more information please refer to the concept guide in the 
13
+[getting started guide](https://kubernetes.io/docs/home/).
14
+
15
+This charm is an encapsulation of the Kubernetes master processes and the 
16
+operations to run on any cloud for the entire lifecycle of the cluster.
17
+
18
+This charm is built from other charm layers using the Juju reactive framework.
19
+The other layers focus on specific subset of operations making this layer 
20
+specific to operations of Kubernetes master processes.
21
+
22
+# Deployment
23
+
24
+This charm is not fully functional when deployed by itself. It requires other
25
+charms to model a complete Kubernetes cluster. A Kubernetes cluster needs a
26
+distributed key value store such as [Etcd](https://coreos.com/etcd/) and the
27
+kubernetes-worker charm which delivers the Kubernetes node services. A cluster
28
+requires a Software Defined Network (SDN) and Transport Layer Security (TLS) so
29
+the components in a cluster communicate securely. 
30
+
31
+Please take a look at the [Canonical Distribution of Kubernetes](https://jujucharms.com/canonical-kubernetes/) 
32
+or the [Kubernetes core](https://jujucharms.com/kubernetes-core/) bundles for 
33
+examples of complete models of Kubernetes clusters.
34
+
35
+# Resources
36
+
37
+The kubernetes-master charm takes advantage of the [Juju Resources](https://jujucharms.com/docs/2.0/developer-resources) 
38
+feature to deliver the Kubernetes software.
39
+
40
+In deployments on public clouds the Charm Store provides the resource to the
41
+charm automatically with no user intervention. Some environments with strict
42
+firewall rules may not be able to contact the Charm Store. In these network
43
+restricted  environments the resource can be uploaded to the model by the Juju
44
+operator.
45
+
46
+# Configuration
47
+
48
+This charm supports some configuration options to set up a Kubernetes cluster 
49
+that works in your environment:
50
+
51
+#### dns_domain
52
+
53
+The domain name to use for the Kubernetes cluster for DNS.
54
+
55
+#### enable-dashboard-addons
56
+
57
+Enables the installation of Kubernetes dashboard, Heapster, Grafana, and
58
+InfluxDB.
59
+
60
+# DNS for the cluster
61
+
62
+The DNS add-on allows the pods to have a DNS names in addition to IP addresses.
63
+The Kubernetes cluster DNS server (based off the SkyDNS library) supports 
64
+forward lookups (A records), service lookups (SRV records) and reverse IP 
65
+address lookups (PTR records). More information about the DNS can be obtained
66
+from the [Kubernetes DNS admin guide](http://kubernetes.io/docs/admin/dns/).
67
+
68
+# Actions
69
+
70
+The kubernetes-master charm models a few one time operations called 
71
+[Juju actions](https://jujucharms.com/docs/stable/actions) that can be run by
72
+Juju users.
73
+
74
+#### create-rbd-pv
75
+
76
+This action creates RADOS Block Device (RBD) in Ceph and defines a Persistent
77
+Volume in Kubernetes so the containers can use durable storage. This action
78
+requires a relation to the ceph-mon charm before it can create the volume.
79
+
80
+#### restart
81
+
82
+This action restarts the master processes `kube-apiserver`, 
83
+`kube-controller-manager`, and `kube-scheduler` when the user needs a restart.
84
+
85
+# More information
86
+
87
+ - [Kubernetes github project](https://github.com/kubernetes/kubernetes)
88
+ - [Kubernetes issue tracker](https://github.com/kubernetes/kubernetes/issues)
89
+ - [Kubernetes documentation](http://kubernetes.io/docs/)
90
+ - [Kubernetes releases](https://github.com/kubernetes/kubernetes/releases)
91
+
92
+# Contact
93
+
94
+The kubernetes-master charm is free and open source operations created
95
+by the containers team at Canonical. 
96
+
97
+Canonical also offers enterprise support and customization services. Please
98
+refer to the [Kubernetes product page](https://www.ubuntu.com/cloud/kubernetes)
99
+for more details.
Back to file index

actions.yaml

 1
--- 
 2
+++ actions.yaml
 3
@@ -0,0 +1,50 @@
 4
+"debug":
 5
+  "description": "Collect debug data"
 6
+"restart":
 7
+  "description": "Restart the Kubernetes master services on demand."
 8
+"create-rbd-pv":
 9
+  "description": "Create RADOS Block Device (RDB) volume in Ceph and creates PersistentVolume."
10
+  "params":
11
+    "name":
12
+      "type": "string"
13
+      "description": "Name the persistent volume."
14
+      "minLength": !!int "1"
15
+    "size":
16
+      "type": "integer"
17
+      "description": "Size in MB of the RBD volume."
18
+      "minimum": !!int "1"
19
+    "mode":
20
+      "type": "string"
21
+      "default": "ReadWriteOnce"
22
+      "description": "Access mode for the persistent volume."
23
+    "filesystem":
24
+      "type": "string"
25
+      "default": "xfs"
26
+      "description": "File system type to format the volume."
27
+    "skip-size-check":
28
+      "type": "boolean"
29
+      "default": !!bool "false"
30
+      "description": "Allow creation of overprovisioned RBD."
31
+  "required":
32
+  - "name"
33
+  - "size"
34
+"namespace-list":
35
+  "description": "List existing k8s namespaces"
36
+"namespace-create":
37
+  "description": "Create new namespace"
38
+  "params":
39
+    "name":
40
+      "type": "string"
41
+      "description": "Namespace name eg. staging"
42
+      "minLength": !!int "2"
43
+  "required":
44
+  - "name"
45
+"namespace-delete":
46
+  "description": "Delete namespace"
47
+  "params":
48
+    "name":
49
+      "type": "string"
50
+      "description": "Namespace name eg. staging"
51
+      "minLength": !!int "2"
52
+  "required":
53
+  - "name"
Back to file index

actions/create-rbd-pv

  1
--- 
  2
+++ actions/create-rbd-pv
  3
@@ -0,0 +1,300 @@
  4
+#!/usr/bin/env python3
  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
+from charms.templating.jinja2 import render
 21
+from charms.reactive import is_state
 22
+from charmhelpers.core.hookenv import action_get
 23
+from charmhelpers.core.hookenv import action_set
 24
+from charmhelpers.core.hookenv import action_fail
 25
+from subprocess import check_call
 26
+from subprocess import check_output
 27
+from subprocess import CalledProcessError
 28
+from tempfile import TemporaryDirectory
 29
+import re
 30
+import os
 31
+import sys
 32
+
 33
+
 34
+os.environ['PATH'] += os.pathsep + os.path.join(os.sep, 'snap', 'bin')
 35
+
 36
+
 37
+def main():
 38
+    ''' Control logic to enlist Ceph RBD volumes as PersistentVolumes in
 39
+    Kubernetes. This will invoke the validation steps, and only execute if
 40
+    this script thinks the environment is 'sane' enough to provision volumes.
 41
+    '''
 42
+
 43
+    # validate relationship pre-reqs before additional steps can be taken
 44
+    if not validate_relation():
 45
+        print('Failed ceph relationship check')
 46
+        action_fail('Failed ceph relationship check')
 47
+        return
 48
+
 49
+    if not is_ceph_healthy():
 50
+        print('Ceph was not healthy.')
 51
+        action_fail('Ceph was not healthy.')
 52
+        return
 53
+
 54
+    context = {}
 55
+
 56
+    context['RBD_NAME'] = action_get_or_default('name').strip()
 57
+    context['RBD_SIZE'] = action_get_or_default('size')
 58
+    context['RBD_FS'] = action_get_or_default('filesystem').strip()
 59
+    context['PV_MODE'] = action_get_or_default('mode').strip()
 60
+
 61
+    # Ensure we're not exceeding available space in the pool
 62
+    if not validate_space(context['RBD_SIZE']):
 63
+        return
 64
+
 65
+    # Ensure our paramters match
 66
+    param_validation = validate_parameters(context['RBD_NAME'],
 67
+                                           context['RBD_FS'],
 68
+                                           context['PV_MODE'])
 69
+    if not param_validation == 0:
 70
+        return
 71
+
 72
+    if not validate_unique_volume_name(context['RBD_NAME']):
 73
+        action_fail('Volume name collision detected. Volume creation aborted.')
 74
+        return
 75
+
 76
+    context['monitors'] = get_monitors()
 77
+
 78
+    # Invoke creation and format the mount device
 79
+    create_rbd_volume(context['RBD_NAME'],
 80
+                      context['RBD_SIZE'],
 81
+                      context['RBD_FS'])
 82
+
 83
+    # Create a temporary workspace to render our persistentVolume template, and
 84
+    # enlist the RDB based PV we've just created
 85
+    with TemporaryDirectory() as active_working_path:
 86
+        temp_template = '{}/pv.yaml'.format(active_working_path)
 87
+        render('rbd-persistent-volume.yaml', temp_template, context)
 88
+
 89
+        cmd = ['kubectl', 'create', '-f', temp_template]
 90
+        debug_command(cmd)
 91
+        check_call(cmd)
 92
+
 93
+
 94
+def action_get_or_default(key):
 95
+    ''' Convenience method to manage defaults since actions dont appear to
 96
+    properly support defaults '''
 97
+
 98
+    value = action_get(key)
 99
+    if value:
100
+        return value
101
+    elif key == 'filesystem':
102
+        return 'xfs'
103
+    elif key == 'size':
104
+        return 0
105
+    elif key == 'mode':
106
+        return "ReadWriteOnce"
107
+    elif key == 'skip-size-check':
108
+        return False
109
+    else:
110
+        return ''
111
+
112
+
113
+def create_rbd_volume(name, size, filesystem):
114
+    ''' Create the RBD volume in Ceph. Then mount it locally to format it for
115
+    the requested filesystem.
116
+
117
+    :param name - The name of the RBD volume
118
+    :param size - The size in MB of the volume
119
+    :param filesystem - The type of filesystem to format the block device
120
+    '''
121
+
122
+    # Create the rbd volume
123
+    # $ rbd create foo --size 50 --image-feature layering
124
+    command = ['rbd', 'create', '--size', '{}'.format(size), '--image-feature',
125
+               'layering', name]
126
+    debug_command(command)
127
+    check_call(command)
128
+
129
+    # Lift the validation sequence to determine if we actually created the
130
+    # rbd volume
131
+    if validate_unique_volume_name(name):
132
+        # we failed to create the RBD volume. whoops
133
+        action_fail('RBD Volume not listed after creation.')
134
+        print('Ceph RBD volume {} not found in rbd list'.format(name))
135
+        # hack, needs love if we're killing the process thread this deep in
136
+        # the call stack.
137
+        sys.exit(0)
138
+
139
+    mount = ['rbd', 'map', name]
140
+    debug_command(mount)
141
+    device_path = check_output(mount).strip()
142
+
143
+    try:
144
+        format_command = ['mkfs.{}'.format(filesystem), device_path]
145
+        debug_command(format_command)
146
+        check_call(format_command)
147
+        unmount = ['rbd', 'unmap', name]
148
+        debug_command(unmount)
149
+        check_call(unmount)
150
+    except CalledProcessError:
151
+        print('Failed to format filesystem and unmount. RBD created but not'
152
+              ' enlisted.')
153
+        action_fail('Failed to format filesystem and unmount.'
154
+                    ' RDB created but not enlisted.')
155
+
156
+
157
+def is_ceph_healthy():
158
+    ''' Probe the remote ceph cluster for health status '''
159
+    command = ['ceph', 'health']
160
+    debug_command(command)
161
+    health_output = check_output(command)
162
+    if b'HEALTH_OK' in health_output:
163
+        return True
164
+    else:
165
+        return False
166
+
167
+
168
+def get_monitors():
169
+    ''' Parse the monitors out of /etc/ceph/ceph.conf '''
170
+    found_hosts = []
171
+    # This is kind of hacky. We should be piping this in from juju relations
172
+    with open('/etc/ceph/ceph.conf', 'r') as ceph_conf:
173
+        for line in ceph_conf.readlines():
174
+            if 'mon host' in line:
175
+                # strip out the key definition
176
+                hosts = line.lstrip('mon host = ').split(' ')
177
+                for host in hosts:
178
+                    found_hosts.append(host)
179
+    return found_hosts
180
+
181
+
182
+def get_available_space():
183
+    ''' Determine the space available in the RBD pool. Throw an exception if
184
+    the RBD pool ('rbd') isn't found. '''
185
+    command = ['ceph', 'df']
186
+    debug_command(command)
187
+    out = check_output(command).decode('utf-8')
188
+    for line in out.splitlines():
189
+        stripped = line.strip()
190
+        if stripped.startswith('rbd'):
191
+            M = stripped.split()[-2].replace('M', '')
192
+            return int(M)
193
+    raise UnknownAvailableSpaceException('Unable to determine available space.')  # noqa
194
+
195
+
196
+def validate_unique_volume_name(name):
197
+    ''' Poll the CEPH-MON services to determine if we have a unique rbd volume
198
+    name to use. If there is naming collisions, block the request for volume
199
+    provisioning.
200
+
201
+    :param name - The name of the RBD volume
202
+    '''
203
+
204
+    command = ['rbd', 'list']
205
+    debug_command(command)
206
+    raw_out = check_output(command)
207
+
208
+    # Split the output on newlines
209
+    # output spec:
210
+    # $ rbd list
211
+    # foo
212
+    # foobar
213
+    volume_list = raw_out.decode('utf-8').splitlines()
214
+
215
+    for volume in volume_list:
216
+        if volume.strip() == name:
217
+            return False
218
+
219
+    return True
220
+
221
+
222
+def validate_relation():
223
+    ''' Determine if we are related to ceph. If we are not, we should
224
+    note this in the action output and fail this action run. We are relying
225
+    on specific files in specific paths to be placed in order for this function
226
+    to work. This method verifies those files are placed. '''
227
+
228
+    # TODO: Validate that the ceph-common package is installed
229
+    if not is_state('ceph-storage.available'):
230
+        message = 'Failed to detect connected ceph-mon'
231
+        print(message)
232
+        action_set({'pre-req.ceph-relation': message})
233
+        return False
234
+
235
+    if not os.path.isfile('/etc/ceph/ceph.conf'):
236
+        message = 'No Ceph configuration found in /etc/ceph/ceph.conf'
237
+        print(message)
238
+        action_set({'pre-req.ceph-configuration': message})
239
+        return False
240
+
241
+    # TODO: Validate ceph key
242
+
243
+    return True
244
+
245
+
246
+def validate_space(size):
247
+    if action_get_or_default('skip-size-check'):
248
+        return True
249
+    available_space = get_available_space()
250
+    if available_space < size:
251
+        msg = 'Unable to allocate RBD of size {}MB, only {}MB are available.'
252
+        action_fail(msg.format(size, available_space))
253
+        return False
254
+    return True
255
+
256
+
257
+def validate_parameters(name, fs, mode):
258
+    ''' Validate the user inputs to ensure they conform to what the
259
+    action expects. This method will check the naming characters used
260
+    for the rbd volume, ensure they have selected a fstype we are expecting
261
+    and the mode against our whitelist '''
262
+    name_regex = '^[a-zA-z0-9][a-zA-Z0-9|-]'
263
+
264
+    fs_whitelist = ['xfs', 'ext4']
265
+
266
+    # see http://kubernetes.io/docs/user-guide/persistent-volumes/#access-modes
267
+    # for supported operations on RBD volumes.
268
+    mode_whitelist = ['ReadWriteOnce', 'ReadOnlyMany']
269
+
270
+    fails = 0
271
+
272
+    if not re.match(name_regex, name):
273
+        message = 'Validation failed for RBD volume-name'
274
+        action_fail(message)
275
+        fails = fails + 1
276
+        action_set({'validation.name': message})
277
+
278
+    if fs not in fs_whitelist:
279
+        message = 'Validation failed for file system'
280
+        action_fail(message)
281
+        fails = fails + 1
282
+        action_set({'validation.filesystem': message})
283
+
284
+    if mode not in mode_whitelist:
285
+        message = "Validation failed for mode"
286
+        action_fail(message)
287
+        fails = fails + 1
288
+        action_set({'validation.mode': message})
289
+
290
+    return fails
291
+
292
+
293
+def debug_command(cmd):
294
+    ''' Print a debug statement of the command invoked '''
295
+    print("Invoking {}".format(cmd))
296
+
297
+
298
+class UnknownAvailableSpaceException(Exception):
299
+    pass
300
+
301
+
302
+if __name__ == '__main__':
303
+    main()
Back to file index

actions/debug

 1
--- 
 2
+++ actions/debug
 3
@@ -0,0 +1,92 @@
 4
+#!/usr/bin/python3
 5
+
 6
+import os
 7
+import subprocess
 8
+import tarfile
 9
+import tempfile
10
+import traceback
11
+from contextlib import contextmanager
12
+from datetime import datetime
13
+from charmhelpers.core.hookenv import action_set, local_unit
14
+
15
+archive_dir = None
16
+log_file = None
17
+
18
+
19
+@contextmanager
20
+def archive_context():
21
+    """ Open a context with a new temporary directory.
22
+
23
+    When the context closes, the directory is archived, and the archive
24
+    location is added to Juju action output. """
25
+    global archive_dir
26
+    global log_file
27
+    with tempfile.TemporaryDirectory() as temp_dir:
28
+        name = "debug-" + datetime.now().strftime("%Y%m%d%H%M%S")
29
+        archive_dir = os.path.join(temp_dir, name)
30
+        os.makedirs(archive_dir)
31
+        with open("%s/debug.log" % archive_dir, "w") as log_file:
32
+            yield
33
+        os.chdir(temp_dir)
34
+        tar_path = "/home/ubuntu/%s.tar.gz" % name
35
+        with tarfile.open(tar_path, "w:gz") as f:
36
+            f.add(name)
37
+        action_set({
38
+            "path": tar_path,
39
+            "command": "juju scp %s:%s ." % (local_unit(), tar_path),
40
+            "message": " ".join([
41
+                "Archive has been created on unit %s." % local_unit(),
42
+                "Use the juju scp command to copy it to your local machine."
43
+            ])
44
+        })
45
+
46
+
47
+def log(msg):
48
+    """ Log a message that will be included in the debug archive.
49
+
50
+    Must be run within archive_context """
51
+    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
52
+    for line in str(msg).splitlines():
53
+        log_file.write(timestamp + " | " + line.rstrip() + "\n")
54
+
55
+
56
+def run_script(script):
57
+    """ Run a single script. Must be run within archive_context """
58
+    log("Running script: " + script)
59
+    script_dir = os.path.join(archive_dir, script)
60
+    os.makedirs(script_dir)
61
+    env = os.environ.copy()
62
+    env["PYTHONPATH"] = "lib"  # allow same imports as reactive code
63
+    env["DEBUG_SCRIPT_DIR"] = script_dir
64
+    with open(script_dir + "/stdout", "w") as stdout:
65
+        with open(script_dir + "/stderr", "w") as stderr:
66
+            process = subprocess.Popen(
67
+                "debug-scripts/" + script,
68
+                stdout=stdout, stderr=stderr, env=env
69
+            )
70
+            exit_code = process.wait()
71
+    if exit_code != 0:
72
+        log("ERROR: %s failed with exit code %d" % (script, exit_code))
73
+
74
+
75
+def run_all_scripts():
76
+    """ Run all scripts. For the sake of robustness, log and ignore any
77
+    exceptions that occur.
78
+
79
+    Must be run within archive_context """
80
+    scripts = os.listdir("debug-scripts")
81
+    for script in scripts:
82
+        try:
83
+            run_script(script)
84
+        except:
85
+            log(traceback.format_exc())
86
+
87
+
88
+def main():
89
+    """ Open an archive context and run all scripts. """
90
+    with archive_context():
91
+        run_all_scripts()
92
+
93
+
94
+if __name__ == "__main__":
95
+    main()
Back to file index

actions/namespace-create

 1
--- 
 2
+++ actions/namespace-create
 3
@@ -0,0 +1,56 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+from yaml import safe_load as load
 7
+from charmhelpers.core.hookenv import (
 8
+    action_get,
 9
+    action_set,
10
+    action_fail,
11
+    action_name
12
+)
13
+from charms.templating.jinja2 import render
14
+from subprocess import check_output
15
+
16
+
17
+def kubectl(args):
18
+    cmd = ['kubectl'] + args
19
+    return check_output(cmd)
20
+
21
+
22
+def namespace_list():
23
+    y = load(kubectl(['get', 'namespaces', '-o', 'yaml']))
24
+    ns = [i['metadata']['name'] for i in y['items']]
25
+    action_set({'namespaces': ', '.join(ns)+'.'})
26
+    return ns
27
+
28
+
29
+def namespace_create():
30
+    name = action_get('name')
31
+    if name in namespace_list():
32
+        action_fail('Namespace "{}" already exists.'.format(name))
33
+        return
34
+
35
+    render('create-namespace.yaml.j2', '/etc/kubernetes/addons/create-namespace.yaml',
36
+           context={'name': name})
37
+    kubectl(['create', '-f', '/etc/kubernetes/addons/create-namespace.yaml'])
38
+    action_set({'msg': 'Namespace "{}" created.'.format(name)})
39
+
40
+
41
+def namespace_delete():
42
+    name = action_get('name')
43
+    if name in ['default', 'kube-system']:
44
+        action_fail('Not allowed to delete "{}".'.format(name))
45
+        return
46
+    if name not in namespace_list():
47
+        action_fail('Namespace "{}" does not exist.'.format(name))
48
+        return
49
+    kubectl(['delete', 'ns/'+name])
50
+    action_set({'msg': 'Namespace "{}" deleted.'.format(name)})
51
+
52
+
53
+action = action_name().replace('namespace-', '')
54
+if action == 'create':
55
+    namespace_create()
56
+elif action == 'list':
57
+    namespace_list()
58
+elif action == 'delete':
59
+    namespace_delete()
Back to file index

actions/namespace-delete

 1
--- 
 2
+++ actions/namespace-delete
 3
@@ -0,0 +1,56 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+from yaml import safe_load as load
 7
+from charmhelpers.core.hookenv import (
 8
+    action_get,
 9
+    action_set,
10
+    action_fail,
11
+    action_name
12
+)
13
+from charms.templating.jinja2 import render
14
+from subprocess import check_output
15
+
16
+
17
+def kubectl(args):
18
+    cmd = ['kubectl'] + args
19
+    return check_output(cmd)
20
+
21
+
22
+def namespace_list():
23
+    y = load(kubectl(['get', 'namespaces', '-o', 'yaml']))
24
+    ns = [i['metadata']['name'] for i in y['items']]
25
+    action_set({'namespaces': ', '.join(ns)+'.'})
26
+    return ns
27
+
28
+
29
+def namespace_create():
30
+    name = action_get('name')
31
+    if name in namespace_list():
32
+        action_fail('Namespace "{}" already exists.'.format(name))
33
+        return
34
+
35
+    render('create-namespace.yaml.j2', '/etc/kubernetes/addons/create-namespace.yaml',
36
+           context={'name': name})
37
+    kubectl(['create', '-f', '/etc/kubernetes/addons/create-namespace.yaml'])
38
+    action_set({'msg': 'Namespace "{}" created.'.format(name)})
39
+
40
+
41
+def namespace_delete():
42
+    name = action_get('name')
43
+    if name in ['default', 'kube-system']:
44
+        action_fail('Not allowed to delete "{}".'.format(name))
45
+        return
46
+    if name not in namespace_list():
47
+        action_fail('Namespace "{}" does not exist.'.format(name))
48
+        return
49
+    kubectl(['delete', 'ns/'+name])
50
+    action_set({'msg': 'Namespace "{}" deleted.'.format(name)})
51
+
52
+
53
+action = action_name().replace('namespace-', '')
54
+if action == 'create':
55
+    namespace_create()
56
+elif action == 'list':
57
+    namespace_list()
58
+elif action == 'delete':
59
+    namespace_delete()
Back to file index

actions/namespace-list

 1
--- 
 2
+++ actions/namespace-list
 3
@@ -0,0 +1,56 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+from yaml import safe_load as load
 7
+from charmhelpers.core.hookenv import (
 8
+    action_get,
 9
+    action_set,
10
+    action_fail,
11
+    action_name
12
+)
13
+from charms.templating.jinja2 import render
14
+from subprocess import check_output
15
+
16
+
17
+def kubectl(args):
18
+    cmd = ['kubectl'] + args
19
+    return check_output(cmd)
20
+
21
+
22
+def namespace_list():
23
+    y = load(kubectl(['get', 'namespaces', '-o', 'yaml']))
24
+    ns = [i['metadata']['name'] for i in y['items']]
25
+    action_set({'namespaces': ', '.join(ns)+'.'})
26
+    return ns
27
+
28
+
29
+def namespace_create():
30
+    name = action_get('name')
31
+    if name in namespace_list():
32
+        action_fail('Namespace "{}" already exists.'.format(name))
33
+        return
34
+
35
+    render('create-namespace.yaml.j2', '/etc/kubernetes/addons/create-namespace.yaml',
36
+           context={'name': name})
37
+    kubectl(['create', '-f', '/etc/kubernetes/addons/create-namespace.yaml'])
38
+    action_set({'msg': 'Namespace "{}" created.'.format(name)})
39
+
40
+
41
+def namespace_delete():
42
+    name = action_get('name')
43
+    if name in ['default', 'kube-system']:
44
+        action_fail('Not allowed to delete "{}".'.format(name))
45
+        return
46
+    if name not in namespace_list():
47
+        action_fail('Namespace "{}" does not exist.'.format(name))
48
+        return
49
+    kubectl(['delete', 'ns/'+name])
50
+    action_set({'msg': 'Namespace "{}" deleted.'.format(name)})
51
+
52
+
53
+action = action_name().replace('namespace-', '')
54
+if action == 'create':
55
+    namespace_create()
56
+elif action == 'list':
57
+    namespace_list()
58
+elif action == 'delete':
59
+    namespace_delete()
Back to file index

actions/restart

 1
--- 
 2
+++ actions/restart
 3
@@ -0,0 +1,17 @@
 4
+#!/bin/bash
 5
+
 6
+set +ex
 7
+
 8
+# Restart the apiserver, controller-manager, and scheduler
 9
+
10
+systemctl restart kube-apiserver
11
+
12
+action-set 'apiserver.status' 'restarted'
13
+
14
+systemctl restart kube-controller-manager
15
+
16
+action-set 'controller-manager.status' 'restarted'
17
+
18
+systemctl restart kube-scheduler
19
+
20
+action-set 'kube-scheduler.status' 'restarted'
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,65 @@
 4
+# Copyright 2016 Canonical Ltd.
 5
+#
 6
+# This file is part of the Snap layer for Juju.
 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
+"options":
20
+  "snap_proxy":
21
+    "description": "HTTP/HTTPS web proxy for Snappy to use when accessing the snap\
22
+      \ store.\n"
23
+    "type": "string"
24
+    "default": ""
25
+  "nagios_context":
26
+    "default": "juju"
27
+    "type": "string"
28
+    "description": |
29
+      Used by the nrpe subordinate charms.
30
+      A string that will be prepended to instance name to set the host name
31
+      in nagios. So for instance the hostname would be something like:
32
+          juju-myservice-0
33
+      If you're running multiple environments with the same services in them
34
+      this allows you to differentiate between them.
35
+  "nagios_servicegroups":
36
+    "default": ""
37
+    "type": "string"
38
+    "description": |
39
+      A comma-separated list of nagios servicegroups.
40
+      If left empty, the nagios_context will be used as the servicegroup
41
+  "enable-dashboard-addons":
42
+    "type": "boolean"
43
+    "default": !!bool "true"
44
+    "description": "Deploy the Kubernetes Dashboard and Heapster addons"
45
+  "dns_domain":
46
+    "type": "string"
47
+    "default": "cluster.local"
48
+    "description": "The local domain for cluster dns"
49
+  "service-cidr":
50
+    "type": "string"
51
+    "default": "10.152.183.0/24"
52
+    "description": "CIDR to user for Kubernetes services. Cannot be changed after\
53
+      \ deployment."
54
+  "allow-privileged":
55
+    "type": "string"
56
+    "default": "auto"
57
+    "description": |
58
+      Allow kube-apiserver to run in privileged mode. Supported values are
59
+      "true", "false", and "auto". If "true", kube-apiserver will run in
60
+      privileged mode by default. If "false", kube-apiserver will never run in
61
+      privileged mode. If "auto", kube-apiserver will not run in privileged
62
+      mode by default, but will switch to privileged mode if gpu hardware is
63
+      detected on a worker node.
64
+  "channel":
65
+    "type": "string"
66
+    "default": "stable"
67
+    "description": |
68
+      Snap channel to install Kubernetes master services from
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

debug-scripts/charm-unitdata

 1
--- 
 2
+++ debug-scripts/charm-unitdata
 3
@@ -0,0 +1,12 @@
 4
+#!/usr/bin/python3
 5
+
 6
+import debug_script
 7
+import json
 8
+from charmhelpers.core import unitdata
 9
+
10
+kv = unitdata.kv()
11
+data = kv.getrange("")
12
+
13
+with debug_script.open_file("unitdata.json", "w") as f:
14
+  json.dump(data, f, indent=2)
15
+  f.write("\n")
Back to file index

debug-scripts/filesystem

 1
--- 
 2
+++ debug-scripts/filesystem
 3
@@ -0,0 +1,17 @@
 4
+#!/bin/sh
 5
+set -ux
 6
+
 7
+# report file system disk space usage
 8
+df -h > $DEBUG_SCRIPT_DIR/df-h
 9
+# estimate file space usage
10
+du -h / 2>&1 > $DEBUG_SCRIPT_DIR/du-h
11
+# list the mounted filesystems
12
+mount > $DEBUG_SCRIPT_DIR/mount
13
+# list the mounted systems with ascii trees
14
+findmnt -A > $DEBUG_SCRIPT_DIR/findmnt
15
+# list block devices
16
+lsblk > $DEBUG_SCRIPT_DIR/lsblk
17
+# list open files
18
+lsof 2>&1 > $DEBUG_SCRIPT_DIR/lsof
19
+# list local system locks
20
+lslocks > $DEBUG_SCRIPT_DIR/lslocks
Back to file index

debug-scripts/juju-logs

1
--- 
2
+++ debug-scripts/juju-logs
3
@@ -0,0 +1,4 @@
4
+#!/bin/sh
5
+set -ux
6
+
7
+cp -v /var/log/juju/* $DEBUG_SCRIPT_DIR
Back to file index

debug-scripts/kubectl

 1
--- 
 2
+++ debug-scripts/kubectl
 3
@@ -0,0 +1,15 @@
 4
+#!/bin/sh
 5
+set -ux
 6
+
 7
+export PATH=$PATH:/snap/bin
 8
+
 9
+alias kubectl="kubectl --kubeconfig=/home/ubuntu/config"
10
+
11
+kubectl cluster-info > $DEBUG_SCRIPT_DIR/cluster-info
12
+kubectl cluster-info dump > $DEBUG_SCRIPT_DIR/cluster-info-dump
13
+for obj in pods svc ingress secrets pv pvc rc; do
14
+  kubectl describe $obj --all-namespaces > $DEBUG_SCRIPT_DIR/describe-$obj
15
+done
16
+for obj in nodes; do
17
+  kubectl describe $obj > $DEBUG_SCRIPT_DIR/describe-$obj
18
+done
Back to file index

debug-scripts/kubernetes-master-services

 1
--- 
 2
+++ debug-scripts/kubernetes-master-services
 3
@@ -0,0 +1,9 @@
 4
+#!/bin/sh
 5
+set -ux
 6
+
 7
+for service in kube-apiserver kube-controller-manager kube-scheduler; do
 8
+  systemctl status snap.$service.daemon > $DEBUG_SCRIPT_DIR/$service-systemctl-status
 9
+  journalctl -u snap.$service.daemon > $DEBUG_SCRIPT_DIR/$service-journal
10
+done
11
+
12
+# FIXME: grab snap config or something
Back to file index

debug-scripts/network

 1
--- 
 2
+++ debug-scripts/network
 3
@@ -0,0 +1,11 @@
 4
+#!/bin/sh
 5
+set -ux
 6
+
 7
+ifconfig -a > $DEBUG_SCRIPT_DIR/ifconfig
 8
+cp -v /etc/resolv.conf $DEBUG_SCRIPT_DIR/resolv.conf
 9
+cp -v /etc/network/interfaces $DEBUG_SCRIPT_DIR/interfaces
10
+netstat -planut > $DEBUG_SCRIPT_DIR/netstat
11
+route -n > $DEBUG_SCRIPT_DIR/route
12
+iptables-save > $DEBUG_SCRIPT_DIR/iptables-save
13
+dig google.com > $DEBUG_SCRIPT_DIR/dig-google
14
+ping -w 2 -i 0.1 google.com > $DEBUG_SCRIPT_DIR/ping-google
Back to file index

debug-scripts/packages

 1
--- 
 2
+++ debug-scripts/packages
 3
@@ -0,0 +1,7 @@
 4
+#!/bin/sh
 5
+set -ux
 6
+
 7
+dpkg --list > $DEBUG_SCRIPT_DIR/dpkg-list
 8
+snap list > $DEBUG_SCRIPT_DIR/snap-list
 9
+pip2 list > $DEBUG_SCRIPT_DIR/pip2-list
10
+pip3 list > $DEBUG_SCRIPT_DIR/pip3-list
Back to file index

debug-scripts/sysctl

1
--- 
2
+++ debug-scripts/sysctl
3
@@ -0,0 +1,4 @@
4
+#!/bin/sh
5
+set -ux
6
+
7
+sysctl -a > $DEBUG_SCRIPT_DIR/sysctl
Back to file index

debug-scripts/systemd

 1
--- 
 2
+++ debug-scripts/systemd
 3
@@ -0,0 +1,10 @@
 4
+#!/bin/sh
 5
+set -ux
 6
+
 7
+systemctl --all > $DEBUG_SCRIPT_DIR/systemctl
 8
+journalctl > $DEBUG_SCRIPT_DIR/journalctl
 9
+systemd-analyze time > $DEBUG_SCRIPT_DIR/systemd-analyze-time
10
+systemd-analyze blame > $DEBUG_SCRIPT_DIR/systemd-analyze-blame
11
+systemd-analyze critical-chain > $DEBUG_SCRIPT_DIR/systemd-analyze-critical-chain
12
+systemd-analyze plot > $DEBUG_SCRIPT_DIR/systemd-analyze-plot.svg
13
+systemd-analyze dump > $DEBUG_SCRIPT_DIR/systemd-analyze-dump
Back to file index

exec.d/vmware-patch/charm-pre-install

 1
--- 
 2
+++ exec.d/vmware-patch/charm-pre-install
 3
@@ -0,0 +1,17 @@
 4
+#!/bin/bash
 5
+MY_HOSTNAME=$(hostname)
 6
+
 7
+: ${JUJU_UNIT_NAME:=`uuidgen`}
 8
+
 9
+
10
+if [ "${MY_HOSTNAME}" == "ubuntuguest" ]; then
11
+    juju-log "Detected broken vsphere integration. Applying hostname override"
12
+
13
+    FRIENDLY_HOSTNAME=$(echo $JUJU_UNIT_NAME | tr / -)
14
+    juju-log "Setting hostname to $FRIENDLY_HOSTNAME"
15
+    if [ ! -f /etc/hostname.orig ]; then
16
+      mv /etc/hostname /etc/hostname.orig
17
+    fi
18
+    echo "${FRIENDLY_HOSTNAME}" > /etc/hostname
19
+    hostname $FRIENDLY_HOSTNAME
20
+fi
Back to file index

hooks/ceph-storage-relation-broken

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

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

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

 1
--- 
 2
+++ hooks/ceph-storage-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/cluster-dns-relation-broken

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

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

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

 1
--- 
 2
+++ hooks/cluster-dns-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/cni-relation-broken

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

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

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

 1
--- 
 2
+++ hooks/cni-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/etcd-relation-broken

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

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

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

 1
--- 
 2
+++ hooks/etcd-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/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/kube-api-endpoint-relation-broken

 1
--- 
 2
+++ hooks/kube-api-endpoint-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/kube-api-endpoint-relation-changed

 1
--- 
 2
+++ hooks/kube-api-endpoint-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/kube-api-endpoint-relation-departed

 1
--- 
 2
+++ hooks/kube-api-endpoint-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/kube-api-endpoint-relation-joined

 1
--- 
 2
+++ hooks/kube-api-endpoint-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/kube-control-relation-broken

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

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

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

 1
--- 
 2
+++ hooks/kube-control-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/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/ceph-admin/.gitignore

1
--- 
2
+++ hooks/relations/ceph-admin/.gitignore
3
@@ -0,0 +1,2 @@
4
+.idea
5
+*.swp
Back to file index

hooks/relations/ceph-admin/README.md

 1
--- 
 2
+++ hooks/relations/ceph-admin/README.md
 3
@@ -0,0 +1,38 @@
 4
+# Overview
 5
+
 6
+This interface layer handles the communication between the Ceph Monitor 
 7
+and a client that requires an admin key.
 8
+
 9
+# Usage
10
+
11
+## Requires
12
+
13
+This interface layer will set the following states, as appropriate:
14
+
15
+  * `{relation_name}.available` The ceph client has been related to a provider.
16
+  The following accessors will be available:
17
+   - key - The admin cephx key
18
+   - auth - Whether or not strict auth is supported
19
+   - mon_hosts - The public addresses list of the monitor cluster
20
+
21
+
22
+Client example:
23
+
24
+```python
25
+@when('ceph-admin.available')
26
+def ceph_connected(ceph_info):
27
+  charm_ceph_conf = os.path.join(os.sep, 'etc', 'ceph', 'ceph.conf')
28
+  cephx_key = os.path.join(os.sep, 'etc', 'ceph', 'ceph.client.admin.keyring')
29
+
30
+  ceph_context = {
31
+      'auth_supported': ceph_client.auth,
32
+      'mon_hosts': ceph_client.mon_hosts,
33
+  }
34
+
35
+  with open(charm_ceph_conf, 'w') as cephconf:
36
+    cephconf.write(render_template('ceph.conf', ceph_context))
37
+
38
+  # Write out the cephx_key also
39
+  with open(cephx_key, 'w') as cephconf:
40
+    cephconf.write(ceph_client.key)
41
+```
Back to file index

hooks/relations/ceph-admin/interface.yaml

1
--- 
2
+++ hooks/relations/ceph-admin/interface.yaml
3
@@ -0,0 +1,3 @@
4
+name: ceph-admin
5
+summary: Ceph Admin Client Interface
6
+version: 1
Back to file index

hooks/relations/ceph-admin/requires.py

 1
--- 
 2
+++ hooks/relations/ceph-admin/requires.py
 3
@@ -0,0 +1,52 @@
 4
+from charms.reactive import hook
 5
+from charms.reactive import RelationBase
 6
+from charms.reactive import scopes
 7
+from charms.reactive import is_state
 8
+
 9
+
10
+class CephClient(RelationBase):
11
+    scope = scopes.GLOBAL
12
+    auto_accessors = ['key', 'fsid', 'auth', 'mon_hosts']
13
+
14
+    @hook('{requires:ceph-admin}-relation-{joined,changed}')
15
+    def changed(self):
16
+        self.set_state('{relation_name}.connected')
17
+        key = None
18
+        fsid = None
19
+        auth = None
20
+        mon_hosts = None
21
+
22
+        try:
23
+            key = self.key
24
+        except AttributeError:
25
+            pass
26
+
27
+        try:
28
+            fsid = self.fsid
29
+        except AttributeError:
30
+            pass
31
+
32
+        try:
33
+            auth = self.auth
34
+        except AttributeError:
35
+            pass
36
+
37
+        try:
38
+            mon_hosts = self.mon_hosts
39
+        except AttributeError:
40
+            pass
41
+
42
+        data = {
43
+            'key': key,
44
+            'fsid': fsid,
45
+            'auth': auth,
46
+            'mon_hosts': mon_hosts
47
+        }
48
+
49
+        if all(data.values()):
50
+            self.set_state('{relation_name}.available')
51
+
52
+    @hook('{requires:ceph-admin}-relation-{broken,departed}')
53
+    def broken(self):
54
+        if is_state('{relation_name}.available'):
55
+            self.remove_state('{relation_name}.available')
Back to file index

hooks/relations/etcd/.gitignore

1
--- 
2
+++ hooks/relations/etcd/.gitignore
3
@@ -0,0 +1 @@
4
+.DS_Store
Back to file index

hooks/relations/etcd/README.md

 1
--- 
 2
+++ hooks/relations/etcd/README.md
 3
@@ -0,0 +1,89 @@
 4
+# Overview
 5
+
 6
+This interface layer handles the communication with Etcd via the `etcd`
 7
+interface.
 8
+
 9
+# Usage
10
+
11
+## Requires
12
+
13
+This interface layer will set the following states, as appropriate:
14
+
15
+  * `{relation_name}.connected` The relation is established, but Etcd may not
16
+  yet have provided any connection or service information.
17
+
18
+  * `{relation_name}.available` Etcd has provided its connection string
19
+    information, and is ready to serve as a KV store.
20
+    The provided information can be accessed via the following methods:
21
+      * `etcd.get_connection_string()`
22
+      * `etcd.get_version()`
23
+  * `{relation_name}.tls.available` Etcd has provided the connection string
24
+    information, and the tls client credentials to communicate with it.
25
+    The client credentials can be accessed via:
26
+    * `{relation_name}.get_client_credentials()` returning a dictionary of
27
+       the clinet certificate, key and CA.
28
+    * `{relation_name}.save_client_credentials(key, cert, ca)` is a convenience
29
+      method to save the client certificate, key and CA to files of your
30
+      choosing.
31
+
32
+
33
+For example, a common application for this is configuring an applications
34
+backend key/value storage, like Docker.
35
+
36
+```python
37
+@when('etcd.available', 'docker.available')
38
+def swarm_etcd_cluster_setup(etcd):
39
+    con_string = etcd.connection_string().replace('http', 'etcd')
40
+    opts = {}
41
+    opts['connection_string'] = con_string
42
+    render('docker-compose.yml', 'files/swarm/docker-compose.yml', opts)
43
+
44
+```
45
+
46
+
47
+## Provides
48
+
49
+A charm providing this interface is providing the Etcd rest api service.
50
+
51
+This interface layer will set the following states, as appropriate:
52
+
53
+  * `{relation_name}.connected` One or more clients of any type have
54
+    been related. The charm should call the following methods to provide the
55
+    appropriate information to the clients:
56
+
57
+    * `{relation_name}.set_connection_string(string, version)`
58
+    * `{relation_name}.set_client_credentials(key, cert, ca)`
59
+
60
+Example:
61
+
62
+```python
63
+@when('db.connected')
64
+def send_connection_details(db):
65
+    cert = leader_get('client_certificate')
66
+    key = leader_get('client_key')
67
+    ca = leader_get('certificate_authority')
68
+    # Set the key, cert, and ca on the db relation
69
+    db.set_client_credentials(key, cert, ca)
70
+
71
+    port = hookenv.config().get('port')
72
+    # Get all the peers participating in the cluster relation.
73
+    addresses = cluster.get_peer_addresses()
74
+    connections = []
75
+    for address in addresses:
76
+        connections.append('http://{0}:{1}'.format(address, port))
77
+    # Set the connection string on the db relation.
78
+    db.set_connection_string(','.join(conections))
79
+```
80
+
81
+
82
+# Contact Information
83
+
84
+### Maintainer
85
+- Charles Butler <charles.butler@canonical.com>
86
+
87
+
88
+# Etcd
89
+
90
+- [Etcd](https://coreos.com/etcd/) home page
91
+- [Etcd bug trackers](https://github.com/coreos/etcd/issues)
92
+- [Etcd Juju Charm](http://jujucharms.com/?text=etcd)
Back to file index

hooks/relations/etcd/interface.yaml

1
--- 
2
+++ hooks/relations/etcd/interface.yaml
3
@@ -0,0 +1,4 @@
4
+name: etcd
5
+summary: Interface for relating to ETCD
6
+version: 2
7
+maintainer: "Charles Butler <charles.butler@canonical.com>"
Back to file index

hooks/relations/etcd/peers.py

 1
--- 
 2
+++ hooks/relations/etcd/peers.py
 3
@@ -0,0 +1,64 @@
 4
+#!/usr/bin/python
 5
+# Licensed under the Apache License, Version 2.0 (the "License");
 6
+# you may not use this file except in compliance with the License.
 7
+# You may obtain a copy of the License at
 8
+#
 9
+#     http://www.apache.org/licenses/LICENSE-2.0
10
+#
11
+# Unless required by applicable law or agreed to in writing, software
12
+# distributed under the License is distributed on an "AS IS" BASIS,
13
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+# See the License for the specific language governing permissions and
15
+# limitations under the License.
16
+
17
+from charms.reactive import RelationBase
18
+from charms.reactive import hook
19
+from charms.reactive import scopes
20
+
21
+
22
+class EtcdPeer(RelationBase):
23
+    '''This class handles peer relation communication by setting states that
24
+    the reactive code can respond to. '''
25
+
26
+    scope = scopes.UNIT
27
+
28
+    @hook('{peers:etcd}-relation-joined')
29
+    def peer_joined(self):
30
+        '''A new peer has joined, set the state on the unit so we can track
31
+        when they are departed. '''
32
+        conv = self.conversation()
33
+        conv.set_state('{relation_name}.joined')
34
+
35
+    @hook('{peers:etcd}-relation-departed')
36
+    def peers_going_away(self):
37
+        '''Trigger a state on the unit that it is leaving. We can use this
38
+        state in conjunction with the joined state to determine which unit to
39
+        unregister from the etcd cluster. '''
40
+        conv = self.conversation()
41
+        conv.remove_state('{relation_name}.joined')
42
+        conv.set_state('{relation_name}.departing')
43
+
44
+    def dismiss(self):
45
+        '''Remove the departing state from all other units in the conversation,
46
+        and we can resume normal operation.
47
+        '''
48
+        for conv in self.conversations():
49
+            conv.remove_state('{relation_name}.departing')
50
+
51
+    def get_peers(self):
52
+        '''Return a list of names for the peers participating in this
53
+        conversation scope. '''
54
+        peers = []
55
+        # Iterate over all the conversations of this type.
56
+        for conversation in self.conversations():
57
+            peers.append(conversation.scope)
58
+        return peers
59
+
60
+    def get_peer_addresses(self):
61
+        '''Return a list of private addresses for the peers participating in
62
+        this conversation scope. '''
63
+        addresses = []
64
+        # Iterate over all the conversations of this type.
65
+        for conversation in self.conversations():
66
+            addresses.append(conversation.get_remote('private-address'))
67
+        return addresses
Back to file index

hooks/relations/etcd/provides.py

 1
--- 
 2
+++ hooks/relations/etcd/provides.py
 3
@@ -0,0 +1,45 @@
 4
+#!/usr/bin/python
 5
+# Licensed under the Apache License, Version 2.0 (the "License");
 6
+# you may not use this file except in compliance with the License.
 7
+# You may obtain a copy of the License at
 8
+#
 9
+#     http://www.apache.org/licenses/LICENSE-2.0
10
+#
11
+# Unless required by applicable law or agreed to in writing, software
12
+# distributed under the License is distributed on an "AS IS" BASIS,
13
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+# See the License for the specific language governing permissions and
15
+# limitations under the License.
16
+
17
+from charms.reactive import RelationBase
18
+from charms.reactive import hook
19
+from charms.reactive import scopes
20
+
21
+
22
+class EtcdProvider(RelationBase):
23
+    scope = scopes.GLOBAL
24
+
25
+    @hook('{provides:etcd}-relation-{joined,changed}')
26
+    def joined_or_changed(self):
27
+        ''' Set the connected state from the provides side of the relation. '''
28
+        self.set_state('{relation_name}.connected')
29
+
30
+    @hook('{provides:etcd}-relation-{broken,departed}')
31
+    def broken_or_departed(self):
32
+        '''Remove connected state from the provides side of the relation. '''
33
+        self.remove_state('{relation_name}.connected')
34
+
35
+    def set_client_credentials(self, key, cert, ca):
36
+        ''' Set the client credentials on the global conversation for this
37
+        relation. '''
38
+        self.set_remote('client_key', key)
39
+        self.set_remote('client_ca', ca)
40
+        self.set_remote('client_cert', cert)
41
+
42
+    def set_connection_string(self, connection_string, version=''):
43
+        ''' Set the connection string on the global conversation for this
44
+        relation. '''
45
+        # Note: Version added as a late-dependency for 2 => 3 migration
46
+        # If no version is specified, consumers should presume etcd 2.x
47
+        self.set_remote('connection_string', connection_string)
48
+        self.set_remote('version', version)
Back to file index

hooks/relations/etcd/requires.py

 1
--- 
 2
+++ hooks/relations/etcd/requires.py
 3
@@ -0,0 +1,80 @@
 4
+#!/usr/bin/python
 5
+# Licensed under the Apache License, Version 2.0 (the "License");
 6
+# you may not use this file except in compliance with the License.
 7
+# You may obtain a copy of the License at
 8
+#
 9
+#     http://www.apache.org/licenses/LICENSE-2.0
10
+#
11
+# Unless required by applicable law or agreed to in writing, software
12
+# distributed under the License is distributed on an "AS IS" BASIS,
13
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+# See the License for the specific language governing permissions and
15
+# limitations under the License.
16
+
17
+import os
18
+
19
+from charms.reactive import RelationBase
20
+from charms.reactive import hook
21
+from charms.reactive import scopes
22
+
23
+
24
+class EtcdClient(RelationBase):
25
+    scope = scopes.GLOBAL
26
+
27
+    @hook('{requires:etcd}-relation-{joined,changed}')
28
+    def changed(self):
29
+        ''' Indicate the relation is connected, and if the relation data is
30
+        set it is also available. '''
31
+        self.set_state('{relation_name}.connected')
32
+
33
+        if self.get_connection_string():
34
+            self.set_state('{relation_name}.available')
35
+            # Get the ca, key, cert from the relation data.
36
+            cert = self.get_client_credentials()
37
+            # The tls state depends on the existance of the ca, key and cert.
38
+            if cert['client_cert'] and cert['client_key'] and cert['client_ca']:  # noqa
39
+                self.set_state('{relation_name}.tls.available')
40
+
41
+    @hook('{requires:etcd}-relation-{broken, departed}')
42
+    def broken(self):
43
+        ''' Indicate the relation is no longer available and not connected. '''
44
+        self.remove_state('{relation_name}.available')
45
+        self.remove_state('{relation_name}.connected')
46
+        self.remove_state('{relation_name}.tls.available')
47
+
48
+    def connection_string(self):
49
+        ''' This method is depreciated but ensures backward compatibility
50
+        @see get_connection_string(self). '''
51
+        return self.get_connection_string()
52
+
53
+    def get_connection_string(self):
54
+        ''' Return the connection string, if available, or None. '''
55
+        return self.get_remote('connection_string')
56
+
57
+    def get_version(self):
58
+        ''' Return the version of the etd protocol being used, or None. '''
59
+        return self.get_remote('version')
60
+
61
+    def get_client_credentials(self):
62
+        ''' Return a dict with the client certificate, ca and key to
63
+        communicate with etcd using tls. '''
64
+        return {'client_cert': self.get_remote('client_cert'),
65
+                'client_key': self.get_remote('client_key'),
66
+                'client_ca': self.get_remote('client_ca')}
67
+
68
+    def save_client_credentials(self, key, cert, ca):
69
+        ''' Save all the client certificates for etcd to local files. '''
70
+        self._save_remote_data('client_cert', cert)
71
+        self._save_remote_data('client_key', key)
72
+        self._save_remote_data('client_ca', ca)
73
+
74
+    def _save_remote_data(self, key, path):
75
+        ''' Save the remote data to a file indicated by path creating the
76
+        parent directory if needed.'''
77
+        value = self.get_remote(key)
78
+        if value:
79
+            parent = os.path.dirname(path)
80
+            if not os.path.isdir(parent):
81
+                os.makedirs(parent)
82
+            with open(path, 'w') as stream:
83
+                stream.write(value)
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/kube-control/README.md

  1
--- 
  2
+++ hooks/relations/kube-control/README.md
  3
@@ -0,0 +1,146 @@
  4
+# kube-control interface
  5
+
  6
+This interface provides communication between master and workers in a
  7
+Kubernetes cluster.
  8
+
  9
+
 10
+## Provides (kubernetes-master side)
 11
+
 12
+
 13
+### States
 14
+
 15
+* `kube-control.connected`
 16
+
 17
+  Enabled when a worker has joined the relation.
 18
+
 19
+* `kube-control.gpu.available`
 20
+
 21
+  Enabled when any worker has indicated that it is running in gpu mode.
 22
+
 23
+* `kube-control.departed`
 24
+
 25
+  Enabled when any worker has indicated that it is leaving the cluster.
 26
+
 27
+
 28
+* `kube-control.auth.requested`
 29
+
 30
+  Enabled when an authentication credential is requested. This state is
 31
+  temporary and will be removed once the units authentication request has
 32
+  been fulfilled.
 33
+
 34
+### Methods
 35
+
 36
+* `kube_control.set_dns(port, domain, sdn_ip)`
 37
+
 38
+  Sends DNS info to the connected worker(s).
 39
+
 40
+
 41
+* `kube_control.auth_user()`
 42
+
 43
+  Returns the requested username and group requested for authentication.
 44
+
 45
+* `kube_control.sign_auth_request(kubelet_token, proxy_token, client_token)`
 46
+
 47
+  Sends authentication tokens to the requesting unit for the requested user
 48
+  and kube-proxy services.
 49
+
 50
+* `kube_control.flush_departed()`
 51
+
 52
+  Returns the unit departing the kube_control relationship so you can do any
 53
+  post removal cleanup. Such as removing authentication tokens for the unit.
 54
+  Invoking this method will also remove the `kube-control.departed` state
 55
+
 56
+### Examples
 57
+
 58
+```python
 59
+
 60
+@when('kube-control.connected')
 61
+def send_dns(kube_control):
 62
+    # send port, domain, sdn_ip to the remote side
 63
+    kube_control.set_dns(53, "cluster.local", "10.1.0.10")
 64
+
 65
+@when('kube-control.gpu.available')
 66
+def on_gpu_available(kube_control):
 67
+    # The remote side is gpu-enable, handle it somehow
 68
+    assert kube_control.get_gpu() == True
 69
+
 70
+
 71
+@when('kube-control.departed')
 72
+@when('leadership.is_leader')
 73
+def flush_auth_for_departed(kube_control):
 74
+    ''' Unit has left the cluster and needs to have its authentication
 75
+    tokens removed from the token registry '''
 76
+    departing_unit = kube_control.flush_departed()
 77
+
 78
+```
 79
+
 80
+## Requires (kubernetes-worker side)
 81
+
 82
+
 83
+### States
 84
+
 85
+* `kube-control.connected`
 86
+
 87
+  Enabled when a master has joined the relation.
 88
+
 89
+* `kube-control.dns.available`
 90
+
 91
+  Enabled when DNS info is available from the master.
 92
+
 93
+* `kube-control.auth.available`
 94
+
 95
+  Enabled when authentication credentials are present from the master.
 96
+
 97
+### Methods
 98
+
 99
+* `kube_control.get_dns()`
100
+
101
+  Returns a dictionary of DNS info sent by the master. The keys in the
102
+  dict are: domain, private-address, sdn-ip, port.
103
+
104
+* `kube_control.set_gpu(enabled=True)`
105
+
106
+  Tell the master that we are gpu-enabled.
107
+
108
+*  `kube_control.get_auth_credentials()`
109
+
110
+  Returns a dict with the returned authentication credentials.
111
+
112
+*  `set_auth_request(kubelet, group='system:nodes')`
113
+
114
+  Issue an authentication request against the master to receive token based
115
+  auth credentials in return.
116
+
117
+### Examples
118
+
119
+```python
120
+
121
+@when('kube-control.dns.available')
122
+def on_dns_available(kube_control):
123
+    # Remote side has sent DNS info
124
+    dns = kube_control.get_dns()
125
+    print(context['domain'])
126
+    print(context['private-address'])
127
+    print(context['sdn-ip'])
128
+    print(context['port'])
129
+
130
+@when('kube-control.connected')
131
+def send_gpu(kube_control):
132
+    # Tell the master that we're gpu-enabled
133
+    kube_control.set_gpu(True)
134
+
135
+@when('kube-control.auth.available')
136
+def display_auth_tokens(kube_control):
137
+    # Remote side has sent auth info
138
+    auth = kube_control.get_auth_credentials()
139
+    print(auth['kubelet_token'])
140
+    print(auth['proxy_token'])
141
+    print(auth['client_token'])
142
+
143
+@when('kube-control.connected')
144
+@when_not('kube-control.auth.available')
145
+def request_auth_credentials(kube_control):
146
+    # Request an admin user with sudo level access named 'root'
147
+    kube_control.set_auth_request('root', group='system:masters')
148
+
149
+```
Back to file index

hooks/relations/kube-control/interface.yaml

1
--- 
2
+++ hooks/relations/kube-control/interface.yaml
3
@@ -0,0 +1,4 @@
4
+name: kube-control
5
+summary: Provides master-worker communication.
6
+version: 1
7
+maintainer: "Tim Van Steenburgh <tim.van.steenburgh@canonical.com>"
Back to file index

hooks/relations/kube-control/provides.py

  1
--- 
  2
+++ hooks/relations/kube-control/provides.py
  3
@@ -0,0 +1,106 @@
  4
+#!/usr/bin/python
  5
+# Licensed under the Apache License, Version 2.0 (the "License");
  6
+# you may not use this file except in compliance with the License.
  7
+# You may obtain a copy of the License at
  8
+#
  9
+#     http://www.apache.org/licenses/LICENSE-2.0
 10
+#
 11
+# Unless required by applicable law or agreed to in writing, software
 12
+# distributed under the License is distributed on an "AS IS" BASIS,
 13
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14
+# See the License for the specific language governing permissions and
 15
+# limitations under the License.
 16
+
 17
+from charms.reactive import RelationBase
 18
+from charms.reactive import hook
 19
+from charms.reactive import scopes
 20
+
 21
+from charmhelpers.core import hookenv
 22
+
 23
+
 24
+class KubeControlProvider(RelationBase):
 25
+    """Implements the kubernetes-master side of the kube-control interface.
 26
+
 27
+    """
 28
+    scope = scopes.UNIT
 29
+
 30
+    @hook('{provides:kube-control}-relation-{joined,changed}')
 31
+    def joined_or_changed(self):
 32
+        conv = self.conversation()
 33
+        conv.set_state('{relation_name}.connected')
 34
+
 35
+        hookenv.log('Checking for gpu-enabled workers')
 36
+        if self._get_gpu():
 37
+            conv.set_state('{relation_name}.gpu.available')
 38
+        else:
 39
+            conv.remove_state('{relation_name}.gpu.available')
 40
+
 41
+        if self._has_auth_request():
 42
+            conv.set_state('{relation_name}.auth.requested')
 43
+
 44
+    @hook('{provides:kube-control}-relation-departed')
 45
+    def departed(self):
 46
+        """Remove all states.
 47
+
 48
+        """
 49
+        conv = self.conversation()
 50
+        conv.remove_state('{relation_name}.connected')
 51
+        conv.remove_state('{relation_name}.gpu.available')
 52
+        conv.set_state('{relation_name}.departed')
 53
+
 54
+    def flush_departed(self):
 55
+        """Remove the signal state that we have a unit departing the
 56
+        relationship. Additionally return the unit departing so the host can
 57
+        do any cleanup logic required. """
 58
+        conv = self.conversation()
 59
+        conv.remove_state('{relation_name}.departed')
 60
+        return conv.scope
 61
+
 62
+    def set_dns(self, port, domain, sdn_ip):
 63
+        """Send DNS info to the remote units.
 64
+
 65
+        We'll need the port, domain, and sdn_ip of the dns service. If
 66
+        sdn_ip is not required in your deployment, the units private-ip
 67
+        is available implicitly.
 68
+
 69
+        """
 70
+        credentials = {
 71
+            'port': port,
 72
+            'domain': domain,
 73
+            'sdn-ip': sdn_ip,
 74
+        }
 75
+        for conv in self.conversations():
 76
+            conv.set_remote(data=credentials)
 77
+
 78
+    def auth_user(self):
 79
+        """ return the kubelet_user value on the wire from the requestor """
 80
+        conv = self.conversation()
 81
+        return (conv.scope, {'user': conv.get_remote('kubelet_user'),
 82
+                             'group': conv.get_remote('auth_group')})
 83
+
 84
+    def sign_auth_request(self, kubelet_token, proxy_token, client_token):
 85
+        """Send authorization tokens to the requesting unit """
 86
+        conv = self.conversation()
 87
+        conv.set_remote(data={'kubelet_token': kubelet_token,
 88
+                              'proxy_token': proxy_token,
 89
+                              'client_token': client_token})
 90
+        conv.remove_state('{relation_name}.auth.requested')
 91
+
 92
+    def _get_gpu(self):
 93
+        """Return True if any remote worker is gpu-enabled.
 94
+
 95
+        """
 96
+        for conv in self.conversations():
 97
+            if conv.get_remote('gpu') == 'True':
 98
+                hookenv.log('Unit {} has gpu enabled'.format(conv.scope))
 99
+                return True
100
+        return False
101
+
102
+    def _has_auth_request(self):
103
+        """Check if there's a kubelet user on the wire requesting auth. This
104
+        action implies requested kube-proxy auth as well, as kube-proxy should
105
+        be run everywhere there is a kubelet.
106
+        """
107
+        conv = self.conversation()
108
+        if conv.get_remote('kubelet_user'):
109
+            return conv.get_remote('kubelet_user')
Back to file index

hooks/relations/kube-control/requires.py

  1
--- 
  2
+++ hooks/relations/kube-control/requires.py
  3
@@ -0,0 +1,108 @@
  4
+#!/usr/bin/python
  5
+# Licensed under the Apache License, Version 2.0 (the "License");
  6
+# you may not use this file except in compliance with the License.
  7
+# You may obtain a copy of the License at
  8
+#
  9
+#     http://www.apache.org/licenses/LICENSE-2.0
 10
+#
 11
+# Unless required by applicable law or agreed to in writing, software
 12
+# distributed under the License is distributed on an "AS IS" BASIS,
 13
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14
+# See the License for the specific language governing permissions and
 15
+# limitations under the License.
 16
+
 17
+from charms.reactive import RelationBase
 18
+from charms.reactive import hook
 19
+from charms.reactive import scopes
 20
+
 21
+from charmhelpers.core import hookenv
 22
+
 23
+
 24
+class KubeControlRequireer(RelationBase):
 25
+    """Implements the kubernetes-worker side of the kube-control interface.
 26
+
 27
+    """
 28
+    scope = scopes.GLOBAL
 29
+
 30
+    @hook('{requires:kube-control}-relation-{joined,changed}')
 31
+    def joined_or_changed(self):
 32
+        """Set states corresponding to the data we have.
 33
+
 34
+        """
 35
+        conv = self.conversation()
 36
+        conv.set_state('{relation_name}.connected')
 37
+
 38
+        if self.dns_ready():
 39
+            conv.set_state('{relation_name}.dns.available')
 40
+        else:
 41
+            conv.remove_state('{relation_name}.dns.available')
 42
+
 43
+        if self._has_auth_credentials():
 44
+            conv.set_state('{relation_name}.auth.available')
 45
+        else:
 46
+            conv.remove_state('{relation_name}.auth.available')
 47
+
 48
+    @hook('{requires:kube-control}-relation-{broken,departed}')
 49
+    def departed(self):
 50
+        """Remove all states.
 51
+
 52
+        """
 53
+        conv = self.conversation()
 54
+        conv.remove_state('{relation_name}.connected')
 55
+        conv.remove_state('{relation_name}.dns.available')
 56
+
 57
+    def get_auth_credentials(self):
 58
+        """ Return the authentication credentials.
 59
+
 60
+        """
 61
+        conv = self.conversation()
 62
+
 63
+        return {
 64
+            'kubelet_token': conv.get_remote('kubelet_token'),
 65
+            'proxy_token': conv.get_remote('proxy_token'),
 66
+            'client_token': conv.get_remote('client_token')
 67
+        }
 68
+
 69
+    def get_dns(self):
 70
+        """Return DNS info provided by the master.
 71
+
 72
+        """
 73
+        conv = self.conversation()
 74
+
 75
+        return {
 76
+            'private-address': conv.get_remote('private-address'),
 77
+            'port': conv.get_remote('port'),
 78
+            'domain': conv.get_remote('domain'),
 79
+            'sdn-ip': conv.get_remote('sdn-ip'),
 80
+        }
 81
+
 82
+    def dns_ready(self):
 83
+        """Return True if we have all DNS info from the master.
 84
+
 85
+        """
 86
+        return all(self.get_dns().values())
 87
+
 88
+    def set_auth_request(self, kubelet, group='system:nodes'):
 89
+        """ Tell the master that we are requesting auth, and to use this
 90
+        hostname for the kubelet system account.
 91
+
 92
+        Param groups - Determines the level of eleveted privleges of the
 93
+        requested user. Can be overridden to request sudo level access on the
 94
+        cluster via changing to system:masters """
 95
+        conv = self.conversation()
 96
+        conv.set_remote(data={'kubelet_user': kubelet,
 97
+                              'auth_group': group})
 98
+
 99
+    def set_gpu(self, enabled=True):
100
+        """Tell the master that we're gpu-enabled (or not).
101
+
102
+        """
103
+        hookenv.log('Setting gpu={} on kube-control relation'.format(enabled))
104
+        conv = self.conversation()
105
+        conv.set_remote(gpu=enabled)
106
+
107
+    def _has_auth_credentials(self):
108
+        """Predicate method to signal we have authentication credentials """
109
+        conv = self.conversation()
110
+        if conv.get_remote('kubelet_token') and conv.get_remote('proxy_token'):
111
+            return True
Back to file index

hooks/relations/kube-dns/README.md

 1
--- 
 2
+++ hooks/relations/kube-dns/README.md
 3
@@ -0,0 +1,38 @@
 4
+# Kube-DNS
 5
+
 6
+This interface provides the DNS details for a Kubernetes cluster.
 7
+
 8
+The majority of kubernetes services will expect the following values:
 9
+
10
+```
11
+--cluster-dns $IP_OF_DNS_SERVER
12
+--cluster-domain $DOMAIN
13
+```
14
+
15
+
16
+# Provides
17
+
18
+Kubernetes API credentials are sent in the following dict structure:
19
+
20
+```python
21
+{"private-address": "",
22
+ "port": "53",
23
+ "domain": "cluster.local",
24
+ "sdn_ip": "10.1.0.10"
25
+}
26
+
27
+```
28
+
29
+# Requires
30
+
31
+```python
32
+@when('kube-dns.available')
33
+def save_dns_credentials(kube_dns):
34
+    context = kube_dns.details()
35
+    print(context['domain'])
36
+    print(context['private-address'])
37
+    print(context['sdn-ip'])
38
+    print(context['port'])
39
+```
40
+
41
+
Back to file index

hooks/relations/kube-dns/interface.yaml

1
--- 
2
+++ hooks/relations/kube-dns/interface.yaml
3
@@ -0,0 +1,4 @@
4
+name: kube-dns
5
+summary: provides the kubernetes dns settings
6
+version: 1
7
+maintainer: "Charles Butler <charles.butler@canonical.com>"
Back to file index

hooks/relations/kube-dns/provides.py

 1
--- 
 2
+++ hooks/relations/kube-dns/provides.py
 3
@@ -0,0 +1,40 @@
 4
+#!/usr/bin/python
 5
+# Licensed under the Apache License, Version 2.0 (the "License");
 6
+# you may not use this file except in compliance with the License.
 7
+# You may obtain a copy of the License at
 8
+#
 9
+#     http://www.apache.org/licenses/LICENSE-2.0
10
+#
11
+# Unless required by applicable law or agreed to in writing, software
12
+# distributed under the License is distributed on an "AS IS" BASIS,
13
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+# See the License for the specific language governing permissions and
15
+# limitations under the License.
16
+
17
+from charms.reactive import RelationBase
18
+from charms.reactive import hook
19
+from charms.reactive import scopes
20
+
21
+
22
+class KubeDNSProvider(RelationBase):
23
+    scope = scopes.GLOBAL
24
+
25
+    @hook('{provides:kube-dns}-relation-{joined,changed}')
26
+    def joined_or_changed(self):
27
+        conv = self.conversation()
28
+        conv.set_state('{relation_name}.connected')
29
+
30
+    @hook('{provides:kube-dns}-relation-{departed}')
31
+    def departed(self):
32
+        conv = self.conversation()
33
+        conv.remove_state('{relation_name}.connected')
34
+
35
+    def set_dns_info(self, port, domain, sdn_ip):
36
+        ''' We will need the domain, sdn_ip, and port of the dns service, if
37
+            sdn_ip is not required in your deployment, the units private-ip
38
+            is availble implicitly.'''
39
+        credentials = {'port': port,
40
+                       'domain': domain,
41
+                       'sdn-ip': sdn_ip}
42
+        conv = self.conversation()
43
+        conv.set_remote(data=credentials)
Back to file index

hooks/relations/kube-dns/requires.py

 1
--- 
 2
+++ hooks/relations/kube-dns/requires.py
 3
@@ -0,0 +1,47 @@
 4
+#!/usr/bin/python
 5
+# Licensed under the Apache License, Version 2.0 (the "License");
 6
+# you may not use this file except in compliance with the License.
 7
+# You may obtain a copy of the License at
 8
+#
 9
+#     http://www.apache.org/licenses/LICENSE-2.0
10
+#
11
+# Unless required by applicable law or agreed to in writing, software
12
+# distributed under the License is distributed on an "AS IS" BASIS,
13
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+# See the License for the specific language governing permissions and
15
+# limitations under the License.
16
+
17
+from charms.reactive import RelationBase
18
+from charms.reactive import hook
19
+from charms.reactive import scopes
20
+
21
+
22
+class KubeDNSRequireer(RelationBase):
23
+    scope = scopes.GLOBAL
24
+
25
+    @hook('{requires:kube-dns}-relation-{joined,changed}')
26
+    def joined_or_changed(self):
27
+        ''' Set the available state if we have the minimum credentials '''
28
+        if self.has_info():
29
+            conv = self.conversation()
30
+            conv.set_state('{relation_name}.available')
31
+
32
+    def details(self):
33
+        ''' Return a small subnet of the data '''
34
+        return {'private-address': self._get_value('private-address'),
35
+                'port': self._get_value('port'),
36
+                'domain': self._get_value('domain'),
37
+                'sdn-ip': self._get_value('sdn-ip')}
38
+
39
+    def has_info(self):
40
+        ''' Determine if we have a hostname and a port and domain '''
41
+        to_find = ['private-address', 'port', 'domain', 'sdn-ip']
42
+        # Iterate through our services and verify we have values
43
+        for value in to_find:
44
+            if not self._get_value(value):
45
+                return False
46
+        return True
47
+
48
+    def _get_value(self, key):
49
+        conv = self.conversation()
50
+        return conv.get_remote(key)
Back to file index

hooks/relations/kubernetes-cni/.gitignore

1
--- 
2
+++ hooks/relations/kubernetes-cni/.gitignore
3
@@ -0,0 +1 @@
4
+.DS_Store
Back to file index

hooks/relations/kubernetes-cni/interface.yaml

1
--- 
2
+++ hooks/relations/kubernetes-cni/interface.yaml
3
@@ -0,0 +1,4 @@
4
+name: kubernetes-cni
5
+summary: Interface for relating various CNI implementations
6
+version: 0
7
+maintainer: "Rye Terrell <rye.terrell@canonical.com>"
Back to file index

hooks/relations/kubernetes-cni/provides.py

 1
--- 
 2
+++ hooks/relations/kubernetes-cni/provides.py
 3
@@ -0,0 +1,43 @@
 4
+#!/usr/bin/python
 5
+
 6
+from charms.reactive import RelationBase
 7
+from charms.reactive import hook
 8
+from charms.reactive import scopes
 9
+
10
+
11
+class CNIPluginProvider(RelationBase):
12
+    scope = scopes.GLOBAL
13
+
14
+    @hook('{provides:kubernetes-cni}-relation-{joined,changed}')
15
+    def joined_or_changed(self):
16
+        ''' Set the connected state from the provides side of the relation. '''
17
+        self.set_state('{relation_name}.connected')
18
+        if self.config_available():
19
+            self.set_state('{relation_name}.available')
20
+
21
+    @hook('{provides:kubernetes-cni}-relation-{departed}')
22
+    def broken_or_departed(self):
23
+        '''Remove connected state from the provides side of the relation. '''
24
+        self.remove_state('{relation_name}.connected')
25
+        self.remove_state('{relation_name}.available')
26
+        self.remove_state('{relation_name}.configured')
27
+
28
+    def set_config(self, is_master, kubeconfig_path):
29
+        ''' Relays a dict of kubernetes configuration information. '''
30
+        self.set_remote(data={
31
+            'is_master': is_master,
32
+            'kubeconfig_path': kubeconfig_path
33
+        })
34
+        self.set_state('{relation_name}.configured')
35
+
36
+    def config_available(self):
37
+        ''' Ensures all config from the CNI plugin is available. '''
38
+        if not self.get_remote('cidr'):
39
+            return False
40
+        return True
41
+
42
+    def get_config(self):
43
+        ''' Returns all config from the CNI plugin. '''
44
+        return {
45
+            'cidr': self.get_remote('cidr'),
46
+        }
Back to file index

hooks/relations/kubernetes-cni/requires.py

 1
--- 
 2
+++ hooks/relations/kubernetes-cni/requires.py
 3
@@ -0,0 +1,40 @@
 4
+#!/usr/bin/python
 5
+
 6
+from charms.reactive import RelationBase
 7
+from charms.reactive import hook
 8
+from charms.reactive import scopes
 9
+
10
+
11
+class CNIPluginClient(RelationBase):
12
+    scope = scopes.GLOBAL
13
+
14
+    @hook('{requires:kubernetes-cni}-relation-{joined,changed}')
15
+    def changed(self):
16
+        ''' Indicate the relation is connected, and if the relation data is
17
+        set it is also available. '''
18
+        self.set_state('{relation_name}.connected')
19
+        config = self.get_config()
20
+        if config['is_master'] == 'True':
21
+            self.set_state('{relation_name}.is-master')
22
+        elif config['is_master'] == 'False':
23
+            self.set_state('{relation_name}.is-worker')
24
+
25
+    @hook('{requires:kubernetes-cni}-relation-{departed}')
26
+    def broken(self):
27
+        ''' Indicate the relation is no longer available and not connected. '''
28
+        self.remove_state('{relation_name}.connected')
29
+        self.remove_state('{relation_name}.is-master')
30
+        self.remove_state('{relation_name}.is-worker')
31
+
32
+    def get_config(self):
33
+        ''' Get the kubernetes configuration information. '''
34
+        return {
35
+            'is_master': self.get_remote('is_master'),
36
+            'kubeconfig_path': self.get_remote('kubeconfig_path')
37
+        }
38
+
39
+    def set_config(self, cidr):
40
+        ''' Sets the CNI configuration information. '''
41
+        self.set_remote(data={
42
+            'cidr': cidr
43
+        })
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

icon.svg

  1
--- 
  2
+++ icon.svg
  3
@@ -0,0 +1,362 @@
  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="kubernetes_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="3.2596288"
229
+     inkscape:cx="-297.64473"
230
+     inkscape:cy="37.187989"
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.18545,693.54003 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
+       inkscape:connector-curvature="0"
322
+       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:162.01495361;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"
323
+       d="m -220.00092,654.47612 a 1.7570372,1.5813881 89.992522 0 0 -1.58016,1.7337 l 0,0 c 0,0.002 0,0.004 0,0.006 a 1.7570372,1.5813881 89.992522 0 0 -0.001,0.0173 1.7570372,1.5813881 89.992522 0 0 0,0.0131 c -0.001,0.13631 -0.006,0.31246 -0.001,0.43612 0.0208,0.55969 0.14346,0.9889 0.21689,1.50455 0.13304,1.10367 0.24493,2.01854 0.17614,2.86887 -0.0625,0.42557 -0.30904,0.59289 -0.51489,0.78987 l 0.0397,0.0262 -0.0412,0 -0.038,0.67223 c -0.95591,0.0788 -1.91024,0.22371 -2.85599,0.43845 -3.98544,0.90489 -7.57612,2.97226 -10.34179,5.89656 l -0.54193,-0.38396 -0.0253,0.0319 0.004,-0.0469 c -0.28236,0.0381 -0.56709,0.1263 -0.93877,-0.0902 -0.70771,-0.47639 -1.35312,-1.13418 -2.13306,-1.92631 -0.35738,-0.37891 -0.61639,-0.74248 -1.04099,-1.10772 -0.1016,-0.0874 -0.26199,-0.20657 -0.37064,-0.29335 l 0,4.4e-4 a 1.7570372,1.5813881 38.563515 0 0 -2.3411,0.15501 1.7570372,1.5813881 38.563515 0 0 0.37023,2.31639 c 0,3.2e-4 0.001,8.6e-4 0.002,0.001 a 1.7570372,1.5813881 38.563515 0 0 0.0163,0.0141 1.7570372,1.5813881 38.563515 0 0 0.0131,0.01 c 0.10575,0.0854 0.23888,0.19822 0.33769,0.27118 0.45058,0.33267 0.86259,0.50424 1.31153,0.76833 0.94583,0.5841 1.73078,1.06695 2.35271,1.65091 0.29378,0.31418 0.27106,0.61128 0.29673,0.89504 l 0.0454,-0.0148 -0.0258,0.0323 0.49758,0.44499 c -0.10015,0.15062 -0.19867,0.30255 -0.29503,0.45618 -2.58821,4.1258 -3.61033,9.02674 -2.92083,13.81975 l -0.66231,0.19113 0.009,0.0397 -0.034,-0.0325 c -0.14624,0.24452 -0.25455,0.52211 -0.65555,0.67772 -0.8137,0.2563 -1.73054,0.35084 -2.83613,0.46674 -0.51908,0.0432 -0.96461,0.0189 -1.51491,0.12313 -0.125,0.0237 -0.30319,0.0703 -0.43716,0.10138 a 1.5813881,1.7570372 77.134524 0 0 -0.001,2.1e-4 1.5813881,1.7570372 77.134524 0 0 -0.002,6.5e-4 c -0.006,0.001 -0.0142,0.004 -0.0201,0.005 l 0,4.3e-4 a 1.5813881,1.7570372 77.134524 0 0 -1.33834,1.92694 1.5813881,1.7570372 77.134524 0 0 2.04183,1.15481 l 0,2.2e-4 c 0.002,-4.3e-4 0.005,-0.001 0.007,-0.001 a 1.5813881,1.7570372 77.134524 0 0 0.0167,-0.003 1.5813881,1.7570372 77.134524 0 0 0.0133,-0.003 c 0.13297,-0.0295 0.30563,-0.0637 0.42493,-0.0957 0.54102,-0.14485 0.932,-0.36002 1.41839,-0.54636 1.04638,-0.3753 1.91325,-0.68798 2.75757,-0.81014 0.4288,-0.0338 0.64706,0.16923 0.88491,0.32608 l 0.0167,-0.0444 0.009,0.0399 0.69102,-0.11722 c 1.48124,4.58169 4.5304,8.52505 8.63978,11.10991 0.15635,0.0983 0.31414,0.19399 0.47265,0.28786 l -0.28025,0.67794 0.0365,0.0175 -0.0465,0.006 c 0.10001,0.26679 0.24935,0.52463 0.12101,0.93517 -0.30694,0.79598 -0.80469,1.5717 -1.40339,2.50836 -0.28989,0.43274 -0.58657,0.76603 -0.84816,1.26126 -0.0626,0.1185 -0.14316,0.30159 -0.20359,0.42682 l 0,2.2e-4 a 1.5813881,1.7570372 25.705524 0 0 0.67223,2.24796 1.5813881,1.7570372 25.705524 0 0 2.17573,-0.87646 l 0,0 a 1.5813881,1.7570372 25.705524 0 0 0.011,-0.0207 1.5813881,1.7570372 25.705524 0 0 0.006,-0.0139 c 0.0598,-0.12223 0.14038,-0.27762 0.18965,-0.3905 0.22406,-0.5133 0.29968,-0.95318 0.45724,-1.44964 0.35898,-1.05209 0.65505,-1.92476 1.08597,-2.66105 0.24093,-0.35632 0.53563,-0.40034 0.80655,-0.4885 l -0.0245,-0.041 0.0372,0.0179 0.35313,-0.63823 c 3.7664,1.43844 7.901,1.74996 11.88393,0.84563 0.92881,-0.21089 1.83569,-0.48616 2.71618,-0.81944 l 0.32482,0.58691 0.0365,-0.0177 -0.0241,0.0403 c 0.27093,0.0882 0.56581,0.13198 0.80677,0.48828 0.43094,0.73627 0.72692,1.60898 1.08596,2.66106 0.15759,0.49644 0.23336,0.93635 0.45745,1.44964 0.0509,0.11657 0.13584,0.2797 0.19599,0.40338 a 1.7570372,1.5813881 64.276524 0 0 4.3e-4,0.001 1.7570372,1.5813881 64.276524 0 0 0.001,0.003 c 0.003,0.005 0.006,0.0125 0.009,0.0177 l 4.3e-4,-2.1e-4 a 1.7570372,1.5813881 64.276524 0 0 2.17636,0.87625 1.7570372,1.5813881 64.276524 0 0 0.6716,-2.24775 c -3.2e-4,-7.6e-4 -7.5e-4,-0.002 -0.001,-0.003 a 1.7570372,1.5813881 64.276524 0 0 -0.008,-0.019 1.7570372,1.5813881 64.276524 0 0 -0.007,-0.0141 c -0.0582,-0.12287 -0.12948,-0.28243 -0.18691,-0.39113 -0.26162,-0.49522 -0.55825,-0.82854 -0.84816,-1.26126 -0.59875,-0.93663 -1.09661,-1.71219 -1.4036,-2.50815 -0.12837,-0.41053 0.021,-0.66838 0.12101,-0.93517 l -0.0471,-0.007 0.0372,-0.0177 -0.25407,-0.61416 c 2.7978,-1.64833 5.19025,-3.95266 6.94747,-6.7538 0.93056,-1.48338 1.65783,-3.06733 2.17699,-4.71007 l 0.6452,0.10919 0.009,-0.0393 0.0165,0.0437 c 0.23784,-0.15687 0.45611,-0.36007 0.88491,-0.3263 0.84433,0.12212 1.71117,0.43469 2.75757,0.80993 0.4864,0.18632 0.87757,0.40154 1.4186,0.54636 0.123,0.0329 0.30378,0.0683 0.43802,0.0984 a 1.7570372,1.5813881 12.847522 0 0 6.5e-4,2.2e-4 1.7570372,1.5813881 12.847522 0 0 0.002,4.3e-4 c 0.006,0.001 0.0148,0.003 0.0209,0.004 l 0,-4.3e-4 a 1.7570372,1.5813881 12.847522 0 0 2.04205,-1.15566 1.7570372,1.5813881 12.847522 0 0 -1.33856,-1.9261 l 0,-2.1e-4 c -0.001,-3.3e-4 -0.003,-7.6e-4 -0.005,-0.001 a 1.7570372,1.5813881 12.847522 0 0 -0.0182,-0.005 1.7570372,1.5813881 12.847522 0 0 -0.0142,-0.003 c -0.13255,-0.0311 -0.30248,-0.0751 -0.42366,-0.098 -0.55029,-0.10421 -0.99583,-0.0802 -1.5149,-0.12333 -1.1056,-0.11585 -2.02242,-0.21028 -2.83614,-0.46653 -0.401,-0.1556 -0.50951,-0.43321 -0.65576,-0.67773 l -0.0344,0.0327 0.009,-0.0399 -0.63971,-0.18458 c 0.33339,-2.4373 0.2306,-4.93876 -0.33116,-7.38443 -0.56693,-2.46819 -1.57919,-4.78593 -2.96306,-6.85201 l 0.54214,-0.48511 -0.0254,-0.0317 0.0448,0.0146 c 0.0256,-0.28376 0.003,-0.58085 0.29652,-0.89505 0.6219,-0.58398 1.40691,-1.06696 2.35271,-1.65112 0.44892,-0.26411 0.86096,-0.43584 1.31152,-0.76854 0.10781,-0.0796 0.25975,-0.20947 0.36853,-0.29609 l -4.3e-4,-4.3e-4 a 1.5813881,1.7570372 51.418518 0 0 0.3698,-2.31681 1.5813881,1.7570372 51.418518 0 0 -2.34067,-0.15438 l -2.2e-4,-2.2e-4 c -0.002,0.001 -0.004,0.003 -0.005,0.004 a 1.5813881,1.7570372 51.418518 0 0 -0.0133,0.01 1.5813881,1.7570372 51.418518 0 0 -0.0101,0.009 c -0.10704,0.0844 -0.24794,0.19008 -0.34171,0.27075 -0.42458,0.36526 -0.68363,0.729 -1.04098,1.10793 -0.7799,0.79217 -1.42517,1.44988 -2.13286,1.92631 -0.37167,0.2165 -0.65641,0.12829 -0.93876,0.0902 l 0.004,0.0473 -0.0256,-0.0321 -0.5981,0.42408 c -1.17349,-1.2354 -2.50418,-2.33114 -3.97046,-3.25345 -2.79201,-1.75621 -5.93598,-2.79809 -9.15911,-3.08176 l -0.0386,-0.68237 -0.041,0 0.0395,-0.026 c -0.20585,-0.19698 -0.45241,-0.3643 -0.5149,-0.78987 -0.0688,-0.85033 0.0431,-1.7652 0.17614,-2.86887 0.0734,-0.51565 0.19607,-0.94486 0.2169,-1.50455 0.005,-0.13393 -0.002,-0.3336 -0.002,-0.47265 l -4.3e-4,0 a 1.7570372,1.5813881 89.992522 0 0 -1.5808,-1.7337 z m -1.9808,12.26789 -0.46864,8.29215 -0.0351,0.0169 0,0.001 c -0.0315,0.74124 -0.64131,1.33328 -1.39029,1.33328 -0.3068,0 -0.59017,-0.0987 -0.82029,-0.26674 l -0.001,-6.5e-4 -0.0167,0.008 -6.79033,-4.81208 c 2.14503,-2.10939 4.84984,-3.60368 7.83449,-4.28134 0.55966,-0.12707 1.12309,-0.22308 1.68787,-0.2904 z m 3.96392,0.0104 c 2.36344,0.29447 4.65849,1.1027 6.71558,2.39664 0.99832,0.62796 1.91746,1.35671 2.74743,2.16897 l -6.74557,4.78293 -0.0239,-0.0114 0.001,-0.003 -0.004,0.003 c -0.59912,0.43759 -1.4422,0.32976 -1.9092,-0.25575 -0.19131,-0.23987 -0.29078,-0.52268 -0.30285,-0.8074 l 0,-0.001 -0.009,-0.004 -0.46907,-8.26807 z m -15.95367,7.66299 6.19921,5.54387 -0.008,0.0351 0.001,8.6e-4 c 0.55991,0.48676 0.64227,1.33271 0.17529,1.91829 -0.1913,0.23988 -0.44485,0.39986 -0.71975,0.47497 l -0.001,2.2e-4 -0.007,0.0289 -7.95973,2.29801 c -0.38458,-3.58212 0.42104,-7.20565 2.31976,-10.30019 z m 27.89759,0.001 c 0.94083,1.51982 1.63722,3.19567 2.04478,4.97005 0.40284,1.75377 0.50993,3.54284 0.33475,5.29824 l -7.98908,-2.30435 -0.007,-0.0304 0.003,-0.001 -0.004,-0.001 c -0.71566,-0.19559 -1.1569,-0.922 -0.99029,-1.65218 0.0683,-0.29912 0.22737,-0.55328 0.44245,-0.74023 l 6.5e-4,-8.7e-4 -0.004,-0.0177 6.16921,-5.52 z m -11.47465,0.60781 0.006,0.003 c 0.0123,0.28499 0.11201,0.56815 0.30349,0.80824 0.46787,0.5866 1.31226,0.6948 1.91279,0.25702 l 0.0226,0.011 -1.03,0.73031 -0.42493,-0.53073 -0.71721,0 -0.0727,-1.27879 z m -4.90204,0.0139 -0.0716,1.26401 -0.728,-2.2e-4 -0.43991,0.54932 -1.02029,-0.72313 0.0133,-0.006 c 0.23053,0.16811 0.51397,0.26695 0.82112,0.26695 0.75044,0 1.36158,-0.59285 1.39368,-1.33539 l 0.0317,-0.0152 z m 10.20451,4.90078 0.003,0.0141 c -0.21516,0.18735 -0.37433,0.44187 -0.44266,0.7413 -0.16691,0.73147 0.27498,1.45891 0.99156,1.65555 l 0.007,0.0291 -1.18101,-0.34066 0.15629,-0.68131 -0.46716,-0.58332 0.93305,-0.83486 z m -15.52324,0.0228 0.92461,0.82704 -0.45365,0.56621 0.1641,0.7168 -1.19368,0.34467 0.006,-0.0253 c 0.27515,-0.0754 0.52909,-0.23548 0.72059,-0.47562 0.46781,-0.58664 0.38525,-1.43399 -0.17508,-1.92208 l 0.007,-0.0317 z m 6.49381,0.42471 2.54089,4.4e-4 1.58418,1.97847 -0.56579,2.46676 -2.28957,1.09737 -2.28935,-1.09822 -0.56494,-2.46697 1.58458,-1.97763 0,-2.1e-4 z m 7.2799,6.56732 1.21014,0.20486 -0.0131,0.0163 c -0.28066,-0.0514 -0.57901,-0.0173 -0.85576,0.11595 -0.67606,0.32562 -0.96952,1.12491 -0.67625,1.80783 l -0.01,0.0122 -0.47117,-1.13855 0.65407,-0.31341 0.16177,-0.70518 z m -12.01277,0.0334 0.15353,0.66991 0.67689,0.32482 -0.48638,1.17635 -0.022,-0.0279 c 0.11254,-0.26213 0.14564,-0.56039 0.0773,-0.85978 -0.16699,-0.73155 -0.88085,-1.1955 -1.61184,-1.06168 l -0.0114,-0.0141 1.22387,-0.2076 z m 12.99736,0.16874 c 0.0717,0.002 0.14281,0.01 0.21288,0.0224 l 8.7e-4,2.2e-4 0.0156,-0.0192 8.2195,1.39135 c -0.39575,1.11054 -0.91289,2.18329 -1.54912,3.19749 -1.29522,2.0647 -3.02131,3.79124 -5.03594,5.07882 l -3.19158,-7.71242 0.0106,-0.0133 0.003,0.002 -0.002,-0.005 c -0.29328,-0.68147 -6.4e-4,-1.47924 0.67414,-1.80424 0.20732,-0.0998 0.42681,-0.14402 0.64182,-0.13812 z m -13.93887,0.0334 c 0.62751,0.009 1.19144,0.44302 1.33729,1.08195 0.0683,0.29912 0.0354,0.59717 -0.0773,0.85893 l 0,0.001 0.0241,0.0304 -3.15631,7.633 c -3.04627,-1.95514 -5.34809,-4.84823 -6.57344,-8.2159 l 8.15909,-1.38333 0.0137,0.0171 0.001,-2.2e-4 c 0.0912,-0.0168 0.18217,-0.0243 0.27182,-0.023 z m 7.5874,2.96476 0.61731,1.11553 -0.0291,-2.2e-4 c -0.13477,-0.25151 -0.34754,-0.46335 -0.62429,-0.59662 -0.67604,-0.32555 -1.48371,-0.0566 -1.83487,0.59831 l -0.004,0 0.61163,-1.1056 0.61943,0.29715 0.64394,-0.30856 z m -0.69125,0.38479 c 0.21862,-0.008 0.44235,0.0355 0.65323,0.13707 0.27643,0.13312 0.48883,0.3447 0.62323,0.59599 l 6.5e-4,8.7e-4 0.0334,2.1e-4 4.022,7.2668 c -0.52538,0.17723 -1.06144,0.32888 -1.6072,0.4528 -2.98163,0.67698 -6.06756,0.49925 -8.91854,-0.47266 l 4.00763,-7.24525 0.004,0 0.003,0 0,-0.003 0.001,0.002 c 0.24058,-0.44975 0.69686,-0.71712 1.17783,-0.73474 z"
324
+       id="path3847" />
325
+  </g>
326
+  <g
327
+     inkscape:groupmode="layer"
328
+     id="layer3"
329
+     inkscape:label="PLACE YOUR PICTOGRAM HERE"
330
+     style="display:inline">
331
+    <g
332
+       id="g4185" />
333
+  </g>
334
+  <style
335
+     id="style4217"
336
+     type="text/css">
337
+	.st0{fill:#419EDA;}
338
+</style>
339
+  <style
340
+     id="style4285"
341
+     type="text/css">
342
+	.st0{clip-path:url(#SVGID_2_);fill:#EFBF1B;}
343
+	.st1{clip-path:url(#SVGID_2_);fill:#40BEB0;}
344
+	.st2{clip-path:url(#SVGID_2_);fill:#0AA5DE;}
345
+	.st3{clip-path:url(#SVGID_2_);fill:#231F20;}
346
+	.st4{fill:#D7A229;}
347
+	.st5{fill:#009B8F;}
348
+</style>
349
+  <style
350
+     id="style4240"
351
+     type="text/css">
352
+	.st0{fill:#E8478B;}
353
+	.st1{fill:#40BEB0;}
354
+	.st2{fill:#37A595;}
355
+	.st3{fill:#231F20;}
356
+</style>
357
+  <style
358
+     id="style4812"
359
+     type="text/css">
360
+	.st0{fill:#0AA5DE;}
361
+	.st1{fill:#40BEB0;}
362
+	.st2{opacity:0.26;fill:#353535;}
363
+	.st3{fill:#231F20;}
364
+</style>
365
+</svg>
Back to file index

layer.yaml

 1
--- 
 2
+++ layer.yaml
 3
@@ -0,0 +1,36 @@
 4
+"options":
 5
+  "basic":
 6
+    "packages":
 7
+    - "socat"
 8
+    "use_venv": !!bool "false"
 9
+    "include_system_packages": !!bool "false"
10
+  "tls-client":
11
+    "ca_certificate_path": "/root/cdk/ca.crt"
12
+    "server_certificate_path": "/root/cdk/server.crt"
13
+    "server_key_path": "/root/cdk/server.key"
14
+    "client_certificate_path": "/root/cdk/client.crt"
15
+    "client_key_path": "/root/cdk/client.key"
16
+  "kubernetes-master": {}
17
+  "nagios": {}
18
+  "debug": {}
19
+  "leadership": {}
20
+  "snap": {}
21
+"includes":
22
+- "layer:basic"
23
+- "interface:tls-certificates"
24
+- "interface:nrpe-external-master"
25
+- "layer:snap"
26
+- "layer:tls-client"
27
+- "layer:leadership"
28
+- "layer:debug"
29
+- "layer:metrics"
30
+- "layer:nagios"
31
+- "interface:ceph-admin"
32
+- "interface:etcd"
33
+- "interface:http"
34
+- "interface:kubernetes-cni"
35
+- "interface:kube-dns"
36
+- "interface:kube-control"
37
+- "interface:public-address"
38
+"repo": "https://github.com/kubernetes/kubernetes.git"
39
+"is": "kubernetes-master"
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/kubernetes/common.py

 1
--- 
 2
+++ lib/charms/kubernetes/common.py
 3
@@ -0,0 +1,71 @@
 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 re
21
+import subprocess
22
+
23
+from time import sleep
24
+
25
+
26
+def get_version(bin_name):
27
+    """Get the version of an installed Kubernetes binary.
28
+
29
+    :param str bin_name: Name of binary
30
+    :return: 3-tuple version (maj, min, patch)
31
+
32
+    Example::
33
+
34
+        >>> `get_version('kubelet')
35
+        (1, 6, 0)
36
+
37
+    """
38
+    cmd = '{} --version'.format(bin_name).split()
39
+    version_string = subprocess.check_output(cmd).decode('utf-8')
40
+    return tuple(int(q) for q in re.findall("[0-9]+", version_string)[:3])
41
+
42
+
43
+def retry(times, delay_secs):
44
+    """ Decorator for retrying a method call.
45
+
46
+    Args:
47
+        times: How many times should we retry before giving up
48
+        delay_secs: Delay in secs
49
+
50
+    Returns: A callable that would return the last call outcome
51
+    """
52
+
53
+    def retry_decorator(func):
54
+        """ Decorator to wrap the function provided.
55
+
56
+        Args:
57
+            func: Provided function should return either True od False
58
+
59
+        Returns: A callable that would return the last call outcome
60
+
61
+        """
62
+        def _wrapped(*args, **kwargs):
63
+            res = func(*args, **kwargs)
64
+            attempt = 0
65
+            while not res and attempt < times:
66
+                sleep(delay_secs)
67
+                res = func(*args, **kwargs)
68
+                if res:
69
+                    break
70
+                attempt += 1
71
+            return res
72
+        return _wrapped
73
+
74
+    return retry_decorator
Back to file index

lib/charms/kubernetes/flagmanager.py

  1
--- 
  2
+++ lib/charms/kubernetes/flagmanager.py
  3
@@ -0,0 +1,149 @@
  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
+from charmhelpers.core import unitdata
 21
+
 22
+
 23
+class FlagManager:
 24
+    '''
 25
+    FlagManager - A Python class for managing the flags to pass to an
 26
+    application without remembering what's been set previously.
 27
+
 28
+    This is a blind class assuming the operator knows what they are doing.
 29
+    Each instance of this class should be initialized with the intended
 30
+    application to manage flags. Flags are then appended to a data-structure
 31
+    and cached in unitdata for later recall.
 32
+
 33
+    THe underlying data-provider is backed by a SQLITE database on each unit,
 34
+    tracking the dictionary, provided from the 'charmhelpers' python package.
 35
+    Summary:
 36
+    opts = FlagManager('docker')
 37
+    opts.add('bip', '192.168.22.2')
 38
+    opts.to_s()
 39
+    '''
 40
+
 41
+    def __init__(self, daemon, opts_path=None):
 42
+        self.db = unitdata.kv()
 43
+        self.daemon = daemon
 44
+        if not self.db.get(daemon):
 45
+            self.data = {}
 46
+        else:
 47
+            self.data = self.db.get(daemon)
 48
+
 49
+    def __save(self):
 50
+        self.db.set(self.daemon, self.data)
 51
+
 52
+    def add(self, key, value, strict=False):
 53
+        '''
 54
+        Adds data to the map of values for the DockerOpts file.
 55
+        Supports single values, or "multiopt variables". If you
 56
+        have a flag only option, like --tlsverify, set the value
 57
+        to None. To preserve the exact value, pass strict
 58
+        eg:
 59
+        opts.add('label', 'foo')
 60
+        opts.add('label', 'foo, bar, baz')
 61
+        opts.add('flagonly', None)
 62
+        opts.add('cluster-store', 'consul://a:4001,b:4001,c:4001/swarm',
 63
+                 strict=True)
 64
+        '''
 65
+        if strict:
 66
+            self.data['{}-strict'.format(key)] = value
 67
+            self.__save()
 68
+            return
 69
+
 70
+        if value:
 71
+            values = [x.strip() for x in value.split(',')]
 72
+            # handle updates
 73
+            if key in self.data and self.data[key] is not None:
 74
+                item_data = self.data[key]
 75
+                for c in values:
 76
+                    c = c.strip()
 77
+                    if c not in item_data:
 78
+                        item_data.append(c)
 79
+                self.data[key] = item_data
 80
+            else:
 81
+                # handle new
 82
+                self.data[key] = values
 83
+        else:
 84
+            # handle flagonly
 85
+            self.data[key] = None
 86
+        self.__save()
 87
+
 88
+    def remove(self, key, value):
 89
+        '''
 90
+        Remove a flag value from the DockerOpts manager
 91
+        Assuming the data is currently {'foo': ['bar', 'baz']}
 92
+        d.remove('foo', 'bar')
 93
+        > {'foo': ['baz']}
 94
+        :params key:
 95
+        :params value:
 96
+        '''
 97
+        self.data[key].remove(value)
 98
+        self.__save()
 99
+
100
+    def destroy(self, key, strict=False):
101
+        '''
102
+        Destructively remove all values and key from the FlagManager
103
+        Assuming the data is currently {'foo': ['bar', 'baz']}
104
+        d.wipe('foo')
105
+        >{}
106
+        :params key:
107
+        :params strict:
108
+        '''
109
+        try:
110
+            if strict:
111
+                self.data.pop('{}-strict'.format(key))
112
+            else:
113
+                self.data.pop(key)
114
+            self.__save()
115
+        except KeyError:
116
+            pass
117
+
118
+    def get(self, key, default=None):
119
+        """Return the value for ``key``, or the default if ``key`` doesn't exist.
120
+
121
+        """
122
+        return self.data.get(key, default)
123
+
124
+    def destroy_all(self):
125
+        '''
126
+        Destructively removes all data from the FlagManager.
127
+        '''
128
+        self.data.clear()
129
+        self.__save()
130
+
131
+    def to_s(self):
132
+        '''
133
+        Render the flags to a single string, prepared for the Docker
134
+        Defaults file. Typically in /etc/default/docker
135
+        d.to_s()
136
+        > "--foo=bar --foo=baz"
137
+        '''
138
+        flags = []
139
+        for key in self.data:
140
+            if self.data[key] is None:
141
+                # handle flagonly
142
+                flags.append("{}".format(key))
143
+            elif '-strict' in key:
144
+                # handle strict values, and do it in 2 steps.
145
+                # If we rstrip -strict it strips a tailing s
146
+                proper_key = key.rstrip('strict').rstrip('-')
147
+                flags.append("{}={}".format(proper_key, self.data[key]))
148
+            else:
149
+                # handle multiopt and typical flags
150
+                for item in self.data[key]:
151
+                    flags.append("{}={}".format(key, item))
152
+        return ' '.join(flags)
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/snap.py

  1
--- 
  2
+++ lib/charms/layer/snap.py
  3
@@ -0,0 +1,194 @@
  4
+# Copyright 2016-2017 Canonical Ltd.
  5
+#
  6
+# This file is part of the Snap layer for Juju.
  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 subprocess
 22
+
 23
+from charmhelpers.core import hookenv
 24
+from charms import layer
 25
+from charms import reactive
 26
+from charms.reactive.helpers import any_file_changed, data_changed
 27
+from time import sleep
 28
+
 29
+
 30
+def install(snapname, **kw):
 31
+    '''Install a snap.
 32
+
 33
+    Snap will be installed from the coresponding resource if available,
 34
+    otherwise from the Snap Store.
 35
+
 36
+    Sets the snap.installed.{snapname} state.
 37
+
 38
+    If the snap.installed.{snapname} state is already set then the refresh()
 39
+    function is called.
 40
+    '''
 41
+    installed_state = 'snap.installed.{}'.format(snapname)
 42
+    if reactive.is_state(installed_state):
 43
+        refresh(snapname, **kw)
 44
+    else:
 45
+        if hookenv.has_juju_version('2.0'):
 46
+            res_path = _resource_get(snapname)
 47
+            if res_path is False:
 48
+                _install_store(snapname, **kw)
 49
+            else:
 50
+                _install_local(res_path, **kw)
 51
+        else:
 52
+            _install_store(snapname, **kw)
 53
+        reactive.set_state(installed_state)
 54
+
 55
+
 56
+def refresh(snapname, **kw):
 57
+    '''Update a snap.
 58
+
 59
+    Snap will be pulled from the coresponding resource if available
 60
+    and reinstalled if it has changed. Otherwise a 'snap refresh' is
 61
+    run updating the snap from the Snap Store, potentially switching
 62
+    channel and changing confinement options.
 63
+    '''
 64
+    # Note that once you upload a resource, you can't remove it.
 65
+    # This means we don't need to cope with an operator switching
 66
+    # from a resource provided to a store provided snap, because there
 67
+    # is no way for them to do that. Well, actually the operator could
 68
+    # upload a zero byte resource, but then we would need to uninstall
 69
+    # the snap before reinstalling from the store and that has the
 70
+    # potential for data loss.
 71
+    if hookenv.has_juju_version('2.0'):
 72
+        res_path = _resource_get(snapname)
 73
+        if res_path is False:
 74
+            _refresh_store(snapname, **kw)
 75
+        else:
 76
+            _install_local(res_path, **kw)
 77
+    else:
 78
+        _refresh_store(snapname, **kw)
 79
+
 80
+
 81
+def remove(snapname):
 82
+    hookenv.log('Removing snap {}'.format(snapname))
 83
+    subprocess.check_call(['snap', 'remove', snapname],
 84
+                          universal_newlines=True)
 85
+    reactive.remove_state('snap.installed.{}'.format(snapname))
 86
+
 87
+
 88
+def connect(plug, slot):
 89
+    '''Connect or reconnect a snap plug with a slot.
 90
+
 91
+    Each argument must be a two element tuple, corresponding to
 92
+    the two arguments to the 'snap connect' command.
 93
+    '''
 94
+    hookenv.log('Connecting {} to {}'.format(plug, slot), hookenv.DEBUG)
 95
+    subprocess.check_call(['snap', 'connect', plug, slot],
 96
+                          universal_newlines=True)
 97
+
 98
+
 99
+def connect_all():
100
+    '''Connect or reconnect all interface connections defined in layer.yaml.
101
+
102
+    This method will fail if called before all referenced snaps have been
103
+    installed.
104
+    '''
105
+    opts = layer.options('snap')
106
+    for snapname, snap_opts in opts.items():
107
+        for plug, slot in snap_opts.get('connect', []):
108
+            connect(plug, slot)
109
+
110
+
111
+def _snap_args(channel='stable', devmode=False, jailmode=False,
112
+               dangerous=False, force_dangerous=False, connect=None,
113
+               classic=False, revision=None):
114
+    if channel != 'stable':
115
+        yield '--channel={}'.format(channel)
116
+    if devmode is True:
117
+        yield '--devmode'
118
+    if jailmode is True:
119
+        yield '--jailmode'
120
+    if force_dangerous is True or dangerous is True:
121
+        yield '--dangerous'
122
+    if classic is True:
123
+        yield '--classic'
124
+    if revision is not None:
125
+        yield '--revision={}'.format(revision)
126
+
127
+
128
+def _install_local(path, **kw):
129
+    key = 'snap.local.{}'.format(path)
130
+    if (data_changed(key, kw) or any_file_changed([path])):
131
+        cmd = ['snap', 'install']
132
+        cmd.extend(_snap_args(**kw))
133
+        cmd.append('--dangerous')
134
+        cmd.append(path)
135
+        hookenv.log('Installing {} from local resource'.format(path))
136
+        subprocess.check_call(cmd, universal_newlines=True)
137
+
138
+
139
+def _install_store(snapname, **kw):
140
+    cmd = ['snap', 'install']
141
+    cmd.extend(_snap_args(**kw))
142
+    cmd.append(snapname)
143
+    hookenv.log('Installing {} from store'.format(snapname))
144
+    # Attempting the snap install 3 times to resolve unexpected EOF.
145
+    # This is a work around to lp:1677557. Stop doing this once it
146
+    # is resolved everywhere.
147
+    for attempt in range(3):
148
+        try:
149
+            out = subprocess.check_output(cmd, universal_newlines=True,
150
+                                          stderr=subprocess.STDOUT)
151
+            print(out)
152
+            break
153
+        except subprocess.CalledProcessError as x:
154
+            print(x.output)
155
+            # Per https://bugs.launchpad.net/bugs/1622782, we don't
156
+            # get a useful error code out of 'snap install', much like
157
+            # 'snap refresh' below. Remove this when we can rely on
158
+            # snap installs everywhere returning 0 for 'already insatlled'
159
+            if "already installed" in x.output:
160
+                break
161
+            if attempt == 2:
162
+                raise
163
+            sleep(5)
164
+
165
+
166
+def _refresh_store(snapname, **kw):
167
+    if not data_changed('snap.opts.{}'.format(snapname), kw):
168
+        return
169
+
170
+    cmd = ['snap', 'refresh']
171
+    cmd.extend(_snap_args(**kw))
172
+    cmd.append(snapname)
173
+    hookenv.log('Refreshing {} from store'.format(snapname))
174
+    # Per https://bugs.launchpad.net/layer-snap/+bug/1588322 we don't get
175
+    # a useful error code out of 'snap refresh'. We are forced to parse
176
+    # the output to see if it is a non-fatal error.
177
+    # subprocess.check_call(cmd, universal_newlines=True)
178
+    try:
179
+        out = subprocess.check_output(cmd, universal_newlines=True,
180
+                                      stderr=subprocess.STDOUT)
181
+        print(out)
182
+    except subprocess.CalledProcessError as x:
183
+        print(x.output)
184
+        if "has no updates available" not in x.output:
185
+            raise
186
+
187
+
188
+def _resource_get(snapname):
189
+    '''Used to fetch the resource path of the given name.
190
+
191
+    This wrapper obtains a resource path and adds an additional
192
+    check to return False if the resource is zero length.
193
+    '''
194
+    res_path = hookenv.resource_get(snapname)
195
+    if res_path and os.stat(res_path).st_size != 0:
196
+        return res_path
197
+    return False
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

lib/debug_script.py

 1
--- 
 2
+++ lib/debug_script.py
 3
@@ -0,0 +1,8 @@
 4
+import os
 5
+
 6
+dir = os.environ["DEBUG_SCRIPT_DIR"]
 7
+
 8
+
 9
+def open_file(path, *args, **kwargs):
10
+    """ Open a file within the debug script dir """
11
+    return open(os.path.join(dir, path), *args, **kwargs)
Back to file index

metadata.yaml

 1
--- 
 2
+++ metadata.yaml
 3
@@ -0,0 +1,62 @@
 4
+"name": "kubernetes-master"
 5
+"summary": "The Kubernetes control plane."
 6
+"maintainers":
 7
+- "Matthew Bruzek <matthew.bruzek@canonical.com>"
 8
+- "Charles Butler <charles.butler@canonical.com>"
 9
+"description": |
10
+  Kubernetes is an open-source platform for deploying, scaling, and operations
11
+  of application containers across a cluster of hosts. Kubernetes is portable
12
+  in that it works with public, private, and hybrid clouds. Extensible through
13
+  a pluggable infrastructure. Self healing in that it will automatically
14
+  restart and place containers on healthy nodes if a node ever goes away.
15
+"tags":
16
+- "misc"
17
+- "infrastructure"
18
+- "kubernetes"
19
+- "master"
20
+"series":
21
+- "xenial"
22
+"requires":
23
+  "certificates":
24
+    "interface": "tls-certificates"
25
+  "etcd":
26
+    "interface": "etcd"
27
+  "loadbalancer":
28
+    "interface": "public-address"
29
+  "ceph-storage":
30
+    "interface": "ceph-admin"
31
+"provides":
32
+  "nrpe-external-master":
33
+    "interface": "nrpe-external-master"
34
+    "scope": "container"
35
+  "kube-api-endpoint":
36
+    "interface": "http"
37
+  "cluster-dns":
38
+    "interface": "kube-dns"
39
+  "kube-control":
40
+    "interface": "kube-control"
41
+  "cni":
42
+    "interface": "kubernetes-cni"
43
+    "scope": "container"
44
+"resources":
45
+  "kubectl":
46
+    "type": "file"
47
+    "filename": "kubectl.snap"
48
+    "description": "kubectl snap"
49
+  "kube-apiserver":
50
+    "type": "file"
51
+    "filename": "kube-apiserver.snap"
52
+    "description": "kube-apiserver snap"
53
+  "kube-controller-manager":
54
+    "type": "file"
55
+    "filename": "kube-controller-manager.snap"
56
+    "description": "kube-controller-manager snap"
57
+  "kube-scheduler":
58
+    "type": "file"
59
+    "filename": "kube-scheduler.snap"
60
+    "description": "kube-scheduler snap"
61
+  "cdk-addons":
62
+    "type": "file"
63
+    "filename": "cdk-addons.snap"
64
+    "description": "CDK addons snap"
65
+"subordinate": !!bool "false"
Back to file index

metrics.yaml

 1
--- 
 2
+++ metrics.yaml
 3
@@ -0,0 +1,34 @@
 4
+metrics:
 5
+  juju-units: {}
 6
+  pods:
 7
+    type: gauge
 8
+    description: number of pods
 9
+    command: /snap/bin/kubectl get po --all-namespaces | tail -n+2 | wc -l
10
+  services:
11
+    type: gauge
12
+    description: number of services
13
+    command: /snap/bin/kubectl get svc --all-namespaces | tail -n+2 | wc -l
14
+  replicasets:
15
+    type: gauge
16
+    description: number of replicasets
17
+    command: /snap/bin/kubectl get rs --all-namespaces | tail -n+2 | wc -l
18
+  replicationcontrollers:
19
+    type: gauge
20
+    description: number of replicationcontrollers
21
+    command: /snap/bin/kubectl get rc --all-namespaces | tail -n+2 | wc -l
22
+  nodes:
23
+    type: gauge
24
+    description: number of kubernetes nodes
25
+    command: /snap/bin/kubectl get nodes | tail -n+2 | wc -l
26
+  persistentvolume:
27
+    type: gauge
28
+    description: number of pv
29
+    command: /snap/bin/kubectl get pv --all-namespaces | tail -n+2 | wc -l
30
+  persistentvolumeclaims:
31
+    type: gauge
32
+    description: number of claims
33
+    command: /snap/bin/kubectl get pvc --all-namespaces | tail -n+2 | wc -l
34
+  serviceaccounts:
35
+    type: gauge
36
+    description: number of sa
37
+    command: /snap/bin/kubectl get sa --all-namespaces | tail -n+2 | wc -l
Back to file index

reactive/kubernetes_master.py

  1
--- 
  2
+++ reactive/kubernetes_master.py
  3
@@ -0,0 +1,877 @@
  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 base64
 21
+import os
 22
+import re
 23
+import random
 24
+import shutil
 25
+import socket
 26
+import string
 27
+import json
 28
+
 29
+import charms.leadership
 30
+
 31
+from shlex import split
 32
+from subprocess import check_call
 33
+from subprocess import check_output
 34
+from subprocess import CalledProcessError
 35
+
 36
+from charms import layer
 37
+from charms.layer import snap
 38
+from charms.reactive import hook
 39
+from charms.reactive import remove_state
 40
+from charms.reactive import set_state
 41
+from charms.reactive import is_state
 42
+from charms.reactive import when, when_any, when_not
 43
+from charms.reactive.helpers import data_changed
 44
+from charms.kubernetes.common import get_version
 45
+from charms.kubernetes.common import retry
 46
+from charms.kubernetes.flagmanager import FlagManager
 47
+
 48
+from charmhelpers.core import hookenv
 49
+from charmhelpers.core import host
 50
+from charmhelpers.core import unitdata
 51
+from charmhelpers.core.templating import render
 52
+from charmhelpers.fetch import apt_install
 53
+from charmhelpers.contrib.charmsupport import nrpe
 54
+
 55
+
 56
+# Override the default nagios shortname regex to allow periods, which we
 57
+# need because our bin names contain them (e.g. 'snap.foo.daemon'). The
 58
+# default regex in charmhelpers doesn't allow periods, but nagios itself does.
 59
+nrpe.Check.shortname_re = '[\.A-Za-z0-9-_]+$'
 60
+
 61
+os.environ['PATH'] += os.pathsep + os.path.join(os.sep, 'snap', 'bin')
 62
+
 63
+
 64
+def service_cidr():
 65
+    ''' Return the charm's service-cidr config '''
 66
+    db = unitdata.kv()
 67
+    frozen_cidr = db.get('kubernetes-master.service-cidr')
 68
+    return frozen_cidr or hookenv.config('service-cidr')
 69
+
 70
+
 71
+def freeze_service_cidr():
 72
+    ''' Freeze the service CIDR. Once the apiserver has started, we can no
 73
+    longer safely change this value. '''
 74
+    db = unitdata.kv()
 75
+    db.set('kubernetes-master.service-cidr', service_cidr())
 76
+
 77
+
 78
+@hook('upgrade-charm')
 79
+def reset_states_for_delivery():
 80
+    '''An upgrade charm event was triggered by Juju, react to that here.'''
 81
+    migrate_from_pre_snaps()
 82
+    install_snaps()
 83
+    remove_state('authentication.setup')
 84
+    remove_state('kubernetes-master.components.started')
 85
+
 86
+
 87
+def rename_file_idempotent(source, destination):
 88
+    if os.path.isfile(source):
 89
+        os.rename(source, destination)
 90
+
 91
+
 92
+def migrate_from_pre_snaps():
 93
+    # remove old states
 94
+    remove_state('kubernetes.components.installed')
 95
+    remove_state('kubernetes.dashboard.available')
 96
+    remove_state('kube-dns.available')
 97
+    remove_state('kubernetes-master.app_version.set')
 98
+
 99
+    # disable old services
100
+    services = ['kube-apiserver',
101
+                'kube-controller-manager',
102
+                'kube-scheduler']
103
+    for service in services:
104
+        hookenv.log('Stopping {0} service.'.format(service))
105
+        host.service_stop(service)
106
+
107
+    # rename auth files
108
+    os.makedirs('/root/cdk', exist_ok=True)
109
+    rename_file_idempotent('/etc/kubernetes/serviceaccount.key',
110
+                           '/root/cdk/serviceaccount.key')
111
+    rename_file_idempotent('/srv/kubernetes/basic_auth.csv',
112
+                           '/root/cdk/basic_auth.csv')
113
+    rename_file_idempotent('/srv/kubernetes/known_tokens.csv',
114
+                           '/root/cdk/known_tokens.csv')
115
+
116
+    # cleanup old files
117
+    files = [
118
+        "/lib/systemd/system/kube-apiserver.service",
119
+        "/lib/systemd/system/kube-controller-manager.service",
120
+        "/lib/systemd/system/kube-scheduler.service",
121
+        "/etc/default/kube-defaults",
122
+        "/etc/default/kube-apiserver.defaults",
123
+        "/etc/default/kube-controller-manager.defaults",
124
+        "/etc/default/kube-scheduler.defaults",
125
+        "/srv/kubernetes",
126
+        "/home/ubuntu/kubectl",
127
+        "/usr/local/bin/kubectl",
128
+        "/usr/local/bin/kube-apiserver",
129
+        "/usr/local/bin/kube-controller-manager",
130
+        "/usr/local/bin/kube-scheduler",
131
+        "/etc/kubernetes"
132
+    ]
133
+    for file in files:
134
+        if os.path.isdir(file):
135
+            hookenv.log("Removing directory: " + file)
136
+            shutil.rmtree(file)
137
+        elif os.path.isfile(file):
138
+            hookenv.log("Removing file: " + file)
139
+            os.remove(file)
140
+
141
+    # clear the flag managers
142
+    FlagManager('kube-apiserver').destroy_all()
143
+    FlagManager('kube-controller-manager').destroy_all()
144
+    FlagManager('kube-scheduler').destroy_all()
145
+
146
+
147
+def install_snaps():
148
+    channel = hookenv.config('channel')
149
+    hookenv.status_set('maintenance', 'Installing kubectl snap')
150
+    snap.install('kubectl', channel=channel, classic=True)
151
+    hookenv.status_set('maintenance', 'Installing kube-apiserver snap')
152
+    snap.install('kube-apiserver', channel=channel)
153
+    hookenv.status_set('maintenance',
154
+                       'Installing kube-controller-manager snap')
155
+    snap.install('kube-controller-manager', channel=channel)
156
+    hookenv.status_set('maintenance', 'Installing kube-scheduler snap')
157
+    snap.install('kube-scheduler', channel=channel)
158
+    hookenv.status_set('maintenance', 'Installing cdk-addons snap')
159
+    snap.install('cdk-addons', channel=channel)
160
+    set_state('kubernetes-master.snaps.installed')
161
+
162
+
163
+@when('config.changed.channel')
164
+def channel_changed():
165
+    install_snaps()
166
+
167
+
168
+@when('cni.connected')
169
+@when_not('cni.configured')
170
+def configure_cni(cni):
171
+    ''' Set master configuration on the CNI relation. This lets the CNI
172
+    subordinate know that we're the master so it can respond accordingly. '''
173
+    cni.set_config(is_master=True, kubeconfig_path='')
174
+
175
+
176
+@when('leadership.is_leader')
177
+@when_not('authentication.setup')
178
+def setup_leader_authentication():
179
+    '''Setup basic authentication and token access for the cluster.'''
180
+    api_opts = FlagManager('kube-apiserver')
181
+    controller_opts = FlagManager('kube-controller-manager')
182
+
183
+    service_key = '/root/cdk/serviceaccount.key'
184
+    basic_auth = '/root/cdk/basic_auth.csv'
185
+    known_tokens = '/root/cdk/known_tokens.csv'
186
+
187
+    api_opts.add('basic-auth-file', basic_auth)
188
+    api_opts.add('token-auth-file', known_tokens)
189
+    hookenv.status_set('maintenance', 'Rendering authentication templates.')
190
+
191
+    keys = [service_key, basic_auth, known_tokens]
192
+    # Try first to fetch data from an old leadership broadcast.
193
+    if not get_keys_from_leader(keys):
194
+        if not os.path.isfile(basic_auth):
195
+            setup_basic_auth('admin', 'admin', 'admin')
196
+        if not os.path.isfile(known_tokens):
197
+            setup_tokens(None, 'admin', 'admin')
198
+            setup_tokens(None, 'kubelet', 'kubelet')
199
+            setup_tokens(None, 'kube_proxy', 'kube_proxy')
200
+        # Generate the default service account token key
201
+        os.makedirs('/root/cdk', exist_ok=True)
202
+        if not os.path.isfile(service_key):
203
+            cmd = ['openssl', 'genrsa', '-out', service_key,
204
+                   '2048']
205
+            check_call(cmd)
206
+
207
+    api_opts.add('service-account-key-file', service_key)
208
+    controller_opts.add('service-account-private-key-file', service_key)
209
+
210
+    # read service account key for syndication
211
+    leader_data = {}
212
+    for f in [known_tokens, basic_auth, service_key]:
213
+        with open(f, 'r') as fp:
214
+            leader_data[f] = fp.read()
215
+
216
+    # this is slightly opaque, but we are sending file contents under its file
217
+    # path as a key.
218
+    # eg:
219
+    # {'/root/cdk/serviceaccount.key': 'RSA:2471731...'}
220
+    charms.leadership.leader_set(leader_data)
221
+
222
+    set_state('authentication.setup')
223
+
224
+
225
+@when_not('leadership.is_leader')
226
+@when_not('authentication.setup')
227
+def setup_non_leader_authentication():
228
+    api_opts = FlagManager('kube-apiserver')
229
+    controller_opts = FlagManager('kube-controller-manager')
230
+
231
+    service_key = '/root/cdk/serviceaccount.key'
232
+    basic_auth = '/root/cdk/basic_auth.csv'
233
+    known_tokens = '/root/cdk/known_tokens.csv'
234
+
235
+    hookenv.status_set('maintenance', 'Rendering authentication templates.')
236
+
237
+    keys = [service_key, basic_auth, known_tokens]
238
+    if not get_keys_from_leader(keys):
239
+        # the keys were not retrieved. Non-leaders have to retry.
240
+        return
241
+
242
+    api_opts.add('basic-auth-file', basic_auth)
243
+    api_opts.add('token-auth-file', known_tokens)
244
+    api_opts.add('service-account-key-file', service_key)
245
+    controller_opts.add('service-account-private-key-file', service_key)
246
+
247
+    set_state('authentication.setup')
248
+
249
+
250
+def get_keys_from_leader(keys):
251
+    """
252
+    Gets the broadcasted keys from the leader and stores them in
253
+    the corresponding files.
254
+
255
+    Args:
256
+        keys: list of keys. Keys are actually files on the FS.
257
+
258
+    Returns: True if all key were fetched, False if not.
259
+
260
+    """
261
+    # This races with other codepaths, and seems to require being created first
262
+    # This block may be extracted later, but for now seems to work as intended
263
+    os.makedirs('/root/cdk', exist_ok=True)
264
+
265
+    for k in keys:
266
+        # If the path does not exist, assume we need it
267
+        if not os.path.exists(k):
268
+            # Fetch data from leadership broadcast
269
+            contents = charms.leadership.leader_get(k)
270
+            # Default to logging the warning and wait for leader data to be set
271
+            if contents is None:
272
+                msg = "Waiting on leaders crypto keys."
273
+                hookenv.status_set('waiting', msg)
274
+                hookenv.log('Missing content for file {}'.format(k))
275
+                return False
276
+            # Write out the file and move on to the next item
277
+            with open(k, 'w+') as fp:
278
+                fp.write(contents)
279
+
280
+    return True
281
+
282
+
283
+@when('kubernetes-master.snaps.installed')
284
+def set_app_version():
285
+    ''' Declare the application version to juju '''
286
+    version = check_output(['kube-apiserver', '--version'])
287
+    hookenv.application_version_set(version.split(b' v')[-1].rstrip())
288
+
289
+
290
+@when('cdk-addons.configured')
291
+def idle_status():
292
+    ''' Signal at the end of the run that we are running. '''
293
+    if not all_kube_system_pods_running():
294
+        hookenv.status_set('waiting', 'Waiting for kube-system pods to start')
295
+    elif hookenv.config('service-cidr') != service_cidr():
296
+        msg = 'WARN: cannot change service-cidr, still using ' + service_cidr()
297
+        hookenv.status_set('active', msg)
298
+    else:
299
+        hookenv.status_set('active', 'Kubernetes master running.')
300
+
301
+
302
+@when('etcd.available', 'tls_client.server.certificate.saved',
303
+      'authentication.setup')
304
+@when_not('kubernetes-master.components.started')
305
+def start_master(etcd):
306
+    '''Run the Kubernetes master components.'''
307
+    hookenv.status_set('maintenance',
308
+                       'Configuring the Kubernetes master services.')
309
+    freeze_service_cidr()
310
+    handle_etcd_relation(etcd)
311
+    configure_master_services()
312
+    hookenv.status_set('maintenance',
313
+                       'Starting the Kubernetes master services.')
314
+
315
+    services = ['kube-apiserver',
316
+                'kube-controller-manager',
317
+                'kube-scheduler']
318
+    for service in services:
319
+        host.service_restart('snap.%s.daemon' % service)
320
+
321
+    hookenv.open_port(6443)
322
+    set_state('kubernetes-master.components.started')
323
+
324
+
325
+@when('etcd.available')
326
+def etcd_data_change(etcd):
327
+    ''' Etcd scale events block master reconfiguration due to the
328
+        kubernetes-master.components.started state. We need a way to
329
+        handle these events consistenly only when the number of etcd
330
+        units has actually changed '''
331
+
332
+    # key off of the connection string
333
+    connection_string = etcd.get_connection_string()
334
+
335
+    # If the connection string changes, remove the started state to trigger
336
+    # handling of the master components
337
+    if data_changed('etcd-connect', connection_string):
338
+        remove_state('kubernetes-master.components.started')
339
+
340
+
341
+@when('kube-control.connected')
342
+@when('cdk-addons.configured')
343
+def send_cluster_dns_detail(kube_control):
344
+    ''' Send cluster DNS info '''
345
+    # Note that the DNS server doesn't necessarily exist at this point. We know
346
+    # where we're going to put it, though, so let's send the info anyway.
347
+    dns_ip = get_dns_ip()
348
+    kube_control.set_dns(53, hookenv.config('dns_domain'), dns_ip)
349
+
350
+
351
+@when_not('kube-control.connected')
352
+def missing_kube_control():
353
+    """Inform the operator they need to add the kube-control relation.
354
+
355
+    If deploying via bundle this won't happen, but if operator is upgrading a
356
+    a charm in a deployment that pre-dates the kube-control relation, it'll be
357
+    missing.
358
+
359
+    """
360
+    hookenv.status_set(
361
+        'blocked',
362
+        'Relate {}:kube-control kubernetes-worker:kube-control'.format(
363
+            hookenv.service_name()))
364
+
365
+
366
+@when('kube-api-endpoint.available')
367
+def push_service_data(kube_api):
368
+    ''' Send configuration to the load balancer, and close access to the
369
+    public interface '''
370
+    kube_api.configure(port=6443)
371
+
372
+
373
+@when('certificates.available')
374
+def send_data(tls):
375
+    '''Send the data that is required to create a server certificate for
376
+    this server.'''
377
+    # Use the public ip of this unit as the Common Name for the certificate.
378
+    common_name = hookenv.unit_public_ip()
379
+
380
+    # Get the SDN gateway based on the cidr address.
381
+    kubernetes_service_ip = get_kubernetes_service_ip()
382
+
383
+    domain = hookenv.config('dns_domain')
384
+    # Create SANs that the tls layer will add to the server cert.
385
+    sans = [
386
+        hookenv.unit_public_ip(),
387
+        hookenv.unit_private_ip(),
388
+        socket.gethostname(),
389
+        kubernetes_service_ip,
390
+        'kubernetes',
391
+        'kubernetes.{0}'.format(domain),
392
+        'kubernetes.default',
393
+        'kubernetes.default.svc',
394
+        'kubernetes.default.svc.{0}'.format(domain)
395
+    ]
396
+    # Create a path safe name by removing path characters from the unit name.
397
+    certificate_name = hookenv.local_unit().replace('/', '_')
398
+    # Request a server cert with this information.
399
+    tls.request_server_cert(common_name, sans, certificate_name)
400
+
401
+
402
+@when('kube-api.connected')
403
+def push_api_data(kube_api):
404
+    ''' Send configuration to remote consumer.'''
405
+    # Since all relations already have the private ip address, only
406
+    # send the port on the relation object to all consumers.
407
+    # The kubernetes api-server uses 6443 for the default secure port.
408
+    kube_api.set_api_port('6443')
409
+
410
+
411
+@when('kubernetes-master.components.started')
412
+def configure_cdk_addons():
413
+    ''' Configure CDK addons '''
414
+    remove_state('cdk-addons.configured')
415
+    dbEnabled = str(hookenv.config('enable-dashboard-addons')).lower()
416
+    args = [
417
+        'arch=' + arch(),
418
+        'dns-ip=' + get_dns_ip(),
419
+        'dns-domain=' + hookenv.config('dns_domain'),
420
+        'enable-dashboard=' + dbEnabled
421
+    ]
422
+    check_call(['snap', 'set', 'cdk-addons'] + args)
423
+    if not addons_ready():
424
+        hookenv.status_set('waiting', 'Waiting to retry addon deployment')
425
+        remove_state('cdk-addons.configured')
426
+        return
427
+
428
+    set_state('cdk-addons.configured')
429
+
430
+
431
+@retry(times=3, delay_secs=20)
432
+def addons_ready():
433
+    """
434
+    Test if the add ons got installed
435
+
436
+    Returns: True is the addons got applied
437
+
438
+    """
439
+    try:
440
+        check_call(['cdk-addons.apply'])
441
+        return True
442
+    except CalledProcessError:
443
+        hookenv.log("Addons are not ready yet.")
444
+        return False
445
+
446
+
447
+@when('loadbalancer.available', 'certificates.ca.available',
448
+      'certificates.client.cert.available')
449
+def loadbalancer_kubeconfig(loadbalancer, ca, client):
450
+    # Get the potential list of loadbalancers from the relation object.
451
+    hosts = loadbalancer.get_addresses_ports()
452
+    # Get the public address of loadbalancers so users can access the cluster.
453
+    address = hosts[0].get('public-address')
454
+    # Get the port of the loadbalancer so users can access the cluster.
455
+    port = hosts[0].get('port')
456
+    server = 'https://{0}:{1}'.format(address, port)
457
+    build_kubeconfig(server)
458
+
459
+
460
+@when('certificates.ca.available', 'certificates.client.cert.available')
461
+@when_not('loadbalancer.available')
462
+def create_self_config(ca, client):
463
+    '''Create a kubernetes configuration for the master unit.'''
464
+    server = 'https://{0}:{1}'.format(hookenv.unit_get('public-address'), 6443)
465
+    build_kubeconfig(server)
466
+
467
+
468
+@when('ceph-storage.available')
469
+def ceph_state_control(ceph_admin):
470
+    ''' Determine if we should remove the state that controls the re-render
471
+    and execution of the ceph-relation-changed event because there
472
+    are changes in the relationship data, and we should re-render any
473
+    configs, keys, and/or service pre-reqs '''
474
+
475
+    ceph_relation_data = {
476
+        'mon_hosts': ceph_admin.mon_hosts(),
477
+        'fsid': ceph_admin.fsid(),
478
+        'auth_supported': ceph_admin.auth(),
479
+        'hostname': socket.gethostname(),
480
+        'key': ceph_admin.key()
481
+    }
482
+
483
+    # Re-execute the rendering if the data has changed.
484
+    if data_changed('ceph-config', ceph_relation_data):
485
+        remove_state('ceph-storage.configured')
486
+
487
+
488
+@when('ceph-storage.available')
489
+@when_not('ceph-storage.configured')
490
+def ceph_storage(ceph_admin):
491
+    '''Ceph on kubernetes will require a few things - namely a ceph
492
+    configuration, and the ceph secret key file used for authentication.
493
+    This method will install the client package, and render the requisit files
494
+    in order to consume the ceph-storage relation.'''
495
+    ceph_context = {
496
+        'mon_hosts': ceph_admin.mon_hosts(),
497
+        'fsid': ceph_admin.fsid(),
498
+        'auth_supported': ceph_admin.auth(),
499
+        'use_syslog': "true",
500
+        'ceph_public_network': '',
501
+        'ceph_cluster_network': '',
502
+        'loglevel': 1,
503
+        'hostname': socket.gethostname(),
504
+    }
505
+    # Install the ceph common utilities.
506
+    apt_install(['ceph-common'], fatal=True)
507
+
508
+    etc_ceph_directory = '/etc/ceph'
509
+    if not os.path.isdir(etc_ceph_directory):
510
+        os.makedirs(etc_ceph_directory)
511
+    charm_ceph_conf = os.path.join(etc_ceph_directory, 'ceph.conf')
512
+    # Render the ceph configuration from the ceph conf template
513
+    render('ceph.conf', charm_ceph_conf, ceph_context)
514
+
515
+    # The key can rotate independently of other ceph config, so validate it
516
+    admin_key = os.path.join(etc_ceph_directory,
517
+                             'ceph.client.admin.keyring')
518
+    try:
519
+        with open(admin_key, 'w') as key_file:
520
+            key_file.write("[client.admin]\n\tkey = {}\n".format(
521
+                ceph_admin.key()))
522
+    except IOError as err:
523
+        hookenv.log("IOError writing admin.keyring: {}".format(err))
524
+
525
+    # Enlist the ceph-admin key as a kubernetes secret
526
+    if ceph_admin.key():
527
+        encoded_key = base64.b64encode(ceph_admin.key().encode('utf-8'))
528
+    else:
529
+        # We didn't have a key, and cannot proceed. Do not set state and
530
+        # allow this method to re-execute
531
+        return
532
+
533
+    context = {'secret': encoded_key.decode('ascii')}
534
+    render('ceph-secret.yaml', '/tmp/ceph-secret.yaml', context)
535
+    try:
536
+        # At first glance this is deceptive. The apply stanza will create if
537
+        # it doesn't exist, otherwise it will update the entry, ensuring our
538
+        # ceph-secret is always reflective of what we have in /etc/ceph
539
+        # assuming we have invoked this anytime that file would change.
540
+        cmd = ['kubectl', 'apply', '-f', '/tmp/ceph-secret.yaml']
541
+        check_call(cmd)
542
+        os.remove('/tmp/ceph-secret.yaml')
543
+    except:
544
+        # the enlistment in kubernetes failed, return and prepare for re-exec
545
+        return
546
+
547
+    # when complete, set a state relating to configuration of the storage
548
+    # backend that will allow other modules to hook into this and verify we
549
+    # have performed the necessary pre-req steps to interface with a ceph
550
+    # deployment.
551
+    set_state('ceph-storage.configured')
552
+
553
+
554
+@when('nrpe-external-master.available')
555
+@when_not('nrpe-external-master.initial-config')
556
+def initial_nrpe_config(nagios=None):
557
+    set_state('nrpe-external-master.initial-config')
558
+    update_nrpe_config(nagios)
559
+
560
+
561
+@when('kubernetes-master.components.started')
562
+@when('nrpe-external-master.available')
563
+@when_any('config.changed.nagios_context',
564
+          'config.changed.nagios_servicegroups')
565
+def update_nrpe_config(unused=None):
566
+    services = (
567
+        'snap.kube-apiserver.daemon',
568
+        'snap.kube-controller-manager.daemon',
569
+        'snap.kube-scheduler.daemon'
570
+    )
571
+    hostname = nrpe.get_nagios_hostname()
572
+    current_unit = nrpe.get_nagios_unit_name()
573
+    nrpe_setup = nrpe.NRPE(hostname=hostname)
574
+    nrpe.add_init_service_checks(nrpe_setup, services, current_unit)
575
+    nrpe_setup.write()
576
+
577
+
578
+@when_not('nrpe-external-master.available')
579
+@when('nrpe-external-master.initial-config')
580
+def remove_nrpe_config(nagios=None):
581
+    remove_state('nrpe-external-master.initial-config')
582
+
583
+    # List of systemd services for which the checks will be removed
584
+    services = (
585
+        'snap.kube-apiserver.daemon',
586
+        'snap.kube-controller-manager.daemon',
587
+        'snap.kube-scheduler.daemon'
588
+    )
589
+
590
+    # The current nrpe-external-master interface doesn't handle a lot of logic,
591
+    # use the charm-helpers code for now.
592
+    hostname = nrpe.get_nagios_hostname()
593
+    nrpe_setup = nrpe.NRPE(hostname=hostname)
594
+
595
+    for service in services:
596
+        nrpe_setup.remove_check(shortname=service)
597
+
598
+
599
+def is_privileged():
600
+    """Return boolean indicating whether or not to set allow-privileged=true.
601
+
602
+    """
603
+    privileged = hookenv.config('allow-privileged')
604
+    if privileged == 'auto':
605
+        return is_state('kubernetes-master.gpu.enabled')
606
+    else:
607
+        return privileged == 'true'
608
+
609
+
610
+@when('config.changed.allow-privileged')
611
+@when('kubernetes-master.components.started')
612
+def on_config_allow_privileged_change():
613
+    """React to changed 'allow-privileged' config value.
614
+
615
+    """
616
+    remove_state('kubernetes-master.components.started')
617
+    remove_state('config.changed.allow-privileged')
618
+
619
+
620
+@when('kube-control.gpu.available')
621
+@when('kubernetes-master.components.started')
622
+@when_not('kubernetes-master.gpu.enabled')
623
+def on_gpu_available(kube_control):
624
+    """The remote side (kubernetes-worker) is gpu-enabled.
625
+
626
+    We need to run in privileged mode.
627
+
628
+    """
629
+    config = hookenv.config()
630
+    if config['allow-privileged'] == "false":
631
+        hookenv.status_set(
632
+            'active',
633
+            'GPUs available. Set allow-privileged="auto" to enable.'
634
+        )
635
+        return
636
+
637
+    remove_state('kubernetes-master.components.started')
638
+    set_state('kubernetes-master.gpu.enabled')
639
+
640
+
641
+@when('kubernetes-master.gpu.enabled')
642
+@when_not('kubernetes-master.privileged')
643
+def disable_gpu_mode():
644
+    """We were in gpu mode, but the operator has set allow-privileged="false",
645
+    so we can't run in gpu mode anymore.
646
+
647
+    """
648
+    remove_state('kubernetes-master.gpu.enabled')
649
+
650
+
651
+def arch():
652
+    '''Return the package architecture as a string. Raise an exception if the
653
+    architecture is not supported by kubernetes.'''
654
+    # Get the package architecture for this system.
655
+    architecture = check_output(['dpkg', '--print-architecture']).rstrip()
656
+    # Convert the binary result into a string.
657
+    architecture = architecture.decode('utf-8')
658
+    return architecture
659
+
660
+
661
+def build_kubeconfig(server):
662
+    '''Gather the relevant data for Kubernetes configuration objects and create
663
+    a config object with that information.'''
664
+    # Get the options from the tls-client layer.
665
+    layer_options = layer.options('tls-client')
666
+    # Get all the paths to the tls information required for kubeconfig.
667
+    ca = layer_options.get('ca_certificate_path')
668
+    ca_exists = ca and os.path.isfile(ca)
669
+    key = layer_options.get('client_key_path')
670
+    key_exists = key and os.path.isfile(key)
671
+    cert = layer_options.get('client_certificate_path')
672
+    cert_exists = cert and os.path.isfile(cert)
673
+    # Do we have everything we need?
674
+    if ca_exists and key_exists and cert_exists:
675
+        # Cache last server string to know if we need to regenerate the config.
676
+        if not data_changed('kubeconfig.server', server):
677
+            return
678
+        # Create an absolute path for the kubeconfig file.
679
+        kubeconfig_path = os.path.join(os.sep, 'home', 'ubuntu', 'config')
680
+        # Create the kubeconfig on this system so users can access the cluster.
681
+        create_kubeconfig(kubeconfig_path, server, ca, key, cert)
682
+        # Make the config file readable by the ubuntu users so juju scp works.
683
+        cmd = ['chown', 'ubuntu:ubuntu', kubeconfig_path]
684
+        check_call(cmd)
685
+
686
+
687
+def create_kubeconfig(kubeconfig, server, ca, key, certificate, user='ubuntu',
688
+                      context='juju-context', cluster='juju-cluster'):
689
+    '''Create a configuration for Kubernetes based on path using the supplied
690
+    arguments for values of the Kubernetes server, CA, key, certificate, user
691
+    context and cluster.'''
692
+    # Create the config file with the address of the master server.
693
+    cmd = 'kubectl config --kubeconfig={0} set-cluster {1} ' \
694
+          '--server={2} --certificate-authority={3} --embed-certs=true'
695
+    check_call(split(cmd.format(kubeconfig, cluster, server, ca)))
696
+    # Create the credentials using the client flags.
697
+    cmd = 'kubectl config --kubeconfig={0} set-credentials {1} ' \
698
+          '--client-key={2} --client-certificate={3} --embed-certs=true'
699
+    check_call(split(cmd.format(kubeconfig, user, key, certificate)))
700
+    # Create a default context with the cluster.
701
+    cmd = 'kubectl config --kubeconfig={0} set-context {1} ' \
702
+          '--cluster={2} --user={3}'
703
+    check_call(split(cmd.format(kubeconfig, context, cluster, user)))
704
+    # Make the config use this new context.
705
+    cmd = 'kubectl config --kubeconfig={0} use-context {1}'
706
+    check_call(split(cmd.format(kubeconfig, context)))
707
+
708
+
709
+def get_dns_ip():
710
+    '''Get an IP address for the DNS server on the provided cidr.'''
711
+    # Remove the range from the cidr.
712
+    ip = service_cidr().split('/')[0]
713
+    # Take the last octet off the IP address and replace it with 10.
714
+    return '.'.join(ip.split('.')[0:-1]) + '.10'
715
+
716
+
717
+def get_kubernetes_service_ip():
718
+    '''Get the IP address for the kubernetes service based on the cidr.'''
719
+    # Remove the range from the cidr.
720
+    ip = service_cidr().split('/')[0]
721
+    # Remove the last octet and replace it with 1.
722
+    return '.'.join(ip.split('.')[0:-1]) + '.1'
723
+
724
+
725
+def handle_etcd_relation(reldata):
726
+    ''' Save the client credentials and set appropriate daemon flags when
727
+    etcd declares itself as available'''
728
+    connection_string = reldata.get_connection_string()
729
+    # Define where the etcd tls files will be kept.
730
+    etcd_dir = '/root/cdk/etcd'
731
+    # Create paths to the etcd client ca, key, and cert file locations.
732
+    ca = os.path.join(etcd_dir, 'client-ca.pem')
733
+    key = os.path.join(etcd_dir, 'client-key.pem')
734
+    cert = os.path.join(etcd_dir, 'client-cert.pem')
735
+
736
+    # Save the client credentials (in relation data) to the paths provided.
737
+    reldata.save_client_credentials(key, cert, ca)
738
+
739
+    api_opts = FlagManager('kube-apiserver')
740
+
741
+    # Never use stale data, always prefer whats coming in during context
742
+    # building. if its stale, its because whats in unitdata is stale
743
+    data = api_opts.data
744
+    if data.get('etcd-servers-strict') or data.get('etcd-servers'):
745
+        api_opts.destroy('etcd-cafile')
746
+        api_opts.destroy('etcd-keyfile')
747
+        api_opts.destroy('etcd-certfile')
748
+        api_opts.destroy('etcd-servers', strict=True)
749
+        api_opts.destroy('etcd-servers')
750
+
751
+    # Set the apiserver flags in the options manager
752
+    api_opts.add('etcd-cafile', ca)
753
+    api_opts.add('etcd-keyfile', key)
754
+    api_opts.add('etcd-certfile', cert)
755
+    api_opts.add('etcd-servers', connection_string, strict=True)
756
+
757
+
758
+def configure_master_services():
759
+    ''' Add remaining flags for the master services and configure snaps to use
760
+    them '''
761
+
762
+    api_opts = FlagManager('kube-apiserver')
763
+    controller_opts = FlagManager('kube-controller-manager')
764
+    scheduler_opts = FlagManager('kube-scheduler')
765
+    scheduler_opts.add('v', '2')
766
+
767
+    # Get the tls paths from the layer data.
768
+    layer_options = layer.options('tls-client')
769
+    ca_cert_path = layer_options.get('ca_certificate_path')
770
+    client_cert_path = layer_options.get('client_certificate_path')
771
+    client_key_path = layer_options.get('client_key_path')
772
+    server_cert_path = layer_options.get('server_certificate_path')
773
+    server_key_path = layer_options.get('server_key_path')
774
+
775
+    if is_privileged():
776
+        api_opts.add('allow-privileged', 'true', strict=True)
777
+        set_state('kubernetes-master.privileged')
778
+    else:
779
+        api_opts.add('allow-privileged', 'false', strict=True)
780
+        remove_state('kubernetes-master.privileged')
781
+
782
+    # Handle static options for now
783
+    api_opts.add('service-cluster-ip-range', service_cidr())
784
+    api_opts.add('min-request-timeout', '300')
785
+    api_opts.add('v', '4')
786
+    api_opts.add('client-ca-file', ca_cert_path)
787
+    api_opts.add('tls-cert-file', server_cert_path)
788
+    api_opts.add('tls-private-key-file', server_key_path)
789
+    api_opts.add('kubelet-certificate-authority', ca_cert_path)
790
+    api_opts.add('kubelet-client-certificate', client_cert_path)
791
+    api_opts.add('kubelet-client-key', client_key_path)
792
+    api_opts.add('logtostderr', 'true')
793
+    api_opts.add('insecure-bind-address', '127.0.0.1')
794
+    api_opts.add('insecure-port', '8080')
795
+    api_opts.add('storage-backend', 'etcd2')  # FIXME: add etcd3 support
796
+    admission_control = [
797
+        'Initializers',
798
+        'NamespaceLifecycle',
799
+        'LimitRanger',
800
+        'ServiceAccount',
801
+        'ResourceQuota',
802
+        'DefaultTolerationSeconds'
803
+    ]
804
+
805
+    if get_version('kube-apiserver') < (1, 6):
806
+        hookenv.log('Removing DefaultTolerationSeconds from admission-control')
807
+        admission_control.remove('DefaultTolerationSeconds')
808
+    api_opts.add('admission-control', ','.join(admission_control), strict=True)
809
+
810
+    # Default to 3 minute resync. TODO: Make this configureable?
811
+    controller_opts.add('min-resync-period', '3m')
812
+    controller_opts.add('v', '2')
813
+    controller_opts.add('root-ca-file', ca_cert_path)
814
+    controller_opts.add('logtostderr', 'true')
815
+    controller_opts.add('master', 'http://127.0.0.1:8080')
816
+
817
+    scheduler_opts.add('v', '2')
818
+    scheduler_opts.add('logtostderr', 'true')
819
+    scheduler_opts.add('master', 'http://127.0.0.1:8080')
820
+
821
+    cmd = ['snap', 'set', 'kube-apiserver'] + api_opts.to_s().split(' ')
822
+    check_call(cmd)
823
+    cmd = (
824
+        ['snap', 'set', 'kube-controller-manager'] +
825
+        controller_opts.to_s().split(' ')
826
+    )
827
+    check_call(cmd)
828
+    cmd = ['snap', 'set', 'kube-scheduler'] + scheduler_opts.to_s().split(' ')
829
+    check_call(cmd)
830
+
831
+
832
+def setup_basic_auth(username='admin', password='admin', user='admin'):
833
+    '''Create the htacces file and the tokens.'''
834
+    root_cdk = '/root/cdk'
835
+    if not os.path.isdir(root_cdk):
836
+        os.makedirs(root_cdk)
837
+    htaccess = os.path.join(root_cdk, 'basic_auth.csv')
838
+    with open(htaccess, 'w') as stream:
839
+        stream.write('{0},{1},{2}'.format(username, password, user))
840
+
841
+
842
+def setup_tokens(token, username, user):
843
+    '''Create a token file for kubernetes authentication.'''
844
+    root_cdk = '/root/cdk'
845
+    if not os.path.isdir(root_cdk):
846
+        os.makedirs(root_cdk)
847
+    known_tokens = os.path.join(root_cdk, 'known_tokens.csv')
848
+    if not token:
849
+        alpha = string.ascii_letters + string.digits
850
+        token = ''.join(random.SystemRandom().choice(alpha) for _ in range(32))
851
+    with open(known_tokens, 'a') as stream:
852
+        stream.write('{0},{1},{2}\n'.format(token, username, user))
853
+
854
+
855
+@retry(times=3, delay_secs=10)
856
+def all_kube_system_pods_running():
857
+    ''' Check pod status in the kube-system namespace. Returns True if all
858
+    pods are running, False otherwise. '''
859
+    cmd = ['kubectl', 'get', 'po', '-n', 'kube-system', '-o', 'json']
860
+
861
+    try:
862
+        output = check_output(cmd).decode('utf-8')
863
+    except CalledProcessError:
864
+        hookenv.log('failed to get kube-system pod status')
865
+        return False
866
+
867
+    result = json.loads(output)
868
+
869
+    for pod in result['items']:
870
+        status = pod['status']['phase']
871
+        if status != 'Running':
872
+            return False
873
+
874
+    return True
875
+
876
+
877
+def apiserverVersion():
878
+    cmd = 'kube-apiserver --version'.split()
879
+    version_string = check_output(cmd).decode('utf-8')
880
+    return tuple(int(q) for q in re.findall("[0-9]+", version_string)[:3])
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

reactive/snap.py

  1
--- 
  2
+++ reactive/snap.py
  3
@@ -0,0 +1,171 @@
  4
+# Copyright 2016 Canonical Ltd.
  5
+#
  6
+# This file is part of the Snap layer for Juju.
  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
+charms.reactive helpers for dealing with Snap packages.
 21
+'''
 22
+import os.path
 23
+import shutil
 24
+import subprocess
 25
+from textwrap import dedent
 26
+import time
 27
+
 28
+from charmhelpers.core import hookenv, host
 29
+from charms import layer
 30
+from charms import reactive
 31
+from charms.layer import snap
 32
+from charms.reactive import hook
 33
+from charms.reactive.helpers import data_changed
 34
+
 35
+
 36
+def install():
 37
+    opts = layer.options('snap')
 38
+    for snapname, snap_opts in opts.items():
 39
+        installed_state = 'snap.installed.{}'.format(snapname)
 40
+        if not reactive.is_state(installed_state):
 41
+            snap.install(snapname, **snap_opts)
 42
+    if data_changed('snap.install.opts', opts):
 43
+        snap.connect_all()
 44
+
 45
+
 46
+def refresh():
 47
+    opts = layer.options('snap')
 48
+    for snapname, snap_opts in opts.items():
 49
+        snap.refresh(snapname, **snap_opts)
 50
+    snap.connect_all()
 51
+
 52