From 32e2fa55e105d5b87e9d57012b35f205e0036d59 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Wed, 14 Dec 2016 22:50:51 -0500 Subject: [PATCH] Ping/pong charm This is the Juju charm layer to build the pingpong charm Change-Id: I732746a53ce6db0faa3e7e528cb60a60e0296afe Signed-off-by: Adam Israel --- layers/pingpong/README.md | 163 ++++++++++++++++ layers/pingpong/actions.yaml | 32 +++ layers/pingpong/actions/get-rate | 18 ++ layers/pingpong/actions/get-server | 18 ++ layers/pingpong/actions/get-state | 18 ++ layers/pingpong/actions/get-stats | 18 ++ layers/pingpong/actions/set-rate | 18 ++ layers/pingpong/actions/set-server | 18 ++ layers/pingpong/actions/start-ping | 18 ++ layers/pingpong/actions/stop-ping | 18 ++ layers/pingpong/config.yaml | 5 + layers/pingpong/icon.svg | 279 +++++++++++++++++++++++++++ layers/pingpong/layer.yaml | 3 + layers/pingpong/metadata.yaml | 13 ++ layers/pingpong/reactive/pingpong.py | 247 ++++++++++++++++++++++++ layers/pingpong/tests/00-setup | 5 + layers/pingpong/tests/10-deploy | 35 ++++ 17 files changed, 926 insertions(+) create mode 100644 layers/pingpong/README.md create mode 100644 layers/pingpong/actions.yaml create mode 100755 layers/pingpong/actions/get-rate create mode 100755 layers/pingpong/actions/get-server create mode 100755 layers/pingpong/actions/get-state create mode 100755 layers/pingpong/actions/get-stats create mode 100755 layers/pingpong/actions/set-rate create mode 100755 layers/pingpong/actions/set-server create mode 100755 layers/pingpong/actions/start-ping create mode 100755 layers/pingpong/actions/stop-ping create mode 100644 layers/pingpong/config.yaml create mode 100644 layers/pingpong/icon.svg create mode 100644 layers/pingpong/layer.yaml create mode 100644 layers/pingpong/metadata.yaml create mode 100644 layers/pingpong/reactive/pingpong.py create mode 100755 layers/pingpong/tests/00-setup create mode 100755 layers/pingpong/tests/10-deploy diff --git a/layers/pingpong/README.md b/layers/pingpong/README.md new file mode 100644 index 00000000..3bec2433 --- /dev/null +++ b/layers/pingpong/README.md @@ -0,0 +1,163 @@ +# 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 diff --git a/layers/pingpong/actions.yaml b/layers/pingpong/actions.yaml new file mode 100644 index 00000000..f283ed08 --- /dev/null +++ b/layers/pingpong/actions.yaml @@ -0,0 +1,32 @@ +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." diff --git a/layers/pingpong/actions/get-rate b/layers/pingpong/actions/get-rate new file mode 100755 index 00000000..959b3e93 --- /dev/null +++ b/layers/pingpong/actions/get-rate @@ -0,0 +1,18 @@ +#!/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)) diff --git a/layers/pingpong/actions/get-server b/layers/pingpong/actions/get-server new file mode 100755 index 00000000..52e00894 --- /dev/null +++ b/layers/pingpong/actions/get-server @@ -0,0 +1,18 @@ +#!/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)) diff --git a/layers/pingpong/actions/get-state b/layers/pingpong/actions/get-state new file mode 100755 index 00000000..446e8d71 --- /dev/null +++ b/layers/pingpong/actions/get-state @@ -0,0 +1,18 @@ +#!/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)) diff --git a/layers/pingpong/actions/get-stats b/layers/pingpong/actions/get-stats new file mode 100755 index 00000000..086afc27 --- /dev/null +++ b/layers/pingpong/actions/get-stats @@ -0,0 +1,18 @@ +#!/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)) diff --git a/layers/pingpong/actions/set-rate b/layers/pingpong/actions/set-rate new file mode 100755 index 00000000..8fb723ef --- /dev/null +++ b/layers/pingpong/actions/set-rate @@ -0,0 +1,18 @@ +#!/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)) diff --git a/layers/pingpong/actions/set-server b/layers/pingpong/actions/set-server new file mode 100755 index 00000000..d1e908f5 --- /dev/null +++ b/layers/pingpong/actions/set-server @@ -0,0 +1,18 @@ +#!/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)) diff --git a/layers/pingpong/actions/start-ping b/layers/pingpong/actions/start-ping new file mode 100755 index 00000000..dee1ce19 --- /dev/null +++ b/layers/pingpong/actions/start-ping @@ -0,0 +1,18 @@ +#!/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)) diff --git a/layers/pingpong/actions/stop-ping b/layers/pingpong/actions/stop-ping new file mode 100755 index 00000000..0a106958 --- /dev/null +++ b/layers/pingpong/actions/stop-ping @@ -0,0 +1,18 @@ +#!/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)) diff --git a/layers/pingpong/config.yaml b/layers/pingpong/config.yaml new file mode 100644 index 00000000..437524e4 --- /dev/null +++ b/layers/pingpong/config.yaml @@ -0,0 +1,5 @@ +options: + mode: + type: string + default: + description: "The service type: [ping, pong]" diff --git a/layers/pingpong/icon.svg b/layers/pingpong/icon.svg new file mode 100644 index 00000000..e092eef7 --- /dev/null +++ b/layers/pingpong/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/layers/pingpong/layer.yaml b/layers/pingpong/layer.yaml new file mode 100644 index 00000000..aa4dc07f --- /dev/null +++ b/layers/pingpong/layer.yaml @@ -0,0 +1,3 @@ +includes: + - layer:basic + - layer:vnfproxy diff --git a/layers/pingpong/metadata.yaml b/layers/pingpong/metadata.yaml new file mode 100644 index 00000000..1840743e --- /dev/null +++ b/layers/pingpong/metadata.yaml @@ -0,0 +1,13 @@ +name: pingpong +summary: +maintainer: Adam Israel +description: | + +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 diff --git a/layers/pingpong/reactive/pingpong.py b/layers/pingpong/reactive/pingpong.py new file mode 100644 index 00000000..e88bd432 --- /dev/null +++ b/layers/pingpong/reactive/pingpong.py @@ -0,0 +1,247 @@ +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 diff --git a/layers/pingpong/tests/00-setup b/layers/pingpong/tests/00-setup new file mode 100755 index 00000000..f0616a56 --- /dev/null +++ b/layers/pingpong/tests/00-setup @@ -0,0 +1,5 @@ +#!/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/layers/pingpong/tests/10-deploy b/layers/pingpong/tests/10-deploy new file mode 100755 index 00000000..d1d4719d --- /dev/null +++ b/layers/pingpong/tests/10-deploy @@ -0,0 +1,35 @@ +#!/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() -- 2.25.1