~mistotebe/odoo

Owner: mistotebe
Status: Needs Fixing
Vote: -1 (+2 needed for approval)

CPP?: No
OIL?: No

This charm replaces the openerp-server charm.


Tests

Substrate Status Results Last Updated
aws FAIL Test Results 8 months ago
lxc FAIL http://juju-ci.vapour.ws/job/charm-bundle-test-lxc/6406/console 8 months ago
gce FAIL Test Results 8 months ago
gce RETRY 19 days ago

Voted: +1
marcoceppi wrote 7 months ago
Hello! Despite the test results above, which are all lint failures on a layer you're using, the charm looks great! I'd like to see a bundle put together with PostgreSQL, Odoo, and a load-balancer with a recommended number of units submitted in the future to better steer a solution driven concept and not so much one that's just here's a charm, assemble the bits.
Voted: +0
tvansteenburgh wrote 7 months ago
Your tests/10-deploy isn't actually running the tests you have written. You need to add this at the end of that file:

if __name__ == '__main__':
unittest.main()

I would prefer to see this fixed and that test executed before giving a +1, but the charm looks good otherwise.
Voted: +0
johnsca wrote 6 months ago
I agree with Tim that it would be best to fix the test before promulgating, so I created https://github.com/credativUK/odoo-charm/pull/1 for that change.
Voted: -1
johnsca wrote 6 months ago
Voting -1 to mark as Needs Fixing to get the above PR included.
Voted: +0
lazypower wrote 5 months ago
I left a comment on the open pull-request above to bring more awareness to this blocking issue on the review. Barring that update, I feel that this is ready for final review and potential promulgation.
Voted: -1
johnsca wrote 4 months ago
Another ping on this review (and on the referenced PR) and another -1 to get it actually to "Needs Fixing". If that PR is accepted, this review will get my +2.

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.

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. marcoceppi
Tests must exercise config. marcoceppi
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. marcoceppi
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.
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. marcoceppi
Should avoid running services as root. marcoceppi

Source Diff

Files changed 74

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,56 @@
 4
+Odoo charm
 5
+==========
 6
+
 7
+# Overview
 8
+
 9
+Odoo is a comprehensive open source management system. It has a large, active
10
+community, which has developed modules to handle all facets of company
11
+management.
12
+
13
+These include, amongst others: Sales Management, CRM, e-commerce,
14
+Manufacturing, Stock, Accounting, Human Resources, Project Management,
15
+Logistics, Productivity and Document Management.
16
+
17
+Odoo allows you to start easily with one module to fit a specific need then
18
+add additional modules as and when you need them enabling you to have a
19
+powerful feature rich Enterprise Resource Planner.
20
+
21
+# Usage
22
+
23
+To deploy and use this charm, you want to relate it to the db-admin PostgreSQL
24
+endpoint:
25
+
26
+juju deploy odoo
27
+juju add-relation odoo postgresql:db-admin
28
+
29
+The charm provides the http relation as well that you can point your other
30
+frontends to, or you can expose the charm and point your browser at the unit.
31
+
32
+Make sure you change the management password after you have created your
33
+database.
34
+
35
+# Configuration
36
+
37
+If you want to install a different Odoo version, all you need to do is set
38
+the `install_sources` configuration parameter to point to a different repository
39
+and possibly also change `install_keys` to set the relevant signing key as well.
40
+
41
+## Known Limitations and Issues
42
+
43
+This charm does not support scale-out yet.
44
+
45
+# Contact Information
46
+
47
+## Authors
48
+  - Ondřej Kuzník <ondrej.kuznik@credativ.co.uk>
49
+
50
+## Odoo
51
+
52
+  - Website: https://odoo.com
53
+  - Bug tracker: https://github.com/odoo/issues
54
+  - Community mailing lists: https://www.odoo.com/groups
55
+
56
+## credativ
57
+
58
+  - Website: https://credativ.co.uk
59
+  - Contact email: info@credativ.co.uk
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,37 @@
 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
+      - deb http://nightly.odoo.com/10.0/nightly/deb/ ./
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
+      - DEF2A2198183CBB5
33
+  "dbname":
34
+    "description": "The database name to request from PostgreSQL"
35
+    "type": "string"
36
+    "default": "odoo"
37
+  "port":
38
+    "description": "The port that Odoo should be accessible at"
39
+    "type": "int"
40
+    "default": !!int "80"
Back to file index

copyright

 1
--- 
 2
+++ copyright
 3
@@ -0,0 +1,8 @@
 4
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
 5
+Upstream-Name: Odoo Charm
 6
+Upstream-Contact: Ondřej Kuzník <ondrej.kuznik@credativ.co.uk>
 7
+Source: https://github.com/credativUK/odoo-charm
 8
+
 9
+Files: *
10
+Copyright: 2016 credativ ltd.
11
+License: GPL-3+
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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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/db-relation-broken

 1
--- 
 2
+++ hooks/db-relation-broken
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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/db-relation-changed

 1
--- 
 2
+++ hooks/db-relation-changed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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/db-relation-departed

 1
--- 
 2
+++ hooks/db-relation-departed
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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/db-relation-joined

 1
--- 
 2
+++ hooks/db-relation-joined
 3
@@ -0,0 +1,19 @@
 4
+#!/usr/bin/env python3
 5
+
 6
+# Load modules from $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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,23 @@
 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):
22
+        relation_info = {
23
+            'hostname': hookenv.unit_get('private-address'),
24
+            'port': port,
25
+        }
26
+        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/pgsql/.gitignore

 1
--- 
 2
+++ hooks/relations/pgsql/.gitignore
 3
@@ -0,0 +1,8 @@
 4
+*~
 5
+*.pyc
 6
+__pycache__/
 7
+.cache
 8
+.tox
 9
+docs/build/
10
+build/
11
+docs/source/_build/
Back to file index

hooks/relations/pgsql/Makefile

 1
--- 
 2
+++ hooks/relations/pgsql/Makefile
 3
@@ -0,0 +1,21 @@
 4
+.PHONY: all
 5
+all:
 6
+	@echo "make clean - Clean all test & doc build artifacts"
 7
+	@echo "make lint  - Run linter"
 8
+	@echo "make docs  - Build html documentation"
 9
+
10
+.PHONY: clean
11
+clean:
12
+	git clean -fX
13
+
14
+.PHONY: lint
15
+lint:
16
+	tox -e lint
17
+
18
+.PHONY: tox
19
+tox:
20
+	tox
21
+
22
+.PHONY: docs
23
+docs:
24
+	tox -e docs
Back to file index

hooks/relations/pgsql/README.md

1
--- 
2
+++ hooks/relations/pgsql/README.md
3
@@ -0,0 +1,3 @@
4
+# pgsql interface for Juju charms.reactive
5
+
6
+Documentation at http://interface-pgsql.readthedocs.io
Back to file index

hooks/relations/pgsql/copyright

 1
--- 
 2
+++ hooks/relations/pgsql/copyright
 3
@@ -0,0 +1,16 @@
 4
+Format: http://dep.debian.net/deps/dep5/
 5
+
 6
+Files: *
 7
+Copyright: Copyright 2015-2016, Canonical Ltd.
 8
+License: GPL-3
 9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License version 3, as
11
+ published by the Free Software Foundation.
12
+ .
13
+ This program is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranties of
15
+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
16
+ PURPOSE.  See the GNU General Public License for more details.
17
+ .
18
+ You should have received a copy of the GNU General Public License
19
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
Back to file index

hooks/relations/pgsql/docs/Makefile

  1
--- 
  2
+++ hooks/relations/pgsql/docs/Makefile
  3
@@ -0,0 +1,192 @@
  4
+# Makefile for Sphinx documentation
  5
+#
  6
+
  7
+# You can set these variables from the command line.
  8
+SPHINXOPTS    =
  9
+SPHINXBUILD   = sphinx-build
 10
+PAPER         =
 11
+BUILDDIR      = build
 12
+
 13
+# User-friendly check for sphinx-build
 14
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
 15
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
 16
+endif
 17
+
 18
+# Internal variables.
 19
+PAPEROPT_a4     = -D latex_paper_size=a4
 20
+PAPEROPT_letter = -D latex_paper_size=letter
 21
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
 22
+# the i18n builder cannot share the environment and doctrees with the others
 23
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
 24
+
 25
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
 26
+
 27
+help:
 28
+	@echo "Please use \`make <target>' where <target> is one of"
 29
+	@echo "  html       to make standalone HTML files"
 30
+	@echo "  dirhtml    to make HTML files named index.html in directories"
 31
+	@echo "  singlehtml to make a single large HTML file"
 32
+	@echo "  pickle     to make pickle files"
 33
+	@echo "  json       to make JSON files"
 34
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
 35
+	@echo "  qthelp     to make HTML files and a qthelp project"
 36
+	@echo "  applehelp  to make an Apple Help Book"
 37
+	@echo "  devhelp    to make HTML files and a Devhelp project"
 38
+	@echo "  epub       to make an epub"
 39
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
 40
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
 41
+	@echo "  latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
 42
+	@echo "  text       to make text files"
 43
+	@echo "  man        to make manual pages"
 44
+	@echo "  texinfo    to make Texinfo files"
 45
+	@echo "  info       to make Texinfo files and run them through makeinfo"
 46
+	@echo "  gettext    to make PO message catalogs"
 47
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
 48
+	@echo "  xml        to make Docutils-native XML files"
 49
+	@echo "  pseudoxml  to make pseudoxml-XML files for display purposes"
 50
+	@echo "  linkcheck  to check all external links for integrity"
 51
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
 52
+	@echo "  coverage   to run coverage check of the documentation (if enabled)"
 53
+
 54
+clean:
 55
+	rm -rf $(BUILDDIR)/*
 56
+
 57
+html:
 58
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
 59
+	@echo
 60
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
 61
+
 62
+dirhtml:
 63
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
 64
+	@echo
 65
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
 66
+
 67
+singlehtml:
 68
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
 69
+	@echo
 70
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
 71
+
 72
+pickle:
 73
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
 74
+	@echo
 75
+	@echo "Build finished; now you can process the pickle files."
 76
+
 77
+json:
 78
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
 79
+	@echo
 80
+	@echo "Build finished; now you can process the JSON files."
 81
+
 82
+htmlhelp:
 83
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
 84
+	@echo
 85
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
 86
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
 87
+
 88
+qthelp:
 89
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
 90
+	@echo
 91
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
 92
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
 93
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pgsqlstub.qhcp"
 94
+	@echo "To view the help file:"
 95
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pgsqlstub.qhc"
 96
+
 97
+applehelp:
 98
+	$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
 99
+	@echo
100
+	@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
101
+	@echo "N.B. You won't be able to view it unless you put it in" \
102
+	      "~/Library/Documentation/Help or install it in your application" \
103
+	      "bundle."
104
+
105
+devhelp:
106
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
107
+	@echo
108
+	@echo "Build finished."
109
+	@echo "To view the help file:"
110
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/pgsqlstub"
111
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pgsqlstub"
112
+	@echo "# devhelp"
113
+
114
+epub:
115
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
116
+	@echo
117
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
118
+
119
+latex:
120
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
121
+	@echo
122
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
123
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
124
+	      "(use \`make latexpdf' here to do that automatically)."
125
+
126
+latexpdf:
127
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
128
+	@echo "Running LaTeX files through pdflatex..."
129
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
130
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
131
+
132
+latexpdfja:
133
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
134
+	@echo "Running LaTeX files through platex and dvipdfmx..."
135
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
136
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
137
+
138
+text:
139
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
140
+	@echo
141
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
142
+
143
+man:
144
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
145
+	@echo
146
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
147
+
148
+texinfo:
149
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
150
+	@echo
151
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
152
+	@echo "Run \`make' in that directory to run these through makeinfo" \
153
+	      "(use \`make info' here to do that automatically)."
154
+
155
+info:
156
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
157
+	@echo "Running Texinfo files through makeinfo..."
158
+	make -C $(BUILDDIR)/texinfo info
159
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
160
+
161
+gettext:
162
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
163
+	@echo
164
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
165
+
166
+changes:
167
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
168
+	@echo
169
+	@echo "The overview file is in $(BUILDDIR)/changes."
170
+
171
+linkcheck:
172
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
173
+	@echo
174
+	@echo "Link check complete; look for any errors in the above output " \
175
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
176
+
177
+doctest:
178
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
179
+	@echo "Testing of doctests in the sources finished, look at the " \
180
+	      "results in $(BUILDDIR)/doctest/output.txt."
181
+
182
+coverage:
183
+	$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
184
+	@echo "Testing of coverage in the sources finished, look at the " \
185
+	      "results in $(BUILDDIR)/coverage/python.txt."
186
+
187
+xml:
188
+	$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
189
+	@echo
190
+	@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
191
+
192
+pseudoxml:
193
+	$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
194
+	@echo
195
+	@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
Back to file index

hooks/relations/pgsql/docs/source/conf.py

  1
--- 
  2
+++ hooks/relations/pgsql/docs/source/conf.py
  3
@@ -0,0 +1,293 @@
  4
+# -*- coding: utf-8 -*-
  5
+#
  6
+# pgsql stub documentation build configuration file, created by
  7
+# sphinx-quickstart on Wed Jun 10 17:13:25 2015.
  8
+#
  9
+# This file is execfile()d with the current directory set to its
 10
+# containing dir.
 11
+#
 12
+# Note that not all possible configuration values are present in this
 13
+# autogenerated file.
 14
+#
 15
+# All configuration values have a default; values that are commented out
 16
+# serve to show the default.
 17
+
 18
+import sys
 19
+import os
 20
+import shlex
 21
+
 22
+# If extensions (or modules to document with autodoc) are in another directory,
 23
+# add these directories to sys.path here. If the directory is relative to the
 24
+# documentation root, use os.path.abspath to make it absolute, like shown here.
 25
+sys.path.insert(0, os.path.abspath('../..'))  # relative to conf.py
 26
+
 27
+# -- General configuration ------------------------------------------------
 28
+
 29
+# If your documentation needs a minimal Sphinx version, state it here.
 30
+#needs_sphinx = '1.0'
 31
+
 32
+# Add any Sphinx extension module names here, as strings. They can be
 33
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 34
+# ones.
 35
+extensions = [
 36
+    'sphinx.ext.autodoc',
 37
+    'sphinx.ext.intersphinx',
 38
+    'sphinx.ext.coverage',
 39
+    'sphinx.ext.viewcode',
 40
+]
 41
+
 42
+# Add any paths that contain templates here, relative to this directory.
 43
+templates_path = ['_templates']
 44
+
 45
+# The suffix(es) of source filenames.
 46
+# You can specify multiple suffix as a list of string:
 47
+# source_suffix = ['.rst', '.md']
 48
+source_suffix = '.rst'
 49
+
 50
+# The encoding of source files.
 51
+#source_encoding = 'utf-8-sig'
 52
+
 53
+# The master toctree document.
 54
+master_doc = 'index'
 55
+
 56
+# General information about the project.
 57
+project = u'pgsql stub'
 58
+copyright = u'2015, me'
 59
+author = u'me'
 60
+
 61
+# The version info for the project you're documenting, acts as replacement for
 62
+# |version| and |release|, also used in various other places throughout the
 63
+# built documents.
 64
+#
 65
+# The short X.Y version.
 66
+version = '1.0'
 67
+# The full version, including alpha/beta/rc tags.
 68
+release = '1.0'
 69
+
 70
+# The language for content autogenerated by Sphinx. Refer to documentation
 71
+# for a list of supported languages.
 72
+#
 73
+# This is also used if you do content translation via gettext catalogs.
 74
+# Usually you set "language" from the command line for these cases.
 75
+language = None
 76
+
 77
+# There are two options for replacing |today|: either, you set today to some
 78
+# non-false value, then it is used:
 79
+#today = ''
 80
+# Else, today_fmt is used as the format for a strftime call.
 81
+#today_fmt = '%B %d, %Y'
 82
+
 83
+# List of patterns, relative to source directory, that match files and
 84
+# directories to ignore when looking for source files.
 85
+exclude_patterns = []
 86
+
 87
+# The reST default role (used for this markup: `text`) to use for all
 88
+# documents.
 89
+#default_role = None
 90
+
 91
+# If true, '()' will be appended to :func: etc. cross-reference text.
 92
+#add_function_parentheses = True
 93
+
 94
+# If true, the current module name will be prepended to all description
 95
+# unit titles (such as .. function::).
 96
+#add_module_names = True
 97
+
 98
+# If true, sectionauthor and moduleauthor directives will be shown in the
 99
+# output. They are ignored by default.
100
+#show_authors = False
101
+
102
+# The name of the Pygments (syntax highlighting) style to use.
103
+pygments_style = 'sphinx'
104
+
105
+# A list of ignored prefixes for module index sorting.
106
+#modindex_common_prefix = []
107
+
108
+# If true, keep warnings as "system message" paragraphs in the built documents.
109
+#keep_warnings = False
110
+
111
+# If true, `todo` and `todoList` produce output, else they produce nothing.
112
+todo_include_todos = False
113
+
114
+
115
+# -- Options for HTML output ----------------------------------------------
116
+
117
+# The theme to use for HTML and HTML Help pages.  See the documentation for
118
+# a list of builtin themes.
119
+html_theme = 'alabaster'
120
+
121
+# Theme options are theme-specific and customize the look and feel of a theme
122
+# further.  For a list of options available for each theme, see the
123
+# documentation.
124
+#html_theme_options = {}
125
+
126
+# Add any paths that contain custom themes here, relative to this directory.
127
+#html_theme_path = []
128
+
129
+# The name for this set of Sphinx documents.  If None, it defaults to
130
+# "<project> v<release> documentation".
131
+#html_title = None
132
+
133
+# A shorter title for the navigation bar.  Default is the same as html_title.
134
+#html_short_title = None
135
+
136
+# The name of an image file (relative to this directory) to place at the top
137
+# of the sidebar.
138
+#html_logo = None
139
+
140
+# The name of an image file (within the static path) to use as favicon of the
141
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
142
+# pixels large.
143
+#html_favicon = None
144
+
145
+# Add any paths that contain custom static files (such as style sheets) here,
146
+# relative to this directory. They are copied after the builtin static files,
147
+# so a file named "default.css" will overwrite the builtin "default.css".
148
+html_static_path = ['_static']
149
+
150
+# Add any extra paths that contain custom files (such as robots.txt or
151
+# .htaccess) here, relative to this directory. These files are copied
152
+# directly to the root of the documentation.
153
+#html_extra_path = []
154
+
155
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
156
+# using the given strftime format.
157
+#html_last_updated_fmt = '%b %d, %Y'
158
+
159
+# If true, SmartyPants will be used to convert quotes and dashes to
160
+# typographically correct entities.
161
+#html_use_smartypants = True
162
+
163
+# Custom sidebar templates, maps document names to template names.
164
+#html_sidebars = {}
165
+
166
+# Additional templates that should be rendered to pages, maps page names to
167
+# template names.
168
+#html_additional_pages = {}
169
+
170
+# If false, no module index is generated.
171
+#html_domain_indices = True
172
+
173
+# If false, no index is generated.
174
+#html_use_index = True
175
+
176
+# If true, the index is split into individual pages for each letter.
177
+#html_split_index = False
178
+
179
+# If true, links to the reST sources are added to the pages.
180
+#html_show_sourcelink = True
181
+
182
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
183
+#html_show_sphinx = True
184
+
185
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
186
+#html_show_copyright = True
187
+
188
+# If true, an OpenSearch description file will be output, and all pages will
189
+# contain a <link> tag referring to it.  The value of this option must be the
190
+# base URL from which the finished HTML is served.
191
+#html_use_opensearch = ''
192
+
193
+# This is the file name suffix for HTML files (e.g. ".xhtml").
194
+#html_file_suffix = None
195
+
196
+# Language to be used for generating the HTML full-text search index.
197
+# Sphinx supports the following languages:
198
+#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
199
+#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
200
+#html_search_language = 'en'
201
+
202
+# A dictionary with options for the search language support, empty by default.
203
+# Now only 'ja' uses this config value
204
+#html_search_options = {'type': 'default'}
205
+
206
+# The name of a javascript file (relative to the configuration directory) that
207
+# implements a search results scorer. If empty, the default will be used.
208
+#html_search_scorer = 'scorer.js'
209
+
210
+# Output file base name for HTML help builder.
211
+htmlhelp_basename = 'pgsqlstubdoc'
212
+
213
+# -- Options for LaTeX output ---------------------------------------------
214
+
215
+latex_elements = {
216
+# The paper size ('letterpaper' or 'a4paper').
217
+#'papersize': 'letterpaper',
218
+
219
+# The font size ('10pt', '11pt' or '12pt').
220
+#'pointsize': '10pt',
221
+
222
+# Additional stuff for the LaTeX preamble.
223
+#'preamble': '',
224
+
225
+# Latex figure (float) alignment
226
+#'figure_align': 'htbp',
227
+}
228
+
229
+# Grouping the document tree into LaTeX files. List of tuples
230
+# (source start file, target name, title,
231
+#  author, documentclass [howto, manual, or own class]).
232
+latex_documents = [
233
+  (master_doc, 'pgsqlstub.tex', u'pgsql stub Documentation',
234
+   u'me', 'manual'),
235
+]
236
+
237
+# The name of an image file (relative to this directory) to place at the top of
238
+# the title page.
239
+#latex_logo = None
240
+
241
+# For "manual" documents, if this is true, then toplevel headings are parts,
242
+# not chapters.
243
+#latex_use_parts = False
244
+
245
+# If true, show page references after internal links.
246
+#latex_show_pagerefs = False
247
+
248
+# If true, show URL addresses after external links.
249
+#latex_show_urls = False
250
+
251
+# Documents to append as an appendix to all manuals.
252
+#latex_appendices = []
253
+
254
+# If false, no module index is generated.
255
+#latex_domain_indices = True
256
+
257
+
258
+# -- Options for manual page output ---------------------------------------
259
+
260
+# One entry per manual page. List of tuples
261
+# (source start file, name, description, authors, manual section).
262
+man_pages = [
263
+    (master_doc, 'pgsqlstub', u'pgsql stub Documentation',
264
+     [author], 1)
265
+]
266
+
267
+# If true, show URL addresses after external links.
268
+#man_show_urls = False
269
+
270
+
271
+# -- Options for Texinfo output -------------------------------------------
272
+
273
+# Grouping the document tree into Texinfo files. List of tuples
274
+# (source start file, target name, title, author,
275
+#  dir menu entry, description, category)
276
+texinfo_documents = [
277
+  (master_doc, 'pgsqlstub', u'pgsql stub Documentation',
278
+   author, 'pgsqlstub', 'One line description of project.',
279
+   'Miscellaneous'),
280
+]
281
+
282
+# Documents to append as an appendix to all manuals.
283
+#texinfo_appendices = []
284
+
285
+# If false, no module index is generated.
286
+#texinfo_domain_indices = True
287
+
288
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
289
+#texinfo_show_urls = 'footnote'
290
+
291
+# If true, do not generate a @detailmenu in the "Top" node's menu.
292
+#texinfo_no_detailmenu = False
293
+
294
+
295
+# Example configuration for intersphinx: refer to the Python standard library.
296
+intersphinx_mapping = {'https://docs.python.org/': None}
Back to file index

hooks/relations/pgsql/docs/source/index.rst

 1
--- 
 2
+++ hooks/relations/pgsql/docs/source/index.rst
 3
@@ -0,0 +1,18 @@
 4
+Juju pgsql Relation Interface Implementation Docs
 5
+=================================================
 6
+
 7
+Contents:
 8
+
 9
+.. toctree::
10
+   :maxdepth: 2
11
+
12
+   requires
13
+
14
+
15
+Indices and tables
16
+==================
17
+
18
+* :ref:`genindex`
19
+* :ref:`modindex`
20
+* :ref:`search`
21
+
Back to file index

hooks/relations/pgsql/docs/source/requires.rst

 1
--- 
 2
+++ hooks/relations/pgsql/docs/source/requires.rst
 3
@@ -0,0 +1,54 @@
 4
+Requires: PostgreSQLClient
 5
+==========================
 6
+
 7
+Example Usage
 8
+-------------
 9
+
10
+This is what a charm using this relation would look like:
11
+
12
+.. code-block:: python
13
+
14
+    from charmhelpers.core import hookenv
15
+    from charmhelpers.core.reactive import hook
16
+    from charmhelpers.core.reactive import when
17
+    from charmhelpers.core.reactive import when_file_changed
18
+    from charmhelpers.core.reactive import set_state
19
+    from charmhelpers.core.reactive import remove_state
20
+
21
+    @when('db.connected')
22
+    def request_db(pgsql):
23
+        pgsql.set_database('mydb')
24
+
25
+    @when('config.changed')
26
+    def check_admin_pass():
27
+        admin_pass = hookenv.config()['admin-pass']
28
+        if admin_pass:
29
+            set_state('admin-pass')
30
+        else:
31
+            remove_state('admin-pass')
32
+
33
+    @when('db.master.available', 'admin-pass')
34
+    def render_config(pgsql):
35
+        render_template('app-config.j2', '/etc/app.conf', {
36
+            'db_conn': pgsql.master,
37
+            'admin_pass': hookenv.config('admin-pass'),
38
+        })
39
+
40
+    @when_file_changed('/etc/app.conf')
41
+    def restart_service():
42
+        hookenv.service_restart('myapp')
43
+
44
+
45
+Reference
46
+---------
47
+.. autoclass::
48
+    requires.ConnectionString
49
+    :members:
50
+
51
+.. autoclass::
52
+    requires.ConnectionStrings
53
+    :members:
54
+
55
+.. autoclass::
56
+    requires.PostgreSQLClient
57
+    :members:
Back to file index

hooks/relations/pgsql/interface.yaml

1
--- 
2
+++ hooks/relations/pgsql/interface.yaml
3
@@ -0,0 +1,3 @@
4
+name: pgsql
5
+summary: PostgreSQL client interface
6
+maintainer: '"Cory Johns" <cory.johns@canonical.com>'
Back to file index

hooks/relations/pgsql/requires.py

  1
--- 
  2
+++ hooks/relations/pgsql/requires.py
  3
@@ -0,0 +1,380 @@
  4
+# Copyright 2016 Canonical Ltd.
  5
+#
  6
+# This file is part of the PostgreSQL Client Interface for Juju charms.reactive
  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 collections import OrderedDict
 21
+import ipaddress
 22
+import itertools
 23
+import urllib.parse
 24
+
 25
+from charmhelpers import context
 26
+from charmhelpers.core import hookenv
 27
+from charms.reactive import hook, scopes, RelationBase
 28
+
 29
+
 30
+# This data structure cannot be in an external library,
 31
+# as interfaces have no way to declare dependencies
 32
+# (https://github.com/juju/charm-tools/issues/243).
 33
+# It also must be defined in this file
 34
+# (https://github.com/juju-solutions/charms.reactive/pull/51)
 35
+#
 36
+class ConnectionString(str):
 37
+    """A libpq connection string.
 38
+
 39
+    >>> c = ConnectionString(host='1.2.3.4', dbname='mydb',
 40
+    ...                      port=5432, user='anon', password='secret')
 41
+    ...
 42
+    >>> c
 43
+    'host=1.2.3.4 dbname=mydb port=5432 user=anon password=secret
 44
+
 45
+    Components may be accessed as attributes.
 46
+
 47
+    >>> c.dbname
 48
+    'mydb'
 49
+    >>> c.host
 50
+    '1.2.3.4'
 51
+    >>> c.port
 52
+    '5432'
 53
+
 54
+    The standard URI format is also accessible:
 55
+
 56
+    >>> c.uri
 57
+    'postgresql://anon:secret@1.2.3.4:5432/mydb'
 58
+
 59
+    """
 60
+    def __new__(self, **kw):
 61
+        def quote(x):
 62
+            return str(x).replace("\\", "\\\\").replace("'", "\\'")
 63
+        c = " ".join("{}={}".format(k, quote(v))
 64
+                     for k, v in sorted(kw.items()))
 65
+        c = str.__new__(self, c)
 66
+
 67
+        for k, v in kw.items():
 68
+            setattr(c, k, v)
 69
+
 70
+        self._keys = set(kw.keys())
 71
+
 72
+        # Construct the documented PostgreSQL URI for applications
 73
+        # that use this format. PostgreSQL docs refer to this as a
 74
+        # URI so we do do, even though it meets the requirements the
 75
+        # more specific term URL.
 76
+        fmt = ['postgresql://']
 77
+        d = {k: urllib.parse.quote(v, safe='') for k, v in kw.items()}
 78
+        if 'user' in d:
 79
+            if 'password' in d:
 80
+                fmt.append('{user}:{password}@')
 81
+            else:
 82
+                fmt.append('{user}@')
 83
+        if 'host' in kw:
 84
+            try:
 85
+                hostaddr = ipaddress.ip_address(kw.get('hostaddr') or
 86
+                                                kw.get('host'))
 87
+                if isinstance(hostaddr, ipaddress.IPv6Address):
 88
+                    d['hostaddr'] = '[{}]'.format(hostaddr)
 89
+                else:
 90
+                    d['hostaddr'] = str(hostaddr)
 91
+            except ValueError:
 92
+                # Not an IP address, but hopefully a resolvable name.
 93
+                d['hostaddr'] = d['host']
 94
+            del d['host']
 95
+            fmt.append('{hostaddr}')
 96
+        if 'port' in d:
 97
+            fmt.append(':{port}')
 98
+        if 'dbname' in d:
 99
+            fmt.append('/{dbname}')
100
+        main_keys = frozenset(['user', 'password',
101
+                               'dbname', 'hostaddr', 'port'])
102
+        extra_fmt = ['{}={{{}}}'.format(extra, extra)
103
+                     for extra in sorted(d.keys()) if extra not in main_keys]
104
+        if extra_fmt:
105
+            fmt.extend(['?', '&'.join(extra_fmt)])
106
+        c.uri = ''.join(fmt).format(**d)
107
+
108
+        return c
109
+
110
+    host = None
111
+    dbname = None
112
+    port = None
113
+    user = None
114
+    password = None
115
+    uri = None
116
+
117
+    def keys(self):
118
+        return iter(self._keys)
119
+
120
+    def items(self):
121
+        return {k: self[k] for k in self.keys()}.items()
122
+
123
+    def values(self):
124
+        return iter(self[k] for k in self.keys())
125
+
126
+    def __getitem__(self, key):
127
+        if isinstance(key, int):
128
+            return super(ConnectionString, self).__getitem__(key)
129
+        try:
130
+            return getattr(self, key)
131
+        except AttributeError:
132
+            raise KeyError(key)
133
+
134
+
135
+class ConnectionStrings(OrderedDict):
136
+    """Collection of :class:`ConnectionString` for a relation.
137
+
138
+    :class:`ConnectionString` may be accessed as a dictionary
139
+    lookup by unit name, or more usefully by the master and
140
+    standbys attributes. Note that the dictionary lookup may
141
+    return None, when the database is not ready for use.
142
+    """
143
+    relname = None
144
+    relid = None
145
+
146
+    def __init__(self, relid):
147
+        super(ConnectionStrings, self).__init__()
148
+        self.relname = relid.split(':', 1)[0]
149
+        self.relid = relid
150
+        relations = context.Relations()
151
+        relation = relations[self.relname][relid]
152
+        for unit, reldata in relation.items():
153
+            self[unit] = _cs(reldata)
154
+
155
+    @property
156
+    def master(self):
157
+        """The :class:`ConnectionString` for the master, or None."""
158
+        relation = context.Relations()[self.relname][self.relid]
159
+        masters = [unit for unit, reldata in relation.items()
160
+                   if reldata.get('state') in ('master', 'standalone')]
161
+        if len(masters) == 1:
162
+            return self[masters[0]]  # One, and only one.
163
+        else:
164
+            # None ready, or multiple due to failover in progress.
165
+            return None
166
+
167
+    @property
168
+    def standbys(self):
169
+        """Iteration of :class:`ConnectionString` for active hot standbys."""
170
+        relation = context.Relations()[self.relname][self.relid]
171
+        for unit, reldata in relation.items():
172
+            if reldata.get('state') == 'hot standby':
173
+                conn_str = self[unit]
174
+                if conn_str:
175
+                    yield conn_str
176
+
177
+
178
+class PostgreSQLClient(RelationBase):
179
+    """
180
+    PostgreSQL client interface.
181
+
182
+    A client may be related to one or more PostgreSQL services.
183
+
184
+    In most cases, a charm will only use a single PostgreSQL
185
+    service being related for each relation defined in metadata.yaml
186
+    (so one per relation name). To access the connection strings, use
187
+    the master and standbys attributes::
188
+
189
+        @when('productdb.master.available')
190
+        def setup_database(pgsql):
191
+            conn_str = pgsql.master  # A ConnectionString.
192
+            update_db_conf(conn_str)
193
+
194
+        @when('productdb.standbys.available')
195
+        def setup_cache_databases(pgsql):
196
+            set_cache_db_list(pgsql.standbys)  # set of ConnectionString.
197
+
198
+    In somecases, a relation name may be related to several PostgreSQL
199
+    services. You can also access the ConnectionStrings for a particular
200
+    service by relation id or by iterating over all of them::
201
+
202
+        @when('db.master.available')
203
+        def set_dbs(pgsql):
204
+            update_monitored_dbs(cs.master
205
+                                 for cs in pgsql  # ConnectionStrings.
206
+                                 if cs.master)
207
+    """
208
+    scope = scopes.SERVICE
209
+
210
+    @hook('{requires:pgsql}-relation-joined')
211
+    def joined(self):
212
+        # There is at least one named relation
213
+        self.set_state('{relation_name}.connected')
214
+        hookenv.log('Joined {} relation'.format(hookenv.relation_id()))
215
+
216
+    @hook('{requires:pgsql}-relation-{joined,changed,departed}')
217
+    def changed(self):
218
+        relid = hookenv.relation_id()
219
+        cs = self[relid]
220
+
221
+        # There is a master in this relation.
222
+        self.toggle_state('{relation_name}.master.available',
223
+                          cs.master)
224
+
225
+        # There is at least one standby in this relation.
226
+        self.toggle_state('{relation_name}.standbys.available',
227
+                          cs.standbys)
228
+
229
+        # There is at least one database in this relation.
230
+        self.toggle_state('{relation_name}.database.available',
231
+                          cs.master or cs.standbys)
232
+
233
+        # Ideally, we could turn logging off using a layer option
234
+        # but that is not available for interfaces.
235
+        if cs.master and cs.standbys:
236
+            hookenv.log('Relation {} has master and standby '
237
+                        'databases available'.format(relid))
238
+        elif cs.master:
239
+            hookenv.log('Relation {} has a master database available, '
240
+                        'but no standbys'.format(relid))
241
+        elif cs.standbys:
242
+            hookenv.log('Relation {} only has standby databases '
243
+                        'available'.format(relid))
244
+        else:
245
+            hookenv.log('Relation {} has no databases available'.format(relid))
246
+
247
+    @hook('{requires:pgsql}-relation-departed')
248
+    def departed(self):
249
+        if not any(u for u in hookenv.related_units() or []
250
+                   if u != hookenv.remote_unit()):
251
+            self.remove_state('{relation_name}.connected')
252
+            self.conversation().depart()
253
+            hookenv.log('Departed {} relation'.format(hookenv.relation_id()))
254
+
255
+    def set_database(self, dbname, relid=None):
256
+        """Set the database that the named relations connect to.
257
+
258
+        The PostgreSQL service will create the database if necessary. It
259
+        will never remove it.
260
+
261
+        :param dbname: The database name. If unspecified, the local service
262
+                       name is used.
263
+
264
+        :param relid: relation id to send the database name setting to.
265
+                      If unset, the setting is broadcast to all relations
266
+                      sharing the relation name.
267
+
268
+        """
269
+        for c in self.conversations():
270
+            if relid is None or c.namespace == relid:
271
+                c.set_remote('database', dbname)
272
+
273
+    def set_roles(self, roles, relid=None):
274
+        """Provide a set of roles to be granted to the database user.
275
+
276
+        Granting permissions to roles allows you to grant database
277
+        access to other charms.
278
+
279
+        The PostgreSQL service will create the roles if necessary.
280
+        """
281
+        if isinstance(roles, str):
282
+            roles = [roles]
283
+        roles = ','.join(sorted(roles))
284
+        for c in self.conversations():
285
+            if relid is None or c.namespace == relid:
286
+                c.set_remote('roles', roles)
287
+
288
+    def set_extensions(self, extensions, relid=None):
289
+        """Provide a set of extensions to be installed into the database.
290
+
291
+        The PostgreSQL service will attempt to install the requested
292
+        extensions into the database. Extensions not bundled with
293
+        PostgreSQL are normally installed onto the PostgreSQL service
294
+        using the `extra_packages` config setting.
295
+        """
296
+        if isinstance(extensions, str):
297
+            extensions = [extensions]
298
+        extensions = ','.join(sorted(extensions))
299
+        for c in self.conversations():
300
+            if relid is None or c.namespace == relid:
301
+                c.set_remote('extensions', extensions)
302
+
303
+    def __getitem__(self, relid):
304
+        """:returns: :class:`ConnectionStrings` for the relation id."""
305
+        return ConnectionStrings(relid)
306
+
307
+    def __iter__(self):
308
+        """:returns: Iterator of :class:`ConnectionStrings` for this named
309
+                     relation, one per relation id.
310
+        """
311
+        return iter(self[relid]
312
+                    for relid in context.Relations()[self.relation_name])
313
+
314
+    @property
315
+    def master(self):
316
+        ''':class:`ConnectionString` to the master, or None.
317
+
318
+        If multiple PostgreSQL services are related using this relation
319
+        name then the first master found is returned.
320
+        '''
321
+        for cs in self:
322
+            if cs.master:
323
+                return cs.master
324
+
325
+    @property
326
+    def standbys(self):
327
+        '''Set of class:`ConnectionString` to the read-only hot standbys.
328
+
329
+        If multiple PostgreSQL services are related using this relation
330
+        name then all standbys found are returned.
331
+        '''
332
+        return set(itertools.chain(*(cs.standbys for cs in self)))
333
+
334
+    def connection_string(self, unit=None):
335
+        ''':class:`ConnectionString` to the remote unit, or None.
336
+
337
+        unit defaults to the active remote unit.
338
+
339
+        You should normally use the master or standbys attributes rather
340
+        than this method.
341
+
342
+        If the unit is related multiple times using the same relation
343
+        name, the first one found is returned.
344
+        '''
345
+        if unit is None:
346
+            unit = hookenv.remote_unit()
347
+
348
+        relations = context.Relations()
349
+        found = False
350
+        for relation in relations[self.relation_name].values():
351
+            if unit in relation:
352
+                found = True
353
+                conn_str = _cs(relation[unit])
354
+                if conn_str:
355
+                    return conn_str
356
+        if found:
357
+            return None  # unit found, but not yet ready.
358
+        raise LookupError(unit)  # unit not related.
359
+
360
+
361
+def _cs(reldata):
362
+    """Generate a ConnectionString from :class:``context.RelationData``"""
363
+    if not reldata:
364
+        return None
365
+    d = dict(host=reldata.get('host'),
366
+             port=reldata.get('port'),
367
+             dbname=reldata.get('database'),
368
+             user=reldata.get('user'),
369
+             password=reldata.get('password'))
370
+    if not all(d.values()):
371
+        return None
372
+    local_unit = hookenv.local_unit()
373
+    if local_unit not in reldata.get('allowed-units', '').split():
374
+        return None  # Not yet authorized
375
+    locdata = context.Relations()[reldata.relname][reldata.relid].local
376
+    if 'database' in locdata and locdata['database'] != d['dbname']:
377
+        return None  # Requested database does not match yet
378
+    if 'roles' in locdata and locdata['roles'] != reldata.get('roles'):
379
+        return None  # Requested roles have not yet been assigned
380
+    if 'extensions' in locdata and (locdata['extensions'] !=
381
+                                    reldata.get('extensions')):
382
+        return None  # Requested extensions have not yet been installed
383
+    return ConnectionString(**d)
Back to file index

hooks/relations/pgsql/setup.py

 1
--- 
 2
+++ hooks/relations/pgsql/setup.py
 3
@@ -0,0 +1,20 @@
 4
+# setup.py for readthedocs. We need this to install into a venv so we
 5
+# can pull in dependencies.
 6
+from setuptools import setup
 7
+
 8
+import os.path
 9
+
10
+if os.path.exists('test_requirements.txt'):
11
+    reqs = open('test_requirements.txt', 'r').read().splitlines()
12
+else:
13
+    reqs = []
14
+
15
+
16
+if __name__ == '__main__':
17
+    setup(name='interface-pgsql',
18
+        version='1.0.0',
19
+        author='Stuart Bishop',
20
+        author_email='stuart.bishop@canonical.com',
21
+        license='GPL3',
22
+        py_modules=['requires'],
23
+        install_requires=reqs)
Back to file index

hooks/relations/pgsql/test_requirements.txt

 1
--- 
 2
+++ hooks/relations/pgsql/test_requirements.txt
 3
@@ -0,0 +1,10 @@
 4
+# Test-only dependencies are unpinned.
 5
+#
 6
+coverage>=3.6
 7
+mock>=1.0.1
 8
+nose>=1.3.1
 9
+flake8
10
+PyYAML
11
+sphinx
12
+charms.reactive
13
+charmhelpers
Back to file index

hooks/relations/pgsql/tox.ini

 1
--- 
 2
+++ hooks/relations/pgsql/tox.ini
 3
@@ -0,0 +1,30 @@
 4
+[tox]
 5
+envlist=lint,py3,docs
 6
+skipsdist = True
 7
+minversion = 2.3
 8
+sitepackages = False
 9
+
10
+[testenv]
11
+basepython=python3
12
+
13
+[testenv:py3]
14
+deps =
15
+    charmhelpers
16
+    charms.reactive
17
+    pytest
18
+commands = py.test {posargs:-v unit_tests/}
19
+
20
+[testenv:lint]
21
+deps =
22
+    flake8
23
+commands =
24
+    flake8 --ignore E402 *.py unit_tests/
25
+
26
+[testenv:docs]
27
+deps =
28
+    Sphinx
29
+    charmhelpers
30
+    charms.reactive
31
+whitelist_externals = make
32
+commands =
33
+    make -C docs html
Back to file index

hooks/relations/pgsql/unit_tests/main.pth

1
--- 
2
+++ hooks/relations/pgsql/unit_tests/main.pth
3
@@ -0,0 +1 @@
4
+..
Back to file index

hooks/relations/pgsql/unit_tests/test_connectionstring.py

  1
--- 
  2
+++ hooks/relations/pgsql/unit_tests/test_connectionstring.py
  3
@@ -0,0 +1,176 @@
  4
+# Copyright 2016 Canonical Ltd.
  5
+#
  6
+# This file is part of the PostgreSQL Client Interface for Juju charms.reactive
  7
+# This program is free software: you can redistribute it and/or modify
  8
+# it under the terms of the GNU General Public License version 3, as
  9
+# published by the Free Software Foundation.
 10
+#
 11
+# This program is distributed in the hope that it will be useful, but
 12
+# WITHOUT ANY WARRANTY; without even the implied warranties of
 13
+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 14
+# PURPOSE.  See the GNU General Public License for more details.
 15
+#
 16
+# You should have received a copy of the GNU General Public License
 17
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 18
+
 19
+import os.path
 20
+import sys
 21
+import unittest
 22
+
 23
+sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
 24
+
 25
+from requires import ConnectionString
 26
+
 27
+
 28
+class TestConnectionString(unittest.TestCase):
 29
+    def setUp(self):
 30
+        self.params = dict(user='mememe',
 31
+                           password='secret',
 32
+                           host='10.9.8.7',
 33
+                           port='5432',
 34
+                           dbname='mydata',
 35
+                           sslmode='require',
 36
+                           connect_timeout='10')
 37
+
 38
+    def test_libpq_str(self):
 39
+        conn_str = ConnectionString(**self.params)
 40
+
 41
+        # It is a real str, usable from C extensions.
 42
+        self.assertTrue(isinstance(conn_str, str))
 43
+
 44
+        self.assertEqual(conn_str,
 45
+                         'connect_timeout=10 '
 46
+                         'dbname=mydata '
 47
+                         'host=10.9.8.7 '
 48
+                         'password=secret '
 49
+                         'port=5432 '
 50
+                         'sslmode=require '
 51
+                         'user=mememe')
 52
+
 53
+    def test_libpq_empty(self):
 54
+        conn_str = ConnectionString()
 55
+        self.assertEqual(conn_str, '')  # Default everything
 56
+
 57
+    def test_attrs(self):
 58
+        conn_str = ConnectionString(**self.params)
 59
+        for k, v in self.params.items():
 60
+            self.assertEqual(getattr(conn_str, k), v)
 61
+
 62
+    def test_quoting(self):
 63
+        params = dict(backslash=r"Back\slash",
 64
+                      quote="quote's")
 65
+        conn_str = ConnectionString(**params)
 66
+        self.assertEqual(conn_str,
 67
+                         r"backslash=Back\\slash "
 68
+                         r"quote=quote\'s")
 69
+
 70
+    def test_keys(self):
 71
+        conn_str = ConnectionString(**self.params)
 72
+        self.assertListEqual(sorted(conn_str.keys()),
 73
+                             sorted(self.params.keys()))
 74
+
 75
+    def test_items(self):
 76
+        conn_str = ConnectionString(**self.params)
 77
+        self.assertListEqual(sorted(conn_str.items()),
 78
+                             sorted(self.params.items()))
 79
+
 80
+    def test_values(self):
 81
+        conn_str = ConnectionString(**self.params)
 82
+        self.assertListEqual(sorted(conn_str.values()),
 83
+                             sorted(self.params.values()))
 84
+
 85
+    def test_getitem(self):
 86
+        conn_str = ConnectionString(**self.params)
 87
+        for k, v in self.params.items():
 88
+            self.assertEqual(conn_str[k], v)
 89
+
 90
+    def test_uri(self):
 91
+        conn_str = ConnectionString(**self.params)
 92
+        self.assertEqual(conn_str.uri,
 93
+                         'postgresql://mememe:secret@10.9.8.7:5432/mydata'
 94
+                         '?connect_timeout=10&sslmode=require')
 95
+
 96
+    def test_uri_no_user(self):
 97
+        del self.params['user']
 98
+        conn_str = ConnectionString(**self.params)
 99
+        self.assertEqual(conn_str.uri,
100
+                         'postgresql://10.9.8.7:5432/mydata'
101
+                         '?connect_timeout=10&sslmode=require')
102
+
103
+    def test_uri_no_password(self):
104
+        del self.params['password']
105
+        conn_str = ConnectionString(**self.params)
106
+        self.assertEqual(conn_str.uri,
107
+                         'postgresql://mememe@10.9.8.7:5432/mydata'
108
+                         '?connect_timeout=10&sslmode=require')
109
+
110
+    def test_uri_no_host(self):
111
+        del self.params['host']
112
+        conn_str = ConnectionString(**self.params)
113
+        self.assertEqual(conn_str.uri,
114
+                         'postgresql://mememe:secret@:5432/mydata'
115
+                         '?connect_timeout=10&sslmode=require')
116
+
117
+    def test_uri_no_port(self):
118
+        del self.params['port']
119
+        conn_str = ConnectionString(**self.params)
120
+        self.assertEqual(conn_str.uri,
121
+                         'postgresql://mememe:secret@10.9.8.7/mydata'
122
+                         '?connect_timeout=10&sslmode=require')
123
+
124
+    def test_uri_no_dbname(self):
125
+        del self.params['dbname']
126
+        conn_str = ConnectionString(**self.params)
127
+        self.assertEqual(conn_str.uri,
128
+                         'postgresql://mememe:secret@10.9.8.7:5432'
129
+                         '?connect_timeout=10&sslmode=require')
130
+
131
+    def test_uri_no_extras(self):
132
+        del self.params['connect_timeout']
133
+        conn_str = ConnectionString(**self.params)
134
+        self.assertEqual(conn_str.uri,
135
+                         'postgresql://mememe:secret@10.9.8.7:5432/mydata'
136
+                         '?sslmode=require')
137
+        del self.params['sslmode']
138
+        conn_str = ConnectionString(**self.params)
139
+        self.assertEqual(conn_str.uri,
140
+                         'postgresql://mememe:secret@10.9.8.7:5432/mydata')
141
+
142
+    def test_uri_empty(self):
143
+        conn_str = ConnectionString()
144
+        self.assertEqual(conn_str.uri, 'postgresql://')  # Default everything
145
+
146
+    def test_uri_quoting(self):
147
+        params = dict(user='fred?',
148
+                      password="secret's",
149
+                      sslmode="&")
150
+        conn_str = ConnectionString(**params)
151
+        self.assertEqual(conn_str.uri,
152
+                         "postgresql://fred%3F:secret%27s@?sslmode=%26")
153
+
154
+    def test_uri_ipv6(self):
155
+        self.params['host'] = '2001:db8::1234'
156
+        conn_str = ConnectionString(**self.params)
157
+        self.assertEqual(conn_str.uri,
158
+                         'postgresql://mememe:secret@[2001:db8::1234]:5432'
159
+                         '/mydata?connect_timeout=10&sslmode=require')
160
+
161
+    def test_uri_hostname(self):
162
+        self.params['host'] = 'hname'
163
+        conn_str = ConnectionString(**self.params)
164
+        self.assertEqual(conn_str.uri,
165
+                         'postgresql://mememe:secret@hname:5432/mydata'
166
+                         '?connect_timeout=10&sslmode=require')
167
+
168
+    def test_uri_hostaddr(self):
169
+        # hostaddr is prefered over host in the URI, so it behaves the
170
+        # same as the libpq connection string. We could set host to a
171
+        # name and hostaddr to the ip addres and it should work
172
+        # (but we won't, because there will be tools that will construct
173
+        # their own connection strings and don't know about hostaddr
174
+        self.params['host'] = 'unit_0'
175
+        self.params['hostaddr'] = '10.0.1.2'
176
+        conn_str = ConnectionString(**self.params)
177
+        self.assertEqual(conn_str.uri,
178
+                         'postgresql://mememe:secret@10.0.1.2:5432/mydata'
179
+                         '?connect_timeout=10&sslmode=require')
Back to file index

hooks/relations/pgsql/unit_tests/test_requires.py

 1
--- 
 2
+++ hooks/relations/pgsql/unit_tests/test_requires.py
 3
@@ -0,0 +1,73 @@
 4
+# Copyright 2016 Canonical Ltd.
 5
+#
 6
+# This file is part of the PostgreSQL Client Interface for Juju charms.reactive
 7
+# This program is free software: you can redistribute it and/or modify
 8
+# it under the terms of the GNU General Public License version 3, as
 9
+# published by the Free Software Foundation.
10
+#
11
+# This program is distributed in the hope that it will be useful, but
12
+# WITHOUT ANY WARRANTY; without even the implied warranties of
13
+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14
+# PURPOSE.  See the GNU General Public License for more details.
15
+#
16
+# You should have received a copy of the GNU General Public License
17
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
+
19
+from collections import UserDict
20
+import os.path
21
+import sys
22
+import unittest
23
+from unittest.mock import patch
24
+
25
+sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
26
+
27
+import requires
28
+from requires import ConnectionString
29
+
30
+
31
+class TestConnectionStringConstructor(unittest.TestCase):
32
+    def setUp(self):
33
+        self.reldata = UserDict({'allowed-units': 'client/0 client/9 client/8',
34
+                                 'host': '10.9.8.7',
35
+                                 'port': '5433',
36
+                                 'database': 'mydata',
37
+                                 'user': 'mememe',
38
+                                 'password': 'secret'})
39
+        self.reldata.relname = 'relname'
40
+        self.reldata.relid = 'relname:42'
41
+
42
+        local_unit = self.patch('charmhelpers.core.hookenv.local_unit')
43
+        local_unit.return_value = 'client/9'
44
+
45
+        rels = self.patch('charmhelpers.context.Relations')
46
+        rels()['relname']['relname:42'].local = {'database': 'mydata'}
47
+
48
+    def patch(self, dotpath):
49
+        patcher = patch(dotpath, autospec=True)
50
+        mock = patcher.start()
51
+        self.addCleanup(patcher.stop)
52
+        return mock
53
+
54
+    def test_normal(self):
55
+        conn_str = requires._cs(self.reldata)
56
+        self.assertIsNotNone(conn_str)
57
+        self.assertIsInstance(conn_str, ConnectionString)
58
+        self.assertEqual(conn_str,
59
+                         'dbname=mydata host=10.9.8.7 password=secret '
60
+                         'port=5433 user=mememe')
61
+
62
+    def test_missing_attr(self):
63
+        del self.reldata['port']
64
+        self.assertIsNone(requires._cs(self.reldata))
65
+
66
+    def test_incorrect_database(self):
67
+        self.reldata['database'] = 'notherdb'
68
+        self.assertIsNone(requires._cs(self.reldata))
69
+
70
+    def test_unauthorized(self):
71
+        self.reldata['allowed-units'] = 'client/90'
72
+        self.assertIsNone(requires._cs(self.reldata))
73
+
74
+    def test_no_auth(self):
75
+        del self.reldata['allowed-units']
76
+        self.assertIsNone(requires._cs(self.reldata))
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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
26
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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 $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 $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
17
+# and $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,297 @@
  4
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
  5
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
  6
+
  7
+<svg
  8
+   xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
  9
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
 10
+   xmlns:cc="http://creativecommons.org/ns#"
 11
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 12
+   xmlns:svg="http://www.w3.org/2000/svg"
 13
+   xmlns="http://www.w3.org/2000/svg"
 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="icon.svg">
 22
+  <defs
 23
+     id="defs6519">
 24
+    <linearGradient
 25
+       id="Background">
 26
+      <stop
 27
+         id="stop4178"
 28
+         offset="0"
 29
+         style="stop-color:#b8b8b8;stop-opacity:1" />
 30
+      <stop
 31
+         id="stop4180"
 32
+         offset="1"
 33
+         style="stop-color:#c9c9c9;stop-opacity:1" />
 34
+    </linearGradient>
 35
+    <filter
 36
+       style="color-interpolation-filters:sRGB;"
 37
+       inkscape:label="Inner Shadow"
 38
+       id="filter1121">
 39
+      <feFlood
 40
+         flood-opacity="0.59999999999999998"
 41
+         flood-color="rgb(0,0,0)"
 42
+         result="flood"
 43
+         id="feFlood1123" />
 44
+      <feComposite
 45
+         in="flood"
 46
+         in2="SourceGraphic"
 47
+         operator="out"
 48
+         result="composite1"
 49
+         id="feComposite1125" />
 50
+      <feGaussianBlur
 51
+         in="composite1"
 52
+         stdDeviation="1"
 53
+         result="blur"
 54
+         id="feGaussianBlur1127" />
 55
+      <feOffset
 56
+         dx="0"
 57
+         dy="2"
 58
+         result="offset"
 59
+         id="feOffset1129" />
 60
+      <feComposite
 61
+         in="offset"
 62
+         in2="SourceGraphic"
 63
+         operator="atop"
 64
+         result="composite2"
 65
+         id="feComposite1131" />
 66
+    </filter>
 67
+    <filter
 68
+       style="color-interpolation-filters:sRGB;"
 69
+       inkscape:label="Drop Shadow"
 70
+       id="filter950">
 71
+      <feFlood
 72
+         flood-opacity="0.25"
 73
+         flood-color="rgb(0,0,0)"
 74
+         result="flood"
 75
+         id="feFlood952" />
 76
+      <feComposite
 77
+         in="flood"
 78
+         in2="SourceGraphic"
 79
+         operator="in"
 80
+         result="composite1"
 81
+         id="feComposite954" />
 82
+      <feGaussianBlur
 83
+         in="composite1"
 84
+         stdDeviation="1"
 85
+         result="blur"
 86
+         id="feGaussianBlur956" />
 87
+      <feOffset
 88
+         dx="0"
 89
+         dy="1"
 90
+         result="offset"
 91
+         id="feOffset958" />
 92
+      <feComposite
 93
+         in="SourceGraphic"
 94
+         in2="offset"
 95
+         operator="over"
 96
+         result="composite2"
 97
+         id="feComposite960" />
 98
+    </filter>
 99
+    <clipPath
100
+       clipPathUnits="userSpaceOnUse"
101
+       id="clipPath873">
102
+      <g
103
+         transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"
104
+         id="g875"
105
+         inkscape:label="Layer 1"
106
+         style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">
107
+        <path
108
+           style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"
109
+           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"
110
+           id="path877"
111
+           inkscape:connector-curvature="0"
112
+           sodipodi:nodetypes="sssssssss" />
113
+      </g>
114
+    </clipPath>
115
+    <filter
116
+       inkscape:collect="always"
117
+       id="filter891"
118
+       inkscape:label="Badge Shadow">
119
+      <feGaussianBlur
120
+         inkscape:collect="always"
121
+         stdDeviation="0.71999962"
122
+         id="feGaussianBlur893" />
123
+    </filter>
124
+  </defs>
125
+  <sodipodi:namedview
126
+     id="base"
127
+     pagecolor="#ffffff"
128
+     bordercolor="#666666"
129
+     borderopacity="1.0"
130
+     inkscape:pageopacity="0.0"
131
+     inkscape:pageshadow="2"
132
+     inkscape:zoom="4.0745362"
133
+     inkscape:cx="-24.435003"
134
+     inkscape:cy="49.018169"
135
+     inkscape:document-units="px"
136
+     inkscape:current-layer="layer3"
137
+     showgrid="true"
138
+     fit-margin-top="0"
139
+     fit-margin-left="0"
140
+     fit-margin-right="0"
141
+     fit-margin-bottom="0"
142
+     inkscape:window-width="1920"
143
+     inkscape:window-height="1041"
144
+     inkscape:window-x="0"
145
+     inkscape:window-y="17"
146
+     inkscape:window-maximized="1"
147
+     showborder="true"
148
+     showguides="true"
149
+     inkscape:guide-bbox="true"
150
+     inkscape:showpageshadow="false">
151
+    <inkscape:grid
152
+       type="xygrid"
153
+       id="grid821" />
154
+    <sodipodi:guide
155
+       orientation="1,0"
156
+       position="16,48"
157
+       id="guide823" />
158
+    <sodipodi:guide
159
+       orientation="0,1"
160
+       position="64,80"
161
+       id="guide825" />
162
+    <sodipodi:guide
163
+       orientation="1,0"
164
+       position="80,40"
165
+       id="guide827" />
166
+    <sodipodi:guide
167
+       orientation="0,1"
168
+       position="64,16"
169
+       id="guide829" />
170
+  </sodipodi:namedview>
171
+  <metadata
172
+     id="metadata6522">
173
+    <rdf:RDF>
174
+      <cc:Work
175
+         rdf:about="">
176
+        <dc:format>image/svg+xml</dc:format>
177
+        <dc:type
178
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
179
+        <dc:title></dc:title>
180
+      </cc:Work>
181
+    </rdf:RDF>
182
+  </metadata>
183
+  <g
184
+     inkscape:label="BACKGROUND"
185
+     inkscape:groupmode="layer"
186
+     id="layer1"
187
+     transform="translate(268,-635.29076)"
188
+     style="display:inline">
189
+    <path
190
+       style="fill:#eaeaea;fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"
191
+       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"
192
+       id="path6455"
193
+       inkscape:connector-curvature="0"
194
+       sodipodi:nodetypes="sssssssss" />
195
+  </g>
196
+  <g
197
+     inkscape:groupmode="layer"
198
+     id="layer3"
199
+     inkscape:label="Odoo"
200
+     style="display:inline">
201
+    <g
202
+       style="display:inline;fill:#888888;fill-rule:evenodd;stroke:none;stroke-width:1"
203
+       sketch:type="MSShapeGroup"
204
+       transform="matrix(0.23130271,0,0,0.23360203,49.270869,5.0121908)"
205
+       id="D">
206
+      <g
207
+         id="d-+-Rectangle-1" />
208
+    </g>
209
+    <g
210
+       id="g4335"
211
+       transform="matrix(0.76654968,0,0,0.75313581,11.810396,9.5818389)">
212
+      <path
213
+         d="m 151.13037,122.77027 c 0.002,0.19817 0.002,0.39653 0.002,0.59506 0,41.69749 -33.8025,75.5 -75.499997,75.5 -41.697498,0 -75.49999968,-33.80251 -75.49999968,-75.5 0,-41.697502 33.80250168,-75.500003 75.49999968,-75.500003 17.094365,0 32.861827,5.68113 45.517327,15.258325 l 0,-48.119574 C 121.14999,6.7175544 127.8639,0 136.14999,0 c 8.28427,0 15,6.723811 15,15.004078 l 0,106.991842 c 0,0.25971 -0.007,0.51788 -0.0196,0.77435 l 0,0 z m -75.497707,45.09506 c 24.576667,0 44.499997,-19.92333 44.499997,-44.5 0,-24.576675 -19.92333,-44.500003 -44.499997,-44.500003 -24.576671,0 -44.5,19.923328 -44.5,44.500003 0,24.57667 19.923329,44.5 44.5,44.5 z"
214
+         id="d"
215
+         inkscape:connector-curvature="0"
216
+         style="display:inline;fill:#888888;fill-rule:evenodd;stroke:none;stroke-width:1"
217
+         transform="matrix(0.23130271,0,0,0.23360203,49.270869,5.0121908)" />
218
+      <path
219
+         d="m 27.225193,51.039046 c 9.43768,0 17.088437,-7.815582 17.088437,-17.456587 0,-9.641011 -7.650757,-17.456593 -17.088437,-17.456593 -9.43769,0 -17.08845,7.815582 -17.08845,17.456593 0,9.641005 7.65076,17.456587 17.08845,17.456587 z m -0.15673,-7.097662 c 5.56261,0 10.07199,-4.60653 10.07199,-10.288984 0,-5.682443 -4.50938,-10.288979 -10.07199,-10.288979 -5.56261,0 -10.072,4.606536 -10.072,10.288979 0,5.682454 4.50939,10.288984 10.072,10.288984 z"
220
+         id="Oval-1"
221
+         sketch:type="MSShapeGroup"
222
+         inkscape:connector-curvature="0"
223
+         style="display:inline;fill:#9c5789;fill-rule:evenodd;stroke:none;stroke-width:1" />
224
+      <path
225
+         d="m 66.620355,91.35163 c 9.57324,0 17.33387,-7.870521 17.33387,-17.579296 0,-9.708779 -7.76063,-17.579298 -17.33387,-17.579298 -9.57323,0 -17.33387,7.870519 -17.33387,17.579298 0,9.708775 7.76064,17.579296 17.33387,17.579296 z m 0,-7.217988 c 5.6425,0 10.21665,-4.638917 10.21665,-10.361308 0,-5.722393 -4.57415,-10.361308 -10.21665,-10.361308 -5.6425,0 -10.21665,4.638915 -10.21665,10.361308 0,5.722391 4.57415,10.361308 10.21665,10.361308 z"
226
+         id="Oval-2"
227
+         sketch:type="MSShapeGroup"
228
+         inkscape:connector-curvature="0"
229
+         style="fill:#888888;fill-rule:evenodd;stroke:none;stroke-width:1" />
230
+      <path
231
+         d="m 26.923723,91.505591 c 9.57324,0 17.33387,-7.870521 17.33387,-17.579296 0,-9.708779 -7.76063,-17.579298 -17.33387,-17.579298 -9.57323,0 -17.3338696,7.870519 -17.3338696,17.579298 0,9.708775 7.7606396,17.579296 17.3338696,17.579296 z m 0,-7.217988 c 5.6425,0 10.21665,-4.638917 10.21665,-10.361308 0,-5.722393 -4.57415,-10.361308 -10.21665,-10.361308 -5.6425,0 -10.21665,4.638915 -10.21665,10.361308 0,5.722391 4.57415,10.361308 10.21665,10.361308 z"
232
+         id="Oval-2-3"
233
+         sketch:type="MSShapeGroup"
234
+         inkscape:connector-curvature="0"
235
+         style="display:inline;fill:#888888;fill-rule:evenodd;stroke:none;stroke-width:1" />
236
+    </g>
237
+  </g>
238
+  <g
239
+     inkscape:groupmode="layer"
240
+     id="layer2"
241
+     inkscape:label="BADGE"
242
+     style="display:none"
243
+     sodipodi:insensitive="true">
244
+    <g
245
+       style="display:inline"
246
+       transform="translate(-340.00001,-581)"
247
+       id="g4394"
248
+       clip-path="none">
249
+      <g
250
+         id="g855">
251
+        <g
252
+           inkscape:groupmode="maskhelper"
253
+           id="g870"
254
+           clip-path="url(#clipPath873)"
255
+           style="opacity:0.6;filter:url(#filter891)">
256
+          <circle
257
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"
258
+             id="path844"
259
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
260
+             cx="252"
261
+             cy="552.36218"
262
+             r="12" />
263
+        </g>
264
+        <g
265
+           id="g862">
266
+          <circle
267
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
268
+             id="path4398"
269
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)"
270
+             cx="252"
271
+             cy="552.36218"
272
+             r="12" />
273
+          <circle
274
+             transform="matrix(1.25,0,0,1.25,33,-100.45273)"
275
+             id="path4400"
276
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
277
+             cx="252"
278
+             cy="552.36218"
279
+             r="12" />
280
+          <path
281
+             sodipodi:type="star"
282
+             style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;enable-background:accumulate"
283
+             id="path4459"
284
+             sodipodi:sides="5"
285
+             sodipodi:cx="666.19574"
286
+             sodipodi:cy="589.50385"
287
+             sodipodi:r1="7.2431178"
288
+             sodipodi:r2="4.3458705"
289
+             sodipodi:arg1="1.0471976"
290
+             sodipodi:arg2="1.6755161"
291
+             inkscape:flatsided="false"
292
+             inkscape:rounded="0.1"
293
+             inkscape:randomized="0"
294
+             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 -0.18379,0.41279 0.0427,4.27917 -0.34859,4.5051 z"
295
+             transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />
296
+        </g>
297
+      </g>
298
+    </g>
299
+  </g>
300
+</svg>
Back to file index

layer.yaml

 1
--- 
 2
+++ layer.yaml
 3
@@ -0,0 +1,16 @@
 4
+"options":
 5
+  "apt":
 6
+    "packages":
 7
+    - "odoo"
 8
+  "odoo": {}
 9
+  "basic":
10
+    "use_venv": !!bool "false"
11
+    "packages": []
12
+    "include_system_packages": !!bool "false"
13
+"includes":
14
+- "layer:basic"
15
+- "layer:apt"
16
+- "interface:pgsql"
17
+- "interface:http"
18
+"repo": "https://github.com/credativUK/odoo-charm"
19
+"is": "odoo"
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,182 @@
  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 subprocess
 29
+
 30
+from charmhelpers import fetch
 31
+from charmhelpers.core import hookenv, unitdata
 32
+from charms import reactive
 33
+
 34
+
 35
+__all__ = ['add_source', 'update', 'queue_install', 'install_queued',
 36
+           'installed', 'purge', 'ensure_package_status']
 37
+
 38
+
 39
+def add_source(source, key=None):
 40
+    '''Add an apt source.
 41
+
 42
+    Sets the apt.needs_update state.
 43
+
 44
+    A source may be either a line that can be added directly to
 45
+    sources.list(5), or in the form ppa:<user>/<ppa-name> for adding
 46
+    Personal Package Archives, or a distribution component to enable.
 47
+
 48
+    The package signing key should be an ASCII armoured GPG key. While
 49
+    GPG key ids are also supported, the retrieval mechanism is insecure.
 50
+    There is no need to specify the package signing key for PPAs or for
 51
+    the main Ubuntu archives.
 52
+    '''
 53
+    # Maybe we should remember which sources have been added already
 54
+    # so we don't waste time re-adding them. Is this time significant?
 55
+    fetch.add_source(source, key)
 56
+    reactive.set_state('apt.needs_update')
 57
+
 58
+
 59
+def queue_install(packages, options=None):
 60
+    """Queue one or more deb packages for install.
 61
+
 62
+    The `apt.installed.{name}` state is set once the package is installed.
 63
+
 64
+    If a package has already been installed it will not be reinstalled.
 65
+
 66
+    If a package has already been queued it will not be requeued, and
 67
+    the install options will not be changed.
 68
+
 69
+    Sets the apt.queued_installs state.
 70
+    """
 71
+    if isinstance(packages, str):
 72
+        packages = [packages]
 73
+    # Filter installed packages.
 74
+    store = unitdata.kv()
 75
+    queued_packages = store.getrange('apt.install_queue.', strip=True)
 76
+    packages = {package: options for package in packages
 77
+                if not (package in queued_packages or
 78
+                        reactive.helpers.is_state('apt.installed.' + package))}
 79
+    if packages:
 80
+        unitdata.kv().update(packages, prefix='apt.install_queue.')
 81
+        reactive.set_state('apt.queued_installs')
 82
+
 83
+
 84
+def installed():
 85
+    '''Return the set of deb packages completed install'''
 86
+    return set(state.split('.', 2)[2] for state in reactive.bus.get_states()
 87
+               if state.startswith('apt.installed.'))
 88
+
 89
+
 90
+def purge(packages):
 91
+    """Purge one or more deb packages from the system"""
 92
+    fetch.apt_purge(packages, fatal=True)
 93
+    store = unitdata.kv()
 94
+    store.unsetrange(packages, prefix='apt.install_queue.')
 95
+    for package in packages:
 96
+        reactive.remove_state('apt.installed.{}'.format(package))
 97
+
 98
+
 99
+def update():
100
+    """Update the apt cache.
101
+
102
+    Removes the apt.needs_update state.
103
+    """
104
+    status_set(None, 'Updating apt cache')
105
+    fetch.apt_update(fatal=True)  # Friends don't let friends set fatal=False
106
+    reactive.remove_state('apt.needs_update')
107
+
108
+
109
+def install_queued():
110
+    '''Installs queued deb packages.
111
+
112
+    Removes the apt.queued_installs state and sets the apt.installed state.
113
+
114
+    On failure, sets the unit's workload state to 'blocked' and returns
115
+    False. Package installs remain queued.
116
+
117
+    On success, sets the apt.installed.{packagename} state for each
118
+    installed package and returns True.
119
+    '''
120
+    store = unitdata.kv()
121
+    queue = sorted((options, package)
122
+                   for package, options in store.getrange('apt.install_queue.',
123
+                                                          strip=True).items())
124
+
125
+    installed = set()
126
+    for options, batch in itertools.groupby(queue, lambda x: x[0]):
127
+        packages = [b[1] for b in batch]
128
+        try:
129
+            status_set(None, 'Installing {}'.format(','.join(packages)))
130
+            fetch.apt_install(packages, options, fatal=True)
131
+            store.unsetrange(packages, prefix='apt.install_queue.')
132
+            installed.update(packages)
133
+        except subprocess.CalledProcessError:
134
+            status_set('blocked',
135
+                       'Unable to install packages {}'
136
+                       .format(','.join(packages)))
137
+            return False  # Without setting reactive state.
138
+
139
+    for package in installed:
140
+        reactive.set_state('apt.installed.{}'.format(package))
141
+
142
+    reactive.remove_state('apt.queued_installs')
143
+    return True
144
+
145
+
146
+def ensure_package_status():
147
+    '''Hold or unhold packages per the package_status configuration option.
148
+
149
+    All packages installed using this module and handlers are affected.
150
+
151
+    An mechanism may be added in the future to override this for a
152
+    subset of installed packages.
153
+    '''
154
+    packages = installed()
155
+    if not packages:
156
+        return
157
+    config = hookenv.config()
158
+    package_status = config['package_status']
159
+    changed = reactive.helpers.data_changed('apt.package_status',
160
+                                            (package_status, sorted(packages)))
161
+    if changed:
162
+        if package_status == 'hold':
163
+            hookenv.log('Holding packages {}'.format(','.join(packages)))
164
+            fetch.apt_hold(packages)
165
+        else:
166
+            hookenv.log('Unholding packages {}'.format(','.join(packages)))
167
+            fetch.apt_unhold(packages)
168
+    reactive.remove_state('apt.needs_hold')
169
+
170
+
171
+def status_set(state, message):
172
+    """Set the unit's workload status.
173
+
174
+    Set state == None to keep the same state and just change the message.
175
+    """
176
+    if state is None:
177
+        state = hookenv.status_get()[0]
178
+        if state == 'unknown':
179
+            state = 'maintenance'  # Guess
180
+    if state in ('error', 'blocked'):
181
+        lvl = hookenv.WARNING
182
+    else:
183
+        lvl = hookenv.INFO
184
+    hookenv.status_set(state, message)
185
+    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('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,159 @@
  4
+import os
  5
+import sys
  6
+import shutil
  7
+import platform
  8
+from glob import glob
  9
+from subprocess import check_call
 10
+
 11
+from charms.layer.execd import execd_preinstall
 12
+
 13
+
 14
+def bootstrap_charm_deps():
 15
+    """
 16
+    Set up the base charm dependencies so that the reactive system can run.
 17
+    """
 18
+    # execd must happen first, before any attempt to install packages or
 19
+    # access the network, because sites use this hook to do bespoke
 20
+    # configuration and install secrets so the rest of this bootstrap
 21
+    # and the charm itself can actually succeed. This call does nothing
 22
+    # unless the operator has created and populated $CHARM_DIR/exec.d.
 23
+    execd_preinstall()
 24
+    # ensure that $CHARM_DIR/bin is on the path, for helper scripts
 25
+    os.environ['PATH'] += ':%s' % os.path.join(os.environ['CHARM_DIR'], 'bin')
 26
+    venv = os.path.abspath('../.venv')
 27
+    vbin = os.path.join(venv, 'bin')
 28
+    vpip = os.path.join(vbin, 'pip')
 29
+    vpy = os.path.join(vbin, 'python')
 30
+    if os.path.exists('wheelhouse/.bootstrapped'):
 31
+        from charms import layer
 32
+        cfg = layer.options('basic')
 33
+        if cfg.get('use_venv') and '.venv' not in sys.executable:
 34
+            # activate the venv
 35
+            os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
 36
+            reload_interpreter(vpy)
 37
+        return
 38
+    # bootstrap wheelhouse
 39
+    if os.path.exists('wheelhouse'):
 40
+        with open('/root/.pydistutils.cfg', 'w') as fp:
 41
+            # make sure that easy_install also only uses the wheelhouse
 42
+            # (see https://github.com/pypa/pip/issues/410)
 43
+            charm_dir = os.environ['CHARM_DIR']
 44
+            fp.writelines([
 45
+                "[easy_install]\n",
 46
+                "allow_hosts = ''\n",
 47
+                "find_links = file://{}/wheelhouse/\n".format(charm_dir),
 48
+            ])
 49
+        apt_install(['python3-pip', 'python3-setuptools', 'python3-yaml'])
 50
+        from charms import layer
 51
+        cfg = layer.options('basic')
 52
+        # include packages defined in layer.yaml
 53
+        apt_install(cfg.get('packages', []))
 54
+        # if we're using a venv, set it up
 55
+        if cfg.get('use_venv'):
 56
+            if not os.path.exists(venv):
 57
+                distname, version, series = platform.linux_distribution()
 58
+                if series in ('precise', 'trusty'):
 59
+                    apt_install(['python-virtualenv'])
 60
+                else:
 61
+                    apt_install(['virtualenv'])
 62
+                cmd = ['virtualenv', '-ppython3', '--never-download', venv]
 63
+                if cfg.get('include_system_packages'):
 64
+                    cmd.append('--system-site-packages')
 65
+                check_call(cmd)
 66
+            os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
 67
+            pip = vpip
 68
+        else:
 69
+            pip = 'pip3'
 70
+            # save a copy of system pip to prevent `pip3 install -U pip`
 71
+            # from changing it
 72
+            if os.path.exists('/usr/bin/pip'):
 73
+                shutil.copy2('/usr/bin/pip', '/usr/bin/pip.save')
 74
+        # need newer pip, to fix spurious Double Requirement error:
 75
+        # https://github.com/pypa/pip/issues/56
 76
+        check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse',
 77
+                    'pip'])
 78
+        # install the rest of the wheelhouse deps
 79
+        check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse'] +
 80
+                   glob('wheelhouse/*'))
 81
+        if not cfg.get('use_venv'):
 82
+            # restore system pip to prevent `pip3 install -U pip`
 83
+            # from changing it
 84
+            if os.path.exists('/usr/bin/pip.save'):
 85
+                shutil.copy2('/usr/bin/pip.save', '/usr/bin/pip')
 86
+                os.remove('/usr/bin/pip.save')
 87
+        os.remove('/root/.pydistutils.cfg')
 88
+        # flag us as having already bootstrapped so we don't do it again
 89
+        open('wheelhouse/.bootstrapped', 'w').close()
 90
+        # Ensure that the newly bootstrapped libs are available.
 91
+        # Note: this only seems to be an issue with namespace packages.
 92
+        # Non-namespace-package libs (e.g., charmhelpers) are available
 93
+        # without having to reload the interpreter. :/
 94
+        reload_interpreter(vpy if cfg.get('use_venv') else sys.argv[0])
 95
+
 96
+
 97
+def reload_interpreter(python):
 98
+    """
 99
+    Reload the python interpreter to ensure that all deps are available.
100
+
101
+    Newly installed modules in namespace packages sometimes seemt to
102
+    not be picked up by Python 3.
103
+    """
104
+    os.execle(python, python, sys.argv[0], os.environ)
105
+
106
+
107
+def apt_install(packages):
108
+    """
109
+    Install apt packages.
110
+
111
+    This ensures a consistent set of options that are often missed but
112
+    should really be set.
113
+    """
114
+    if isinstance(packages, (str, bytes)):
115
+        packages = [packages]
116
+
117
+    env = os.environ.copy()
118
+
119
+    if 'DEBIAN_FRONTEND' not in env:
120
+        env['DEBIAN_FRONTEND'] = 'noninteractive'
121
+
122
+    cmd = ['apt-get',
123
+           '--option=Dpkg::Options::=--force-confold',
124
+           '--assume-yes',
125
+           'install']
126
+    check_call(cmd + packages, env=env)
127
+
128
+
129
+def init_config_states():
130
+    import yaml
131
+    from charmhelpers.core import hookenv
132
+    from charms.reactive import set_state
133
+    from charms.reactive import toggle_state
134
+    config = hookenv.config()
135
+    config_defaults = {}
136
+    config_defs = {}
137
+    config_yaml = os.path.join(hookenv.charm_dir(), 'config.yaml')
138
+    if os.path.exists(config_yaml):
139
+        with open(config_yaml) as fp:
140
+            config_defs = yaml.safe_load(fp).get('options', {})
141
+            config_defaults = {key: value.get('default')
142
+                               for key, value in config_defs.items()}
143
+    for opt in config_defs.keys():
144
+        if config.changed(opt):
145
+            set_state('config.changed')
146
+            set_state('config.changed.{}'.format(opt))
147
+        toggle_state('config.set.{}'.format(opt), config.get(opt))
148
+        toggle_state('config.default.{}'.format(opt),
149
+                     config.get(opt) == config_defaults[opt])
150
+    hookenv.atexit(clear_config_states)
151
+
152
+
153
+def clear_config_states():
154
+    from charmhelpers.core import hookenv, unitdata
155
+    from charms.reactive import remove_state
156
+    config = hookenv.config()
157
+    remove_state('config.changed')
158
+    for opt in config.keys():
159
+        remove_state('config.changed.{}'.format(opt))
160
+        remove_state('config.set.{}'.format(opt))
161
+        remove_state('config.default.{}'.format(opt))
162
+    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
+    $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['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

metadata.yaml

 1
--- 
 2
+++ metadata.yaml
 3
@@ -0,0 +1,24 @@
 4
+description: |
 5
+  Odoo is a comprehensive open source management system. It has a large, active
 6
+  community, which has developed modules to handle all facets of company
 7
+  management.
 8
+  These include, amongst others: Sales Management, CRM, e-commerce,
 9
+  Manufacturing, Stock, Accounting, Human Resources, Project Management,
10
+  Logistics, Productivity and Document Management.
11
+  Odoo allows you to start easily with one module to fit a specific need then
12
+  add additional modules as and when you need them enabling you to have a
13
+  powerful feature rich Enterprise Resource Planner.
14
+maintainer: Ondřej Kuzník <ondrej.kuznik@credativ.co.uk>
15
+name: odoo
16
+provides:
17
+  website:
18
+    interface: http
19
+requires:
20
+  db:
21
+    interface: pgsql
22
+subordinate: false
23
+summary: Odoo ERP
24
+tags:
25
+- ecommerce
26
+- cms
27
+- social
Back to file index

reactive/apt.py

  1
--- 
  2
+++ reactive/apt.py
  3
@@ -0,0 +1,131 @@
  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
+    # TODO: Move this to charm-helpers.fetch
 59
+    cmd = ['dpkg-query', '--show', r'--showformat=${Package}\n']
 60
+    installed = set(subprocess.check_output(cmd,
 61
+                                            universal_newlines=True).split())
 62
+    return set(packages) - installed
 63
+
 64
+
 65
+def clear_removed_package_states():
 66
+    """On hook startup, clear install states for removed packages."""
 67
+    removed = filter_installed_packages(charms.apt.installed())
 68
+    if removed:
 69
+        hookenv.log('{} missing packages ({})'.format(len(removed),
 70
+                                                      ','.join(removed)),
 71
+                    WARNING)
 72
+        for package in removed:
 73
+            reactive.remove_state('apt.installed.{}'.format(package))
 74
+
 75
+
 76
+def configure_sources():
 77
+    """Add user specified package sources from the service configuration.
 78
+
 79
+    See charmhelpers.fetch.configure_sources for details.
 80
+    """
 81
+    config = hookenv.config()
 82
+
 83
+    # We don't have enums, so we need to validate this ourselves.
 84
+    package_status = config.get('package_status')
 85
+    if package_status not in ('hold', 'install'):
 86
+        charms.apt.status_set('blocked',
 87
+                              'Unknown package_status {}'
 88
+                              ''.format(package_status))
 89
+        # Die before further hooks are run. This isn't very nice, but
 90
+        # there is no other way to inform the operator that they have
 91
+        # invalid configuration.
 92
+        raise SystemExit(0)
 93
+
 94
+    sources = config.get('install_sources')
 95
+    keys = config.get('install_keys')
 96
+    if reactive.helpers.data_changed('apt.configure_sources', (sources, keys)):
 97
+        fetch.configure_sources(update=False,
 98
+                                sources_var='install_sources',
 99
+                                keys_var='install_keys')
100
+        reactive.set_state('apt.needs_update')
101
+
102
+    extra_packages = sorted(config.get('extra_packages', '').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
+    reactive._apt_registered = True
Back to file index

reactive/odoo.py

 1
--- 
 2
+++ reactive/odoo.py
 3
@@ -0,0 +1,77 @@
 4
+import os
 5
+import os.path
 6
+import shutil
 7
+
 8
+from charmhelpers.core.hookenv import config, log, open_port, status_set
 9
+from charmhelpers.core.host import (
10
+        service_pause,
11
+        service_restart,
12
+        service_resume,
13
+        service_running,
14
+)
15
+from charmhelpers.core.templating import render
16
+from charms.reactive import (
17
+        set_state,
18
+        when,
19
+        when_not,
20
+)
21
+
22
+
23
+@when('apt.installed.odoo')
24
+@when_not('odoo.installed')
25
+def install_odoo():
26
+    for path in ['/opt/odoo', '/opt/odoo/data', '/opt/odoo/addons']:
27
+        if not os.path.exists(path):
28
+            os.mkdir(path, mode=0o755)
29
+            shutil.chown(path, 'odoo')
30
+    render(source='odoo.unit',
31
+           target='/etc/systemd/system/odoo.service',
32
+           context={
33
+                'conf': config(),
34
+           })
35
+    service_pause('odoo')
36
+    set_state('odoo.installed')
37
+
38
+
39
+@when('odoo.installed')
40
+@when_not('db.master.available')
41
+def blocked():
42
+    status_set('blocked', 'Please link to a PostgreSQL service')
43
+
44
+
45
+@when('db.connected')
46
+def request_db(pgsql):
47
+    pgsql.set_database(config('dbname'))
48
+
49
+
50
+@when('odoo.installed', 'db.master.available')
51
+@when_not('odoo.ready')
52
+def update_conf(psql):
53
+    render(source='odoo.conf',
54
+           target='/etc/odoo/openerp-server.conf',
55
+           owner='odoo',
56
+           perms=0o400,
57
+           context={
58
+               'db': psql.master,
59
+               'conf': config(),
60
+           })
61
+    service_resume('odoo')
62
+    service_restart('odoo')
63
+    log('Exposing Odoo on port %d' % config('port'), 'DEBUG')
64
+    open_port(config('port'))
65
+    set_state('odoo.ready')
66
+
67
+
68
+@when('website.available', 'odoo.ready')
69
+@when_not('odoo.website.configured')
70
+def configure_website(website):
71
+    website.configure(port=config('port'))
72
+    set_state('odoo.website.configured')
73
+
74
+
75
+@when('odoo.ready')
76
+def update_status():
77
+    if service_running('odoo'):
78
+        status_set('active', 'Odoo is running')
79
+    else:
80
+        status_set('blocked', 'Odoo has stopped')
Back to file index

requirements.txt

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

revision

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

templates/odoo.conf

 1
--- 
 2
+++ templates/odoo.conf
 3
@@ -0,0 +1,12 @@
 4
+[options]
 5
+without_demo = True
 6
+addons_path = /opt/odoo/addons
 7
+data_dir = /opt/odoo/data
 8
+
 9
+xmlrpc_port = {{ conf.port }}
10
+
11
+db_name = {{ db.dbname }}
12
+db_user = {{ db.user }}
13
+db_password = {{ db.password }}
14
+db_host = {{ db.host }}
15
+db_port = {{ db.port }}
Back to file index

templates/odoo.unit

 1
--- 
 2
+++ templates/odoo.unit
 3
@@ -0,0 +1,9 @@
 4
+[Unit]
 5
+Description=Odoo server
 6
+
 7
+[Service]
 8
+User=odoo
 9
+ExecStart=/usr/bin/odoo --config /etc/odoo/openerp-server.conf --logfile /var/log/odoo/odoo-server.log
10
+
11
+[Install]
12
+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,40 @@
 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
+        self.port = 8123
15
+
16
+        self.d.add('postgresql')
17
+        self.d.add('odoo')
18
+        self.d.configure('odoo', {'port': self.port})
19
+        self.d.relate('odoo:db', 'postgresql:db-admin')
20
+        self.d.expose('odoo')
21
+
22
+        self.d.setup(timeout=900)
23
+        self.d.sentry.wait()
24
+
25
+        self.unit = self.d.sentry['odoo'][0]
26
+
27
+    def test_service(self):
28
+        '''Tests Odoo is alive, jsonrpc requires at least Odoo 8.0'''
29
+
30
+        url = 'http://{}:{}'.format(self.unit.info['public-address'],
31
+                                    self.port)
32
+        params = {
33
+            'service': 'common',
34
+            'method': 'version',
35
+            'args': {},
36
+        }
37
+
38
+        response = requests.get(url, json={'params': params})
39
+        self.assertEqual(response.status_code, 200)
40
+
41
+        result = response.json()['result']
42
+        self.assertIn('server_version_info', result)
43
+        self.assertIsInstance(result['server_version_info'], list)
Back to file index

tox.ini

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