--- /dev/null
+{
+ "layers": [
+ "layer:basic",
+ "vpe-router",
+ "build"
+ ],
+ "signatures": {
+ "wheelhouse/ecdsa-0.13.tar.gz": [
+ "vpe-router",
+ "dynamic",
+ "64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa"
+ ],
+ "hooks/leader-settings-changed": [
+ "layer:basic",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "deps/layer/layer-basic/copyright": [
+ "vpe-router",
+ "static",
+ "1e2afbd75c71affa132ae7ee3327cb29b5e4b9d9705f27dfd03857c326f50c5c"
+ ],
+ "deps/layer/layer-basic/Makefile": [
+ "vpe-router",
+ "static",
+ "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301"
+ ],
+ "lib/charms/layer/execd.py": [
+ "layer:basic",
+ "static",
+ "c0eb4c90e93a14f04d851f78bc6592e093226321fbe328125958633397aed014"
+ ],
+ "hooks/leader-elected": [
+ "layer:basic",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "deps/layer/layer-basic/hooks/install": [
+ "vpe-router",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "Makefile": [
+ "layer:basic",
+ "static",
+ "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301"
+ ],
+ "reactive/__init__.py": [
+ "layer:basic",
+ "static",
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ ],
+ "deps/layer/layer-basic/hooks/stop": [
+ "vpe-router",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "wheelhouse/paramiko-1.16.3.tar.gz": [
+ "vpe-router",
+ "dynamic",
+ "97d932fdb4fec9aadf6bea368123f3ee15b92199f92eb62666370c7fed62d072"
+ ],
+ "bin/layer_option": [
+ "layer:basic",
+ "static",
+ "621b556cd208005e131e9f648859294347da9376609745a73ca2e808dd2032f9"
+ ],
+ ".build.manifest": [
+ "build",
+ "dynamic",
+ "unchecked"
+ ],
+ "actions/delete-corporation": [
+ "vpe-router",
+ "static",
+ "4f74675749113846e3602919aa7d536510f6995877474f5b377629a767792030"
+ ],
+ "deps/layer/layer-basic/lib/charms/layer/execd.py": [
+ "vpe-router",
+ "static",
+ "c0eb4c90e93a14f04d851f78bc6592e093226321fbe328125958633397aed014"
+ ],
+ "hooks/upgrade-charm": [
+ "layer:basic",
+ "static",
+ "8b9c153a61cc78067af8d016a8fa1c764caad3a40c82bb0b5121fc4c4c6a04f1"
+ ],
+ "wheelhouse/charms.reactive-0.4.4.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "97455ce944a85adb2ceb897af81a1b93746f43527e6c4ac44798b193132158c7"
+ ],
+ "deps/layer/layer-basic/wheelhouse.txt": [
+ "vpe-router",
+ "static",
+ "2f19b10e72d7299f4fc53c33caa0ad425ebcef59231238b49bc45b10ab49ae16"
+ ],
+ "wheelhouse/Tempita-0.5.2.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c"
+ ],
+ "actions.yaml": [
+ "vpe-router",
+ "dynamic",
+ "8124c33231f4b04673ec658b09e9fd393f1012dcf7bdeaf6ce23518b976c2b2a"
+ ],
+ "deps/layer/layer-basic/README.md": [
+ "vpe-router",
+ "static",
+ "83d8c3dbf1dd3be70ba9774a16780d130ebdcfc722d89d86ed596e7feac70c9f"
+ ],
+ "hooks/hook.template": [
+ "layer:basic",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "tox.ini": [
+ "layer:basic",
+ "static",
+ "00b57a776ac313e92a52daa05d1fdb6056c21b3105b643d32610fd937664321b"
+ ],
+ "metadata.yaml": [
+ "vpe-router",
+ "dynamic",
+ "0aaad2d64b51036f39d17b3120c38b498de270c6363db154477ea76d308e3709"
+ ],
+ "copyright": [
+ "layer:basic",
+ "static",
+ "1e2afbd75c71affa132ae7ee3327cb29b5e4b9d9705f27dfd03857c326f50c5c"
+ ],
+ "deps/layer/layer-basic/hooks/hook.template": [
+ "vpe-router",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "wheelhouse/PyYAML-3.12.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab"
+ ],
+ "wheelhouse/netaddr-0.7.18.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "a1f5c9fcf75ac2579b9995c843dade33009543c04f218ff7c007b3c81695bd19"
+ ],
+ "deps/layer/layer-basic/bin/layer_option": [
+ "vpe-router",
+ "static",
+ "621b556cd208005e131e9f648859294347da9376609745a73ca2e808dd2032f9"
+ ],
+ ".gitignore": [
+ "layer:basic",
+ "static",
+ "0da5c4dcda27cd6406e5bb81cbf68ddccaf728ac764ec15053a165c1449d87d9"
+ ],
+ "hooks/install": [
+ "layer:basic",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "wheelhouse/MarkupSafe-0.23.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "a4ec1aff59b95a14b45eb2e23761a0179e98319da5a7eb76b56ea8cdc7b871c3"
+ ],
+ "deps/layer/layer-basic/hooks/config-changed": [
+ "vpe-router",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "wheelhouse/pyaml-16.9.0.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "f8d394144d3bc5360cd48f54bfaa2d100a47040216d6f8703c17bd4b235e9bb3"
+ ],
+ "deps/layer/layer-basic/metadata.yaml": [
+ "vpe-router",
+ "static",
+ "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356"
+ ],
+ "hooks/config-changed": [
+ "layer:basic",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "hooks/stop": [
+ "layer:basic",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "wheelhouse/six-1.10.0.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a"
+ ],
+ "lib/charms/layer/basic.py": [
+ "layer:basic",
+ "static",
+ "32204327952d4ea4ef1063f45bdbe7be379a7a7b8220f3e5f2f5e11003b25654"
+ ],
+ "wheelhouse/pycrypto-2.6.1.tar.gz": [
+ "vpe-router",
+ "dynamic",
+ "f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c"
+ ],
+ "deps/layer/layer-basic/hooks/update-status": [
+ "vpe-router",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "lib/charms/router.py": [
+ "vpe-router",
+ "static",
+ "f9afb193fa59c6b23558e11b7f680bcff79f9567f474fa14ba3202c974fa0cc2"
+ ],
+ "deps/layer/layer-basic/hooks/upgrade-charm": [
+ "vpe-router",
+ "static",
+ "8b9c153a61cc78067af8d016a8fa1c764caad3a40c82bb0b5121fc4c4c6a04f1"
+ ],
+ "wheelhouse/Jinja2-2.8.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "bc1ff2ff88dbfacefde4ddde471d1417d3b304e8df103a7a9437d47269201bf4"
+ ],
+ "deps/layer/layer-basic/hooks/start": [
+ "vpe-router",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "actions/add-corporation": [
+ "vpe-router",
+ "static",
+ "e17238f57178836974b3dff358318ffefdacc54988fbf6099c454be06e7fcf92"
+ ],
+ "wheelhouse/pip-8.1.2.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "4d24b03ffa67638a3fa931c09fd9e0273ffa904e95ebebe7d4b1a54c93d7b732"
+ ],
+ "deps/layer/layer-basic/hooks/leader-settings-changed": [
+ "vpe-router",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "hooks/update-status": [
+ "layer:basic",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "deps/layer/layer-basic/lib/charms/layer/__init__.py": [
+ "vpe-router",
+ "static",
+ "4ffd74384bd81d737572acf2d14fe55431eab8f49d6212d9aabedf24e1d992b2"
+ ],
+ "deps/layer/layer-basic/.gitignore": [
+ "vpe-router",
+ "static",
+ "0da5c4dcda27cd6406e5bb81cbf68ddccaf728ac764ec15053a165c1449d87d9"
+ ],
+ "deps/layer/layer-basic/lib/charms/layer/basic.py": [
+ "vpe-router",
+ "static",
+ "32204327952d4ea4ef1063f45bdbe7be379a7a7b8220f3e5f2f5e11003b25654"
+ ],
+ "README.md": [
+ "vpe-router",
+ "static",
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ ],
+ "deps/layer/layer-basic/reactive/__init__.py": [
+ "vpe-router",
+ "static",
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ ],
+ "actions/configure-interface": [
+ "vpe-router",
+ "static",
+ "760002fbc2c7a666e0698f2e7179d5c2286151f0e4dd66dfb7465fafb050a55d"
+ ],
+ "deps/layer/layer-basic/tox.ini": [
+ "vpe-router",
+ "static",
+ "00b57a776ac313e92a52daa05d1fdb6056c21b3105b643d32610fd937664321b"
+ ],
+ "hooks/start": [
+ "layer:basic",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "deps/layer/layer-basic/hooks/leader-elected": [
+ "vpe-router",
+ "static",
+ "3a854a5fd180c5bacbb80b501d968a8d8bdc1468361e7123ea4022a1e0363c9a"
+ ],
+ "actions/connect-domains": [
+ "vpe-router",
+ "static",
+ "26e2825afa84388e333c09a21d3ffa1ca1830e0c4c9738bdb5ffe1c946672200"
+ ],
+ "reactive/vpe_router.py": [
+ "vpe-router",
+ "static",
+ "1f1312855d6b44d4f7578b3b3e43271ea15865ab6eced72b74ce6f87ca523030"
+ ],
+ "deps/layer/layer-basic/requirements.txt": [
+ "vpe-router",
+ "static",
+ "0f1c70d27e26005a96d66ad54482877ae20f7737693c833e29dd72bd6ac24892"
+ ],
+ "deps/layer/layer-basic/layer.yaml": [
+ "vpe-router",
+ "static",
+ "fbd3e3a22f3b8b1e7656e12f5db0ebf69c145b8e8b59c125e6faf043cba50b9b"
+ ],
+ "config.yaml": [
+ "vpe-router",
+ "dynamic",
+ "826c6a9af5ccee2dbcc0dd890f2c7dbc9ffee81946c2e3f3944cc612d6115f14"
+ ],
+ "layer.yaml": [
+ "vpe-router",
+ "dynamic",
+ "175d7cdddccf1f1e7b3df8caf7098a39d05000525c61d7e6eee1c81bc5d1966c"
+ ],
+ "lib/charms/layer/__init__.py": [
+ "layer:basic",
+ "static",
+ "4ffd74384bd81d737572acf2d14fe55431eab8f49d6212d9aabedf24e1d992b2"
+ ],
+ "wheelhouse/charmhelpers-0.9.1.tar.gz": [
+ "layer:basic",
+ "dynamic",
+ "60de745f3ff1424b9aa294c9d28d0b863f0827314331f330264ecc9a89142218"
+ ],
+ "actions/delete-domain-connection": [
+ "vpe-router",
+ "static",
+ "b7f42af9d853c5e69e691b7ee56691984730334436f88c97689318038940db4e"
+ ],
+ "requirements.txt": [
+ "layer:basic",
+ "static",
+ "0f1c70d27e26005a96d66ad54482877ae20f7737693c833e29dd72bd6ac24892"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+*.pyc
+*~
+.ropeproject
+.settings
+.tox
--- /dev/null
+#!/usr/bin/make
+
+all: lint unit_test
+
+
+.PHONY: clean
+clean:
+ @rm -rf .tox
+
+.PHONY: apt_prereqs
+apt_prereqs:
+ @# Need tox, but don't install the apt version unless we have to (don't want to conflict with pip)
+ @which tox >/dev/null || (sudo apt-get install -y python-pip && sudo pip install tox)
+
+.PHONY: lint
+lint: apt_prereqs
+ @tox --notest
+ @PATH=.tox/py34/bin:.tox/py35/bin flake8 $(wildcard hooks reactive lib unit_tests tests)
+ @charm proof
+
+.PHONY: unit_test
+unit_test: apt_prereqs
+ @echo Starting tests...
+ tox
--- /dev/null
+"configure-interface":
+ "description": "Configure an ethernet interface."
+ "params":
+ "iface-name":
+ "type": "string"
+ "description": "Device name, e.g. eth1"
+ "cidr":
+ "type": "string"
+ "description": "Network range to assign to the interface"
+ "required": ["iface-name"]
+"add-corporation":
+ "description": "Add a new corporation to the router"
+ "params":
+ "domain-name":
+ "type": "string"
+ "description": "Name of the vlan corporation"
+ "iface-name":
+ "type": "string"
+ "description": "Device name. eg eth1"
+ "vlan-id":
+ "type": "integer"
+ "description": "The name of the vlan?"
+ "cidr":
+ "type": "string"
+ "description": "Network range to assign to the tagged vlan-id"
+ "area":
+ "type": "string"
+ "description": "Link State Advertisements (LSA) type"
+ "subnet-cidr":
+ "type": "string"
+ "description": "Network range"
+ "subnet-area":
+ "type": "string"
+ "description": "Link State Advertisements (LSA) type"
+ "required": ["domain-name", "iface-name", "vlan-id", "cidr", "area", "subnet-cidr",
+ "subnet-area"]
+"delete-corporation":
+ "description": "Remove the corporation from the router completely"
+ "params":
+ "domain-name":
+ "type": "string"
+ "description": "The domain of the corporation to remove"
+ "cidr":
+ "type": "string"
+ "description": "Network range to assign to the tagged vlan-id"
+ "area":
+ "type": "string"
+ "description": "Link State Advertisements (LSA) type"
+ "subnet-cidr":
+ "type": "string"
+ "description": "Network range"
+ "subnet-area":
+ "type": "string"
+ "description": "Link State Advertisements (LSA) type"
+ "required": ["domain-name", "cidr", "area", "subnet-cidr", "subnet-area"]
+"connect-domains":
+ "description": "Connect the router to another router, where the same domain is present"
+ "params":
+ "domain-name":
+ "type": "string"
+ "description": "The domain of the coproration to connect"
+ "iface-name":
+ "type": "string"
+ "description": "Device name. eg eth1"
+ "tunnel-name":
+ "type": "string"
+ "description": "Name of the tunnel ?"
+ "local-ip":
+ "type": "string"
+ "description": "local ip ?"
+ "remote-ip":
+ "type": "string"
+ "description": "remote ip ?"
+ "tunnel-key":
+ "type": "string"
+ "description": "tunnel key?"
+ "internal-local-ip":
+ "type": "string"
+ "description": "internal local ip?"
+ "internal-remote-ip":
+ "type": "string"
+ "description": "internal remote ip?"
+ "tunnel-type":
+ "type": "string"
+ "default": "gre"
+ "description": "The type of tunnel to establish."
+ "required": ["domain-name", "iface-name", "tunnel-name", "local-ip", "remote-ip",
+ "tunnel-key", "internal-local-ip", "internal-remote-ip"]
+"delete-domain-connection":
+ "description": "Remove the tunnel to another router where the domain is present."
+ "params":
+ "domain-name":
+ "type": "string"
+ "description": "The domain of the corporation to unlink"
+ "tunnel-name":
+ "type": "string"
+ "description": "The name of the tunnel to unlink that the domain-name is attached\
+ \ to"
+ "required": ["domain-name", "tunnel-name"]
--- /dev/null
+#!/usr/bin/env python3
+import sys
+sys.path.append('lib')
+
+from charms.reactive import main
+from charms.reactive import set_state
+from charmhelpers.core.hookenv import action_fail
+
+"""
+`set_state` only works here because it's flushed to disk inside the `main()`
+loop. remove_state will need to be called inside the action method.
+"""
+set_state('vpe.add-corporation')
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
--- /dev/null
+#!/usr/bin/env python3
+import sys
+sys.path.append('lib')
+
+from charms.reactive import main
+from charms.reactive import set_state
+from charmhelpers.core.hookenv import action_fail
+
+"""
+`set_state` only works here because it's flushed to disk inside the `main()`
+loop. remove_state will need to be called inside the action method.
+"""
+set_state('vpe.configure-interface')
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.reactive import main
+from charms.reactive import set_state
+from charmhelpers.core.hookenv import action_fail
+
+"""
+`set_state` only works here because it's flushed to disk inside the `main()`
+loop. remove_state will need to be called inside the action method.
+"""
+set_state('vpe.connect-domains')
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
--- /dev/null
+#!/usr/bin/env python3
+import sys
+sys.path.append('lib')
+
+from charms.reactive import main
+from charms.reactive import set_state
+from charmhelpers.core.hookenv import action_fail
+
+
+"""
+`set_state` only works here because it's flushed to disk inside the `main()`
+loop. remove_state will need to be called inside the action method.
+"""
+set_state('vpe.delete-corporation')
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
--- /dev/null
+#!/usr/bin/env python3
+import sys
+sys.path.append('lib')
+
+from charms.reactive import main
+from charms.reactive import set_state
+from charmhelpers.core.hookenv import action_fail
+
+
+"""
+`set_state` only works here because it's flushed to disk inside the `main()`
+loop. remove_state will need to be called inside the action method.
+"""
+set_state('vpe.delete-domain-connection')
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
--- /dev/null
+#!/usr/bin/env python3
+
+import sys
+sys.path.append('lib')
+
+import argparse
+from charms.layer import options
+
+
+parser = argparse.ArgumentParser(description='Access layer options.')
+parser.add_argument('section',
+ help='the section, or layer, the option is from')
+parser.add_argument('option',
+ help='the option to access')
+
+args = parser.parse_args()
+value = options(args.section).get(args.option, '')
+if isinstance(value, bool):
+ sys.exit(0 if value else 1)
+elif isinstance(value, list):
+ for val in value:
+ print(val)
+else:
+ print(value)
--- /dev/null
+"options":
+ "vpe-router":
+ "default": !!null ""
+ "type": "string"
+ "description": "Hostname or IP of the vpe router to connect to"
+ "user":
+ "type": "string"
+ "default": "root"
+ "description": "Username for VPE Router"
+ "pass":
+ "type": "string"
+ "default": !!null ""
+ "description": "Password for VPE Router"
+ "hostname":
+ "type": "string"
+ "default": !!null ""
+ "description": "The hostname to set the vpe router to."
--- /dev/null
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0
+
+Files: *
+Copyright: 2015, Canonical Ltd.
+License: GPL-3
+
+License: GPL-3
+ On Debian GNU/Linux system you can find the complete text of the
+ GPL-3 license in '/usr/share/common-licenses/GPL-3'
--- /dev/null
+*.pyc
+*~
+.ropeproject
+.settings
+.tox
--- /dev/null
+#!/usr/bin/make
+
+all: lint unit_test
+
+
+.PHONY: clean
+clean:
+ @rm -rf .tox
+
+.PHONY: apt_prereqs
+apt_prereqs:
+ @# Need tox, but don't install the apt version unless we have to (don't want to conflict with pip)
+ @which tox >/dev/null || (sudo apt-get install -y python-pip && sudo pip install tox)
+
+.PHONY: lint
+lint: apt_prereqs
+ @tox --notest
+ @PATH=.tox/py34/bin:.tox/py35/bin flake8 $(wildcard hooks reactive lib unit_tests tests)
+ @charm proof
+
+.PHONY: unit_test
+unit_test: apt_prereqs
+ @echo Starting tests...
+ tox
--- /dev/null
+# Overview
+
+This is the base layer for all charms [built using layers][building]. It
+provides all of the standard Juju hooks and runs the
+[charms.reactive.main][charms.reactive] loop for them. It also bootstraps the
+[charm-helpers][] and [charms.reactive][] libraries and all of their
+dependencies for use by the charm.
+
+# Usage
+
+To create a charm layer using this base layer, you need only include it in
+a `layer.yaml` file:
+
+```yaml
+includes: ['layer:basic']
+```
+
+This will fetch this layer from [interfaces.juju.solutions][] and incorporate
+it into your charm layer. You can then add handlers under the `reactive/`
+directory. Note that **any** file under `reactive/` will be expected to
+contain handlers, whether as Python decorated functions or [executables][non-python]
+using the [external handler protocol][].
+
+### Charm Dependencies
+
+Each layer can include a `wheelhouse.txt` file with Python requirement lines.
+For example, this layer's `wheelhouse.txt` includes:
+
+```
+pip>=7.0.0,<8.0.0
+charmhelpers>=0.4.0,<1.0.0
+charms.reactive>=0.1.0,<2.0.0
+```
+
+All of these dependencies from each layer will be fetched (and updated) at build
+time and will be automatically installed by this base layer before any reactive
+handlers are run.
+
+Note that the `wheelhouse.txt` file is intended for **charm** dependencies only.
+That is, for libraries that the charm code itself needs to do its job of deploying
+and configuring the payload. If the payload itself has Python dependencies, those
+should be handled separately, by the charm.
+
+See [PyPI][pypi charms.X] for packages under the `charms.` namespace which might
+be useful for your charm.
+
+### Layer Namespace
+
+Each layer has a reserved section in the `charms.layer.` Python package namespace,
+which it can populate by including a `lib/charms/layer/<layer-name>.py` file or
+by placing files under `lib/charms/layer/<layer-name>/`. (If the layer name
+includes hyphens, replace them with underscores.) These can be helpers that the
+layer uses internally, or it can expose classes or functions to be used by other
+layers to interact with that layer.
+
+For example, a layer named `foo` could include a `lib/charms/layer/foo.py` file
+with some helper functions that other layers could access using:
+
+```python
+from charms.layer.foo import my_helper
+```
+
+### Layer Options
+
+Any layer can define options in its `layer.yaml`. Those options can then be set
+by other layers to change the behavior of your layer. The options are defined
+using [jsonschema][], which is the same way that [action paramters][] are defined.
+
+For example, the `foo` layer could include the following option definitons:
+
+```yaml
+includes: ['layer:basic']
+defines: # define some options for this layer (the layer "foo")
+ enable-bar: # define an "enable-bar" option for this layer
+ description: If true, enable support for "bar".
+ type: boolean
+ default: false
+```
+
+A layer using `foo` could then set it:
+
+```yaml
+includes: ['layer:foo']
+options:
+ foo: # setting options for the "foo" layer
+ enable-bar: true # set the "enable-bar" option to true
+```
+
+The `foo` layer can then use the `charms.layer.options` helper to load the values
+for the options that it defined. For example:
+
+```python
+from charms import layer
+
+@when('state')
+def do_thing():
+ layer_opts = layer.options('foo') # load all of the options for the "foo" layer
+ if layer_opts['enable-bar']: # check the value of the "enable-bar" option
+ hookenv.log("Bar is enabled")
+```
+
+You can also access layer options in other handlers, such as Bash, using
+the command-line interface:
+
+```bash
+. charms.reactive.sh
+
+@when 'state'
+function do_thing() {
+ if layer_option foo enable-bar; then
+ juju-log "Bar is enabled"
+ juju-log "bar-value is: $(layer_option foo bar-value)"
+ fi
+}
+
+reactive_handler_main
+```
+
+Note that options of type `boolean` will set the exit code, while other types
+will be printed out.
+
+# Hooks
+
+This layer provides hooks that other layers can react to using the decorators
+of the [charms.reactive][] library:
+
+ * `config-changed`
+ * `install`
+ * `leader-elected`
+ * `leader-settings-changed`
+ * `start`
+ * `stop`
+ * `upgrade-charm`
+ * `update-status`
+
+Other hooks are not implemented at this time. A new layer can implement storage
+or relation hooks in their own layer by putting them in the `hooks` directory.
+
+**Note:** Because `update-status` is invoked every 5 minutes, you should take
+care to ensure that your reactive handlers only invoke expensive operations
+when absolutely necessary. It is recommended that you use helpers like
+[`@only_once`][], [`@when_file_changed`][], and [`data_changed`][] to ensure
+that handlers run only when necessary.
+
+# Layer Configuration
+
+This layer supports the following options, which can be set in `layer.yaml`:
+
+ * **packages** A list of system packages to be installed before the reactive
+ handlers are invoked.
+
+ * **use_venv** If set to true, the charm dependencies from the various
+ layers' `wheelhouse.txt` files will be installed in a Python virtualenv
+ located at `$CHARM_DIR/../.venv`. This keeps charm dependencies from
+ conflicting with payload dependencies, but you must take care to preserve
+ the environment and interpreter if using `execl` or `subprocess`.
+
+ * **include_system_packages** If set to true and using a venv, include
+ the `--system-site-packages` options to make system Python libraries
+ visible within the venv.
+
+An example `layer.yaml` using these options might be:
+
+```yaml
+includes: ['layer:basic']
+options:
+ basic:
+ packages: ['git']
+ use_venv: true
+ include_system_packages: true
+```
+
+
+# Reactive States
+
+This layer will set the following states:
+
+ * **`config.changed`** Any config option has changed from its previous value.
+ This state is cleared automatically at the end of each hook invocation.
+
+ * **`config.changed.<option>`** A specific config option has changed.
+ **`<option>`** will be replaced by the config option name from `config.yaml`.
+ This state is cleared automatically at the end of each hook invocation.
+
+ * **`config.set.<option>`** A specific config option has a True or non-empty
+ value set. **`<option>`** will be replaced by the config option name from
+ `config.yaml`. This state is cleared automatically at the end of each hook
+ invocation.
+
+ * **`config.default.<option>`** A specific config option is set to its
+ default value. **`<option>`** will be replaced by the config option name
+ from `config.yaml`. This state is cleared automatically at the end of
+ each hook invocation.
+
+An example using the config states would be:
+
+```python
+@when('config.changed.my-opt')
+def my_opt_changed():
+ update_config()
+ restart_service()
+```
+
+
+# Actions
+
+This layer currently does not define any actions.
+
+
+[building]: https://jujucharms.com/docs/devel/authors-charm-building
+[charm-helpers]: https://pythonhosted.org/charmhelpers/
+[charms.reactive]: https://pythonhosted.org/charms.reactive/
+[interfaces.juju.solutions]: http://interfaces.juju.solutions/
+[non-python]: https://pythonhosted.org/charms.reactive/#non-python-reactive-handlers
+[external handler protocol]: https://pythonhosted.org/charms.reactive/charms.reactive.bus.html#charms.reactive.bus.ExternalHandler
+[jsonschema]: http://json-schema.org/
+[action paramters]: https://jujucharms.com/docs/stable/authors-charm-actions
+[pypi charms.X]: https://pypi.python.org/pypi?%3Aaction=search&term=charms.&submit=search
+[`@only_once`]: https://pythonhosted.org/charms.reactive/charms.reactive.decorators.html#charms.reactive.decorators.only_once
+[`@when_file_changed`]: https://pythonhosted.org/charms.reactive/charms.reactive.decorators.html#charms.reactive.decorators.when_file_changed
+[`data_changed`]: https://pythonhosted.org/charms.reactive/charms.reactive.helpers.html#charms.reactive.helpers.data_changed
--- /dev/null
+#!/usr/bin/env python3
+
+import sys
+sys.path.append('lib')
+
+import argparse
+from charms.layer import options
+
+
+parser = argparse.ArgumentParser(description='Access layer options.')
+parser.add_argument('section',
+ help='the section, or layer, the option is from')
+parser.add_argument('option',
+ help='the option to access')
+
+args = parser.parse_args()
+value = options(args.section).get(args.option, '')
+if isinstance(value, bool):
+ sys.exit(0 if value else 1)
+elif isinstance(value, list):
+ for val in value:
+ print(val)
+else:
+ print(value)
--- /dev/null
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0
+
+Files: *
+Copyright: 2015, Canonical Ltd.
+License: GPL-3
+
+License: GPL-3
+ On Debian GNU/Linux system you can find the complete text of the
+ GPL-3 license in '/usr/share/common-licenses/GPL-3'
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import os
+import sys
+sys.path.append('lib')
+
+# This is an upgrade-charm context, make sure we install latest deps
+if not os.path.exists('wheelhouse/.upgrade'):
+ open('wheelhouse/.upgrade', 'w').close()
+ if os.path.exists('wheelhouse/.bootstrapped'):
+ os.unlink('wheelhouse/.bootstrapped')
+else:
+ os.unlink('wheelhouse/.upgrade')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+defines:
+ packages:
+ type: array
+ default: []
+ description: Additional packages to be installed at time of bootstrap
+ use_venv:
+ type: boolean
+ default: false
+ description: >
+ Install charm dependencies (wheelhouse) into a Python virtual environment
+ to help avoid conflicts with other charms or libraries on the machine.
+ include_system_packages:
+ type: boolean
+ default: false
+ description: >
+ If using a virtual environment, allow the venv to see Python packages
+ installed at the system level. This reduces isolation, but is necessary
+ to use Python packages installed via apt-get.
--- /dev/null
+import os
+
+
+class LayerOptions(dict):
+ def __init__(self, layer_file, section=None):
+ import yaml # defer, might not be available until bootstrap
+ with open(layer_file) as f:
+ layer = yaml.safe_load(f.read())
+ opts = layer.get('options', {})
+ if section and section in opts:
+ super(LayerOptions, self).__init__(opts.get(section))
+ else:
+ super(LayerOptions, self).__init__(opts)
+
+
+def options(section=None, layer_file=None):
+ if not layer_file:
+ base_dir = os.environ.get('CHARM_DIR', os.getcwd())
+ layer_file = os.path.join(base_dir, 'layer.yaml')
+
+ return LayerOptions(layer_file, section)
--- /dev/null
+import os
+import sys
+import shutil
+import platform
+from glob import glob
+from subprocess import check_call
+
+from charms.layer.execd import execd_preinstall
+
+
+def bootstrap_charm_deps():
+ """
+ Set up the base charm dependencies so that the reactive system can run.
+ """
+ # execd must happen first, before any attempt to install packages or
+ # access the network, because sites use this hook to do bespoke
+ # configuration and install secrets so the rest of this bootstrap
+ # and the charm itself can actually succeed. This call does nothing
+ # unless the operator has created and populated $CHARM_DIR/exec.d.
+ execd_preinstall()
+ # ensure that $CHARM_DIR/bin is on the path, for helper scripts
+ os.environ['PATH'] += ':%s' % os.path.join(os.environ['CHARM_DIR'], 'bin')
+ venv = os.path.abspath('../.venv')
+ vbin = os.path.join(venv, 'bin')
+ vpip = os.path.join(vbin, 'pip')
+ vpy = os.path.join(vbin, 'python')
+ if os.path.exists('wheelhouse/.bootstrapped'):
+ from charms import layer
+ cfg = layer.options('basic')
+ if cfg.get('use_venv') and '.venv' not in sys.executable:
+ # activate the venv
+ os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
+ reload_interpreter(vpy)
+ return
+ # bootstrap wheelhouse
+ if os.path.exists('wheelhouse'):
+ with open('/root/.pydistutils.cfg', 'w') as fp:
+ # make sure that easy_install also only uses the wheelhouse
+ # (see https://github.com/pypa/pip/issues/410)
+ charm_dir = os.environ['CHARM_DIR']
+ fp.writelines([
+ "[easy_install]\n",
+ "allow_hosts = ''\n",
+ "find_links = file://{}/wheelhouse/\n".format(charm_dir),
+ ])
+ apt_install(['python3-pip', 'python3-setuptools', 'python3-yaml'])
+ from charms import layer
+ cfg = layer.options('basic')
+ # include packages defined in layer.yaml
+ apt_install(cfg.get('packages', []))
+ # if we're using a venv, set it up
+ if cfg.get('use_venv'):
+ if not os.path.exists(venv):
+ distname, version, series = platform.linux_distribution()
+ if series in ('precise', 'trusty'):
+ apt_install(['python-virtualenv'])
+ else:
+ apt_install(['virtualenv'])
+ cmd = ['virtualenv', '-ppython3', '--never-download', venv]
+ if cfg.get('include_system_packages'):
+ cmd.append('--system-site-packages')
+ check_call(cmd)
+ os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
+ pip = vpip
+ else:
+ pip = 'pip3'
+ # save a copy of system pip to prevent `pip3 install -U pip`
+ # from changing it
+ if os.path.exists('/usr/bin/pip'):
+ shutil.copy2('/usr/bin/pip', '/usr/bin/pip.save')
+ # need newer pip, to fix spurious Double Requirement error:
+ # https://github.com/pypa/pip/issues/56
+ check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse',
+ 'pip'])
+ # install the rest of the wheelhouse deps
+ check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse'] +
+ glob('wheelhouse/*'))
+ if not cfg.get('use_venv'):
+ # restore system pip to prevent `pip3 install -U pip`
+ # from changing it
+ if os.path.exists('/usr/bin/pip.save'):
+ shutil.copy2('/usr/bin/pip.save', '/usr/bin/pip')
+ os.remove('/usr/bin/pip.save')
+ os.remove('/root/.pydistutils.cfg')
+ # flag us as having already bootstrapped so we don't do it again
+ open('wheelhouse/.bootstrapped', 'w').close()
+ # Ensure that the newly bootstrapped libs are available.
+ # Note: this only seems to be an issue with namespace packages.
+ # Non-namespace-package libs (e.g., charmhelpers) are available
+ # without having to reload the interpreter. :/
+ reload_interpreter(vpy if cfg.get('use_venv') else sys.argv[0])
+
+
+def reload_interpreter(python):
+ """
+ Reload the python interpreter to ensure that all deps are available.
+
+ Newly installed modules in namespace packages sometimes seemt to
+ not be picked up by Python 3.
+ """
+ os.execle(python, python, sys.argv[0], os.environ)
+
+
+def apt_install(packages):
+ """
+ Install apt packages.
+
+ This ensures a consistent set of options that are often missed but
+ should really be set.
+ """
+ if isinstance(packages, (str, bytes)):
+ packages = [packages]
+
+ env = os.environ.copy()
+
+ if 'DEBIAN_FRONTEND' not in env:
+ env['DEBIAN_FRONTEND'] = 'noninteractive'
+
+ cmd = ['apt-get',
+ '--option=Dpkg::Options::=--force-confold',
+ '--assume-yes',
+ 'install']
+ check_call(cmd + packages, env=env)
+
+
+def init_config_states():
+ import yaml
+ from charmhelpers.core import hookenv
+ from charms.reactive import set_state
+ from charms.reactive import toggle_state
+ config = hookenv.config()
+ config_defaults = {}
+ config_defs = {}
+ config_yaml = os.path.join(hookenv.charm_dir(), 'config.yaml')
+ if os.path.exists(config_yaml):
+ with open(config_yaml) as fp:
+ config_defs = yaml.safe_load(fp).get('options', {})
+ config_defaults = {key: value.get('default')
+ for key, value in config_defs.items()}
+ for opt in config_defs.keys():
+ if config.changed(opt):
+ set_state('config.changed')
+ set_state('config.changed.{}'.format(opt))
+ toggle_state('config.set.{}'.format(opt), config.get(opt))
+ toggle_state('config.default.{}'.format(opt),
+ config.get(opt) == config_defaults[opt])
+ hookenv.atexit(clear_config_states)
+
+
+def clear_config_states():
+ from charmhelpers.core import hookenv, unitdata
+ from charms.reactive import remove_state
+ config = hookenv.config()
+ remove_state('config.changed')
+ for opt in config.keys():
+ remove_state('config.changed.{}'.format(opt))
+ remove_state('config.set.{}'.format(opt))
+ remove_state('config.default.{}'.format(opt))
+ unitdata.kv().flush()
--- /dev/null
+# Copyright 2014-2016 Canonical Limited.
+#
+# This file is part of layer-basic, the reactive base layer for Juju.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
+
+# This module may only import from the Python standard library.
+import os
+import sys
+import subprocess
+import time
+
+'''
+execd/preinstall
+
+It is often necessary to configure and reconfigure machines
+after provisioning, but before attempting to run the charm.
+Common examples are specialized network configuration, enabling
+of custom hardware, non-standard disk partitioning and filesystems,
+adding secrets and keys required for using a secured network.
+
+The reactive framework's base layer invokes this mechanism as
+early as possible, before any network access is made or dependencies
+unpacked or non-standard modules imported (including the charms.reactive
+framework itself).
+
+Operators needing to use this functionality may branch a charm and
+create an exec.d directory in it. The exec.d directory in turn contains
+one or more subdirectories, each of which contains an executable called
+charm-pre-install and any other required resources. The charm-pre-install
+executables are run, and if successful, state saved so they will not be
+run again.
+
+ $CHARM_DIR/exec.d/mynamespace/charm-pre-install
+
+An alternative to branching a charm is to compose a new charm that contains
+the exec.d directory, using the original charm as a layer,
+
+A charm author could also abuse this mechanism to modify the charm
+environment in unusual ways, but for most purposes it is saner to use
+charmhelpers.core.hookenv.atstart().
+'''
+
+
+def default_execd_dir():
+ return os.path.join(os.environ['CHARM_DIR'], 'exec.d')
+
+
+def execd_module_paths(execd_dir=None):
+ """Generate a list of full paths to modules within execd_dir."""
+ if not execd_dir:
+ execd_dir = default_execd_dir()
+
+ if not os.path.exists(execd_dir):
+ return
+
+ for subpath in os.listdir(execd_dir):
+ module = os.path.join(execd_dir, subpath)
+ if os.path.isdir(module):
+ yield module
+
+
+def execd_submodule_paths(command, execd_dir=None):
+ """Generate a list of full paths to the specified command within exec_dir.
+ """
+ for module_path in execd_module_paths(execd_dir):
+ path = os.path.join(module_path, command)
+ if os.access(path, os.X_OK) and os.path.isfile(path):
+ yield path
+
+
+def execd_sentinel_path(submodule_path):
+ module_path = os.path.dirname(submodule_path)
+ execd_path = os.path.dirname(module_path)
+ module_name = os.path.basename(module_path)
+ submodule_name = os.path.basename(submodule_path)
+ return os.path.join(execd_path,
+ '.{}_{}.done'.format(module_name, submodule_name))
+
+
+def execd_run(command, execd_dir=None, stop_on_error=True, stderr=None):
+ """Run command for each module within execd_dir which defines it."""
+ if stderr is None:
+ stderr = sys.stdout
+ for submodule_path in execd_submodule_paths(command, execd_dir):
+ # Only run each execd once. We cannot simply run them in the
+ # install hook, as potentially storage hooks are run before that.
+ # We cannot rely on them being idempotent.
+ sentinel = execd_sentinel_path(submodule_path)
+ if os.path.exists(sentinel):
+ continue
+
+ try:
+ subprocess.check_call([submodule_path], stderr=stderr,
+ universal_newlines=True)
+ with open(sentinel, 'w') as f:
+ f.write('{} ran successfully {}\n'.format(submodule_path,
+ time.ctime()))
+ f.write('Removing this file will cause it to be run again\n')
+ except subprocess.CalledProcessError as e:
+ # Logs get the details. We can't use juju-log, as the
+ # output may be substantial and exceed command line
+ # length limits.
+ print("ERROR ({}) running {}".format(e.returncode, e.cmd),
+ file=stderr)
+ print("STDOUT<<EOM", file=stderr)
+ print(e.output, file=stderr)
+ print("EOM", file=stderr)
+
+ # Unit workload status gets a shorter fail message.
+ short_path = os.path.relpath(submodule_path)
+ block_msg = "Error ({}) running {}".format(e.returncode,
+ short_path)
+ try:
+ subprocess.check_call(['status-set', 'blocked', block_msg],
+ universal_newlines=True)
+ if stop_on_error:
+ sys.exit(0) # Leave unit in blocked state.
+ except Exception:
+ pass # We care about the exec.d/* failure, not status-set.
+
+ if stop_on_error:
+ sys.exit(e.returncode or 1) # Error state for pre-1.24 Juju
+
+
+def execd_preinstall(execd_dir=None):
+ """Run charm-pre-install for each module within execd_dir."""
+ execd_run('charm-pre-install', execd_dir=execd_dir)
--- /dev/null
+flake8
+pytest
--- /dev/null
+[tox]
+skipsdist=True
+envlist = py34, py35
+skip_missing_interpreters = True
+
+[testenv]
+commands = py.test -v
+deps =
+ -r{toxinidir}/requirements.txt
+
+[flake8]
+exclude=docs
--- /dev/null
+pip>=7.0.0,<8.2.0
+charmhelpers>=0.4.0,<1.0.0
+charms.reactive>=0.1.0,<2.0.0
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import sys
+sys.path.append('lib')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+#!/usr/bin/env python3
+
+# Load modules from $CHARM_DIR/lib
+import os
+import sys
+sys.path.append('lib')
+
+# This is an upgrade-charm context, make sure we install latest deps
+if not os.path.exists('wheelhouse/.upgrade'):
+ open('wheelhouse/.upgrade', 'w').close()
+ if os.path.exists('wheelhouse/.bootstrapped'):
+ os.unlink('wheelhouse/.bootstrapped')
+else:
+ os.unlink('wheelhouse/.upgrade')
+
+from charms.layer import basic
+basic.bootstrap_charm_deps()
+basic.init_config_states()
+
+
+# This will load and run the appropriate @hook and other decorated
+# handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive,
+# and $CHARM_DIR/hooks/relations.
+#
+# See https://jujucharms.com/docs/stable/authors-charm-building
+# for more information on this pattern.
+from charms.reactive import main
+main()
--- /dev/null
+"options":
+ "layer-basic":
+ "use_venv": !!bool "false"
+ "packages": []
+ "include_system_packages": !!bool "false"
+ "vpe-router": {}
+"includes":
+- "layer:basic"
+"is": "vpe-router"
--- /dev/null
+import os
+
+
+class LayerOptions(dict):
+ def __init__(self, layer_file, section=None):
+ import yaml # defer, might not be available until bootstrap
+ with open(layer_file) as f:
+ layer = yaml.safe_load(f.read())
+ opts = layer.get('options', {})
+ if section and section in opts:
+ super(LayerOptions, self).__init__(opts.get(section))
+ else:
+ super(LayerOptions, self).__init__(opts)
+
+
+def options(section=None, layer_file=None):
+ if not layer_file:
+ base_dir = os.environ.get('CHARM_DIR', os.getcwd())
+ layer_file = os.path.join(base_dir, 'layer.yaml')
+
+ return LayerOptions(layer_file, section)
--- /dev/null
+import os
+import sys
+import shutil
+import platform
+from glob import glob
+from subprocess import check_call
+
+from charms.layer.execd import execd_preinstall
+
+
+def bootstrap_charm_deps():
+ """
+ Set up the base charm dependencies so that the reactive system can run.
+ """
+ # execd must happen first, before any attempt to install packages or
+ # access the network, because sites use this hook to do bespoke
+ # configuration and install secrets so the rest of this bootstrap
+ # and the charm itself can actually succeed. This call does nothing
+ # unless the operator has created and populated $CHARM_DIR/exec.d.
+ execd_preinstall()
+ # ensure that $CHARM_DIR/bin is on the path, for helper scripts
+ os.environ['PATH'] += ':%s' % os.path.join(os.environ['CHARM_DIR'], 'bin')
+ venv = os.path.abspath('../.venv')
+ vbin = os.path.join(venv, 'bin')
+ vpip = os.path.join(vbin, 'pip')
+ vpy = os.path.join(vbin, 'python')
+ if os.path.exists('wheelhouse/.bootstrapped'):
+ from charms import layer
+ cfg = layer.options('basic')
+ if cfg.get('use_venv') and '.venv' not in sys.executable:
+ # activate the venv
+ os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
+ reload_interpreter(vpy)
+ return
+ # bootstrap wheelhouse
+ if os.path.exists('wheelhouse'):
+ with open('/root/.pydistutils.cfg', 'w') as fp:
+ # make sure that easy_install also only uses the wheelhouse
+ # (see https://github.com/pypa/pip/issues/410)
+ charm_dir = os.environ['CHARM_DIR']
+ fp.writelines([
+ "[easy_install]\n",
+ "allow_hosts = ''\n",
+ "find_links = file://{}/wheelhouse/\n".format(charm_dir),
+ ])
+ apt_install(['python3-pip', 'python3-setuptools', 'python3-yaml'])
+ from charms import layer
+ cfg = layer.options('basic')
+ # include packages defined in layer.yaml
+ apt_install(cfg.get('packages', []))
+ # if we're using a venv, set it up
+ if cfg.get('use_venv'):
+ if not os.path.exists(venv):
+ distname, version, series = platform.linux_distribution()
+ if series in ('precise', 'trusty'):
+ apt_install(['python-virtualenv'])
+ else:
+ apt_install(['virtualenv'])
+ cmd = ['virtualenv', '-ppython3', '--never-download', venv]
+ if cfg.get('include_system_packages'):
+ cmd.append('--system-site-packages')
+ check_call(cmd)
+ os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']])
+ pip = vpip
+ else:
+ pip = 'pip3'
+ # save a copy of system pip to prevent `pip3 install -U pip`
+ # from changing it
+ if os.path.exists('/usr/bin/pip'):
+ shutil.copy2('/usr/bin/pip', '/usr/bin/pip.save')
+ # need newer pip, to fix spurious Double Requirement error:
+ # https://github.com/pypa/pip/issues/56
+ check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse',
+ 'pip'])
+ # install the rest of the wheelhouse deps
+ check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse'] +
+ glob('wheelhouse/*'))
+ if not cfg.get('use_venv'):
+ # restore system pip to prevent `pip3 install -U pip`
+ # from changing it
+ if os.path.exists('/usr/bin/pip.save'):
+ shutil.copy2('/usr/bin/pip.save', '/usr/bin/pip')
+ os.remove('/usr/bin/pip.save')
+ os.remove('/root/.pydistutils.cfg')
+ # flag us as having already bootstrapped so we don't do it again
+ open('wheelhouse/.bootstrapped', 'w').close()
+ # Ensure that the newly bootstrapped libs are available.
+ # Note: this only seems to be an issue with namespace packages.
+ # Non-namespace-package libs (e.g., charmhelpers) are available
+ # without having to reload the interpreter. :/
+ reload_interpreter(vpy if cfg.get('use_venv') else sys.argv[0])
+
+
+def reload_interpreter(python):
+ """
+ Reload the python interpreter to ensure that all deps are available.
+
+ Newly installed modules in namespace packages sometimes seemt to
+ not be picked up by Python 3.
+ """
+ os.execle(python, python, sys.argv[0], os.environ)
+
+
+def apt_install(packages):
+ """
+ Install apt packages.
+
+ This ensures a consistent set of options that are often missed but
+ should really be set.
+ """
+ if isinstance(packages, (str, bytes)):
+ packages = [packages]
+
+ env = os.environ.copy()
+
+ if 'DEBIAN_FRONTEND' not in env:
+ env['DEBIAN_FRONTEND'] = 'noninteractive'
+
+ cmd = ['apt-get',
+ '--option=Dpkg::Options::=--force-confold',
+ '--assume-yes',
+ 'install']
+ check_call(cmd + packages, env=env)
+
+
+def init_config_states():
+ import yaml
+ from charmhelpers.core import hookenv
+ from charms.reactive import set_state
+ from charms.reactive import toggle_state
+ config = hookenv.config()
+ config_defaults = {}
+ config_defs = {}
+ config_yaml = os.path.join(hookenv.charm_dir(), 'config.yaml')
+ if os.path.exists(config_yaml):
+ with open(config_yaml) as fp:
+ config_defs = yaml.safe_load(fp).get('options', {})
+ config_defaults = {key: value.get('default')
+ for key, value in config_defs.items()}
+ for opt in config_defs.keys():
+ if config.changed(opt):
+ set_state('config.changed')
+ set_state('config.changed.{}'.format(opt))
+ toggle_state('config.set.{}'.format(opt), config.get(opt))
+ toggle_state('config.default.{}'.format(opt),
+ config.get(opt) == config_defaults[opt])
+ hookenv.atexit(clear_config_states)
+
+
+def clear_config_states():
+ from charmhelpers.core import hookenv, unitdata
+ from charms.reactive import remove_state
+ config = hookenv.config()
+ remove_state('config.changed')
+ for opt in config.keys():
+ remove_state('config.changed.{}'.format(opt))
+ remove_state('config.set.{}'.format(opt))
+ remove_state('config.default.{}'.format(opt))
+ unitdata.kv().flush()
--- /dev/null
+# Copyright 2014-2016 Canonical Limited.
+#
+# This file is part of layer-basic, the reactive base layer for Juju.
+#
+# charm-helpers is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# charm-helpers is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
+
+# This module may only import from the Python standard library.
+import os
+import sys
+import subprocess
+import time
+
+'''
+execd/preinstall
+
+It is often necessary to configure and reconfigure machines
+after provisioning, but before attempting to run the charm.
+Common examples are specialized network configuration, enabling
+of custom hardware, non-standard disk partitioning and filesystems,
+adding secrets and keys required for using a secured network.
+
+The reactive framework's base layer invokes this mechanism as
+early as possible, before any network access is made or dependencies
+unpacked or non-standard modules imported (including the charms.reactive
+framework itself).
+
+Operators needing to use this functionality may branch a charm and
+create an exec.d directory in it. The exec.d directory in turn contains
+one or more subdirectories, each of which contains an executable called
+charm-pre-install and any other required resources. The charm-pre-install
+executables are run, and if successful, state saved so they will not be
+run again.
+
+ $CHARM_DIR/exec.d/mynamespace/charm-pre-install
+
+An alternative to branching a charm is to compose a new charm that contains
+the exec.d directory, using the original charm as a layer,
+
+A charm author could also abuse this mechanism to modify the charm
+environment in unusual ways, but for most purposes it is saner to use
+charmhelpers.core.hookenv.atstart().
+'''
+
+
+def default_execd_dir():
+ return os.path.join(os.environ['CHARM_DIR'], 'exec.d')
+
+
+def execd_module_paths(execd_dir=None):
+ """Generate a list of full paths to modules within execd_dir."""
+ if not execd_dir:
+ execd_dir = default_execd_dir()
+
+ if not os.path.exists(execd_dir):
+ return
+
+ for subpath in os.listdir(execd_dir):
+ module = os.path.join(execd_dir, subpath)
+ if os.path.isdir(module):
+ yield module
+
+
+def execd_submodule_paths(command, execd_dir=None):
+ """Generate a list of full paths to the specified command within exec_dir.
+ """
+ for module_path in execd_module_paths(execd_dir):
+ path = os.path.join(module_path, command)
+ if os.access(path, os.X_OK) and os.path.isfile(path):
+ yield path
+
+
+def execd_sentinel_path(submodule_path):
+ module_path = os.path.dirname(submodule_path)
+ execd_path = os.path.dirname(module_path)
+ module_name = os.path.basename(module_path)
+ submodule_name = os.path.basename(submodule_path)
+ return os.path.join(execd_path,
+ '.{}_{}.done'.format(module_name, submodule_name))
+
+
+def execd_run(command, execd_dir=None, stop_on_error=True, stderr=None):
+ """Run command for each module within execd_dir which defines it."""
+ if stderr is None:
+ stderr = sys.stdout
+ for submodule_path in execd_submodule_paths(command, execd_dir):
+ # Only run each execd once. We cannot simply run them in the
+ # install hook, as potentially storage hooks are run before that.
+ # We cannot rely on them being idempotent.
+ sentinel = execd_sentinel_path(submodule_path)
+ if os.path.exists(sentinel):
+ continue
+
+ try:
+ subprocess.check_call([submodule_path], stderr=stderr,
+ universal_newlines=True)
+ with open(sentinel, 'w') as f:
+ f.write('{} ran successfully {}\n'.format(submodule_path,
+ time.ctime()))
+ f.write('Removing this file will cause it to be run again\n')
+ except subprocess.CalledProcessError as e:
+ # Logs get the details. We can't use juju-log, as the
+ # output may be substantial and exceed command line
+ # length limits.
+ print("ERROR ({}) running {}".format(e.returncode, e.cmd),
+ file=stderr)
+ print("STDOUT<<EOM", file=stderr)
+ print(e.output, file=stderr)
+ print("EOM", file=stderr)
+
+ # Unit workload status gets a shorter fail message.
+ short_path = os.path.relpath(submodule_path)
+ block_msg = "Error ({}) running {}".format(e.returncode,
+ short_path)
+ try:
+ subprocess.check_call(['status-set', 'blocked', block_msg],
+ universal_newlines=True)
+ if stop_on_error:
+ sys.exit(0) # Leave unit in blocked state.
+ except Exception:
+ pass # We care about the exec.d/* failure, not status-set.
+
+ if stop_on_error:
+ sys.exit(e.returncode or 1) # Error state for pre-1.24 Juju
+
+
+def execd_preinstall(execd_dir=None):
+ """Run charm-pre-install for each module within execd_dir."""
+ execd_run('charm-pre-install', execd_dir=execd_dir)
--- /dev/null
+
+import paramiko
+import subprocess
+
+from charmhelpers.core.hookenv import config
+
+
+class NetNS(object):
+ def __init__(self, name):
+ pass
+
+ @classmethod
+ def create(cls, name):
+ # @TODO: Need to check if namespace exists already
+ try:
+ ip('netns', 'add', name)
+ except Exception as e:
+ raise Exception('could not create net namespace: %s' % e)
+
+ return cls(name)
+
+ def up(self, iface, cidr):
+ self.do('ip', 'link', 'set', 'dev', iface, 'up')
+ self.do('ip', 'address', 'add', cidr, 'dev', iface)
+
+ def add_iface(self, iface):
+ ip('link', 'set', 'dev', iface, 'netns', self.name)
+
+ def do(self, *cmd):
+ ip(*['netns', 'exec', self.name] + cmd)
+
+
+def ip(*args):
+ return _run(['ip'] + list(args))
+
+
+def _run(cmd, env=None):
+ if isinstance(cmd, str):
+ cmd = cmd.split() if ' ' in cmd else [cmd]
+
+ cfg = config()
+ if all(k in cfg for k in ['pass', 'vpe-router', 'user']):
+ router = cfg['vpe-router']
+ user = cfg['user']
+ passwd = cfg['pass']
+
+ if router and user and passwd:
+ return ssh(cmd, router, user, passwd)
+
+ p = subprocess.Popen(cmd,
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ retcode = p.poll()
+ if retcode > 0:
+ raise subprocess.CalledProcessError(returncode=retcode,
+ cmd=cmd,
+ output=stderr.decode("utf-8").strip())
+ return (''.join(stdout), ''.join(stderr))
+
+
+def ssh(cmd, host, user, password=None):
+ ''' Suddenly this project needs to SSH to something. So we replicate what
+ _run was doing with subprocess using the Paramiko library. This is
+ temporary until this charm /is/ the VPE Router '''
+
+ cmds = ' '.join(cmd)
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ client.connect(host, port=22, username=user, password=password)
+
+ stdin, stdout, stderr = client.exec_command(cmds)
+ retcode = stdout.channel.recv_exit_status()
+ client.close() # @TODO re-use connections
+ if retcode > 0:
+ output = stderr.read().strip()
+ raise subprocess.CalledProcessError(returncode=retcode, cmd=cmd,
+ output=output)
+ return (''.join(stdout), ''.join(stderr))
--- /dev/null
+"name": "vpe-router"
+"summary": "setup a virtualized PE Router with GRE tunnels"
+"maintainers":
+- "Marco Ceppi <marco.ceppi@canonical.com>"
+- "Adam Israel <adam.israel@canonical.com>"
+"description": |
+ this charm, when deployed and configured, will provide a secure virtualized
+ provider edge router.
+"peers":
+ "loadbalance":
+ "interface": "vpe-router"
+"series":
+- "trusty"
+- "xenial"
--- /dev/null
+
+from charmhelpers.core.hookenv import (
+ config,
+ status_set,
+ action_get,
+ action_fail,
+ log,
+)
+
+from charms.reactive import (
+ hook,
+ when,
+ when_not,
+ helpers,
+ set_state,
+ remove_state,
+)
+
+from charms import router
+import subprocess
+
+cfg = config()
+
+
+@hook('config-changed')
+def validate_config():
+ try:
+ """
+ If the ssh credentials are available, we'll act as a proxy charm.
+ Otherwise, we execute against the unit we're deployed on to.
+ """
+ if all(k in cfg for k in ['pass', 'vpe-router', 'user']):
+ routerip = cfg['vpe-router']
+ user = cfg['user']
+ passwd = cfg['pass']
+
+ if routerip and user and passwd:
+ # Assumption: this will be a root user
+ out, err = router.ssh(['whoami'], routerip,
+ user, passwd)
+ if out.strip() != user:
+ remove_state('vpe.configured')
+ status_set('blocked', 'vpe is not configured')
+ raise Exception('invalid credentials')
+
+ # Set the router's hostname
+ try:
+ if user == 'root' and 'hostname' in cfg:
+ hostname = cfg['hostname']
+ out, err = router.ssh(['hostname', hostname],
+ routerip,
+ user, passwd)
+ out, err = router.ssh(['sed',
+ '-i',
+ '"s/hostname.*$/hostname %s/"'
+ % hostname,
+ '/usr/admin/global/hostname.sh'
+ ],
+ routerip,
+ user, passwd)
+ set_state('vpe.configured')
+ status_set('active', 'ready!')
+ else:
+ remove_state('vpe.configured')
+ status_set('blocked', 'vpe is not configured')
+ except subprocess.CalledProcessError as e:
+ remove_state('vpe.configured')
+ status_set('blocked', 'validation failed: %s' % e)
+ log('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ raise
+
+ except Exception as e:
+ log(repr(e))
+ remove_state('vpe.configured')
+ status_set('blocked', 'validation failed: %s' % e)
+
+
+@when_not('vpe.configured')
+def not_ready_add():
+ actions = [
+ 'vpe.add-corporation',
+ 'vpe.connect-domains',
+ 'vpe.delete-domain-connections',
+ 'vpe.remove-corporation',
+ 'vpe.configure-interface',
+ 'vpe.configure-ospf',
+ ]
+
+ if helpers.any_states(*actions):
+ action_fail('VPE is not configured')
+
+ status_set('blocked', 'vpe is not configured')
+
+
+def start_ospfd():
+ # We may want to make this configurable via config setting
+ ospfd = '/usr/local/bin/ospfd'
+
+ try:
+ (stdout, stderr) = router._run(['touch',
+ '/usr/admin/global/ospfd.conf'])
+ (stdout, stderr) = router._run([ospfd, '-d', '-f',
+ '/usr/admin/global/ospfd.conf'])
+ except subprocess.CalledProcessError as e:
+ log('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+
+
+def configure_ospf(domain, cidr, area, subnet_cidr, subnet_area, enable=True):
+ """Configure the OSPF service"""
+
+ # Check to see if the OSPF daemon is running, and start it if not
+ try:
+ (stdout, stderr) = router._run(['pgrep', 'ospfd'])
+ except subprocess.CalledProcessError as e:
+ # If pgrep fails, the process wasn't found.
+ start_ospfd()
+ log('Command failed (ospfd not running): %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+
+ upordown = ''
+ if not enable:
+ upordown = 'no'
+ try:
+ vrfctl = '/usr/local/bin/vrfctl'
+ vtysh = '/usr/local/bin/vtysh'
+
+ (stdout, stderr) = router._run([vrfctl, 'list'])
+
+ domain_id = 0
+ for line in stdout.split('\n'):
+ if domain in line:
+ domain_id = int(line[3:5])
+
+ if domain_id > 0:
+ router._run([vtysh,
+ '-c',
+ '"configure terminal"',
+ '-c',
+ '"router ospf %d vr %d"' % (domain_id, domain_id),
+ '-c',
+ '"%s network %s area %s"' % (upordown, cidr, area),
+ '-c',
+ '"%s network %s area %s"' % (upordown,
+ subnet_cidr,
+ subnet_area),
+ ])
+
+ else:
+ log("Invalid domain id")
+ except subprocess.CalledProcessError as e:
+ action_fail('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ finally:
+ remove_state('vpe.configure-interface')
+ status_set('active', 'ready!')
+
+
+@when('vpe.configured')
+@when('vpe.configure-interface')
+def configure_interface():
+ """
+ Configure an ethernet interface
+ """
+ iface_name = action_get('iface-name')
+ cidr = action_get('cidr')
+
+ # cidr is optional
+ if cidr:
+ try:
+ # Add may fail, but change seems to add or update
+ router.ip('address', 'change', cidr, 'dev', iface_name)
+ except subprocess.CalledProcessError as e:
+ action_fail('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ return
+ finally:
+ remove_state('vpe.configure-interface')
+ status_set('active', 'ready!')
+
+ try:
+ router.ip('link', 'set', 'dev', iface_name, 'up')
+ except subprocess.CalledProcessError as e:
+ action_fail('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ finally:
+ remove_state('vpe.configure-interface')
+ status_set('active', 'ready!')
+
+
+@when('vpe.configured')
+@when('vpe.add-corporation')
+def add_corporation():
+ '''
+ Create and Activate the network corporation
+ '''
+ domain_name = action_get('domain-name')
+ iface_name = action_get('iface-name')
+ # HACK: python's list, used deeper, throws an exception on ints in a tuple
+ vlan_id = str(action_get('vlan-id'))
+ cidr = action_get('cidr')
+ area = action_get('area')
+ subnet_cidr = action_get('subnet-cidr')
+ subnet_area = action_get('subnet-area')
+
+ iface_vlanid = '%s.%s' % (iface_name, vlan_id)
+
+ status_set('maintenance', 'adding corporation {}'.format(domain_name))
+
+ """
+ Attempt to run all commands to add the network corporation. If any step
+ fails, abort and call `delete_corporation()` to undo.
+ """
+ try:
+ """
+ $ ip link add link eth3 name eth3.103 type vlan id 103
+ """
+ router.ip('link',
+ 'add',
+ 'link',
+ iface_name,
+ 'name',
+ iface_vlanid,
+ 'type',
+ 'vlan',
+ 'id',
+ vlan_id)
+
+ """
+ $ ip netns add domain
+ """
+ router.ip('netns',
+ 'add',
+ domain_name)
+
+ """
+ $ ip link set dev eth3.103 netns corpB
+ """
+ router.ip('link',
+ 'set',
+ 'dev',
+ iface_vlanid,
+ 'netns',
+ domain_name)
+
+ """
+ $ ifconfig eth3 up
+ """
+ router._run(['ifconfig', iface_name, 'up'])
+
+ """
+ $ ip netns exec corpB ip link set dev eth3.103 up
+ """
+ router.ip('netns',
+ 'exec',
+ domain_name,
+ 'ip',
+ 'link',
+ 'set',
+ 'dev',
+ iface_vlanid,
+ 'up')
+
+ """
+ $ ip netns exec corpB ip address add 10.0.1.1/24 dev eth3.103
+ """
+ mask = cidr.split("/")[1]
+ ip = '%s/%s' % (area, mask)
+ router.ip('netns',
+ 'exec',
+ domain_name,
+ 'ip',
+ 'address',
+ 'add',
+ ip,
+ 'dev',
+ iface_vlanid)
+
+ configure_ospf(domain_name, cidr, area, subnet_cidr, subnet_area, True)
+
+ except subprocess.CalledProcessError as e:
+ delete_corporation()
+ action_fail('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ finally:
+ remove_state('vpe.add-corporation')
+ status_set('active', 'ready!')
+
+
+@when('vpe.configured')
+@when('vpe.delete-corporation')
+def delete_corporation():
+
+ domain_name = action_get('domain-name')
+ cidr = action_get('cidr')
+ area = action_get('area')
+ subnet_cidr = action_get('subnet-cidr')
+ subnet_area = action_get('subnet-area')
+
+ status_set('maintenance', 'deleting corporation {}'.format(domain_name))
+
+ try:
+ """
+ Remove all tunnels defined for this domain
+
+ $ ip netns exec domain_name ip tun show
+ | grep gre
+ | grep -v "remote any"
+ | cut -d":" -f1
+ """
+ p = router.ip(
+ 'netns',
+ 'exec',
+ domain_name,
+ 'ip',
+ 'tun',
+ 'show',
+ '|',
+ 'grep',
+ 'gre',
+ '|',
+ 'grep',
+ '-v',
+ '"remote any"',
+ '|',
+ 'cut -d":" -f1'
+ )
+
+ # `p` should be a tuple of (stdout, stderr)
+ tunnels = p[0].split('\n')
+
+ for tunnel in tunnels:
+ try:
+ """
+ $ ip netns exec domain_name ip link set $tunnel_name down
+ """
+ router.ip(
+ 'netns',
+ 'exec',
+ domain_name,
+ 'ip',
+ 'link',
+ 'set',
+ tunnel,
+ 'down'
+ )
+ except subprocess.CalledProcessError as e:
+ log('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ pass
+
+ try:
+ """
+ $ ip netns exec domain_name ip tunnel del $tunnel_name
+ """
+ router.ip(
+ 'netns',
+ 'exec',
+ domain_name,
+ 'ip',
+ 'tunnel',
+ 'del',
+ tunnel
+ )
+ except subprocess.CalledProcessError as e:
+ log('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ pass
+
+ """
+ Remove all interfaces associated to the domain
+
+ $ ip netns exec domain_name ifconfig | grep mtu | cut -d":" -f1
+ """
+ p = router.ip(
+ 'netns',
+ 'exec',
+ domain_name,
+ 'ifconfig',
+ '|',
+ 'grep mtu',
+ '|',
+ 'cut -d":" -f1'
+ )
+
+ ifaces = p[0].split('\n')
+ for iface in ifaces:
+
+ try:
+ """
+ $ ip netns exec domain_name ip link set $iface down
+ """
+ router.ip(
+ 'netns',
+ 'exec',
+ domain_name,
+ 'ip',
+ 'link',
+ 'set',
+ iface,
+ 'down'
+ )
+ except subprocess.CalledProcessError as e:
+ log('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+
+ try:
+ """
+ $ ifconfig eth3 down
+ """
+ router._run(['ifconfig', iface, 'down'])
+ except subprocess.CalledProcessError as e:
+ log('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ pass
+
+ try:
+ """
+ $ ip link del dev $iface
+ """
+ router.ip(
+ 'link',
+ 'del',
+ 'dev',
+ iface
+ )
+ except subprocess.CalledProcessError as e:
+ log('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ pass
+
+ try:
+ """
+ Remove the domain
+
+ $ ip netns del domain_name
+ """
+ router.ip(
+ 'netns',
+ 'del',
+ domain_name
+ )
+ except subprocess.CalledProcessError as e:
+ log('Command failed: %s (%s)' % (' '.join(e.cmd), str(e.output)))
+ pass
+
+ try:
+ configure_ospf(domain_name,
+ cidr,
+ area,
+ subnet_cidr,
+ subnet_area,
+ False)
+ except subprocess.CalledProcessError as e:
+ action_fail('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+
+ except:
+ # Do nothing
+ log('delete-corporation failed.')
+ pass
+
+ finally:
+ remove_state('vpe.delete-corporation')
+ status_set('active', 'ready!')
+
+
+@when('vpe.configured')
+@when('vpe.connect-domains')
+def connect_domains():
+
+ params = [
+ 'domain-name',
+ 'iface-name',
+ 'tunnel-name',
+ 'local-ip',
+ 'remote-ip',
+ 'tunnel-key',
+ 'internal-local-ip',
+ 'internal-remote-ip',
+ 'tunnel-type',
+ ]
+
+ config = {}
+ for p in params:
+ config[p] = action_get(p)
+
+ status_set('maintenance', 'connecting domains')
+
+ try:
+ """
+ $ ip tunnel add tunnel_name mode gre local local_ip remote remote_ip
+ dev iface_name key tunnel_key csum
+ """
+ router.ip(
+ 'tunnel',
+ 'add',
+ config['tunnel-name'],
+ 'mode',
+ config['tunnel-type'],
+ 'local',
+ config['local-ip'],
+ 'remote',
+ config['remote-ip'],
+ 'dev',
+ config['iface-name'],
+ 'key',
+ config['tunnel-key'],
+ 'csum'
+ )
+
+ except subprocess.CalledProcessError as e:
+ log('Command failed (retrying with ip tunnel change): %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ try:
+ """
+ If the tunnel already exists (like gre0) and can't be deleted,
+ modify it instead of trying to add it.
+ """
+ router.ip(
+ 'tunnel',
+ 'change',
+ config['tunnel-name'],
+ 'mode',
+ config['tunnel-type'],
+ 'local',
+ config['local-ip'],
+ 'remote',
+ config['remote-ip'],
+ 'dev',
+ config['iface-name'],
+ 'key',
+ config['tunnel-key'],
+ 'csum'
+ )
+ except subprocess.CalledProcessError as e:
+ delete_domain_connection()
+ action_fail('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ finally:
+ remove_state('vpe.connect-domains')
+ status_set('active', 'ready!')
+
+ try:
+ """
+ $ ip link set dev tunnel_name netns domain_name
+ """
+ router.ip(
+ 'link',
+ 'set',
+ 'dev',
+ config['tunnel-name'],
+ 'netns',
+ config['domain-name']
+ )
+
+ """
+ $ ip netns exec domain_name ip link set dev tunnel_name up
+ """
+ router.ip(
+ 'netns',
+ 'exec',
+ config['domain-name'],
+ 'ip',
+ 'link',
+ 'set',
+ 'dev',
+ config['tunnel-name'],
+ 'up'
+ )
+
+ """
+ $ ip netns exec domain_name ip address add internal_local_ip peer
+ internal_remote_ip dev tunnel_name
+ """
+ router.ip(
+ 'netns',
+ 'exec',
+ config['domain-name'],
+ 'ip',
+ 'address',
+ 'add',
+ config['internal-local-ip'],
+ 'peer',
+ config['internal-remote-ip'],
+ 'dev',
+ config['tunnel-name']
+ )
+ except subprocess.CalledProcessError as e:
+ delete_domain_connection()
+ action_fail('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ finally:
+ remove_state('vpe.connect-domains')
+ status_set('active', 'ready!')
+
+
+@when('vpe.configured')
+@when('vpe.delete-domain-connection')
+def delete_domain_connection():
+ ''' Remove the tunnel to another router where the domain is present '''
+ domain = action_get('domain-name')
+ tunnel_name = action_get('tunnel-name')
+
+ status_set('maintenance', 'deleting domain connection: {}'.format(domain))
+
+ try:
+
+ try:
+ """
+ $ ip netns exec domain_name ip link set tunnel_name down
+ """
+ router.ip('netns',
+ 'exec',
+ domain,
+ 'ip',
+ 'link',
+ 'set',
+ tunnel_name,
+ 'down')
+ except subprocess.CalledProcessError as e:
+ action_fail('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+
+ try:
+ """
+ $ ip netns exec domain_name ip tunnel del tunnel_name
+ """
+ router.ip('netns',
+ 'exec',
+ domain,
+ 'ip',
+ 'tunnel',
+ 'del',
+ tunnel_name)
+ except subprocess.CalledProcessError as e:
+ action_fail('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ except:
+ pass
+ finally:
+ remove_state('vpe.delete-domain-connection')
+ status_set('active', 'ready!')
--- /dev/null
+flake8
+pytest
--- /dev/null
+[tox]
+skipsdist=True
+envlist = py34, py35
+skip_missing_interpreters = True
+
+[testenv]
+commands = py.test -v
+deps =
+ -r{toxinidir}/requirements.txt
+
+[flake8]
+exclude=docs
--- /dev/null
+# Set the Juju env variables for building a layer
+export JUJU_REPOSITORY=`pwd`
+export INTERFACE_PATH=$JUJU_REPOSITORY/interfaces
+export LAYER_PATH=$JUJU_REPOSITORY/layers