--- /dev/null
+# Overview
+
+This repository contains the [Juju] layer that represents a working example of a proxy charm.
+
+# 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.
+
+# Usage
+
+```bash
+# Clone this repository
+git clone https://osm.etsi.org/gerrit/osm/juju-charms
+cd juju-charms
+
+# Setup environment variables
+source juju-env.sh
+
+cd layers/pingpong
+charm build
+
+# Examine the built charm
+cd ../../builds/pingpong
+ls
+actions config.yaml icon.svg metadata.yaml tests
+actions.yaml copyright layer.yaml reactive tox.ini
+bin deps lib README.md wheelhouse
+builds hooks Makefile requirements.txt
+
+```
+
+You can view a screencast of this: https://asciinema.org/a/96738
+
+The `charm build` process combines this pingpong layer with each layer that it
+has included in the `metadata.yaml` file, along with their various dependencies.
+
+This built charm is what will then be used by the SO to communicate with the
+VNF.
+
+# Configuration
+
+The pingpong charm has several configuration properties that can be set via
+the SO:
+
+- ssh-hostname
+- ssh-username
+- ssh-password
+- ssh-private-key
+- mode
+
+The ssh-* keys are included by the `sshproxy` layer, and enable the charm to
+connect to the VNF image.
+
+The mode key must be one of two values: `ping` or `pong`. This informs the
+charm as to which function it is serving.
+
+# Contact Information
+For support, please send an email to the [OSM Tech] list.
+
+
+[OSM Tech]: mailto:OSM_TECH@list.etsi.org
+[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
+
+
+
+
+
+-----
+
+
+# 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
--- /dev/null
+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"
+start-ping:
+ description: "Start the traffic generator."
+stop-ping:
+ description: "Stop the traffic generator."
--- /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('actions.get-rate')
+
+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('actions.get-server')
+
+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('actions.get-state')
+
+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('actions.get-stats')
+
+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('actions.set-rate')
+
+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('actions.set-server')
+
+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('actions.start-ping')
+
+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('actions.stop-ping')
+
+try:
+ main()
+except Exception as e:
+ action_fail(repr(e))
--- /dev/null
+options:
+ mode:
+ type: string
+ default:
+ description: "The service type: [ping, pong]"
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>\r
+<!-- Created with Inkscape (http://www.inkscape.org/) -->\r
+\r
+<svg\r
+ xmlns:dc="http://purl.org/dc/elements/1.1/"\r
+ xmlns:cc="http://creativecommons.org/ns#"\r
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\r
+ xmlns:svg="http://www.w3.org/2000/svg"\r
+ xmlns="http://www.w3.org/2000/svg"\r
+ xmlns:xlink="http://www.w3.org/1999/xlink"\r
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"\r
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"\r
+ width="96"\r
+ height="96"\r
+ id="svg6517"\r
+ version="1.1"\r
+ inkscape:version="0.48+devel r12274"\r
+ sodipodi:docname="Juju_charm_icon_template.svg">\r
+ <defs\r
+ id="defs6519">\r
+ <linearGradient\r
+ inkscape:collect="always"\r
+ xlink:href="#Background"\r
+ id="linearGradient6461"\r
+ gradientUnits="userSpaceOnUse"\r
+ x1="0"\r
+ y1="970.29498"\r
+ x2="144"\r
+ y2="970.29498"\r
+ gradientTransform="matrix(0,-0.66666669,0.6660448,0,-866.25992,731.29077)" />\r
+ <linearGradient\r
+ id="Background">\r
+ <stop\r
+ id="stop4178"\r
+ offset="0"\r
+ style="stop-color:#b8b8b8;stop-opacity:1" />\r
+ <stop\r
+ id="stop4180"\r
+ offset="1"\r
+ style="stop-color:#c9c9c9;stop-opacity:1" />\r
+ </linearGradient>\r
+ <filter\r
+ style="color-interpolation-filters:sRGB;"\r
+ inkscape:label="Inner Shadow"\r
+ id="filter1121">\r
+ <feFlood\r
+ flood-opacity="0.59999999999999998"\r
+ flood-color="rgb(0,0,0)"\r
+ result="flood"\r
+ id="feFlood1123" />\r
+ <feComposite\r
+ in="flood"\r
+ in2="SourceGraphic"\r
+ operator="out"\r
+ result="composite1"\r
+ id="feComposite1125" />\r
+ <feGaussianBlur\r
+ in="composite1"\r
+ stdDeviation="1"\r
+ result="blur"\r
+ id="feGaussianBlur1127" />\r
+ <feOffset\r
+ dx="0"\r
+ dy="2"\r
+ result="offset"\r
+ id="feOffset1129" />\r
+ <feComposite\r
+ in="offset"\r
+ in2="SourceGraphic"\r
+ operator="atop"\r
+ result="composite2"\r
+ id="feComposite1131" />\r
+ </filter>\r
+ <filter\r
+ style="color-interpolation-filters:sRGB;"\r
+ inkscape:label="Drop Shadow"\r
+ id="filter950">\r
+ <feFlood\r
+ flood-opacity="0.25"\r
+ flood-color="rgb(0,0,0)"\r
+ result="flood"\r
+ id="feFlood952" />\r
+ <feComposite\r
+ in="flood"\r
+ in2="SourceGraphic"\r
+ operator="in"\r
+ result="composite1"\r
+ id="feComposite954" />\r
+ <feGaussianBlur\r
+ in="composite1"\r
+ stdDeviation="1"\r
+ result="blur"\r
+ id="feGaussianBlur956" />\r
+ <feOffset\r
+ dx="0"\r
+ dy="1"\r
+ result="offset"\r
+ id="feOffset958" />\r
+ <feComposite\r
+ in="SourceGraphic"\r
+ in2="offset"\r
+ operator="over"\r
+ result="composite2"\r
+ id="feComposite960" />\r
+ </filter>\r
+ <clipPath\r
+ clipPathUnits="userSpaceOnUse"\r
+ id="clipPath873">\r
+ <g\r
+ transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"\r
+ id="g875"\r
+ inkscape:label="Layer 1"\r
+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">\r
+ <path\r
+ style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"\r
+ 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"\r
+ id="path877"\r
+ inkscape:connector-curvature="0"\r
+ sodipodi:nodetypes="sssssssss" />\r
+ </g>\r
+ </clipPath>\r
+ <filter\r
+ inkscape:collect="always"\r
+ id="filter891"\r
+ inkscape:label="Badge Shadow">\r
+ <feGaussianBlur\r
+ inkscape:collect="always"\r
+ stdDeviation="0.71999962"\r
+ id="feGaussianBlur893" />\r
+ </filter>\r
+ </defs>\r
+ <sodipodi:namedview\r
+ id="base"\r
+ pagecolor="#ffffff"\r
+ bordercolor="#666666"\r
+ borderopacity="1.0"\r
+ inkscape:pageopacity="0.0"\r
+ inkscape:pageshadow="2"\r
+ inkscape:zoom="4.0745362"\r
+ inkscape:cx="18.514671"\r
+ inkscape:cy="49.018169"\r
+ inkscape:document-units="px"\r
+ inkscape:current-layer="layer1"\r
+ showgrid="true"\r
+ fit-margin-top="0"\r
+ fit-margin-left="0"\r
+ fit-margin-right="0"\r
+ fit-margin-bottom="0"\r
+ inkscape:window-width="1920"\r
+ inkscape:window-height="1029"\r
+ inkscape:window-x="0"\r
+ inkscape:window-y="24"\r
+ inkscape:window-maximized="1"\r
+ showborder="true"\r
+ showguides="true"\r
+ inkscape:guide-bbox="true"\r
+ inkscape:showpageshadow="false">\r
+ <inkscape:grid\r
+ type="xygrid"\r
+ id="grid821" />\r
+ <sodipodi:guide\r
+ orientation="1,0"\r
+ position="16,48"\r
+ id="guide823" />\r
+ <sodipodi:guide\r
+ orientation="0,1"\r
+ position="64,80"\r
+ id="guide825" />\r
+ <sodipodi:guide\r
+ orientation="1,0"\r
+ position="80,40"\r
+ id="guide827" />\r
+ <sodipodi:guide\r
+ orientation="0,1"\r
+ position="64,16"\r
+ id="guide829" />\r
+ </sodipodi:namedview>\r
+ <metadata\r
+ id="metadata6522">\r
+ <rdf:RDF>\r
+ <cc:Work\r
+ rdf:about="">\r
+ <dc:format>image/svg+xml</dc:format>\r
+ <dc:type\r
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />\r
+ <dc:title></dc:title>\r
+ </cc:Work>\r
+ </rdf:RDF>\r
+ </metadata>\r
+ <g\r
+ inkscape:label="BACKGROUND"\r
+ inkscape:groupmode="layer"\r
+ id="layer1"\r
+ transform="translate(268,-635.29076)"\r
+ style="display:inline">\r
+ <path\r
+ style="fill:url(#linearGradient6461);fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"\r
+ 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"\r
+ id="path6455"\r
+ inkscape:connector-curvature="0"\r
+ sodipodi:nodetypes="sssssssss" />\r
+ </g>\r
+ <g\r
+ inkscape:groupmode="layer"\r
+ id="layer3"\r
+ inkscape:label="PLACE YOUR PICTOGRAM HERE"\r
+ style="display:inline" />\r
+ <g\r
+ inkscape:groupmode="layer"\r
+ id="layer2"\r
+ inkscape:label="BADGE"\r
+ style="display:none"\r
+ sodipodi:insensitive="true">\r
+ <g\r
+ style="display:inline"\r
+ transform="translate(-340.00001,-581)"\r
+ id="g4394"\r
+ clip-path="none">\r
+ <g\r
+ id="g855">\r
+ <g\r
+ inkscape:groupmode="maskhelper"\r
+ id="g870"\r
+ clip-path="url(#clipPath873)"\r
+ style="opacity:0.6;filter:url(#filter891)">\r
+ <path\r
+ transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"\r
+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+ sodipodi:ry="12"\r
+ sodipodi:rx="12"\r
+ sodipodi:cy="552.36218"\r
+ sodipodi:cx="252"\r
+ id="path844"\r
+ 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"\r
+ sodipodi:type="arc" />\r
+ </g>\r
+ <g\r
+ id="g862">\r
+ <path\r
+ sodipodi:type="arc"\r
+ 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"\r
+ id="path4398"\r
+ sodipodi:cx="252"\r
+ sodipodi:cy="552.36218"\r
+ sodipodi:rx="12"\r
+ sodipodi:ry="12"\r
+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+ transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />\r
+ <path\r
+ transform="matrix(1.25,0,0,1.25,33,-100.45273)"\r
+ d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+ sodipodi:ry="12"\r
+ sodipodi:rx="12"\r
+ sodipodi:cy="552.36218"\r
+ sodipodi:cx="252"\r
+ id="path4400"\r
+ 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"\r
+ sodipodi:type="arc" />\r
+ <path\r
+ sodipodi:type="star"\r
+ 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"\r
+ id="path4459"\r
+ sodipodi:sides="5"\r
+ sodipodi:cx="666.19574"\r
+ sodipodi:cy="589.50385"\r
+ sodipodi:r1="7.2431178"\r
+ sodipodi:r2="4.3458705"\r
+ sodipodi:arg1="1.0471976"\r
+ sodipodi:arg2="1.6755161"\r
+ inkscape:flatsided="false"\r
+ inkscape:rounded="0.1"\r
+ inkscape:randomized="0"\r
+ 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"\r
+ transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />\r
+ </g>\r
+ </g>\r
+ </g>\r
+ </g>\r
+</svg>\r
--- /dev/null
+includes:
+ - layer:basic
+ - layer:vnfproxy
--- /dev/null
+name: pingpong
+summary: <Fill in summary here>
+maintainer: Adam Israel <Adam.Israel@ronin>
+description: |
+ <Multi-line description here>
+tags:
+ # Replace "misc" with one or more whitelisted tags from this list:
+ # https://jujucharms.com/docs/stable/authors-charm-metadata
+ - misc
+subordinate: false
+series:
+ - trusty
+ - xenial
--- /dev/null
+from charmhelpers.core.hookenv import (
+ action_get,
+ action_fail,
+ action_set,
+ config,
+ status_set,
+)
+
+from charms.reactive import (
+ remove_state as remove_flag,
+ set_state as set_flag,
+ when,
+)
+import charms.sshproxy
+import json
+
+
+cfg = config()
+
+
+@when('config.changed')
+def config_changed():
+ if all(k in cfg for k in ['mode']):
+ if cfg['mode'] in ['ping', 'pong']:
+ set_flag('pingpong.configured')
+ status_set('active', 'ready!')
+ return
+ status_set('blocked', 'Waiting for configuration')
+
+
+def is_ping():
+ if cfg['mode'] == 'ping':
+ return True
+ return False
+
+
+def is_pong():
+ return not is_ping()
+
+
+def get_port():
+ port = 18888
+ if is_pong():
+ port = 18889
+ return port
+
+
+@when('pingpong.configured')
+@when('actions.start')
+def start():
+ err = ''
+ try:
+ cmd = "service {} start".format(cfg['mode'])
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.start')
+
+
+@when('pingpong.configured')
+@when('actions.stop')
+def stop():
+ err = ''
+ try:
+ # Enter the command to stop your service(s)
+ cmd = "service {} stop".format(cfg['mode'])
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.stop')
+
+
+@when('pingpong.configured')
+@when('actions.restart')
+def restart():
+ err = ''
+ try:
+ # Enter the command to restart your service(s)
+ cmd = "service {} restart".format(cfg['mode'])
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.restart')
+
+
+@when('pingpong.configured')
+@when('actions.set-server')
+def set_server():
+ err = ''
+ try:
+ # Get the target service info
+ target_ip = action_get('server-ip')
+ target_port = action_get('server-port')
+
+ data = json.dumps({'ip': target_ip, 'port': target_port})
+
+ cmd = format_curl(
+ 'POST',
+ '/server',
+ data,
+ )
+
+ result, err = charms.sshproxy._run(cmd)
+ except Exception as err:
+ print("error: {0}".format(err))
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.set-server')
+
+
+@when('pingpong.configured')
+@when('actions.set-rate')
+def set_rate():
+ err = ''
+ try:
+ if is_ping():
+ rate = action_get('rate')
+ cmd = format_curl('POST', '/rate', '{"rate": {}}'.format(rate))
+
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.set-rate')
+
+
+@when('pingpong.configured')
+@when('actions.get-rate')
+def get_rate():
+ err = ''
+ try:
+ if is_ping():
+ cmd = format_curl('GET', '/rate')
+
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.get-rate')
+
+
+@when('pingpong.configured')
+@when('actions.get-state')
+def get_state():
+ err = ''
+ try:
+ cmd = format_curl('GET', '/state')
+
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.get-state')
+
+
+@when('pingpong.configured')
+@when('actions.get-stats')
+def get_stats():
+ err = ''
+ try:
+ cmd = format_curl('GET', '/stats')
+
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.get-stats')
+
+
+@when('pingpong.configured')
+@when('actions.start-ping')
+def start_ping():
+ err = ''
+ try:
+ cmd = format_curl('POST', '/adminstatus/state', '{"enable":true}')
+
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.start-ping')
+
+
+@when('pingpong.configured')
+@when('actions.stop-ping')
+def stop_ping():
+ err = ''
+ try:
+ cmd = format_curl('POST', '/adminstatus/state', '{"enable":false}')
+
+ result, err = charms.sshproxy._run(cmd)
+ except:
+ action_fail('command failed:' + err)
+ else:
+ action_set({'outout': result})
+ finally:
+ remove_flag('actions.stop-ping')
+
+
+def format_curl(method, path, data=None):
+ """ A utility function to build the curl command line. """
+
+ # method must be GET or POST
+ if method not in ['GET', 'POST']:
+ # Throw exception
+ return None
+
+ # Get our service info
+ host = cfg['ssh-hostname']
+ port = get_port()
+ mode = cfg['mode']
+
+ cmd = ['curl',
+ # '-D', '/dev/stdout',
+ '-H', '"Accept: application/vnd.yang.data+xml"',
+ '-H', '"Content-Type: application/vnd.yang.data+json"',
+ '-X', method]
+
+ if method == "POST" and data:
+ cmd.append('-d')
+ cmd.append("'{}'".format(data))
+
+ cmd.append(
+ 'http://{}:{}/api/v1/{}{}'.format(host, port, mode, path)
+ )
+ return cmd
--- /dev/null
+#!/bin/bash
+
+sudo add-apt-repository ppa:juju/stable -y
+sudo apt-get update
+sudo apt-get install amulet python-requests -y
--- /dev/null
+#!/usr/bin/python3
+
+import amulet
+import requests
+import unittest
+
+
+class TestCharm(unittest.TestCase):
+ def setUp(self):
+ self.d = amulet.Deployment()
+
+ self.d.add('pingpong')
+ self.d.expose('pingpong')
+
+ self.d.setup(timeout=900)
+ self.d.sentry.wait()
+
+ self.unit = self.d.sentry['pingpong'][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()