~jamesbeedy/shout-irc-0

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

CPP?: No
OIL?: No

This current https://jujucharms.com/u/marcoceppi/shout-irc/xenial/0 is broken in many ways ...

I fixed what was broken so the layer now builds and deploys. It didn't come with a README when I pulled down the current source from the charmstore here https://jujucharms.com/u/marcoceppi/shout-irc/xenial/0, but it would be nice to add one WIP

https://jujucharms.com/u/jamesbeedy/shout-irc/0
https://github.com/jamesbeedy/layer-shout


Tests

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

Voted: +0
jamesbeedy wrote 1 months ago
This needs to be deleted in favor of lounge-irc charm.

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.
  • 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.
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.
Should be built using charm layers.
Should use Juju Resources to deliver required payloads.

Testing and Quality

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

Metadata

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

Security

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

All changes | Changes since last revision

Source Diff

Inline diff comments 0

No comments yet.

Back to file index

Makefile

1
--- 
2
+++ Makefile
3
@@ -0,0 +1,3 @@
4
+.PHONY: test
5
+test:
6
+	@tox
Back to file index

README.md

 1
--- 
 2
+++ README.md
 3
@@ -0,0 +1,52 @@
 4
+# layer-node
 5
+> Juju charms.reactive layer for NodeJS
 6
+
 7
+# emitters
 8
+
 9
+**nodejs.available** - This state is automatically emitted once Node.js has been
10
+installed. Rely on this state to perform an application deployment when Node.js
11
+is ready to be used.
12
+
13
+# api
14
+
15
+All helper modules are found in `lib/nodejs.py`
16
+
17
+Example,
18
+
19
+```python
20
+
21
+from nodejs import npm, node_dist_dir
22
+
23
+print(node_dist_dir())
24
+# /srv/app
25
+
26
+@when('nodejs.available')
27
+def install_deps():
28
+    npm('install')
29
+    npm('test')
30
+
31
+```
32
+
33
+# license
34
+
35
+The MIT License (MIT)
36
+
37
+Copyright (c) 2015 Adam Stokes <adam.stokes@ubuntu.com>
38
+
39
+Permission is hereby granted, free of charge, to any person obtaining a copy
40
+of this software and associated documentation files (the "Software"), to deal
41
+in the Software without restriction, including without limitation the rights
42
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
43
+copies of the Software, and to permit persons to whom the Software is
44
+furnished to do so, subject to the following conditions:
45
+
46
+The above copyright notice and this permission notice shall be included in
47
+all copies or substantial portions of the Software.
48
+
49
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
50
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
51
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
52
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
53
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
54
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
55
+THE SOFTWARE.
Back to file index

actions.yaml

 1
--- 
 2
+++ actions.yaml
 3
@@ -0,0 +1,19 @@
 4
+"add-user":
 5
+  "description": "add new user to shout"
 6
+  "params":
 7
+    "name":
 8
+      "description": "username"
 9
+      "type": "string"
10
+    "password":
11
+      "description": "password"
12
+      "type": "string"
13
+  "required": ["name", "password"]
14
+"remove-user":
15
+  "description": "remove user from shout"
16
+  "params":
17
+    "name":
18
+      "description": "username"
19
+      "type": "string"
20
+  "required": ["name"]
21
+"list-users":
22
+  "description": "list all users in shout"
Back to file index

actions/add-user

 1
--- 
 2
+++ actions/add-user
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/python3
 5
+
 6
+import sys
 7
+sys.path.append('lib')
 8
+
 9
+from charms.layer.shout import Shout
10
+from charms.layer.nodejs import node_dist_path
11
+
12
+from charmhelpers.core.hookenv import (
13
+    action-get,
14
+    action-fail,
15
+)
16
+
17
+shout = Shout(node_dist_path())
18
+
19
+try:
20
+    shout.add(action_get('name'), action_get('password'))
21
+except Exception as e:
22
+    action_fail("%s" % e)
Back to file index

actions/list-users

 1
--- 
 2
+++ actions/list-users
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/python3
 5
+
 6
+import sys
 7
+sys.path.append('lib')
 8
+
 9
+from charms.layer.shout import Shout
10
+from charms.layer.nodejs import node_dist_path
11
+
12
+from charmhelpers.core.hookenv import (
13
+    action_get,
14
+    action_fail,
15
+)
16
+
17
+shout = Shout(node_dist_path())
18
+
19
+try:
20
+    action_set({'users': shout.list()})
21
+except Exception as e:
22
+    action_fail("%s" % e)
Back to file index

actions/remove-user

 1
--- 
 2
+++ actions/remove-user
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/python3
 5
+
 6
+import sys
 7
+sys.path.append('lib')
 8
+
 9
+from charms.layer.shout import Shout
10
+from charms.layer.nodejs import node_dist_path
11
+
12
+from charmhelpers.core.hookenv import (
13
+    action-get,
14
+    action-fail,
15
+)
16
+
17
+shout = Shout(node_dist_path())
18
+
19
+try:
20
+    shout.remove(action_get('name'))
21
+except Exception as e:
22
+    action_fail("%s" % e)
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,43 @@
 4
+"options":
 5
+  "extra_packages":
 6
+    "description": "Space separated list of extra deb packages to install.\n"
 7
+    "type": "string"
 8
+    "default": ""
 9
+  "package_status":
10
+    "default": "install"
11
+    "type": "string"
12
+    "description": "The status of service-affecting packages will be set to this value\
13
+      \ in the dpkg database. Valid values are \"install\" and \"hold\".\n"
14
+  "install_sources":
15
+    "description": "List of extra apt sources, per charm-helpers standard format (a\
16
+      \ yaml list of strings encoded as a string). Each source may be either a line\
17
+      \ that can be added directly to sources.list(5), or in the form ppa:<user>/<ppa-name>\
18
+      \ for adding Personal Package Archives, or a distribution component to enable.\n"
19
+    "type": "string"
20
+    "default": |
21
+      - https://deb.nodesource.com/node_4.x main
22
+  "install_keys":
23
+    "description": "List of signing keys for install_sources package sources, per\
24
+      \ charmhelpers standard format (a yaml list of strings encoded as a string).\
25
+      \ The keys should be the full ASCII armoured GPG public keys. While GPG key\
26
+      \ ids are also supported and looked up on a keyserver, operators should be aware\
27
+      \ that this mechanism is insecure. null can be used if a standard package signing\
28
+      \ key is used that will already be installed on the machine, and for PPA sources\
29
+      \ where the package signing key is securely retrieved from Launchpad.\n"
30
+    "type": "string"
31
+    "default": |
32
+      - '68576280'
33
+  "app-dir":
34
+    "type": "string"
35
+    "default": "/srv/app"
36
+    "description": |
37
+      Directory where shout will live
38
+  "public":
39
+    "description": "require login or make a public accessible irc bouncer"
40
+    "default": !!bool "false"
41
+    "type": "boolean"
42
+  "port":
43
+    "default": !!int "80"
44
+    "description": |
45
+      Port to run shout on
46
+    "type": "int"
Back to file index

copyright

 1
--- 
 2
+++ copyright
 3
@@ -0,0 +1,21 @@
 4
+The MIT License (MIT)
 5
+
 6
+Copyright (c) 2015 Adam Stokes <adam.stokes@ubuntu.com>
 7
+
 8
+Permission is hereby granted, free of charge, to any person obtaining a copy
 9
+of this software and associated documentation files (the "Software"), to deal
10
+in the Software without restriction, including without limitation the rights
11
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+copies of the Software, and to permit persons to whom the Software is
13
+furnished to do so, subject to the following conditions:
14
+
15
+The above copyright notice and this permission notice shall be included in
16
+all copies or substantial portions of the Software.
17
+
18
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
+THE SOFTWARE.
Back to file index

hooks/config-changed

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

hooks/hook.template

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

hooks/install

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

hooks/leader-elected

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

hooks/leader-settings-changed

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

hooks/relations/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/start

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

hooks/stop

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

hooks/update-status

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

hooks/upgrade-charm

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

hooks/website-relation-broken

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

hooks/website-relation-changed

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

hooks/website-relation-departed

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

hooks/website-relation-joined

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

icon.svg

  1
--- 
  2
+++ icon.svg
  3
@@ -0,0 +1,279 @@
  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.48+devel r12274"
 21
+   sodipodi:docname="Juju_charm_icon_template.svg">
 22
+  <defs
 23
+     id="defs6519">
 24
+    <linearGradient
 25
+       inkscape:collect="always"
 26
+       xlink:href="#Background"
 27
+       id="linearGradient6461"
 28
+       gradientUnits="userSpaceOnUse"
 29
+       x1="0"
 30
+       y1="970.29498"
 31
+       x2="144"
 32
+       y2="970.29498"
 33
+       gradientTransform="matrix(0,-0.66666669,0.6660448,0,-866.25992,731.29077)" />
 34
+    <linearGradient
 35
+       id="Background">
 36
+      <stop
 37
+         id="stop4178"
 38
+         offset="0"
 39
+         style="stop-color:#b8b8b8;stop-opacity:1" />
 40
+      <stop
 41
+         id="stop4180"
 42
+         offset="1"
 43
+         style="stop-color:#c9c9c9;stop-opacity:1" />
 44
+    </linearGradient>
 45
+    <filter
 46
+       style="color-interpolation-filters:sRGB;"
 47
+       inkscape:label="Inner Shadow"
 48
+       id="filter1121">
 49
+      <feFlood
 50
+         flood-opacity="0.59999999999999998"
 51
+         flood-color="rgb(0,0,0)"
 52
+         result="flood"
 53
+         id="feFlood1123" />
 54
+      <feComposite
 55
+         in="flood"
 56
+         in2="SourceGraphic"
 57
+         operator="out"
 58
+         result="composite1"
 59
+         id="feComposite1125" />
 60
+      <feGaussianBlur
 61
+         in="composite1"
 62
+         stdDeviation="1"
 63
+         result="blur"
 64
+         id="feGaussianBlur1127" />
 65
+      <feOffset
 66
+         dx="0"
 67
+         dy="2"
 68
+         result="offset"
 69
+         id="feOffset1129" />
 70
+      <feComposite
 71
+         in="offset"
 72
+         in2="SourceGraphic"
 73
+         operator="atop"
 74
+         result="composite2"
 75
+         id="feComposite1131" />
 76
+    </filter>
 77
+    <filter
 78
+       style="color-interpolation-filters:sRGB;"
 79
+       inkscape:label="Drop Shadow"
 80
+       id="filter950">
 81
+      <feFlood
 82
+         flood-opacity="0.25"
 83
+         flood-color="rgb(0,0,0)"
 84
+         result="flood"
 85
+         id="feFlood952" />
 86
+      <feComposite
 87
+         in="flood"
 88
+         in2="SourceGraphic"
 89
+         operator="in"
 90
+         result="composite1"
 91
+         id="feComposite954" />
 92
+      <feGaussianBlur
 93
+         in="composite1"
 94
+         stdDeviation="1"
 95
+         result="blur"
 96
+         id="feGaussianBlur956" />
 97
+      <feOffset
 98
+         dx="0"
 99
+         dy="1"
100
+         result="offset"
101
+         id="feOffset958" />
102
+      <feComposite
103
+         in="SourceGraphic"
104
+         in2="offset"
105
+         operator="over"
106
+         result="composite2"
107
+         id="feComposite960" />
108
+    </filter>
109
+    <clipPath
110
+       clipPathUnits="userSpaceOnUse"
111
+       id="clipPath873">
112
+      <g
113
+         transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"
114
+         id="g875"
115
+         inkscape:label="Layer 1"
116
+         style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">
117
+        <path
118
+           style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"
119
+           d="m 46.702703,898.22775 50.594594,0 C 138.16216,898.22775 144,904.06497 144,944.92583 l 0,50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 l -50.594594,0 C 5.8378378,1042.3622 0,1036.525 0,995.66429 L 0,944.92583 C 0,904.06497 5.8378378,898.22775 46.702703,898.22775 Z"
120
+           id="path877"
121
+           inkscape:connector-curvature="0"
122
+           sodipodi:nodetypes="sssssssss" />
123
+      </g>
124
+    </clipPath>
125
+    <filter
126
+       inkscape:collect="always"
127
+       id="filter891"
128
+       inkscape:label="Badge Shadow">
129
+      <feGaussianBlur
130
+         inkscape:collect="always"
131
+         stdDeviation="0.71999962"
132
+         id="feGaussianBlur893" />
133
+    </filter>
134
+  </defs>
135
+  <sodipodi:namedview
136
+     id="base"
137
+     pagecolor="#ffffff"
138
+     bordercolor="#666666"
139
+     borderopacity="1.0"
140
+     inkscape:pageopacity="0.0"
141
+     inkscape:pageshadow="2"
142
+     inkscape:zoom="4.0745362"
143
+     inkscape:cx="18.514671"
144
+     inkscape:cy="49.018169"
145
+     inkscape:document-units="px"
146
+     inkscape:current-layer="layer1"
147
+     showgrid="true"
148
+     fit-margin-top="0"
149
+     fit-margin-left="0"
150
+     fit-margin-right="0"
151
+     fit-margin-bottom="0"
152
+     inkscape:window-width="1920"
153
+     inkscape:window-height="1029"
154
+     inkscape:window-x="0"
155
+     inkscape:window-y="24"
156
+     inkscape:window-maximized="1"
157
+     showborder="true"
158
+     showguides="true"
159
+     inkscape:guide-bbox="true"
160
+     inkscape:showpageshadow="false">
161
+    <inkscape:grid
162
+       type="xygrid"
163
+       id="grid821" />
164
+    <sodipodi:guide
165
+       orientation="1,0"
166
+       position="16,48"
167
+       id="guide823" />
168
+    <sodipodi:guide
169
+       orientation="0,1"
170
+       position="64,80"
171
+       id="guide825" />
172
+    <sodipodi:guide
173
+       orientation="1,0"
174
+       position="80,40"
175
+       id="guide827" />
176
+    <sodipodi:guide
177
+       orientation="0,1"
178
+       position="64,16"
179
+       id="guide829" />
180
+  </sodipodi:namedview>
181
+  <metadata
182
+     id="metadata6522">
183
+    <rdf:RDF>
184
+      <cc:Work
185
+         rdf:about="">
186
+        <dc:format>image/svg+xml</dc:format>
187
+        <dc:type
188
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
189
+        <dc:title></dc:title>
190
+      </cc:Work>
191
+    </rdf:RDF>
192
+  </metadata>
193
+  <g
194
+     inkscape:label="BACKGROUND"
195
+     inkscape:groupmode="layer"
196
+     id="layer1"
197
+     transform="translate(268,-635.29076)"
198
+     style="display:inline">
199
+    <path
200
+       style="fill:url(#linearGradient6461);fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"
201
+       d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 C -264.11215,731.29077 -268,727.39888 -268,700.15563 Z"
202
+       id="path6455"
203
+       inkscape:connector-curvature="0"
204
+       sodipodi:nodetypes="sssssssss" />
205
+  </g>
206
+  <g
207
+     inkscape:groupmode="layer"
208
+     id="layer3"
209
+     inkscape:label="PLACE YOUR PICTOGRAM HERE"
210
+     style="display:inline" />
211
+  <g
212
+     inkscape:groupmode="layer"
213
+     id="layer2"
214
+     inkscape:label="BADGE"
215
+     style="display:none"
216
+     sodipodi:insensitive="true">
217
+    <g
218
+       style="display:inline"
219
+       transform="translate(-340.00001,-581)"
220
+       id="g4394"
221
+       clip-path="none">
222
+      <g
223
+         id="g855">
224
+        <g
225
+           inkscape:groupmode="maskhelper"
226
+           id="g870"
227
+           clip-path="url(#clipPath873)"
228
+           style="opacity:0.6;filter:url(#filter891)">
229
+          <path
230
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"
231
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"
232
+             sodipodi:ry="12"
233
+             sodipodi:rx="12"
234
+             sodipodi:cy="552.36218"
235
+             sodipodi:cx="252"
236
+             id="path844"
237
+             style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
238
+             sodipodi:type="arc" />
239
+        </g>
240
+        <g
241
+           id="g862">
242
+          <path
243
+             sodipodi:type="arc"
244
+             style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
245
+             id="path4398"
246
+             sodipodi:cx="252"
247
+             sodipodi:cy="552.36218"
248
+             sodipodi:rx="12"
249
+             sodipodi:ry="12"
250
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"
251
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />
252
+          <path
253
+             transform="matrix(1.25,0,0,1.25,33,-100.45273)"
254
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"
255
+             sodipodi:ry="12"
256
+             sodipodi:rx="12"
257
+             sodipodi:cy="552.36218"
258
+             sodipodi:cx="252"
259
+             id="path4400"
260
+             style="color:#000000;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
261
+             sodipodi:type="arc" />
262
+          <path
263
+             sodipodi:type="star"
264
+             style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
265
+             id="path4459"
266
+             sodipodi:sides="5"
267
+             sodipodi:cx="666.19574"
268
+             sodipodi:cy="589.50385"
269
+             sodipodi:r1="7.2431178"
270
+             sodipodi:r2="4.3458705"
271
+             sodipodi:arg1="1.0471976"
272
+             sodipodi:arg2="1.6755161"
273
+             inkscape:flatsided="false"
274
+             inkscape:rounded="0.1"
275
+             inkscape:randomized="0"
276
+             d="m 669.8173,595.77657 c -0.39132,0.22593 -3.62645,-1.90343 -4.07583,-1.95066 -0.44938,-0.0472 -4.05653,1.36297 -4.39232,1.06062 -0.3358,-0.30235 0.68963,-4.03715 0.59569,-4.47913 -0.0939,-0.44198 -2.5498,-3.43681 -2.36602,-3.8496 0.18379,-0.41279 4.05267,-0.59166 4.44398,-0.81759 0.39132,-0.22593 2.48067,-3.48704 2.93005,-3.4398 0.44938,0.0472 1.81505,3.67147 2.15084,3.97382 0.3358,0.30236 4.08294,1.2817 4.17689,1.72369 0.0939,0.44198 -2.9309,2.86076 -3.11469,3.27355 C 669.9821,591.68426 670.20862,595.55064 669.8173,595.77657 Z"
277
+             transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />
278
+        </g>
279
+      </g>
280
+    </g>
281
+  </g>
282
+</svg>
Back to file index

layer.yaml

 1
--- 
 2
+++ layer.yaml
 3
@@ -0,0 +1,18 @@
 4
+"options":
 5
+  "shout-irc": {}
 6
+  "basic":
 7
+    "use_venv": !!bool "false"
 8
+    "packages": []
 9
+    "include_system_packages": !!bool "false"
10
+  "apt":
11
+    "version_package": ""
12
+    "packages": []
13
+    "full_version": !!bool "false"
14
+  "nodejs": {}
15
+"includes":
16
+- "layer:basic"
17
+- "layer:apt"
18
+- "layer:nodejs"
19
+- "interface:http"
20
+"repo": "https://github.com/jamesbeedy/layer-shout"
21
+"is": "shout-irc"
Back to file index

lib/charms/__init__.py

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

lib/charms/apt.py

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

lib/charms/layer/__init__.py

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

lib/charms/layer/basic.py

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

lib/charms/layer/execd.py

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

lib/charms/layer/nodejs.py

 1
--- 
 2
+++ lib/charms/layer/nodejs.py
 3
@@ -0,0 +1,47 @@
 4
+import sys
 5
+
 6
+from subprocess import Popen, PIPE
 7
+
 8
+from charmhelpers.core import hookenv
 9
+from charmhelpers.core import host
10
+
11
+
12
+def node_dist_dir():
13
+    """ Absolute path of Node.js application dir
14
+
15
+    Returns:
16
+    Absolute string of node application directory
17
+    """
18
+    return hookenv.config('app-dir')
19
+
20
+
21
+def npm(cmd):
22
+    """ Runs npm
23
+
24
+    This layer relies on the use of npm scripts defined in `package.json`,
25
+    see here https://docs.npmjs.com/misc/scripts for more information.
26
+
27
+    Usage:
28
+
29
+       npm('install')
30
+       npm('run', 'build')
31
+
32
+    Arguments:
33
+    cmd: Command to run.  The list of all positional args will be passed in
34
+      as the first arg to `subprocess.run`.
35
+
36
+    Returns:
37
+    Will halt on error
38
+    """
39
+    dist_dir = node_dist_dir()
40
+    hookenv.status_set(
41
+        'maintenance',
42
+        'installing NPM dependencies for {}'.format(dist_dir))
43
+    with host.chdir(dist_dir):
44
+        with Popen(['npm'] + cmd.split(), stderr=PIPE) as process:
45
+            _, errout = process.communicate()
46
+            retcode = process.poll()
47
+    if retcode != 0:
48
+        hookenv.log('NPM error: {}'.format(errout))
49
+        hookenv.status_set("blocked", "NPM error: {}".format(errout))
50
+        sys.exit(0)
Back to file index

lib/charms/layer/shout.py

 1
--- 
 2
+++ lib/charms/layer/shout.py
 3
@@ -0,0 +1,37 @@
 4
+import os
 5
+import subprocess
 6
+
 7
+
 8
+class ShoutCLIError(Exception):
 9
+    pass
10
+
11
+
12
+class Shout(object):
13
+    def __init__(self, home):
14
+        self.home = home
15
+        self.users_dir = os.path.join(self.home, 'users')
16
+
17
+    def add(self, name, password):
18
+        cmd = ' '.join(self._prep(['add', name]))
19
+        try:
20
+            subprocess.check_call('echo %s | %s' % (password, cmd), shell=True)
21
+        except subprocess.CalledProcessError:
22
+            raise ShoutCLIError('%s failed' % cmd)
23
+
24
+    def list(self):
25
+        return self._run(['list']).split()[2::2]
26
+
27
+    def remove(self, name):
28
+        if not os.path.exists(os.path.join(self.users_dir, '%s.json' % name)):
29
+            return
30
+
31
+        try:
32
+            self._run(['remove', name])
33
+        except subprocess.CalledProcessError:
34
+            raise ShoutCLIError('removing %s failed' % name)
35
+
36
+    def _prep(self, cmd):
37
+        return ['shout', '--home', self.home] + cmd
38
+
39
+    def _run(self, cmd):
40
+        return subprocess.check_output(self._prep(cmd)).decode('UTF-8').strip()
Back to file index

metadata.yaml

 1
--- 
 2
+++ metadata.yaml
 3
@@ -0,0 +1,12 @@
 4
+"name": "shout-irc"
 5
+"summary": "The self-hosted web IRC client"
 6
+"maintainer": "Marco Ceppi <marco@ceppi.net>"
 7
+"description": |
 8
+  <Multi-line description here>
 9
+"tags":
10
+- "irc"
11
+"series":
12
+- "xenial"
13
+"provides":
14
+  "website":
15
+    "interface": "http"
Back to file index

reactive/apt.py

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

reactive/node.py

 1
--- 
 2
+++ reactive/node.py
 3
@@ -0,0 +1,63 @@
 4
+import os
 5
+from charms.reactive import (
 6
+    hook,
 7
+    set_state,
 8
+    remove_state,
 9
+    main,
10
+    when_not,
11
+    when,
12
+)
13
+
14
+from charmhelpers.core import (
15
+    hookenv,
16
+    unitdata,
17
+    host
18
+)
19
+
20
+from charms import apt
21
+
22
+
23
+config = hookenv.config()
24
+kv = unitdata.kv()
25
+
26
+
27
+@when_not('app-dir.available')
28
+def create_app_dir():
29
+    """Create node dir
30
+    """
31
+    app_dir = hookenv.config('app-dir')
32
+    if not os.path.exists(app_dir):
33
+        host.mkdir(app_dir, perms=0o755)
34
+    set_state('app-dir.available')
35
+
36
+
37
+@when('app-dir.available')
38
+@when_not('nodejs.available')
39
+def install_nodejs():
40
+    """ Installs defined node runtime
41
+
42
+    Emits:
43
+    nodejs.available: Emitted once the runtime has been installed
44
+    """
45
+
46
+    kv.set('nodejs.url', config.get('install_sources'))
47
+    kv.set('nodejs.key', config.get('install_keys'))
48
+
49
+    apt.queue_install(['nodejs'])
50
+
51
+
52
+@when('apt.installed.nodejs')
53
+@when_not('nodejs.available')
54
+def node_js_ready():
55
+    hookenv.status_set('active', 'node.js is ready')
56
+    set_state('nodejs.available')
57
+
58
+
59
+@hook('config-changed')
60
+def version_check():
61
+    url = config.get('install_sources')
62
+    key = config.get('install_keys')
63
+
64
+    if url != kv.get('nodejs.url') or key != kv.get('nodejs.key'):
65
+        apt.purge(['nodejs'])
66
+        remove_state('nodejs.available')
Back to file index

reactive/shout_irc.py

 1
--- 
 2
+++ reactive/shout_irc.py
 3
@@ -0,0 +1,94 @@
 4
+import os
 5
+import subprocess
 6
+
 7
+from charms.reactive import when, when_not, set_state, hook, remove_state
 8
+
 9
+from charmhelpers.core import host
10
+
11
+from charmhelpers.core.hookenv import config
12
+from charmhelpers.core.hookenv import status_set
13
+from charmhelpers.core.hookenv import open_port
14
+from charmhelpers.core.hookenv import close_port
15
+
16
+from charmhelpers.core.templating import render
17
+
18
+from charms.layer.nodejs import npm, node_dist_dir
19
+
20
+
21
+@when('nodejs.available')
22
+@when_not('shout-irc.installed')
23
+def install_shout():
24
+    status_set('maintenance', 'installing shout-irc')
25
+    npm('-g install shout')
26
+
27
+    render(
28
+        source='config.js.j2',
29
+        target=os.path.join(node_dist_dir(), 'config.js'),
30
+        context={
31
+            'public': 'true' if config('public') else 'false',
32
+            'port': config('port')
33
+        },
34
+    )
35
+
36
+    set_state('shout-irc.installed')
37
+    remove_state('shout-irc.started')
38
+    status_set('active', 'shout-irc installed')
39
+
40
+
41
+@when('website.available')
42
+@when('shout-irc.installed')
43
+def configure_website(website):
44
+    website.configure(port=80)
45
+
46
+
47
+@when('shout-irc.installed')
48
+@when_not('shout-irc.started')
49
+def start():
50
+    render(
51
+        source='shout-irc.service.j2',
52
+        target='/etc/systemd/system/shout-irc.service',
53
+        context={
54
+            'home': node_dist_dir(),
55
+        },
56
+    )
57
+
58
+    if not config('public'):
59
+        users_dir = os.path.join(node_dist_dir(), 'users')
60
+        if not os.path.exists(users_dir):
61
+            os.mkdir(users_dir)
62
+
63
+        render(source='_.json', target=os.path.join(users_dir, '_.json'), context={})
64
+
65
+    if host.service_running('shout-irc'):
66
+        host.service_restart('shout-irc')
67
+    else:
68
+        host.service_start('shout-irc')
69
+
70
+    set_state('shout-irc.started')
71
+    update()
72
+
73
+
74
+@when('shout-irc.started')
75
+def ports():
76
+    open_port(80)
77
+
78
+
79
+@when_not('shout-irc.started')
80
+def close():
81
+    close_port(80)
82
+
83
+
84
+@hook('update-status')
85
+def update():
86
+    extra = 'running' if host.service_running('shout-irc') else 'stopped'
87
+    status_set('active', 'version %s - %s' % (shout_version(), extra))
88
+
89
+
90
+@when_not('nodejs.available')
91
+def reset():
92
+    remove_state('shout-irc.installed')
93
+
94
+
95
+def shout_version():
96
+    return subprocess.check_output(['shout', '--version']).decode('UTF-8').strip()
97
+
Back to file index

requirements.txt

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

revision

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

templates/_.json

1
--- 
2
+++ templates/_.json
3
@@ -0,0 +1,4 @@
4
+{
5
+  "name": "_",
6
+  "log": false
7
+}
Back to file index

templates/config.js.j2

 1
--- 
 2
+++ templates/config.js.j2
 3
@@ -0,0 +1,35 @@
 4
+module.exports = {
 5
+	public: {{ public }},
 6
+	host: "0.0.0.0",
 7
+	port: {{ port }},
 8
+	bind: undefined,
 9
+	theme: "themes/example.css",
10
+	autoload: true,
11
+	prefetch: false,
12
+	prefetchMaxImageSize: 512,
13
+	displayNetwork: true,
14
+	logs: {
15
+		format: "YYYY-MM-DD HH:mm:ss",
16
+		timezone: "UTC+00:00"
17
+	},
18
+	defaults: {
19
+		name: "Freenode",
20
+		host: "irc.freenode.org",
21
+		port: 6697,
22
+		password: "",
23
+		tls: true,
24
+		nick: "shout-user",
25
+		username: "shout-user",
26
+		join: "#juju, #shout-irc"
27
+	},
28
+	transports: ["polling", "websocket"],
29
+	https: {
30
+		enable: false,
31
+		key: "",
32
+		certificate: ""
33
+	},
34
+	identd: {
35
+		enable: false,
36
+		port: 113
37
+	}
38
+};
Back to file index

templates/shout-irc.service.j2

 1
--- 
 2
+++ templates/shout-irc.service.j2
 3
@@ -0,0 +1,11 @@
 4
+[Unit]
 5
+Description=Shout IRC
 6
+
 7
+[Service]
 8
+User=root
 9
+Group=root
10
+ExecStart=/usr/bin/shout --home {{ home }}
11
+ExecStop=/bin/kill -s TERM $MAINPID
12
+
13
+[Install]
14
+WantedBy=multi-user.target
Back to file index

tests/00-setup

1
--- 
2
+++ tests/00-setup
3
@@ -0,0 +1,5 @@
4
+#!/bin/bash
5
+
6
+sudo add-apt-repository ppa:juju/stable -y
7
+sudo apt-get update
8
+sudo apt-get install amulet python-requests -y
Back to file index

tests/10-deploy

 1
--- 
 2
+++ tests/10-deploy
 3
@@ -0,0 +1,31 @@
 4
+#!/usr/bin/python3
 5
+
 6
+import amulet
 7
+import requests
 8
+import unittest
 9
+
10
+
11
+class TestCharm(unittest.TestCase):
12
+    def setUp(self):
13
+        self.d = amulet.Deployment()
14
+
15
+        self.d.add('shout-irc')
16
+        self.d.expose('shout-irc')
17
+
18
+        self.d.setup(timeout=900)
19
+        self.d.sentry.wait()
20
+
21
+        self.unit = self.d.sentry['shout-irc'][0]
22
+
23
+    def test_service(self):
24
+        # test we can access over http
25
+        page = requests.get('http://{}'.format(self.unit.info['public-address']))
26
+        self.assertEqual(page.status_code, 200)
27
+        # Now you can use self.d.sentry[SERVICE][UNIT] to address each of the units and perform
28
+        # more in-depth steps. Each self.d.sentry[SERVICE][UNIT] has the following methods:
29
+        # - .info - An array of the information of that unit from Juju
30
+        # - .file(PATH) - Get the details of a file on that unit
31
+        # - .file_contents(PATH) - Get plain text output of PATH file from that unit
32
+        # - .directory(PATH) - Get details of directory
33
+        # - .directory_contents(PATH) - List files and folders in PATH on that unit
34
+        # - .relation(relation, service:rel) - Get relation data from return service
Back to file index

tox.ini

 1
--- 
 2
+++ tox.ini
 3
@@ -0,0 +1,14 @@
 4
+[tox]
 5
+envlist = flake, pep8
 6
+skipsdist = True
 7
+
 8
+[testenv]
 9
+deps = -r{toxinidir}/requirements.txt
10
+
11
+[testenv:flake]
12
+commands = flake8 {posargs} reactive
13
+deps = flake8
14
+
15
+[testenv:pep8]
16
+commands = pep8 {posargs} reactive
17
+deps = pep8