Import of core OSM charm layers
Signed-off-by: Adam Israel <adam.israel@canonical.com>
Change-Id: Iff2c30e3b952d52891c190ec2d7a31a650e5ad53
diff --git a/charms/layers/.gitignore b/charms/layers/.gitignore
new file mode 100644
index 0000000..7da5881
--- /dev/null
+++ b/charms/layers/.gitignore
@@ -0,0 +1,3 @@
+deps/
+builds/
+
diff --git a/charms/layers/sshproxy/.gitignore b/charms/layers/sshproxy/.gitignore
new file mode 100644
index 0000000..b8e7ba3
--- /dev/null
+++ b/charms/layers/sshproxy/.gitignore
@@ -0,0 +1 @@
+trusty/
diff --git a/charms/layers/sshproxy/README.md b/charms/layers/sshproxy/README.md
new file mode 100644
index 0000000..c66ff75
--- /dev/null
+++ b/charms/layers/sshproxy/README.md
@@ -0,0 +1,97 @@
+# Overview
+
+This is a [Juju] layer intended to ease the development of charms that need
+to execute commands over SSH, such as proxy charms.
+
+# What is a proxy charm?
+
+A proxy charm is a limited type of charm that does not interact with software running on the same host, such as controlling and configuring a remote device (a static VM image, a router/switch, etc.). It cannot take advantage of some of Juju's key features, such as [scaling], [relations], and [leadership].
+
+Proxy charms are primarily a stop-gap, intended to prototype quickly, with the end goal being to develop it into a full-featured charm, which installs and executes code on the same machine as the charm is running.
+
+# Integration
+
+After you've [created your charm], open `interfaces.yaml` and add
+`layer:sshproxy` to the includes stanza, as shown below:
+```
+includes: ['layer:basic', 'layer:sshproxy']
+```
+
+## Reactive states
+
+This layer will set the following states:
+
+- `sshproxy.configured` This state is set when SSH credentials have been supplied to the charm.
+
+
+## Example
+In `reactive/mycharm.py`, you can add logic to execute commands over SSH. This
+example is run via a `start` action, and starts a service running on a remote
+host.
+```
+...
+import charms.sshproxy
+
+
+@when('sshproxy.configured')
+@when('actions.start')
+def start():
+ """ Execute's the command, via the start action` using the
+ configured SSH credentials
+ """
+ sshproxy.ssh("service myservice start")
+
+```
+
+## Actions
+This layer includes a built-in `run` action useful for debugging or running arbitrary commands:
+
+```
+$ juju run-action mycharm/0 run command=hostname
+Action queued with id: 014b72f3-bc02-4ecb-8d38-72bce03bbb63
+
+$ juju show-action-output 014b72f3-bc02-4ecb-8d38-72bce03bbb63
+results:
+ output: juju-66a5f3-11
+status: completed
+timing:
+ completed: 2016-10-27 19:53:49 +0000 UTC
+ enqueued: 2016-10-27 19:53:44 +0000 UTC
+ started: 2016-10-27 19:53:48 +0000 UTC
+
+```
+## Known Limitations and Issues
+
+### Security issues
+
+- Password and key-based authentications are supported, with the caveat that
+both (password and private key) are stored plaintext within the Juju controller.
+
+# Configuration and Usage
+
+This layer adds the following configuration options:
+- ssh-hostname
+- ssh-username
+- ssh-password
+- ssh-private-key
+
+Once [configure] those values at any time. Once they are set, the `sshproxy.configured` state flag will be toggled:
+
+```
+juju deploy mycharm ssh-hostname=10.10.10.10 ssh-username=ubuntu ssh-password=yourpassword
+```
+or
+```
+juju deploy mycharm ssh-hostname=10.10.10.10 ssh-username=ubuntu ssh-private-key="`cat ~/.ssh/id_rsa`"
+```
+
+
+# Contact Information
+Homepage: https://github.com/AdamIsrael/layer-sshproxy
+
+[Juju]: https://jujucharms.com/about
+[configure]: https://jujucharms.com/docs/2.0/charms-config
+[scaling]: https://jujucharms.com/docs/2.0/charms-scaling
+[relations]: https://jujucharms.com/docs/2.0/charms-relations
+[leadership]: https://jujucharms.com/docs/2.0/developer-leadership
+[created your charm]: https://jujucharms.com/docs/2.0/developer-getting-started
diff --git a/charms/layers/sshproxy/actions.yaml b/charms/layers/sshproxy/actions.yaml
new file mode 100644
index 0000000..501b2f9
--- /dev/null
+++ b/charms/layers/sshproxy/actions.yaml
@@ -0,0 +1,26 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+run:
+ description: "Run an arbitrary command"
+ params:
+ command:
+ description: "The command to execute."
+ type: string
+ default: ""
+ required:
+ - command
diff --git a/charms/layers/sshproxy/actions/run b/charms/layers/sshproxy/actions/run
new file mode 100755
index 0000000..d85d3fa
--- /dev/null
+++ b/charms/layers/sshproxy/actions/run
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+import sys
+sys.path.append('lib')
+
+from charms.reactive import main
+from charms.reactive import set_state
+from charmhelpers.core.hookenv import action_fail, action_name
+
+"""
+`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('actions.{}'.format(action_name()))
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
diff --git a/charms/layers/sshproxy/config.yaml b/charms/layers/sshproxy/config.yaml
new file mode 100644
index 0000000..07f3756
--- /dev/null
+++ b/charms/layers/sshproxy/config.yaml
@@ -0,0 +1,34 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+options:
+ ssh-hostname:
+ type: string
+ default: ""
+ description: "The hostname or IP address of the machine to"
+ ssh-username:
+ type: string
+ default: ""
+ description: "The username to login as."
+ ssh-password:
+ type: string
+ default: ""
+ description: "The password used to authenticate."
+ ssh-private-key:
+ type: string
+ default: ""
+ description: "The private ssh key to be used to authenticate."
diff --git a/charms/layers/sshproxy/layer.yaml b/charms/layers/sshproxy/layer.yaml
new file mode 100644
index 0000000..4f095a6
--- /dev/null
+++ b/charms/layers/sshproxy/layer.yaml
@@ -0,0 +1,18 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+includes: ['layer:basic'] # if you use any interfaces, add them here
diff --git a/charms/layers/sshproxy/lib/charms/sshproxy.py b/charms/layers/sshproxy/lib/charms/sshproxy.py
new file mode 100644
index 0000000..b247bcf
--- /dev/null
+++ b/charms/layers/sshproxy/lib/charms/sshproxy.py
@@ -0,0 +1,124 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+from charmhelpers.core.hookenv import (
+ config,
+)
+import io
+import paramiko
+
+from subprocess import (
+ Popen,
+ CalledProcessError,
+ PIPE,
+)
+
+
+def _run(cmd, env=None):
+ """ Run a command, either on the local machine or remotely via SSH. """
+ if isinstance(cmd, str):
+ cmd = cmd.split(' ') if ' ' in cmd else [cmd]
+
+ cfg = config()
+ if all(k in cfg for k in ['ssh-hostname', 'ssh-username',
+ 'ssh-password', 'ssh-private-key']):
+ host = cfg['ssh-hostname']
+ user = cfg['ssh-username']
+ passwd = cfg['ssh-password']
+ key = cfg['ssh-private-key']
+
+ if host and user and (passwd or key):
+ return ssh(cmd, host, user, passwd, key)
+
+ p = Popen(cmd,
+ env=env,
+ shell=True,
+ stdout=PIPE,
+ stderr=PIPE)
+ stdout, stderr = p.communicate()
+ retcode = p.poll()
+ if retcode > 0:
+ raise CalledProcessError(returncode=retcode,
+ cmd=cmd,
+ output=stderr.decode("utf-8").strip())
+ return (stdout.decode('utf-8').strip(), stderr.decode('utf-8').strip())
+
+
+def get_ssh_client(host, user, password=None, key=None):
+ """Return a connected Paramiko ssh object"""
+
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+ pkey = None
+ if key:
+ f = io.StringIO(key)
+ pkey = paramiko.RSAKey.from_private_key(f)
+
+ ###########################################################################
+ # There is a bug in some versions of OpenSSH 4.3 (CentOS/RHEL 5) where #
+ # the server may not send the SSH_MSG_USERAUTH_BANNER message except when #
+ # responding to an auth_none request. For example, paramiko will attempt #
+ # to use password authentication when a password is set, but the server #
+ # could deny that, instead requesting keyboard-interactive. The hack to #
+ # workaround this is to attempt a reconnect, which will receive the right #
+ # banner, and authentication can proceed. See the following for more info #
+ # https://github.com/paramiko/paramiko/issues/432 #
+ # https://github.com/paramiko/paramiko/pull/438 #
+ ###########################################################################
+
+ try:
+ client.connect(host, port=22, username=user,
+ password=password, pkey=pkey)
+ except paramiko.ssh_exception.SSHException as e:
+ if 'Error reading SSH protocol banner' == str(e):
+ # Once more, with feeling
+ client.connect(host, port=22, username=user,
+ password=password, pkey=pkey)
+ pass
+ else:
+ raise paramiko.ssh_exception.SSHException(e)
+
+ return client
+
+
+def sftp(local_file, remote_file, host, user, password=None, key=None):
+ """Copy a local file to a remote host"""
+ client = get_ssh_client(host, user, password, key)
+
+ # Create an sftp connection from the underlying transport
+ sftp = paramiko.SFTPClient.from_transport(client.get_transport())
+ sftp.put(local_file, remote_file)
+ client.close()
+
+
+def ssh(cmd, host, user, password=None, key=None):
+ """ Run an arbitrary command over SSH. """
+ client = get_ssh_client(host, user, password, key)
+
+ cmds = ' '.join(cmd)
+ stdin, stdout, stderr = client.exec_command(cmds, get_pty=True)
+ retcode = stdout.channel.recv_exit_status()
+ client.close() # @TODO re-use connections
+ if retcode > 0:
+ output = stderr.read().strip()
+ raise CalledProcessError(returncode=retcode, cmd=cmd,
+ output=output)
+ return (
+ stdout.read().decode('utf-8').strip(),
+ stderr.read().decode('utf-8').strip()
+ )
diff --git a/charms/layers/sshproxy/metadata.yaml b/charms/layers/sshproxy/metadata.yaml
new file mode 100644
index 0000000..effeb6a
--- /dev/null
+++ b/charms/layers/sshproxy/metadata.yaml
@@ -0,0 +1,31 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+name: sshproxy
+summary: Layer to copy files to or run commands on a remote host over ssh
+maintainer: Adam Israel <adam.israel@canonical.com>
+description: |
+ This layer is intended to provide common ssh functionality, such as
+ running a command on a remote host.
+series:
+ - trusty
+ - xenial
+tags:
+ # Replace "misc" with one or more whitelisted tags from this list:
+ # https://jujucharms.com/docs/stable/authors-charm-metadata
+ - misc
+subordinate: false
diff --git a/charms/layers/sshproxy/reactive/sshproxy.py b/charms/layers/sshproxy/reactive/sshproxy.py
new file mode 100644
index 0000000..e1508ca
--- /dev/null
+++ b/charms/layers/sshproxy/reactive/sshproxy.py
@@ -0,0 +1,73 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+from charmhelpers.core.hookenv import (
+ action_fail,
+ action_get,
+ action_set,
+ config,
+)
+from charms.reactive import (
+ remove_state,
+ set_state,
+ when,
+)
+import charms.sshproxy
+import subprocess
+
+
+@when('config.changed')
+def ssh_configured():
+ """ Checks to see if the charm is configured with SSH credentials. If so,
+ set a state flag that can be used to execute ssh-only actions.
+
+ For example:
+
+ @when('sshproxy.configured')
+ def run_remote_command(cmd):
+ ...
+
+ @when_not('sshproxy.configured')
+ def run_local_command(cmd):
+ ...
+ """
+ cfg = config()
+ if all(k in cfg for k in ['ssh-hostname', 'ssh-username',
+ 'ssh-password', 'ssh-private-key']):
+ set_state('sshproxy.configured')
+ else:
+ remove_state('sshproxy.configured')
+
+
+@when('actions.run')
+def run_command():
+ """
+ Run an arbitrary command, either locally or over SSH with the configured
+ credentials.
+ """
+ try:
+ cmd = action_get('command')
+ output, err = charms.sshproxy._run(cmd)
+ if len(err):
+ action_fail("Command '{}' returned error code {}".format(cmd, err))
+ else:
+ action_set({'output': output})
+ except subprocess.CalledProcessError as e:
+ action_fail('Command failed: %s (%s)' %
+ (' '.join(e.cmd), str(e.output)))
+ finally:
+ remove_state('actions.run')
diff --git a/charms/layers/sshproxy/tests/00-setup b/charms/layers/sshproxy/tests/00-setup
new file mode 100755
index 0000000..1e5f91b
--- /dev/null
+++ b/charms/layers/sshproxy/tests/00-setup
@@ -0,0 +1,22 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+#!/bin/bash
+
+sudo add-apt-repository ppa:juju/stable -y
+sudo apt-get update
+sudo apt-get install amulet python-requests -y
diff --git a/charms/layers/sshproxy/tests/10-deploy b/charms/layers/sshproxy/tests/10-deploy
new file mode 100755
index 0000000..f5d2f5d
--- /dev/null
+++ b/charms/layers/sshproxy/tests/10-deploy
@@ -0,0 +1,76 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+#!/usr/bin/python3
+
+import amulet
+import requests
+import unittest
+import string
+import random
+
+
+class TestCharm(unittest.TestCase):
+ user = None
+ passwd = None
+
+ def id_generator(self, size=6, chars=string.ascii_uppercase + string.digits):
+ return ''.join(random.choice(chars) for _ in range(size))
+
+ def setUp(self):
+
+ # Setup random user/password
+ self.user = self.id_generator()
+ self.passwd = self.id_generator()
+
+ self.d = amulet.Deployment()
+
+ self.d.add('sshproxy')
+ self.d.add('ubuntu')
+
+ self.d.expose('sshproxy')
+
+ self.d.setup(timeout=900)
+ self.d.sentry.wait()
+
+ # Add
+ ubuntu_0 = d.sentry['ubuntu'][0]
+ ubuntu_0.ssh("sudo adduser {}".format(self.user))
+ ubuntu_0.ssh("echo '{}' | sudo passwd {} --stdin".format(self.passwd, self.user))
+
+ self.unit = self.d.sentry['sshproxy'][0]
+
+ def test_service(self):
+
+ # Configure the unit
+
+ # Run a command
+
+ # Verify the output
+
+ # test we can access over http
+ # page = requests.get('http://{}'.format(self.unit.info['public-address']))
+ # self.assertEqual(page.status_code, 200)
+ # Now you can use self.d.sentry[SERVICE][UNIT] to address each of the units and perform
+ # more in-depth steps. Each self.d.sentry[SERVICE][UNIT] has the following methods:
+ # - .info - An array of the information of that unit from Juju
+ # - .file(PATH) - Get the details of a file on that unit
+ # - .file_contents(PATH) - Get plain text output of PATH file from that unit
+ # - .directory(PATH) - Get details of directory
+ # - .directory_contents(PATH) - List files and folders in PATH on that unit
+ # - .relation(relation, service:rel) - Get relation data from return service
+ pass
diff --git a/charms/layers/sshproxy/wheelhouse.txt b/charms/layers/sshproxy/wheelhouse.txt
new file mode 100644
index 0000000..ab6de07
--- /dev/null
+++ b/charms/layers/sshproxy/wheelhouse.txt
@@ -0,0 +1,18 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+paramiko>=1.16.0,<1.17
diff --git a/charms/layers/vnfproxy/README.md b/charms/layers/vnfproxy/README.md
new file mode 100644
index 0000000..8379e50
--- /dev/null
+++ b/charms/layers/vnfproxy/README.md
@@ -0,0 +1,172 @@
+# vnfproxy
+
+## Overview
+
+This charm layer is intended for use by vendors who wish to integrate with
+OSM. The current release of OSM only supports a lightweight version of Juju
+charms, which we will refer to as "proxy charms". Consider the diagram below:
+
+```
++---------------------+ +---------------------+
+| <----+ |
+| Resource | | Service |
+| Orchestrator (RO) +----> Orchestrator (SO) |
+| | | |
++------------------+--+ +-------+----^--------+
+ | | |
+ | | |
+ | | |
+ +-----v-----+ +-v----+--+
+ | <-------+ |
+ | Virtual | | Proxy |
+ | Machine | | Charm |
+ | +-------> |
+ +-----------+ +---------+
+```
+
+The Virtual Machine (VM) is created by the Resource Orchestrator (RO), at the
+request of the Service Orchestrator (SO). Once the VM has been created, a
+"proxy charm" is deployed in order to facilitate operations between the SO and
+your service running within the VM.
+
+As such, a proxy charm will expose a number of "actions" that are run via the
+SO. By default, the following actions are exposed:
+
+```bash
+actions
+├── reboot
+├── restart
+├── run
+├── start
+└── stop
+```
+
+Some actions, such as `run` and `reboot`, do not require any additional configuration. `start`, `stop` and `restart`, however, will require you to
+implement the command(s) required to interact with your service.
+
+## Usage
+
+Create the framework for your proxy charm:
+
+```bash
+$ charm create pingpong
+$ cd pingpong
+```
+
+Modify `layer.yaml` to the following:
+```yaml
+includes:
+ - layer:basic
+ - layer:vnfproxy
+```
+
+The `metadata.yaml` describes your service. It should look similar to the following:
+
+```yaml
+name: vnfproxy
+summary: A layer for developing OSM "proxy" charms.
+maintainer: Adam Israel <adam.israel@canonical.com>
+description: |
+ VNF "proxy" charms are a lightweight version of a charm that, rather than
+ installing software on the same machine, execute commands over an ssh channel.
+series:
+ - trusty
+ - xenial
+tags:
+ - osm
+ - vnf
+subordinate: false
+```
+
+Implement the default action(s) you wish to support by adding the following code to reactive/pingpong.py and fill in the cmd to be run:
+
+```python
+@when('actions.start')
+def start():
+ err = ''
+ try:
+ cmd = ""
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.start')
+
+
+@when('actions.stop')
+def stop():
+ err = ''
+ try:
+ # Enter the command to stop your service(s)
+ cmd = "service myname stop"
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.stop')
+
+
+@when('actions.restart')
+def restart():
+ err = ''
+ try:
+ # Enter the command to restart your service(s)
+ cmd = "service myname restart"
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.restart')
+```
+
+Rename `README.ex` to `README.md` and describe your application and its usage.
+
+-- fix this. there are cases where the config is useful -- Delete `config.yaml`, since the charm's configuration will be driven by the SO.
+
+Create the `actions.yaml` file; this will describe the additional operations you would like to perform on or against your service.
+
+```yaml
+set-server:
+ description: "Set the target IP address and port"
+ params:
+ server-ip:
+ description: "IP on which the target service is listening."
+ type: string
+ default: ""
+ server-port:
+ description: "Port on which the target service is listening."
+ type: integer
+ default: 5555
+ required:
+ - server-ip
+set-rate:
+ description: "Set the rate of packet generation."
+ params:
+ rate:
+ description: "Packet rate."
+ type: integer
+ default: 5
+get-stats:
+ description: "Get the stats."
+get-state:
+ description: "Get the admin state of the target service."
+get-rate:
+ description: "Get the rate set on the target service."
+get-server:
+ description: "Get the target server and IP set"
+```
+
+
+Once you've implemented your actions, you need to compile the various charm layers:
+```bash
+$ charm build
+
+```
+
+## Contact
diff --git a/charms/layers/vnfproxy/actions.yaml b/charms/layers/vnfproxy/actions.yaml
new file mode 100644
index 0000000..a9ee2ba
--- /dev/null
+++ b/charms/layers/vnfproxy/actions.yaml
@@ -0,0 +1,25 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+start:
+ description: "Stop the service"
+stop:
+ description: "Stop the service"
+restart:
+ description: "Stop the service"
+reboot:
+ description: "Reboot the machine"
diff --git a/charms/layers/vnfproxy/actions/reboot b/charms/layers/vnfproxy/actions/reboot
new file mode 100755
index 0000000..751c88c
--- /dev/null
+++ b/charms/layers/vnfproxy/actions/reboot
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+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('actions.reboot')
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
diff --git a/charms/layers/vnfproxy/actions/restart b/charms/layers/vnfproxy/actions/restart
new file mode 100755
index 0000000..f62929b
--- /dev/null
+++ b/charms/layers/vnfproxy/actions/restart
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+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('actions.restart')
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
diff --git a/charms/layers/vnfproxy/actions/start b/charms/layers/vnfproxy/actions/start
new file mode 100755
index 0000000..a9db3d0
--- /dev/null
+++ b/charms/layers/vnfproxy/actions/start
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+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('actions.start')
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
diff --git a/charms/layers/vnfproxy/actions/stop b/charms/layers/vnfproxy/actions/stop
new file mode 100755
index 0000000..3890486
--- /dev/null
+++ b/charms/layers/vnfproxy/actions/stop
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+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('actions.stop')
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
diff --git a/charms/layers/vnfproxy/icon.svg b/charms/layers/vnfproxy/icon.svg
new file mode 100644
index 0000000..e092eef
--- /dev/null
+++ b/charms/layers/vnfproxy/icon.svg
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="96"
+ height="96"
+ id="svg6517"
+ version="1.1"
+ inkscape:version="0.48+devel r12274"
+ sodipodi:docname="Juju_charm_icon_template.svg">
+ <defs
+ id="defs6519">
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#Background"
+ id="linearGradient6461"
+ gradientUnits="userSpaceOnUse"
+ x1="0"
+ y1="970.29498"
+ x2="144"
+ y2="970.29498"
+ gradientTransform="matrix(0,-0.66666669,0.6660448,0,-866.25992,731.29077)" />
+ <linearGradient
+ id="Background">
+ <stop
+ id="stop4178"
+ offset="0"
+ style="stop-color:#b8b8b8;stop-opacity:1" />
+ <stop
+ id="stop4180"
+ offset="1"
+ style="stop-color:#c9c9c9;stop-opacity:1" />
+ </linearGradient>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Inner Shadow"
+ id="filter1121">
+ <feFlood
+ flood-opacity="0.59999999999999998"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood1123" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="out"
+ result="composite1"
+ id="feComposite1125" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="1"
+ result="blur"
+ id="feGaussianBlur1127" />
+ <feOffset
+ dx="0"
+ dy="2"
+ result="offset"
+ id="feOffset1129" />
+ <feComposite
+ in="offset"
+ in2="SourceGraphic"
+ operator="atop"
+ result="composite2"
+ id="feComposite1131" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter950">
+ <feFlood
+ flood-opacity="0.25"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood952" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite954" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="1"
+ result="blur"
+ id="feGaussianBlur956" />
+ <feOffset
+ dx="0"
+ dy="1"
+ result="offset"
+ id="feOffset958" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite960" />
+ </filter>
+ <clipPath
+ clipPathUnits="userSpaceOnUse"
+ id="clipPath873">
+ <g
+ transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"
+ id="g875"
+ inkscape:label="Layer 1"
+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">
+ <path
+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"
+ 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"
+ id="path877"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sssssssss" />
+ </g>
+ </clipPath>
+ <filter
+ inkscape:collect="always"
+ id="filter891"
+ inkscape:label="Badge Shadow">
+ <feGaussianBlur
+ inkscape:collect="always"
+ stdDeviation="0.71999962"
+ id="feGaussianBlur893" />
+ </filter>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="4.0745362"
+ inkscape:cx="18.514671"
+ inkscape:cy="49.018169"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1920"
+ inkscape:window-height="1029"
+ inkscape:window-x="0"
+ inkscape:window-y="24"
+ inkscape:window-maximized="1"
+ showborder="true"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:showpageshadow="false">
+ <inkscape:grid
+ type="xygrid"
+ id="grid821" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="16,48"
+ id="guide823" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="64,80"
+ id="guide825" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="80,40"
+ id="guide827" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="64,16"
+ id="guide829" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata6522">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="BACKGROUND"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(268,-635.29076)"
+ style="display:inline">
+ <path
+ style="fill:url(#linearGradient6461);fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"
+ 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"
+ id="path6455"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sssssssss" />
+ </g>
+ <g
+ inkscape:groupmode="layer"
+ id="layer3"
+ inkscape:label="PLACE YOUR PICTOGRAM HERE"
+ style="display:inline" />
+ <g
+ inkscape:groupmode="layer"
+ id="layer2"
+ inkscape:label="BADGE"
+ style="display:none"
+ sodipodi:insensitive="true">
+ <g
+ style="display:inline"
+ transform="translate(-340.00001,-581)"
+ id="g4394"
+ clip-path="none">
+ <g
+ id="g855">
+ <g
+ inkscape:groupmode="maskhelper"
+ id="g870"
+ clip-path="url(#clipPath873)"
+ style="opacity:0.6;filter:url(#filter891)">
+ <path
+ transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"
+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"
+ sodipodi:ry="12"
+ sodipodi:rx="12"
+ sodipodi:cy="552.36218"
+ sodipodi:cx="252"
+ id="path844"
+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ </g>
+ <g
+ id="g862">
+ <path
+ sodipodi:type="arc"
+ style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path4398"
+ sodipodi:cx="252"
+ sodipodi:cy="552.36218"
+ sodipodi:rx="12"
+ sodipodi:ry="12"
+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"
+ transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />
+ <path
+ transform="matrix(1.25,0,0,1.25,33,-100.45273)"
+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"
+ sodipodi:ry="12"
+ sodipodi:rx="12"
+ sodipodi:cy="552.36218"
+ sodipodi:cx="252"
+ id="path4400"
+ style="color:#000000;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ sodipodi:type="arc" />
+ <path
+ sodipodi:type="star"
+ style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="path4459"
+ sodipodi:sides="5"
+ sodipodi:cx="666.19574"
+ sodipodi:cy="589.50385"
+ sodipodi:r1="7.2431178"
+ sodipodi:r2="4.3458705"
+ sodipodi:arg1="1.0471976"
+ sodipodi:arg2="1.6755161"
+ inkscape:flatsided="false"
+ inkscape:rounded="0.1"
+ inkscape:randomized="0"
+ d="m 669.8173,595.77657 c -0.39132,0.22593 -3.62645,-1.90343 -4.07583,-1.95066 -0.44938,-0.0472 -4.05653,1.36297 -4.39232,1.06062 -0.3358,-0.30235 0.68963,-4.03715 0.59569,-4.47913 -0.0939,-0.44198 -2.5498,-3.43681 -2.36602,-3.8496 0.18379,-0.41279 4.05267,-0.59166 4.44398,-0.81759 0.39132,-0.22593 2.48067,-3.48704 2.93005,-3.4398 0.44938,0.0472 1.81505,3.67147 2.15084,3.97382 0.3358,0.30236 4.08294,1.2817 4.17689,1.72369 0.0939,0.44198 -2.9309,2.86076 -3.11469,3.27355 C 669.9821,591.68426 670.20862,595.55064 669.8173,595.77657 Z"
+ transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/charms/layers/vnfproxy/layer.yaml b/charms/layers/vnfproxy/layer.yaml
new file mode 100644
index 0000000..89bf245
--- /dev/null
+++ b/charms/layers/vnfproxy/layer.yaml
@@ -0,0 +1,20 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+includes:
+ - layer:basic
+ - layer:sshproxy
diff --git a/charms/layers/vnfproxy/metadata.yaml b/charms/layers/vnfproxy/metadata.yaml
new file mode 100644
index 0000000..909643d
--- /dev/null
+++ b/charms/layers/vnfproxy/metadata.yaml
@@ -0,0 +1,30 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+name: vnfproxy
+summary: A layer for developing OSM "proxy" charms.
+maintainer: Adam Israel <adam.israel@canonical.com>
+description: |
+ VNF "proxy" charms are a lightweight version of a charm that, rather than
+ installing software on the same machine, execute commands over an ssh channel.
+series:
+ - trusty
+ - xenial
+tags:
+ - osm
+ - vnf
+subordinate: false
diff --git a/charms/layers/vnfproxy/reactive/vnfproxy.py b/charms/layers/vnfproxy/reactive/vnfproxy.py
new file mode 100644
index 0000000..2b26212
--- /dev/null
+++ b/charms/layers/vnfproxy/reactive/vnfproxy.py
@@ -0,0 +1,75 @@
+from charmhelpers.core.hookenv import (
+ action_fail,
+ action_set,
+)
+
+from charms.reactive import (
+ when,
+ remove_state as remove_flag,
+)
+import charms.sshproxy
+
+
+@when('actions.reboot')
+def reboot():
+ err = ''
+ try:
+ result, err = charms.sshproxy._run("reboot")
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.reboot')
+
+
+###############################################################################
+# Below is an example implementation of the start/stop/restart actions. #
+# To use this, copy the below code into your layer and add the appropriate #
+# command(s) necessary to perform the action. #
+###############################################################################
+
+# @when('actions.start')
+# def start():
+# err = ''
+# try:
+# cmd = "service myname start"
+# result, err = charms.sshproxy._run(cmd)
+# except:
+# action_fail('command failed:' + err)
+# else:
+# action_set({'outout': result})
+# finally:
+# remove_flag('actions.start')
+#
+#
+# @when('actions.stop')
+# def stop():
+# err = ''
+# try:
+# # Enter the command to stop your service(s)
+# cmd = "service myname stop"
+# result, err = charms.sshproxy._run(cmd)
+# except:
+# action_fail('command failed:' + err)
+# else:
+# action_set({'outout': result})
+# finally:
+# remove_flag('actions.stop')
+#
+#
+# @when('actions.restart')
+# def restart():
+# err = ''
+# try:
+# # Enter the command to restart your service(s)
+# cmd = "service myname restart"
+# result, err = charms.sshproxy._run(cmd)
+# except:
+# action_fail('command failed:' + err)
+# else:
+# action_set({'outout': result})
+# finally:
+# remove_flag('actions.restart')
+#
+#
diff --git a/charms/layers/vnfproxy/tests/00-setup b/charms/layers/vnfproxy/tests/00-setup
new file mode 100755
index 0000000..1e5f91b
--- /dev/null
+++ b/charms/layers/vnfproxy/tests/00-setup
@@ -0,0 +1,22 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+#!/bin/bash
+
+sudo add-apt-repository ppa:juju/stable -y
+sudo apt-get update
+sudo apt-get install amulet python-requests -y
diff --git a/charms/layers/vnfproxy/tests/10-deploy b/charms/layers/vnfproxy/tests/10-deploy
new file mode 100755
index 0000000..5bb9044
--- /dev/null
+++ b/charms/layers/vnfproxy/tests/10-deploy
@@ -0,0 +1,52 @@
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+##
+
+#!/usr/bin/python3
+
+import amulet
+import requests
+import unittest
+
+
+class TestCharm(unittest.TestCase):
+ def setUp(self):
+ self.d = amulet.Deployment()
+
+ self.d.add('vnfproxy')
+ self.d.expose('vnfproxy')
+
+ self.d.setup(timeout=900)
+ self.d.sentry.wait()
+
+ self.unit = self.d.sentry['vnfproxy'][0]
+
+ def test_service(self):
+ # test we can access over http
+ page = requests.get('http://{}'.format(self.unit.info['public-address']))
+ self.assertEqual(page.status_code, 200)
+ # Now you can use self.d.sentry[SERVICE][UNIT] to address each of the units and perform
+ # more in-depth steps. Each self.d.sentry[SERVICE][UNIT] has the following methods:
+ # - .info - An array of the information of that unit from Juju
+ # - .file(PATH) - Get the details of a file on that unit
+ # - .file_contents(PATH) - Get plain text output of PATH file from that unit
+ # - .directory(PATH) - Get details of directory
+ # - .directory_contents(PATH) - List files and folders in PATH on that unit
+ # - .relation(relation, service:rel) - Get relation data from return service
+
+
+if __name__ == '__main__':
+ unittest.main()