From: Adam Israel Date: Wed, 14 Nov 2018 16:30:40 +0000 (-0500) Subject: Rename juju-charms -> charms X-Git-Tag: v5.0.0~19^2 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=refs%2Fchanges%2F86%2F6886%2F2;p=osm%2Fdevops.git Rename juju-charms -> charms Per discussion at the previous F2F, we are renaming "juju-charms" to simply be "charms". This makes the necessary changes across the devops repo. Signed-off-by: Adam Israel Change-Id: I95651dbdbb29f9c3f9425db551642778625ea9a4 --- diff --git a/charms/.gitignore b/charms/.gitignore new file mode 100644 index 00000000..9f751ef9 --- /dev/null +++ b/charms/.gitignore @@ -0,0 +1,2 @@ +deps/ +builds/ diff --git a/charms/Dockerfile b/charms/Dockerfile new file mode 100644 index 00000000..36248e2d --- /dev/null +++ b/charms/Dockerfile @@ -0,0 +1,4 @@ +FROM ubuntu:16.04 + +RUN apt-get update && apt install -y charm-tools + diff --git a/charms/Makefile b/charms/Makefile new file mode 100644 index 00000000..ad8af987 --- /dev/null +++ b/charms/Makefile @@ -0,0 +1,42 @@ +# +# Copyright 2016 RIFT.io Inc +# +# 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. +# +# + +BUILD_DIR = . + +CHARMS:= pingpong vyos-proxy netutils simple ansible-charm +CHARM_SRC_DIR := layers +CHARM_BUILD_DIR := $(BUILD_DIR)/builds + +CHARM_SRC_DIRS := $(addprefix $(CHARM_SRC_DIR)/, $(CHARMS)) +CHARM_BUILD_DIRS := $(addprefix $(CHARM_BUILD_DIR)/, $(CHARMS)) +CHARM_DOCKER_TAG := charm-tools +DOCKER_BUILD ?= + +all: $(CHARM_BUILD_DIRS) + +clean: + -@ $(RM) -rf $(CHARM_BUILD_DIR) + + +ifdef DOCKER_BUILD +$(CHARM_BUILD_DIR)/%: $(CHARM_SRC_DIR)/% + docker build -t $(CHARM_DOCKER_TAG) . + docker run -u $$(id -u):$$(id -g) -v$$(pwd):$$(pwd) -w$$(pwd) $(CHARM_DOCKER_TAG) charm-build -o $(BUILD_DIR) $< +else +$(CHARM_BUILD_DIR)/%: $(CHARM_SRC_DIR)/% + charm build -o $(BUILD_DIR) $< +endif diff --git a/charms/README.md b/charms/README.md new file mode 100644 index 00000000..6eaf43f8 --- /dev/null +++ b/charms/README.md @@ -0,0 +1,79 @@ +# Juju Charm usage and development + +This document is intended to provide a brief overview of the components included +in this repository as well as recommendations for how to develop, build, and +publish charms. + +Please read the [develper geting started guide](https://jujucharms.com/docs/2.0/developer-getting-started) before proceeding. + +## Directory structure + +``` +. +├── builds +│   └── vpe-router +├── interfaces +├── layers +│   └── vpe-router +└── module-blueprints +``` + +The source code of a charm is referred to as a "layer". This layer is compiled +into a charm and placed in the `builds/` directory. Interfaces, currently +unused in this context, extend relationships between applications. + +## Development workflow +### Prepare your build environment +``` +# Source the environment variables JUJU_REPOSITORY, INTERFACE_PATH, and +# LAYER_PATH, which are needed to build a charm. You could also place these +# in your $HOME/.bashrc +$ source juju-env.sh +``` +#### Install the `charm` command, either via apt: + +``` +$ sudo apt install charm +``` + +or with [snap](http://snapcraft.io/) + +``` +$ snap install charm --edge +``` + +To build a charm, simply run `charm build` inside of a layer. +``` +$ cd $LAYER_PATH/vpe-router +$ charm build +$ charm deploy $JUJU_REPOSITORY/builds/vpe-router +``` + +## Publishing to jujucharms.com + +Publishing to the Juju Charm store requires a launchpad login. With that, login +to [jujucharms.com](http://www.jujucharms.com/). + +Next, you'll use the charm command to publish your compiled charm. This will +put the charm into the store where it can be used by anyone with access. + +For example, if I wanted to publish the latest version of the vpe-router charm: + +# Step 1: Upload the charm to the "unpublished" channel +``` +$ cd $JUJU_REPOSITORY/builds +$ charm push vpe-router/ cs:~aisrael/vpe-router +url: cs:~aisrael/vpe-router-0 +channel: unpublished +``` + +# There are four channels to release a charm: edge, beta, candidate, and stable +``` +$ charm release cs:~aisrael/vpe-router-0 --channel=edge +url: cs:~aisrael/vpe-router-0 +channel: edge +``` +The charm can then be deployed directly from the charm store: +``` +$ juju deploy cs:~aisrael/vpe-router --channel=edge +``` diff --git a/charms/juju-env.sh b/charms/juju-env.sh new file mode 100644 index 00000000..59fa9e71 --- /dev/null +++ b/charms/juju-env.sh @@ -0,0 +1,4 @@ +# Set the Juju env variables for building a layer +export JUJU_REPOSITORY=`pwd` +export INTERFACE_PATH=$JUJU_REPOSITORY/interfaces +export LAYER_PATH=$JUJU_REPOSITORY/layers diff --git a/charms/layers/ansible-charm/LICENSE b/charms/layers/ansible-charm/LICENSE new file mode 100644 index 00000000..830e5599 --- /dev/null +++ b/charms/layers/ansible-charm/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Borja Nogales , Iván Vidal + + 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. diff --git a/charms/layers/ansible-charm/README.md b/charms/layers/ansible-charm/README.md new file mode 100755 index 00000000..05291377 --- /dev/null +++ b/charms/layers/ansible-charm/README.md @@ -0,0 +1,23 @@ +# Overview +This base charm layer provides a template to create a *proxy charm*, which enables to configure a Virtual Network Function (VNF) instantiated trough Open Source MANO (OSM) using an Ansible playbook. The files included in this charm layer are free to use under the terms provided by the license information specified below. + + +# Usage +The base charm layer includes the following base layers: [vnfproxy](https://github.com/AdamIsrael/vnfproxy) and [ansible-base](https://github.com/chuckbutler/ansible-base). It provides a template ready for customization, which enables to create a *proxy charm* that supports the execution of an Ansible playbook to configure a VNF. For more information on *proxy charms*, the reader is referred to the [OSM wiki](https://osm.etsi.org/wikipub/index.php/Creating_your_own_VNF_charm_(Release_TWO)). + +Step by step instructions to use the base charm layer: + +1. Include the playbook under the *playbook* folder of the base charm layer; name it as *playbook.yaml*. Alternatively, you can open the file *playbook.yaml* alreay existing in this directory and paste the playbook in this file. + +2. The base charm layer already implements a Juju action, _**ansible-playbook**_, which runs the playbook *playbook/playbook.yaml*. You can optionally define additional actions, if needed by your VNF. + +3. Build the charm, via the *charm build* command. + +4. Update the VNF descriptor (VNFD) to use the charm: a) specify the name of the Juju charm in in the VNF configuration; b) Include the action “ansible-playbook” with no arguments as a service primitive and as an initial configuration primitive. + +5. Include the compiled charm in the VNF package. + +More comprehensive and complementary information on building *Proxy charms* can be found in the [OSM wiki](https://osm.etsi.org/wikipub/index.php/Creating_your_own_VNF_charm_(Release_TWO)) and in the documentation of the [vnfproxy layer](https://github.com/AdamIsrael/vnfproxy). + +# Upstream Project Name +This work has been supported by the European H2020 5GinFIRE project (grant agreement 732497). diff --git a/charms/layers/ansible-charm/actions.yaml b/charms/layers/ansible-charm/actions.yaml new file mode 100755 index 00000000..88073033 --- /dev/null +++ b/charms/layers/ansible-charm/actions.yaml @@ -0,0 +1,20 @@ +# +# OSM devops/charms - Ansible charm inside OSM devops +# +# Copyright 2017-2018 Universidad Carlos III de Madrid +# +# 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. +# + +ansible-playbook: + description: "Configure needed ansible files and run the playbook" diff --git a/charms/layers/ansible-charm/actions/ansible-playbook b/charms/layers/ansible-charm/actions/ansible-playbook new file mode 100755 index 00000000..67f581f4 --- /dev/null +++ b/charms/layers/ansible-charm/actions/ansible-playbook @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# OSM devops/charms - Ansible charm inside OSM devops +# +# Copyright 2017-2018 Universidad Carlos III de Madrid +# +# 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/ansible-charm/icon.svg b/charms/layers/ansible-charm/icon.svg new file mode 100755 index 00000000..e092eef7 --- /dev/null +++ b/charms/layers/ansible-charm/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/charms/layers/ansible-charm/layer.yaml b/charms/layers/ansible-charm/layer.yaml new file mode 100755 index 00000000..788e67c6 --- /dev/null +++ b/charms/layers/ansible-charm/layer.yaml @@ -0,0 +1,19 @@ +# +# OSM devops/charms - Ansible charm inside OSM devops +# +# Copyright 2017-2018 Universidad Carlos III de Madrid +# +# 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:ansible-base', 'layer:vnfproxy'] diff --git a/charms/layers/ansible-charm/metadata.yaml b/charms/layers/ansible-charm/metadata.yaml new file mode 100755 index 00000000..2837b35b --- /dev/null +++ b/charms/layers/ansible-charm/metadata.yaml @@ -0,0 +1,30 @@ +# +# OSM devops/charms - Ansible charm inside OSM devops +# +# Copyright 2017-2018 Universidad Carlos III de Madrid +# +# 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: ansible-charm +summary: Base charm layer to configure a VNF using ansible. +maintainer: borja +description: | + Base charm layer to configure a VNF using ansible, through the Juju framework of Open Source MANO (OSM). +series: + - trusty + - xenial +tags: + - osm + - vnf +subordinate: false diff --git a/charms/layers/ansible-charm/playbook/playbook.yaml b/charms/layers/ansible-charm/playbook/playbook.yaml new file mode 100755 index 00000000..67a34930 --- /dev/null +++ b/charms/layers/ansible-charm/playbook/playbook.yaml @@ -0,0 +1,24 @@ +# +# OSM devops/charms - Ansible charm inside OSM devops +# +# Copyright 2017-2018 Universidad Carlos III de Madrid +# +# 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. +# +# Place here the ansible playbook to be run + +--- +- hosts: test + tasks: + - name: Create file + file: path=/tmp/playbook_created_file state=touch diff --git a/charms/layers/ansible-charm/reactive/ansible_charm.py b/charms/layers/ansible-charm/reactive/ansible_charm.py new file mode 100755 index 00000000..61425f35 --- /dev/null +++ b/charms/layers/ansible-charm/reactive/ansible_charm.py @@ -0,0 +1,91 @@ +# OSM devops/charms - Ansible charm inside OSM devops +# +# Copyright 2017-2018 Universidad Carlos III de Madrid +# Copyright 2018 Altran +# +# 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_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 + +from subprocess import ( + Popen, + CalledProcessError, + PIPE, +) + +#from charms.ansible import apply_playbook +import os, fnmatch +import subprocess + +cfg = config() + + +# Sets the status of the charm to show in OSM: configured +@when('config.changed') +def config_changed(): + set_flag('ansible-charm.configured') + status_set('active', 'ready!') + return + + +# Edits ansible config files and executes ansible-playbook +@when('ansible-charm.configured') +@when('actions.ansible-playbook') +def ansible_playbook(): + try: + # Retrieve the ssh parameter + cfg = config() + # edit ansible hosts file with the VNF parameters + h = open("/etc/ansible/hosts", "wt") + h.write("[test]\n") + h1 = "{} ansible_connection=ssh ansible_ssh_user={} ansible_ssh_pass={} ansible_python_interpreter=/usr/bin/python3\n".format(cfg['ssh-hostname'],cfg['ssh-username'],cfg['ssh-password']) + h.write(h1) + h.close() + # edit ansible config to enable ssh connection with th VNF + c = open("/etc/ansible/ansible.cfg", "wt") + c.write("[defaults]\n") + c.write("host_key_checking = False\n") + c.close() + # execute the ansible playbook + path = find('playbook.yaml','/var/lib/juju/agents/') + call = ['ansible-playbook', path] + subprocess.check_call(call) + except Exception as e: + action_fail('command failed: {}, errors: {}'.format(e, e.output)) + remove_flag('actions.ansible-playbook') + return + finally: + remove_flag('actions.ansible-playbook') + + +# Function to find the playbook path +def find(pattern, path): + result = '' + for root, dirs, files in os.walk(path): + for name in files: + if fnmatch.fnmatch(name, pattern): + result = os.path.join(root, name) + return result diff --git a/charms/layers/ansible-charm/tests/00-setup b/charms/layers/ansible-charm/tests/00-setup new file mode 100755 index 00000000..8c0ff38b --- /dev/null +++ b/charms/layers/ansible-charm/tests/00-setup @@ -0,0 +1,22 @@ +#!/bin/bash +# +# OSM devops/charms - Ansible charm inside OSM devops +# +# Copyright 2017-2018 Universidad Carlos III de Madrid +# +# 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. +# + +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/ansible-charm/tests/10-deploy b/charms/layers/ansible-charm/tests/10-deploy new file mode 100755 index 00000000..35e09186 --- /dev/null +++ b/charms/layers/ansible-charm/tests/10-deploy @@ -0,0 +1,52 @@ +#!/usr/bin/python3 +# +# OSM devops/charms - Ansible charm inside OSM devops +# +# Copyright 2017-2018 Universidad Carlos III de Madrid +# +# 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 amulet +import requests +import unittest + + +class TestCharm(unittest.TestCase): + def setUp(self): + self.d = amulet.Deployment() + + self.d.add('ansible-charm') + self.d.expose('ansible-charm') + + self.d.setup(timeout=900) + self.d.sentry.wait() + + self.unit = self.d.sentry['ansible-charm'][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() diff --git a/charms/layers/netutils/LICENSE b/charms/layers/netutils/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/charms/layers/netutils/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/charms/layers/netutils/README.md b/charms/layers/netutils/README.md new file mode 100644 index 00000000..e8258c03 --- /dev/null +++ b/charms/layers/netutils/README.md @@ -0,0 +1,64 @@ +# Overview + +This charm provides basic network utilities that can be run from a Juju-deployed +machine. + +# Usage + +To deploy the charm: +```bash +$ juju deploy cs:~nfv/netutils +``` + +To run an action: +```bash +$ juju run-action netutils/0 ping destination=google.com +$ juju run-action netutils/0 traceroute destination=google.com +``` + +To fetch the output of an action: +```bash +$ juju show-action-output 026b3d4c-0bb2-4818-8d24-9855936cdcdf +results: + output: | + traceroute to google.com (216.58.198.78), 30 hops max, 60 byte packets + 1 ec2-79-125-0-86.eu-west-1.compute.amazonaws.com (79.125.0.86) 1.431 ms 1.410 ms 1.380 ms + 2 100.64.2.73 (100.64.2.73) 1.647 ms 100.64.2.103 (100.64.2.103) 1.247 ms 100.64.2.121 (100.64.2.121) 1.224 ms + 3 100.64.0.232 (100.64.0.232) 1.296 ms 100.64.0.184 (100.64.0.184) 1.515 ms 100.64.0.234 (100.64.0.234) 1.079 ms + 4 100.64.16.37 (100.64.16.37) 0.377 ms 100.64.16.49 (100.64.16.49) 0.347 ms 100.64.16.1 (100.64.16.1) 0.340 ms + 5 176.32.107.12 (176.32.107.12) 0.739 ms 176.32.107.4 (176.32.107.4) 0.875 ms 0.748 ms + 6 178.236.0.111 (178.236.0.111) 0.650 ms 0.641 ms 0.645 ms + 7 72.14.215.85 (72.14.215.85) 0.544 ms 1.508 ms 1.498 ms + 8 209.85.252.198 (209.85.252.198) 0.680 ms 0.659 ms 0.618 ms + 9 64.233.174.27 (64.233.174.27) 0.690 ms 0.682 ms 0.634 ms + 10 dub08s02-in-f14.1e100.net (216.58.198.78) 0.568 ms 0.560 ms 0.595 ms +status: completed +timing: + completed: 2016-06-29 14:50:04 +0000 UTC + enqueued: 2016-06-29 14:50:03 +0000 UTC + started: 2016-06-29 14:50:03 +0000 UTC +``` +## iperf3 + +Because iperf3 has a client and server component, the netutils charm can operate +as both. Setting the iperf3 configuration value to True will start iperf3 in +server mode, running as a daemon. +``` +$ juju deploy cs:~nfv/netutils client +$ juju deploy cs:~nfv/netutils server iperf3=True +$ juju run-action client/0 iperf host= [...] +``` + +## Scale out Usage + +With great scalability comes great power, but please don't use this to DDoS anyone without their permission. + +## Known Limitations and Issues + +# Contact Information + +## Contributing to the charm + + - The compiled charm can be found [here](https://www.jujucharms.com/u/nfv/netutils). + - [layer/netutils](https://osm.etsi.org/gitweb/?p=osm/juju-charms.git;a=summary/) contains the source of the layer. + - Please add any bugs or feature requests to the [bugzilla](https://osm.etsi.org/bugzilla/buglist.cgi?component=Juju-charms&list_id=426&product=OSM&resolution=---). diff --git a/charms/layers/netutils/actions.yaml b/charms/layers/netutils/actions.yaml new file mode 100644 index 00000000..f4f78843 --- /dev/null +++ b/charms/layers/netutils/actions.yaml @@ -0,0 +1,133 @@ +nmap: + description: "nmap a thing!" + params: + destination: + description: "destination to scan" + type: string + required: + - destination +ping: + description: 'ping a thing!' + params: + count: + description: "Stop after sending count ECHO_REQUEST packets" + type: integer + default: 30 + destination: + description: "destination of ping request" + type: string + required: + - destination +traceroute: + description: 'trace a thing!' + params: + hops: + description: "Stop tracing after count hops" + type: integer + default: 30 + destination: + description: "destination of traceroute request" + type: string + required: + - destination +dig: + description: "DNS lookup" + params: + nsserver: + description: "The nameserver to lookup against." + type: string + host: + description: "The host to lookup" + type: string + type: + description: "The DNS record type to lookup" + type: string + required: + - host +iperf: + description: "" + params: + host: + description: "" + type: string + port: + description: "" + type: integer + default: 5201 + format: + description: "" + type: string + interval: + description: "" + type: string + affinity: + description: "" + type: string + udp: + description: "Use UDP rather than TCP" + type: boolean + default: False + bandwidth: + description: "Set the target bandwidth to n bits/sec (default 1Mbit/sec for UDP, unlimited for TCP)" + type: integer + default: 1 + time: + description: "Time, in seconds, to transmit for." + type: integer + default: 10 + blockcount: + description: "The number of blocks to transmit" + type: integer + length: + description: "The length of buffer to read or write (default 128KB for TCP, 8KB for UDP)" + type: integer + parallel: + description: "The number of parallel client streams to run" + type: integer + reverse: + description: "Run in reverse mode (server sends, client receives)." + type: boolean + default: false + window: + description: "Window size/socket buffer size." + type: integer + bind: + description: "Bind to a specific interface or multicast address" + type: string + mss: + description: "Set the TCP maximum segment size (MTU - 40 bytes)" + type: integer + no-delay: + description: "Set the TCP no delay, disabling Nagle's algorithm." + type: boolean + default: false + ipv4: + description: "Only use IPv4" + type: boolean + default: false + ipv6: + description: "Only use IPv6" + type: boolean + default: false + tos: + description: "Set the IP 'type of service'" + type: integer + flowlabel: + description: "Set the IPv6 flow label (linux-only)" + type: string + zerocopy: + description: "Use a 'zero copy' method of sending data, such as sendfile(s), instead of the usual write(2)." + type: boolean + default: false + omit: + description: "Omit the first n seconds of the test, to skip past the TCP slow-start period." + type: integer + title: + description: "Prefix every output line with this string." + type: string + congestion: + description: "Set the linux congestion control algorithm." + type: string + + required: + - host diff --git a/charms/layers/netutils/actions/dig b/charms/layers/netutils/actions/dig new file mode 100755 index 00000000..736a4069 --- /dev/null +++ b/charms/layers/netutils/actions/dig @@ -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.dig') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/charms/layers/netutils/actions/iperf b/charms/layers/netutils/actions/iperf new file mode 100755 index 00000000..750028e0 --- /dev/null +++ b/charms/layers/netutils/actions/iperf @@ -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.iperf3') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/charms/layers/netutils/actions/nmap b/charms/layers/netutils/actions/nmap new file mode 100755 index 00000000..ede4f5b9 --- /dev/null +++ b/charms/layers/netutils/actions/nmap @@ -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.nmap') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/charms/layers/netutils/actions/ping b/charms/layers/netutils/actions/ping new file mode 100755 index 00000000..9850fe76 --- /dev/null +++ b/charms/layers/netutils/actions/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.ping') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/charms/layers/netutils/actions/traceroute b/charms/layers/netutils/actions/traceroute new file mode 100755 index 00000000..229ed327 --- /dev/null +++ b/charms/layers/netutils/actions/traceroute @@ -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.traceroute') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/charms/layers/netutils/config.yaml b/charms/layers/netutils/config.yaml new file mode 100644 index 00000000..61010636 --- /dev/null +++ b/charms/layers/netutils/config.yaml @@ -0,0 +1,5 @@ +options: + iperf3: + type: boolean + default: false + description: "Enabling this option will start iperf3 in server mode." diff --git a/charms/layers/netutils/icon.svg b/charms/layers/netutils/icon.svg new file mode 100644 index 00000000..e092eef7 --- /dev/null +++ b/charms/layers/netutils/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/charms/layers/netutils/layer.yaml b/charms/layers/netutils/layer.yaml new file mode 100644 index 00000000..e13c81a8 --- /dev/null +++ b/charms/layers/netutils/layer.yaml @@ -0,0 +1,8 @@ +repo: git@github.com:AdamIsrael/layer-netutils.git +includes: ['layer:basic', 'layer:sshproxy'] +options: + basic: + packages: + - traceroute + - nmap + - iperf3 diff --git a/charms/layers/netutils/metadata.yaml b/charms/layers/netutils/metadata.yaml new file mode 100644 index 00000000..c42d9637 --- /dev/null +++ b/charms/layers/netutils/metadata.yaml @@ -0,0 +1,15 @@ +name: netutils +summary: A suite of network-related utilities. +maintainer: Adam Israel +description: | + A suite of network-related utilities, such as ping and traceroute, that + can be deployed into a data center in order to diagnose connectivity issues. +tags: + # https://jujucharms.com/docs/stable/authors-charm-metadata + - ops + - network + - performance +series: + - trusty + - xenial +subordinate: false diff --git a/charms/layers/netutils/reactive/layer_netutils.py b/charms/layers/netutils/reactive/layer_netutils.py new file mode 100644 index 00000000..1fd4cb24 --- /dev/null +++ b/charms/layers/netutils/reactive/layer_netutils.py @@ -0,0 +1,137 @@ +from charmhelpers.core.hookenv import ( + action_get, + action_fail, + action_set, + config, + log, + status_set, +) + +from charms.reactive import ( + remove_state as remove_flag, + set_state as set_flag, + when, + when_not, +) +import charms.sshproxy +from subprocess import CalledProcessError + + +@when_not('netutils.ready') +def ready(): + status_set('active', 'Ready!') + set_flag('netutils.ready') + + +@when('actions.dig') +def dig(): + err = '' + try: + nsserver = action_get('nsserver') + host = action_get('host') + nstype = action_get('type') + cmd = "dig" + + if nsserver: + cmd += " @{}".format(nsserver) + if host: + cmd += " {}".format(host) + else: + action_fail('Hostname required.') + if nstype: + cmd += " -t {}".format(nstype) + + result, err = charms.sshproxy._run(cmd) + except: + action_fail('dig command failed:' + err) + else: + action_set({'outout': result}) + finally: + remove_flag('actions.dig') + + +@when('actions.nmap') +def nmap(): + err = '' + try: + result, err = charms.sshproxy._run( + 'nmap {}'.format(action_get('destination')) + ) + except: + action_fail('nmap command failed:' + err) + else: + action_set({'outout': result}) + finally: + remove_flag('actions.nmap') + + +@when('actions.ping') +def ping(): + err = '' + try: + result, err = charms.sshproxy._run('ping -qc {} {}'.format( + action_get('count'), action_get('destination')) + ) + + except: + action_fail('ping command failed:' + err) + else: + # Here you can send results back from ping, if you had time to parse it + action_set({'output': result}) + finally: + remove_flag('actions.ping') + + +@when('actions.traceroute') +def traceroute(): + try: + result, err = charms.sshproxy._run( + 'traceroute -m {} {}'.format( + action_get('hops'), + action_get('destination') + ) + ) + except: + action_fail('traceroute command failed') + else: + # Here you can send results back from ping, if you had time to parse it + action_set({'output': result}) + finally: + remove_flag('actions.traceroute') + + +@when('actions.iperf3') +def iperf3(): + err = '' + try: + # TODO: read all the flags via action_get and build the + # proper command line to run iperf3 + host = action_get('host') + + cmd = 'iperf3 -c {} --json'.format(host) + result, err = charms.sshproxy._run(cmd) + except CalledProcessError as e: + action_fail('iperf3 command failed:' + e.output) + else: + action_set({'outout': result}) + finally: + remove_flag('actions.iperf3') + + +@when('config.changed') +def config_changed(): + """ Handle configuration changes """ + cfg = config() + if cfg.changed('iperf3'): + if cfg['iperf3']: + # start iperf in server + daemon mode + cmd = "iperf3 -s -D" + else: + cmd = "killall iperf3" + try: + charms.sshproxy._run(cmd) + log("iperf3 stopped.") + except CalledProcessError: + log("iperf3 not running.") + else: + log("iperf3 started.") diff --git a/charms/layers/netutils/tests/00-setup b/charms/layers/netutils/tests/00-setup new file mode 100755 index 00000000..f0616a56 --- /dev/null +++ b/charms/layers/netutils/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/charms/layers/netutils/tests/10-deploy b/charms/layers/netutils/tests/10-deploy new file mode 100755 index 00000000..ef269cda --- /dev/null +++ b/charms/layers/netutils/tests/10-deploy @@ -0,0 +1,31 @@ +#!/usr/bin/python3 + +import amulet +import requests +import unittest + + +class TestCharm(unittest.TestCase): + def setUp(self): + self.d = amulet.Deployment() + + self.d.add('layer-netutils') + self.d.expose('layer-netutils') + + self.d.setup(timeout=900) + self.d.sentry.wait() + + self.unit = self.d.sentry['layer-netutils'][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 diff --git a/charms/layers/pingpong/README.md b/charms/layers/pingpong/README.md new file mode 100644 index 00000000..3bec2433 --- /dev/null +++ b/charms/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/charms/layers/pingpong/actions.yaml b/charms/layers/pingpong/actions.yaml new file mode 100644 index 00000000..a5928f1f --- /dev/null +++ b/charms/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-traffic: + description: "Start the traffic generator or echo." +stop-traffic: + description: "Stop the traffic generator or echo." diff --git a/charms/layers/pingpong/actions/get-rate b/charms/layers/pingpong/actions/get-rate new file mode 100755 index 00000000..959b3e93 --- /dev/null +++ b/charms/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/charms/layers/pingpong/actions/get-server b/charms/layers/pingpong/actions/get-server new file mode 100755 index 00000000..52e00894 --- /dev/null +++ b/charms/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/charms/layers/pingpong/actions/get-state b/charms/layers/pingpong/actions/get-state new file mode 100755 index 00000000..446e8d71 --- /dev/null +++ b/charms/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/charms/layers/pingpong/actions/get-stats b/charms/layers/pingpong/actions/get-stats new file mode 100755 index 00000000..086afc27 --- /dev/null +++ b/charms/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/charms/layers/pingpong/actions/set-rate b/charms/layers/pingpong/actions/set-rate new file mode 100755 index 00000000..8fb723ef --- /dev/null +++ b/charms/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/charms/layers/pingpong/actions/set-server b/charms/layers/pingpong/actions/set-server new file mode 100755 index 00000000..d1e908f5 --- /dev/null +++ b/charms/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/charms/layers/pingpong/actions/start-traffic b/charms/layers/pingpong/actions/start-traffic new file mode 100755 index 00000000..562ac4c7 --- /dev/null +++ b/charms/layers/pingpong/actions/start-traffic @@ -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-traffic') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/charms/layers/pingpong/actions/stop-traffic b/charms/layers/pingpong/actions/stop-traffic new file mode 100755 index 00000000..9352b331 --- /dev/null +++ b/charms/layers/pingpong/actions/stop-traffic @@ -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-traffic') + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/charms/layers/pingpong/config.yaml b/charms/layers/pingpong/config.yaml new file mode 100644 index 00000000..437524e4 --- /dev/null +++ b/charms/layers/pingpong/config.yaml @@ -0,0 +1,5 @@ +options: + mode: + type: string + default: + description: "The service type: [ping, pong]" diff --git a/charms/layers/pingpong/icon.svg b/charms/layers/pingpong/icon.svg new file mode 100644 index 00000000..e092eef7 --- /dev/null +++ b/charms/layers/pingpong/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/charms/layers/pingpong/layer.yaml b/charms/layers/pingpong/layer.yaml new file mode 100644 index 00000000..833eea68 --- /dev/null +++ b/charms/layers/pingpong/layer.yaml @@ -0,0 +1,4 @@ +includes: + - layer:basic + - layer:vnfproxy +repo: https://osm.etsi.org/gerrit/osm/juju-charms diff --git a/charms/layers/pingpong/metadata.yaml b/charms/layers/pingpong/metadata.yaml new file mode 100644 index 00000000..1840743e --- /dev/null +++ b/charms/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/charms/layers/pingpong/reactive/pingpong.py b/charms/layers/pingpong/reactive/pingpong.py new file mode 100755 index 00000000..7eedc84a --- /dev/null +++ b/charms/layers/pingpong/reactive/pingpong.py @@ -0,0 +1,308 @@ +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, + when_not, +) +import charms.sshproxy +# from subprocess import ( +# Popen, +# CalledProcessError, +# PIPE, +# ) + + +cfg = config() + + +@when_not('pingpong.configured') +def not_configured(): + """Check the current configuration. + + Check the current values in config to see if we have enough + information to continue. + """ + config_changed() + + +@when('config.changed', 'sshproxy.configured') +def config_changed(): + """Verify the configuration. + + Verify that the charm has been configured + """ + + try: + status_set('maintenance', 'Verifying configuration data...') + + (validated, output) = charms.sshproxy.verify_ssh_credentials() + if not validated: + status_set('blocked', 'Unable to verify SSH credentials: {}'.format( + output + )) + return + + 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') + + except Exception as err: + status_set('blocked', 'Waiting for valid configuration ({})'.format(err)) + + +@when('config.changed') +@when_not('sshproxy.configured') +def invalid_credentials(): + status_set('blocked', 'Waiting for SSH credentials.') + pass + + +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(): + try: + # Bring up the eth1 interface. + # The selinux label on the file needs to be set correctly + cmd = "sudo timeout 5 /sbin/restorecon -v /etc/sysconfig/network-scripts/ifcfg-eth1" + result, err = charms.sshproxy._run(cmd) + except Exception as e: + err = "{}".format(e) + action_fail('command failed: {}, errors: {}'.format(err, e.output)) + remove_flag('actions.start') + return + + # Attempt to raise the non-mgmt interface, but ignore failures if + # the interface is already up. + try: + cmd = "sudo timeout 30 /sbin/ifup eth1" + result, err = charms.sshproxy._run(cmd) + except Exception as e: + pass + + try: + cmd = "sudo timeout 30 /usr/bin/systemctl start {}". \ + format(cfg['mode']) + result, err = charms.sshproxy._run(cmd) + except Exception as e: + action_fail('command failed: {}, errors: {}'.format(e, e.output)) + else: + action_set({'stdout': result, + 'errors': err}) + finally: + remove_flag('actions.start') + + +@when('pingpong.configured') +@when('actions.stop') +def stop(): + try: + # Enter the command to stop your service(s) + cmd = "sudo timeout 30 /usr/bin/systemctl stop {}".format(cfg['mode']) + result, err = charms.sshproxy._run(cmd) + except Exception as e: + action_fail('command failed: {}, errors: {}'.format(e, e.output)) + else: + action_set({'stdout': result, + 'errors': err}) + finally: + remove_flag('actions.stop') + + +@when('pingpong.configured') +@when('actions.restart') +def restart(): + try: + # Enter the command to restart your service(s) + cmd = "sudo timeout 30 /usr/bin/systemctl restart {}".format(cfg['mode']) + result, err = charms.sshproxy._run(cmd) + except Exception as e: + action_fail('command failed: {}, errors: {}'.format(e, e.output)) + else: + action_set({'stdout': result, + 'errors': err}) + finally: + remove_flag('actions.restart') + + +@when('pingpong.configured') +@when('actions.set-server') +def set_server(): + try: + # Get the target service info + target_ip = action_get('server-ip') + target_port = action_get('server-port') + + data = '{{"ip" : "{}", "port" : {} }}'. \ + format(target_ip, target_port) + + cmd = format_curl( + 'POST', + '/server', + data, + ) + + result, err = charms.sshproxy._run(cmd) + except Exception as e: + action_fail('command failed: {}, errors: {}'.format(e, e.output)) + else: + action_set({'stdout': result, + 'errors': err}) + finally: + remove_flag('actions.set-server') + + +@when('pingpong.configured') +@when('actions.set-rate') +def set_rate(): + try: + if is_ping(): + rate = action_get('rate') + cmd = format_curl('POST', '/rate', '{{"rate" : {}}}'.format(rate)) + + result, err = charms.sshproxy._run(cmd) + except Exception as e: + err = "{}".format(e) + action_fail('command failed: {}, errors: {}'.format(err, e.output)) + else: + action_set({'stdout': result, + 'errors': err}) + finally: + remove_flag('actions.set-rate') + + +@when('pingpong.configured') +@when('actions.get-rate') +def get_rate(): + try: + if is_ping(): + cmd = format_curl('GET', '/rate') + + result, err = charms.sshproxy._run(cmd) + except Exception as e: + action_fail('command failed: {}, errors: {}'.format(e, e.output)) + else: + action_set({'stdout': result, + 'errors': err}) + finally: + remove_flag('actions.get-rate') + + +@when('pingpong.configured') +@when('actions.get-state') +def get_state(): + try: + cmd = format_curl('GET', '/state') + + result, err = charms.sshproxy._run(cmd) + except Exception as e: + action_fail('command failed: {}, errors: {}'.format(e, e.output)) + else: + action_set({'stdout': result, + 'errors': err}) + finally: + remove_flag('actions.get-state') + + +@when('pingpong.configured') +@when('actions.get-stats') +def get_stats(): + try: + cmd = format_curl('GET', '/stats') + + result, err = charms.sshproxy._run(cmd) + except Exception as e: + action_fail('command failed: {}, errors: {}'.format(e, e.output)) + else: + action_set({'stdout': result, + 'errors': err}) + finally: + remove_flag('actions.get-stats') + + +@when('pingpong.configured') +@when('actions.start-traffic') +def start_traffic(): + try: + cmd = format_curl('POST', '/adminstatus/state', '{"enable" : true}') + + result, err = charms.sshproxy._run(cmd) + except Exception as e: + action_fail('command failed: {}, errors: {}'.format(e, e.output)) + else: + action_set({'stdout': result, + 'errors': err}) + finally: + remove_flag('actions.start-traffic') + + +@when('pingpong.configured') +@when('actions.stop-traffic') +def stop_traffic(): + try: + cmd = format_curl('POST', '/adminstatus/state', '{"enable" : false}') + + result, err = charms.sshproxy._run(cmd) + except Exception as e: + action_fail('command failed: {}, errors: {}'.format(e, e.output)) + else: + action_set({'stdout': result, + 'errors': err}) + finally: + remove_flag('actions.stop-traffic') + + +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 = '127.0.0.1' + 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/charms/layers/pingpong/tests/00-setup b/charms/layers/pingpong/tests/00-setup new file mode 100755 index 00000000..f0616a56 --- /dev/null +++ b/charms/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/charms/layers/pingpong/tests/10-deploy b/charms/layers/pingpong/tests/10-deploy new file mode 100755 index 00000000..d1d4719d --- /dev/null +++ b/charms/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() diff --git a/charms/layers/simple/README.md b/charms/layers/simple/README.md new file mode 100644 index 00000000..f9d6eedf --- /dev/null +++ b/charms/layers/simple/README.md @@ -0,0 +1,53 @@ +# Overview + +This is an example charm as demonstrated in the OSM [Hackfest](https://osm.etsi.org/wikipub/index.php/OSM_workshops_and_events) series. + +This is intended to provide a well-documented example of the proxy charm written by Hackfest participants. + +# Prerequisites + +There are two ways that you can exercise this charm: install the latest stable release of OSM or use Juju directly. + +The workshop materials and tutorials cover using charms as part of OSM. You can follow that approach, but this README will focus on using Juju directly. We highly recommend that vendors and charm developers use this approach for the initial development of the charm. + +## Ubuntu 16.04 or higher + +We recommend using Ubuntu 16.04 or higher for the development and testing of charms. It is assumed that you have installed Ubuntu either on physical hardware or in a Virtual Machine. + +## Install LXD and Juju + +We will be installing the required software via snap. Snaps are containerised software packages, preferred because they are easy to create and install, will automatically update to the latest stable version, and contain bundled dependencies. + +``` +snap install lxd +snap install juju +snap install charm +``` + +# Usage + + +## Known Limitations and Issues + +This not only helps users but gives people a place to start if they want to help +you add features to your charm. + +# Configuration + +The configuration options will be listed on the charm store, however If you're +making assumptions or opinionated decisions in the charm (like setting a default +administrator password), you should detail that here so the user knows how to +change it immediately, etc. + +# Contact Information + +## Upstream Project Name + + - Upstream website + - Upstream bug tracker + - Upstream mailing list or contact information + - Feel free to add things if it's useful for users + + +[service]: http://example.com +[icon guidelines]: https://jujucharms.com/docs/stable/authors-charm-icon diff --git a/charms/layers/simple/actions.yaml b/charms/layers/simple/actions.yaml new file mode 100644 index 00000000..6cd6f8c7 --- /dev/null +++ b/charms/layers/simple/actions.yaml @@ -0,0 +1,9 @@ +touch: + description: "Touch a file on the VNF." + params: + filename: + description: "The name of the file to touch." + type: string + default: "" + required: + - filename diff --git a/charms/layers/simple/actions/touch b/charms/layers/simple/actions/touch new file mode 100755 index 00000000..7e30af4c --- /dev/null +++ b/charms/layers/simple/actions/touch @@ -0,0 +1,33 @@ +#!/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, set_flag +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_flag('actions.{}'.format(action_name())) + +try: + main() +except Exception as e: + action_fail(repr(e)) diff --git a/charms/layers/simple/config.yaml b/charms/layers/simple/config.yaml new file mode 100644 index 00000000..51f2ce4a --- /dev/null +++ b/charms/layers/simple/config.yaml @@ -0,0 +1,14 @@ +options: + string-option: + type: string + default: "Default Value" + description: "A short description of the configuration option" + boolean-option: + type: boolean + default: False + description: "A short description of the configuration option" + int-option: + type: int + default: 9001 + description: "A short description of the configuration option" + diff --git a/charms/layers/simple/icon.svg b/charms/layers/simple/icon.svg new file mode 100644 index 00000000..e092eef7 --- /dev/null +++ b/charms/layers/simple/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/charms/layers/simple/layer.yaml b/charms/layers/simple/layer.yaml new file mode 100644 index 00000000..3fed5e24 --- /dev/null +++ b/charms/layers/simple/layer.yaml @@ -0,0 +1,4 @@ +includes: ['layer:basic', 'layer:vnfproxy'] +options: + basic: + use_venv: false diff --git a/charms/layers/simple/metadata.yaml b/charms/layers/simple/metadata.yaml new file mode 100644 index 00000000..fd80d1a7 --- /dev/null +++ b/charms/layers/simple/metadata.yaml @@ -0,0 +1,5 @@ +name: simple +summary: A simple VNF proxy charm +maintainer: Adam Israel +subordinate: false +series: ['xenial'] diff --git a/charms/layers/simple/metrics.yaml b/charms/layers/simple/metrics.yaml new file mode 100644 index 00000000..6ebb605c --- /dev/null +++ b/charms/layers/simple/metrics.yaml @@ -0,0 +1,5 @@ +metrics: + uptime: + type: gauge + description: "Uptime of the VNF" + command: awk '{print $1}' /proc/uptime diff --git a/charms/layers/simple/reactive/simple.py b/charms/layers/simple/reactive/simple.py new file mode 100644 index 00000000..228be3c3 --- /dev/null +++ b/charms/layers/simple/reactive/simple.py @@ -0,0 +1,44 @@ +from charmhelpers.core.hookenv import ( + action_get, + action_fail, + action_set, + status_set, +) +from charms.reactive import ( + clear_flag, + set_flag, + when, + when_not, +) +import charms.sshproxy + + +@when('sshproxy.configured') +@when_not('simple.installed') +def install_simple_proxy_charm(): + """Post-install actions. + + This function will run when two conditions are met: + 1. The 'sshproxy.configured' state is set + 2. The 'simple.installed' state is not set + + This ensures that the workload status is set to active only when the SSH + proxy is properly configured. + """ + set_flag('simple.installed') + status_set('active', 'Ready!') + + +@when('actions.touch') +def touch(): + err = '' + try: + filename = action_get('filename') + cmd = ['touch {}'.format(filename)] + result, err = charms.sshproxy._run(cmd) + except: + action_fail('command failed:' + err) + else: + action_set({'output': result}) + finally: + clear_flag('actions.touch') diff --git a/charms/layers/simple/tests/00-setup b/charms/layers/simple/tests/00-setup new file mode 100755 index 00000000..f0616a56 --- /dev/null +++ b/charms/layers/simple/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/charms/layers/simple/tests/10-deploy b/charms/layers/simple/tests/10-deploy new file mode 100755 index 00000000..9a261170 --- /dev/null +++ b/charms/layers/simple/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('simple') + self.d.expose('simple') + + self.d.setup(timeout=900) + self.d.sentry.wait() + + self.unit = self.d.sentry['simple'][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() diff --git a/charms/layers/vyos-proxy/Makefile b/charms/layers/vyos-proxy/Makefile new file mode 100644 index 00000000..a1ad3a5c --- /dev/null +++ b/charms/layers/vyos-proxy/Makefile @@ -0,0 +1,24 @@ +#!/usr/bin/make + +all: lint unit_test + + +.PHONY: clean +clean: + @rm -rf .tox + +.PHONY: apt_prereqs +apt_prereqs: + @# Need tox, but don't install the apt version unless we have to (don't want to conflict with pip) + @which tox >/dev/null || (sudo apt-get install -y python-pip && sudo pip install tox) + +.PHONY: lint +lint: apt_prereqs + @tox --notest + @PATH=.tox/py34/bin:.tox/py35/bin flake8 $(wildcard hooks reactive lib unit_tests tests) + @charm proof + +.PHONY: unit_test +unit_test: apt_prereqs + @echo Starting tests... + tox diff --git a/charms/layers/vyos-proxy/README.md b/charms/layers/vyos-proxy/README.md new file mode 100644 index 00000000..0337c83b --- /dev/null +++ b/charms/layers/vyos-proxy/README.md @@ -0,0 +1,221 @@ +# Overview + +This is the base layer for all charms [built using layers][building]. It +provides all of the standard Juju hooks and runs the +[charms.reactive.main][charms.reactive] loop for them. It also bootstraps the +[charm-helpers][] and [charms.reactive][] libraries and all of their +dependencies for use by the charm. + +# Usage + +To create a charm layer using this base layer, you need only include it in +a `layer.yaml` file: + +```yaml +includes: ['layer:basic'] +``` + +This will fetch this layer from [interfaces.juju.solutions][] and incorporate +it into your charm layer. You can then add handlers under the `reactive/` +directory. Note that **any** file under `reactive/` will be expected to +contain handlers, whether as Python decorated functions or [executables][non-python] +using the [external handler protocol][]. + +### Charm Dependencies + +Each layer can include a `wheelhouse.txt` file with Python requirement lines. +For example, this layer's `wheelhouse.txt` includes: + +``` +pip>=7.0.0,<8.0.0 +charmhelpers>=0.4.0,<1.0.0 +charms.reactive>=0.1.0,<2.0.0 +``` + +All of these dependencies from each layer will be fetched (and updated) at build +time and will be automatically installed by this base layer before any reactive +handlers are run. + +Note that the `wheelhouse.txt` file is intended for **charm** dependencies only. +That is, for libraries that the charm code itself needs to do its job of deploying +and configuring the payload. If the payload itself has Python dependencies, those +should be handled separately, by the charm. + +See [PyPI][pypi charms.X] for packages under the `charms.` namespace which might +be useful for your charm. + +### Layer Namespace + +Each layer has a reserved section in the `charms.layer.` Python package namespace, +which it can populate by including a `lib/charms/layer/.py` file or +by placing files under `lib/charms/layer//`. (If the layer name +includes hyphens, replace them with underscores.) These can be helpers that the +layer uses internally, or it can expose classes or functions to be used by other +layers to interact with that layer. + +For example, a layer named `foo` could include a `lib/charms/layer/foo.py` file +with some helper functions that other layers could access using: + +```python +from charms.layer.foo import my_helper +``` + +### Layer Options + +Any layer can define options in its `layer.yaml`. Those options can then be set +by other layers to change the behavior of your layer. The options are defined +using [jsonschema][], which is the same way that [action paramters][] are defined. + +For example, the `foo` layer could include the following option definitons: + +```yaml +includes: ['layer:basic'] +defines: # define some options for this layer (the layer "foo") + enable-bar: # define an "enable-bar" option for this layer + description: If true, enable support for "bar". + type: boolean + default: false +``` + +A layer using `foo` could then set it: + +```yaml +includes: ['layer:foo'] +options: + foo: # setting options for the "foo" layer + enable-bar: true # set the "enable-bar" option to true +``` + +The `foo` layer can then use the `charms.layer.options` helper to load the values +for the options that it defined. For example: + +```python +from charms import layer + +@when('state') +def do_thing(): + layer_opts = layer.options('foo') # load all of the options for the "foo" layer + if layer_opts['enable-bar']: # check the value of the "enable-bar" option + hookenv.log("Bar is enabled") +``` + +You can also access layer options in other handlers, such as Bash, using +the command-line interface: + +```bash +. charms.reactive.sh + +@when 'state' +function do_thing() { + if layer_option foo enable-bar; then + juju-log "Bar is enabled" + juju-log "bar-value is: $(layer_option foo bar-value)" + fi +} + +reactive_handler_main +``` + +Note that options of type `boolean` will set the exit code, while other types +will be printed out. + +# Hooks + +This layer provides hooks that other layers can react to using the decorators +of the [charms.reactive][] library: + + * `config-changed` + * `install` + * `leader-elected` + * `leader-settings-changed` + * `start` + * `stop` + * `upgrade-charm` + * `update-status` + +Other hooks are not implemented at this time. A new layer can implement storage +or relation hooks in their own layer by putting them in the `hooks` directory. + +**Note:** Because `update-status` is invoked every 5 minutes, you should take +care to ensure that your reactive handlers only invoke expensive operations +when absolutely necessary. It is recommended that you use helpers like +[`@only_once`][], [`@when_file_changed`][], and [`data_changed`][] to ensure +that handlers run only when necessary. + +# Layer Configuration + +This layer supports the following options, which can be set in `layer.yaml`: + + * **packages** A list of system packages to be installed before the reactive + handlers are invoked. + + * **use_venv** If set to true, the charm dependencies from the various + layers' `wheelhouse.txt` files will be installed in a Python virtualenv + located at `$CHARM_DIR/../.venv`. This keeps charm dependencies from + conflicting with payload dependencies, but you must take care to preserve + the environment and interpreter if using `execl` or `subprocess`. + + * **include_system_packages** If set to true and using a venv, include + the `--system-site-packages` options to make system Python libraries + visible within the venv. + +An example `layer.yaml` using these options might be: + +```yaml +includes: ['layer:basic'] +options: + basic: + packages: ['git'] + use_venv: true + include_system_packages: true +``` + + +# Reactive States + +This layer will set the following states: + + * **`config.changed`** Any config option has changed from its previous value. + This state is cleared automatically at the end of each hook invocation. + + * **`config.changed.