diff --git a/hackfest_k8sproxycharm_ns/hackfest_k8sproxycharm_nsd.yaml b/hackfest_k8sproxycharm_ns/hackfest_k8sproxycharm_nsd.yaml deleted file mode 100644 index 28b9fbae91d2e5506aad0305be77c464c0bc7e44..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_ns/hackfest_k8sproxycharm_nsd.yaml +++ /dev/null @@ -1,37 +0,0 @@ -nsd: - nsd: - - description: NS with 2 VNFs with cloudinit connected by datanet and mgmtnet VLs - df: - - id: default-df - vnf-profile: - - id: vnf1 - virtual-link-connectivity: - - constituent-cpd-id: - - constituent-base-element-id: vnf1 - constituent-cpd-id: vnf-mgmt-ext - virtual-link-profile-id: mgmtnet - - constituent-cpd-id: - - constituent-base-element-id: vnf1 - constituent-cpd-id: vnf-data-ext - virtual-link-profile-id: datanet - vnfd-id: hackfest_k8sproxycharm-vnf - - id: vnf2 - virtual-link-connectivity: - - constituent-cpd-id: - - constituent-base-element-id: vnf2 - constituent-cpd-id: vnf-mgmt-ext - virtual-link-profile-id: mgmtnet - - constituent-cpd-id: - - constituent-base-element-id: vnf2 - constituent-cpd-id: vnf-data-ext - virtual-link-profile-id: datanet - vnfd-id: hackfest_k8sproxycharm-vnf - id: hackfest_k8sproxycharm-ns - name: hackfest_k8sproxycharm-ns - version: 1.0 - virtual-link-desc: - - id: mgmtnet - mgmt-network: true - - id: datanet - vnfd-id: - - hackfest_k8sproxycharm-vnf diff --git a/hackfest_k8sproxycharm_ns/icons/osm.png b/hackfest_k8sproxycharm_ns/icons/osm.png deleted file mode 100644 index 62012d2a2b491bdcd536d62c3c3c863c0d8c1b33..0000000000000000000000000000000000000000 Binary files a/hackfest_k8sproxycharm_ns/icons/osm.png and /dev/null differ diff --git a/hackfest_k8sproxycharm_vnf/README b/hackfest_k8sproxycharm_vnf/README deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/.gitignore b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/.gitignore deleted file mode 100644 index 722d5e71d93ca0aa0db6fd22452e46be5604a84d..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.vscode diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/.gitmodules b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/.gitmodules deleted file mode 100644 index fce0171f506bc643bbfa3604c306f6f3f49f72eb..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "mod/operator"] - path = mod/operator - url = https://github.com/canonical/operator -[submodule "mod/charms.osm"] - path = mod/charms.osm - url = https://github.com/charmed-osm/charms.osm -[submodule "mod/charms"] - path = mod/charms - url = https://github.com/AdamIsrael/charms.requirementstxt diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/README.md b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/README.md deleted file mode 100644 index 21d1a44c39ffa3b32b2d5d9cd0e38866b205641b..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# charm-simple-k8s - -This is a WORK IN PROGRESS example of a simple proxy charm used by Open Source Mano (OSM), written in the [Python Operator Framwork](https://github.com/canonical/operator) - - -## Usage - -To get the charm: -```bash -git clone https://github.com/charmed-osm/charm-simple-k8s -cd charm-simple-k8s -# Install the submodules -git submodule update --init -``` - -To configure the charm, you'll need to have an SSH-accessible machine. You'll need the hostname, and the username and password to login to. Password authentication is useful for testing but key-based authentication is preferred when deploying through OSM. - -To deploy to juju: -``` -juju deploy . --config ssh-hostname=10.135.22.x --config ssh-username=ubuntu --config ssh-password=ubuntu --resource ubuntu_image=ubuntu/ubuntu:latest -``` - -``` -# Make sure the charm is in an Active state -juju status -``` - -To test the SSH credentials, run the `verify-ssh-credentials` action and inspect it's output: -``` -$ juju run-action simple-k8s/0 verify-ssh-credentials -Action queued with id: "9" - -$ juju show-action-output 9 -UnitId: simple-k8s/0 -results: - Stdout: | - Verified! - verified: "True" -status: completed -timing: - completed: 2020-02-14 19:30:38 +0000 UTC - enqueued: 2020-02-14 19:30:33 +0000 UTC - started: 2020-02-14 19:30:36 +0000 UTC -``` - -To exercise the charm, run the `touch` function - -``` -juju run-action simple-k8s/0 touch filename=/home/ubuntu/firsttouch -``` - -Then ssh to the remote machine and verify that the file has been created. diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/actions.yaml b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/actions.yaml deleted file mode 100644 index 956e1f1960e74cab1d30a5a7a68184339f8d8e09..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/actions.yaml +++ /dev/null @@ -1,38 +0,0 @@ -touch: - description: "Touch a file on the VNF." - params: - filename: - description: "The name of the file to touch." - type: string - default: "" - required: - - filename - -# Standard OSM functions -start: - description: "Stop the service on the VNF." -stop: - description: "Stop the service on the VNF." -restart: - description: "Stop the service on the VNF." -reboot: - description: "Reboot the VNF virtual machine." -upgrade: - description: "Upgrade the software on the VNF." - -# Required by charms.osm.sshproxy -run: - description: "Run an arbitrary command" - params: - command: - description: "The command to execute." - type: string - default: "" - required: - - command -generate-ssh-key: - description: "Generate a new SSH keypair for this unit. This will replace any existing previously generated keypair." -verify-ssh-credentials: - description: "Verify that this unit can authenticate with server specified by ssh-hostname and ssh-username." -get-ssh-public-key: - description: "Get the public SSH key for this unit." diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/config.yaml b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/config.yaml deleted file mode 100644 index 5b908ae0a19da5d540c59d887ab4ca7939ebdd08..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/config.yaml +++ /dev/null @@ -1,29 +0,0 @@ -options: - ssh-hostname: - type: string - default: "" - description: "The hostname or IP address of the machine to" - ssh-username: - type: string - default: "" - description: "The username to login as." - ssh-password: - type: string - default: "" - description: "The password used to authenticate." - # ssh-private-key: - # type: string - # default: "" - # description: "DEPRECATED. The private ssh key to be used to authenticate." - ssh-public-key: - type: string - default: "" - description: "The public key of this unit." - ssh-key-type: - type: string - default: "rsa" - description: "The type of encryption to use for the SSH key." - ssh-key-bits: - type: int - default: 4096 - description: "The number of bits to use for the SSH key." diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/hooks/start b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/hooks/start deleted file mode 120000 index 25b1f68fa39d58d33c08ca420c3d439d19be0c55..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/hooks/start +++ /dev/null @@ -1 +0,0 @@ -../src/charm.py \ No newline at end of file diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/charms/osm/sshproxy.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/charms/osm/sshproxy.py deleted file mode 100644 index 60057c925944a28019e457faee9600b3dea7badc..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/charms/osm/sshproxy.py +++ /dev/null @@ -1,419 +0,0 @@ -"""Module to help with executing commands over SSH.""" -## -# Copyright 2016 Canonical Ltd. -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -## - -# from charmhelpers.core import unitdata -# from charmhelpers.core.hookenv import log - -import io -import ipaddress -import paramiko -import os -import socket -import shlex -import traceback - -from subprocess import ( - check_call, - Popen, - CalledProcessError, - PIPE, -) - - -class SSHProxy: - private_key_path = "/root/.ssh/id_sshproxy" - public_key_path = "/root/.ssh/id_sshproxy.pub" - key_type = "rsa" - key_bits = 4096 - - def __init__(self, hostname: str, username: str, password: str = ""): - self.hostname = hostname - self.username = username - self.password = password - - @staticmethod - def generate_ssh_key(): - """Generate a 4096-bit rsa keypair.""" - if not os.path.exists(SSHProxy.private_key_path): - cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format( - SSHProxy.key_type, SSHProxy.key_bits, SSHProxy.private_key_path, - ) - - try: - check_call(cmd, shell=True) - except CalledProcessError: - return False - - return True - - @staticmethod - def get_ssh_public_key(): - publickey = "" - if os.path.exists(SSHProxy.private_key_path): - with open(SSHProxy.public_key_path, "r") as f: - publickey = f.read() - return publickey - - @staticmethod - def has_ssh_key(): - if os.path.exists(SSHProxy.private_key_path): - return True - else: - return False - - def run(self, cmd: str) -> (str, str): - """Run a command remotely via SSH. - - Note: The previous behavior was to run the command locally if SSH wasn't - configured, but that can lead to cases where execution succeeds when you'd - expect it not to. - """ - if isinstance(cmd, str): - cmd = shlex.split(cmd) - - host = self._get_hostname() - user = self.username - passwd = self.password - key = self.private_key_path - - # Make sure we have everything we need to connect - if host and user: - return self._ssh(cmd) - - raise Exception("Invalid SSH credentials.") - - def sftp(self, local, remote): - client = self._get_ssh_client() - - # Create an sftp connection from the underlying transport - sftp = paramiko.SFTPClient.from_transport(client.get_transport()) - sftp.put(local_file, remote_file) - client.close() - pass - - def verify_credentials(self): - """Verify the SSH credentials.""" - try: - (stdout, stderr) = self.run("hostname") - except CalledProcessError as e: - stderr = "Command failed: {} ({})".format(" ".join(e.cmd), str(e.output)) - except paramiko.ssh_exception.AuthenticationException as e: - stderr = "{}.".format(e) - except paramiko.ssh_exception.BadAuthenticationType as e: - stderr = "{}".format(e.explanation) - except paramiko.ssh_exception.BadHostKeyException as e: - stderr = "Host key mismatch: expected {} but got {}.".format( - e.expected_key, e.got_key, - ) - except (TimeoutError, socket.timeout): - stderr = "Timeout attempting to reach {}".format(cfg["ssh-hostname"]) - except Exception as error: - tb = traceback.format_exc() - stderr = "Unhandled exception: {}".format(tb) - - if len(stderr) == 0: - return True, stderr - return False, stderr - - ################### - # Private methods # - ################### - def _get_hostname(self): - """Get the hostname for the ssh target. - - HACK: This function was added to work around an issue where the - ssh-hostname was passed in the format of a.b.c.d;a.b.c.d, where the first - is the floating ip, and the second the non-floating ip, for an Openstack - instance. - """ - return self.hostname.split(";")[0] - - def _get_ssh_client(self): - """Return a connected Paramiko ssh object.""" - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - pkey = None - - # Otherwise, check for the auto-generated private key - if os.path.exists(self.private_key_path): - with open(self.private_key_path) as f: - pkey = paramiko.RSAKey.from_private_key(f) - - ########################################################################### - # There is a bug in some versions of OpenSSH 4.3 (CentOS/RHEL 5) where # - # the server may not send the SSH_MSG_USERAUTH_BANNER message except when # - # responding to an auth_none request. For example, paramiko will attempt # - # to use password authentication when a password is set, but the server # - # could deny that, instead requesting keyboard-interactive. The hack to # - # workaround this is to attempt a reconnect, which will receive the right # - # banner, and authentication can proceed. See the following for more info # - # https://github.com/paramiko/paramiko/issues/432 # - # https://github.com/paramiko/paramiko/pull/438 # - ########################################################################### - - try: - client.connect( - self.hostname, - port=22, - username=self.username, - password=self.password, - pkey=pkey - ) - except paramiko.ssh_exception.SSHException as e: - if "Error reading SSH protocol banner" == str(e): - # Once more, with feeling - client.connect( - host, port=22, username=user, password=password, pkey=pkey - ) - else: - # Reraise the original exception - raise e - - return client - - def _ssh(self, cmd): - """Run an arbitrary command over SSH. - - Returns a tuple of (stdout, stderr) - """ - client = self._get_ssh_client() - - cmds = " ".join(cmd) - stdin, stdout, stderr = client.exec_command(cmds, get_pty=True) - retcode = stdout.channel.recv_exit_status() - client.close() # @TODO re-use connections - if retcode > 0: - output = stderr.read().strip() - raise CalledProcessError(returncode=retcode, cmd=cmd, output=output) - return ( - stdout.read().decode("utf-8").strip(), - stderr.read().decode("utf-8").strip(), - ) - - -## OLD ## - -# def get_config(): -# """Get the current charm configuration. - -# Get the "live" kv store every time we need to access the charm config, in -# case it has recently been changed by a config-changed event. -# """ -# db = unitdata.kv() -# return db.get('config') - - -# def get_host_ip(): -# """Get the IP address for the ssh host. - -# HACK: This function was added to work around an issue where the -# ssh-hostname was passed in the format of a.b.c.d;a.b.c.d, where the first -# is the floating ip, and the second the non-floating ip, for an Openstack -# instance. -# """ -# cfg = get_config() -# return cfg['ssh-hostname'].split(';')[0] - - -# def is_valid_hostname(hostname): -# """Validate the ssh-hostname.""" -# print("Hostname: {}".format(hostname)) -# if hostname == "0.0.0.0": -# return False - -# try: -# ipaddress.ip_address(hostname) -# except ValueError: -# return False - -# return True - - -# def verify_ssh_credentials(): -# """Verify the ssh credentials have been installed to the VNF. - -# Attempts to run a stock command - `hostname` on the remote host. -# """ -# verified = False -# status = '' -# cfg = get_config() - -# try: -# host = get_host_ip() -# if is_valid_hostname(host): -# if len(cfg['ssh-hostname']) and len(cfg['ssh-username']): -# cmd = 'hostname' -# status, err = _run(cmd) - -# if len(err) == 0: -# verified = True -# else: -# status = "Invalid IP address." -# except CalledProcessError as e: -# status = 'Command failed: {} ({})'.format( -# ' '.join(e.cmd), -# str(e.output) -# ) -# except paramiko.ssh_exception.AuthenticationException as e: -# status = '{}.'.format(e) -# except paramiko.ssh_exception.BadAuthenticationType as e: -# status = '{}'.format(e.explanation) -# except paramiko.ssh_exception.BadHostKeyException as e: -# status = 'Host key mismatch: expected {} but got {}.'.format( -# e.expected_key, -# e.got_key, -# ) -# except (TimeoutError, socket.timeout): -# status = "Timeout attempting to reach {}".format(cfg['ssh-hostname']) -# except Exception as error: -# tb = traceback.format_exc() -# status = 'Unhandled exception: {}'.format(tb) - -# return (verified, status) - - -# def charm_dir(): -# """Return the root directory of the current charm.""" -# d = os.environ.get('JUJU_CHARM_DIR') -# if d is not None: -# return d -# return os.environ.get('CHARM_DIR') - - -# def run_local(cmd, env=None): -# """Run a command locally.""" -# if isinstance(cmd, str): -# cmd = shlex.split(cmd) if ' ' in cmd else [cmd] - -# if type(cmd) is not list: -# cmd = [cmd] - -# p = Popen(cmd, -# env=env, -# shell=True, -# stdout=PIPE, -# stderr=PIPE) -# stdout, stderr = p.communicate() -# retcode = p.poll() -# if retcode > 0: -# raise CalledProcessError(returncode=retcode, -# cmd=cmd, -# output=stderr.decode("utf-8").strip()) -# return (stdout.decode('utf-8').strip(), stderr.decode('utf-8').strip()) - - -# def _run(cmd, env=None): -# """Run a command remotely via SSH. - -# Note: The previous behavior was to run the command locally if SSH wasn't -# configured, but that can lead to cases where execution succeeds when you'd -# expect it not to. -# """ -# if isinstance(cmd, str): -# cmd = shlex.split(cmd) - -# if type(cmd) is not list: -# cmd = [cmd] - -# cfg = get_config() - -# if cfg: -# if all(k in cfg for k in ['ssh-hostname', 'ssh-username', -# 'ssh-password', 'ssh-private-key']): -# host = get_host_ip() -# user = cfg['ssh-username'] -# passwd = cfg['ssh-password'] -# key = cfg['ssh-private-key'] # DEPRECATED - -# if host and user: -# return ssh(cmd, host, user, passwd, key) - -# raise Exception("Invalid SSH credentials.") - - -# def get_ssh_client(host, user, password=None, key=None): -# """Return a connected Paramiko ssh object.""" -# client = paramiko.SSHClient() -# client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - -# pkey = None - -# # Check for the DEPRECATED private-key -# if key: -# f = io.StringIO(key) -# pkey = paramiko.RSAKey.from_private_key(f) -# else: -# # Otherwise, check for the auto-generated private key -# if os.path.exists('/root/.ssh/id_juju_sshproxy'): -# with open('/root/.ssh/id_juju_sshproxy', 'r') as f: -# pkey = paramiko.RSAKey.from_private_key(f) - -# ########################################################################### -# # There is a bug in some versions of OpenSSH 4.3 (CentOS/RHEL 5) where # -# # the server may not send the SSH_MSG_USERAUTH_BANNER message except when # -# # responding to an auth_none request. For example, paramiko will attempt # -# # to use password authentication when a password is set, but the server # -# # could deny that, instead requesting keyboard-interactive. The hack to # -# # workaround this is to attempt a reconnect, which will receive the right # -# # banner, and authentication can proceed. See the following for more info # -# # https://github.com/paramiko/paramiko/issues/432 # -# # https://github.com/paramiko/paramiko/pull/438 # -# ########################################################################### - -# try: -# client.connect(host, port=22, username=user, -# password=password, pkey=pkey) -# except paramiko.ssh_exception.SSHException as e: -# if 'Error reading SSH protocol banner' == str(e): -# # Once more, with feeling -# client.connect(host, port=22, username=user, -# password=password, pkey=pkey) -# else: -# # Reraise the original exception -# raise e - -# return client - - -# def sftp(local_file, remote_file, host, user, password=None, key=None): -# """Copy a local file to a remote host.""" -# client = get_ssh_client(host, user, password, key) - -# # Create an sftp connection from the underlying transport -# sftp = paramiko.SFTPClient.from_transport(client.get_transport()) -# sftp.put(local_file, remote_file) -# client.close() - - -# def ssh(cmd, host, user, password=None, key=None): -# """Run an arbitrary command over SSH.""" -# client = get_ssh_client(host, user, password, key) - -# cmds = ' '.join(cmd) -# stdin, stdout, stderr = client.exec_command(cmds, get_pty=True) -# retcode = stdout.channel.recv_exit_status() -# client.close() # @TODO re-use connections -# if retcode > 0: -# output = stderr.read().strip() -# raise CalledProcessError(returncode=retcode, cmd=cmd, -# output=output) -# return ( -# stdout.read().decode('utf-8').strip(), -# stderr.read().decode('utf-8').strip() -# ) diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/charms/requirementstxt.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/charms/requirementstxt.py deleted file mode 100644 index 298d584530ba46f0d7941b88c870a4fe20b6c44f..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/charms/requirementstxt.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -# Requirements.txt support - -import sys - -sys.path.append("lib") - -from ops.framework import StoredState - -import os -import subprocess -import sys -from remote_pdb import RemotePdb - -REQUIREMENTS_TXT = "{}/requirements.txt".format(os.environ["JUJU_CHARM_DIR"]) - - -def install_requirements(): - if os.path.exists(REQUIREMENTS_TXT): - - # First, make sure python3 and python3-pip are installed - if not os.path.exists("/usr/bin/python3") or not os.path.exists("/usr/bin/pip3"): - # Update the apt cache - subprocess.check_call(["apt-get", "update"]) - # Install the Python3 package - subprocess.check_call( - ["apt-get", "install", "-y", "python3", "python3-pip", "python3-paramiko"], - # Eat stdout so it's not returned in an action's stdout - # TODO: redirect to a file handle and log to juju log - # stdout=subprocess.DEVNULL, - ) - - # Lastly, install the python requirements - cmd = [sys.executable, "-m", "pip", "install", "-r", REQUIREMENTS_TXT] - # stdout = subprocess.check_output(cmd) - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) - - stdout, stderr = p.communicate() - - print(stdout) - print(stderr) - # subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", REQUIREMENTS_TXT], - # # Eat stdout so it's not returned in an action's stdout - # # TODO: redirect to a file handle and log to juju log - # # stdout=subprocess.DEVNULL, - # ) - - -# Use StoredState to make sure we're run exactly once automatically -# RemotePdb('127.0.0.1', 4444).set_trace() - -state = StoredState() - -installed = getattr(state, "requirements_txt_installed", None) -if not installed: - install_requirements() - state.requirements_txt_installed = True - diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/__init__.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/charm.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/charm.py deleted file mode 100755 index 71472f963a82461a02fe36a0aa47260151f4cafe..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/charm.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 os - -import yaml - -from ops.framework import Object, EventSource, EventBase, EventsBase - - -class HookEvent(EventBase): - pass - - -class ActionEvent(EventBase): - - def defer(self): - raise RuntimeError('cannot defer action events') - - def restore(self, snapshot): - env_action_name = os.environ.get('JUJU_ACTION_NAME') - event_action_name = self.handle.kind[:-len('_action')].replace('_', '-') - if event_action_name != env_action_name: - # This could only happen if the dev manually emits the action, or from a bug. - raise RuntimeError('action event kind does not match current action') - # Params are loaded at restore rather than __init__ because the model is not available in __init__. - self.params = self.framework.model._backend.action_get() - - def set_results(self, results): - self.framework.model._backend.action_set(results) - - def log(self, message): - self.framework.model._backend.action_log(message) - - def fail(self, message=''): - self.framework.model._backend.action_fail(message) - - -class InstallEvent(HookEvent): - pass - - -class StartEvent(HookEvent): - pass - - -class StopEvent(HookEvent): - pass - - -class ConfigChangedEvent(HookEvent): - pass - - -class UpdateStatusEvent(HookEvent): - pass - - -class UpgradeCharmEvent(HookEvent): - pass - - -class PreSeriesUpgradeEvent(HookEvent): - pass - - -class PostSeriesUpgradeEvent(HookEvent): - pass - - -class LeaderElectedEvent(HookEvent): - pass - - -class LeaderSettingsChangedEvent(HookEvent): - pass - - -class RelationEvent(HookEvent): - def __init__(self, handle, relation, app=None, unit=None): - super().__init__(handle) - - if unit and unit.app != app: - raise RuntimeError(f'cannot create RelationEvent with application {app} and unit {unit}') - - self.relation = relation - self.app = app - self.unit = unit - - def snapshot(self): - snapshot = { - 'relation_name': self.relation.name, - 'relation_id': self.relation.id, - } - if self.app: - snapshot['app_name'] = self.app.name - if self.unit: - snapshot['unit_name'] = self.unit.name - return snapshot - - def restore(self, snapshot): - self.relation = self.framework.model.get_relation(snapshot['relation_name'], snapshot['relation_id']) - - app_name = snapshot.get('app_name') - if app_name: - self.app = self.framework.model.get_app(app_name) - else: - self.app = None - - unit_name = snapshot.get('unit_name') - if unit_name: - self.unit = self.framework.model.get_unit(unit_name) - else: - self.unit = None - - -class RelationJoinedEvent(RelationEvent): - pass - - -class RelationChangedEvent(RelationEvent): - pass - - -class RelationDepartedEvent(RelationEvent): - pass - - -class RelationBrokenEvent(RelationEvent): - pass - - -class StorageEvent(HookEvent): - pass - - -class StorageAttachedEvent(StorageEvent): - pass - - -class StorageDetachingEvent(StorageEvent): - pass - - -class CharmEvents(EventsBase): - - install = EventSource(InstallEvent) - start = EventSource(StartEvent) - stop = EventSource(StopEvent) - update_status = EventSource(UpdateStatusEvent) - config_changed = EventSource(ConfigChangedEvent) - upgrade_charm = EventSource(UpgradeCharmEvent) - pre_series_upgrade = EventSource(PreSeriesUpgradeEvent) - post_series_upgrade = EventSource(PostSeriesUpgradeEvent) - leader_elected = EventSource(LeaderElectedEvent) - leader_settings_changed = EventSource(LeaderSettingsChangedEvent) - - -class CharmBase(Object): - - on = CharmEvents() - - def __init__(self, framework, key): - super().__init__(framework, key) - - for relation_name in self.framework.meta.relations: - relation_name = relation_name.replace('-', '_') - self.on.define_event(f'{relation_name}_relation_joined', RelationJoinedEvent) - self.on.define_event(f'{relation_name}_relation_changed', RelationChangedEvent) - self.on.define_event(f'{relation_name}_relation_departed', RelationDepartedEvent) - self.on.define_event(f'{relation_name}_relation_broken', RelationBrokenEvent) - - for storage_name in self.framework.meta.storages: - storage_name = storage_name.replace('-', '_') - self.on.define_event(f'{storage_name}_storage_attached', StorageAttachedEvent) - self.on.define_event(f'{storage_name}_storage_detaching', StorageDetachingEvent) - - for action_name in self.framework.meta.actions: - action_name = action_name.replace('-', '_') - self.on.define_event(f'{action_name}_action', ActionEvent) - - -class CharmMeta: - """Object containing the metadata for the charm. - - The maintainers, tags, terms, series, and extra_bindings attributes are all - lists of strings. The requires, provides, peers, relations, storage, - resources, and payloads attributes are all mappings of names to instances - of the respective RelationMeta, StorageMeta, ResourceMeta, or PayloadMeta. - - The relations attribute is a convenience accessor which includes all of the - requires, provides, and peers RelationMeta items. If needed, the role of - the relation definition can be obtained from its role attribute. - """ - - def __init__(self, raw={}, actions_raw={}): - self.name = raw.get('name', '') - self.summary = raw.get('summary', '') - self.description = raw.get('description', '') - self.maintainers = [] - if 'maintainer' in raw: - self.maintainers.append(raw['maintainer']) - if 'maintainers' in raw: - self.maintainers.extend(raw['maintainers']) - self.tags = raw.get('tags', []) - self.terms = raw.get('terms', []) - self.series = raw.get('series', []) - self.subordinate = raw.get('subordinate', False) - self.min_juju_version = raw.get('min-juju-version') - self.requires = {name: RelationMeta('requires', name, rel) - for name, rel in raw.get('requires', {}).items()} - self.provides = {name: RelationMeta('provides', name, rel) - for name, rel in raw.get('provides', {}).items()} - self.peers = {name: RelationMeta('peers', name, rel) - for name, rel in raw.get('peers', {}).items()} - self.relations = {} - self.relations.update(self.requires) - self.relations.update(self.provides) - self.relations.update(self.peers) - self.storages = {name: StorageMeta(name, storage) - for name, storage in raw.get('storage', {}).items()} - self.resources = {name: ResourceMeta(name, res) - for name, res in raw.get('resources', {}).items()} - self.payloads = {name: PayloadMeta(name, payload) - for name, payload in raw.get('payloads', {}).items()} - self.extra_bindings = raw.get('extra-bindings', []) - self.actions = {name: ActionMeta(name, action) for name, action in actions_raw.items()} - - @classmethod - def from_yaml(cls, metadata, actions=None): - meta = yaml.safe_load(metadata) - raw_actions = {} - if actions is not None: - raw_actions = yaml.safe_load(actions) - return cls(meta, raw_actions) - - -class RelationMeta: - """Object containing metadata about a relation definition.""" - - def __init__(self, role, relation_name, raw): - self.role = role - self.relation_name = relation_name - self.interface_name = raw['interface'] - self.scope = raw.get('scope') - - -class StorageMeta: - """Object containing metadata about a storage definition.""" - - def __init__(self, name, raw): - self.storage_name = name - self.type = raw['type'] - self.description = raw.get('description', '') - self.shared = raw.get('shared', False) - self.read_only = raw.get('read-only', False) - self.minimum_size = raw.get('minimum-size') - self.location = raw.get('location') - self.multiple_range = None - if 'multiple' in raw: - range = raw['multiple']['range'] - if '-' not in range: - self.multiple_range = (int(range), int(range)) - else: - range = range.split('-') - self.multiple_range = (int(range[0]), int(range[1]) if range[1] else None) - - -class ResourceMeta: - """Object containing metadata about a resource definition.""" - - def __init__(self, name, raw): - self.resource_name = name - self.type = raw['type'] - self.filename = raw.get('filename', None) - self.description = raw.get('description', '') - - -class PayloadMeta: - """Object containing metadata about a payload definition.""" - - def __init__(self, name, raw): - self.payload_name = name - self.type = raw['type'] - - -class ActionMeta: - - def __init__(self, name, raw=None): - raw = raw or {} - self.name = name - self.title = raw.get('title', '') - self.description = raw.get('description', '') - self.parameters = raw.get('params', {}) # {: } - self.required = raw.get('required', []) # [, ...] diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/framework.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/framework.py deleted file mode 100755 index d95eb61fae1c8a0207687ab759f15ab6399ca4db..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/framework.py +++ /dev/null @@ -1,941 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 inspect -import pickle -import marshal -import types -import sqlite3 -import collections -import collections.abc -import keyword -import weakref -from datetime import timedelta - - -class Handle: - """Handle defines a name for an object in the form of a hierarchical path. - - The provided parent is the object (or that object's handle) that this handle - sits under, or None if the object identified by this handle stands by itself - as the root of its own hierarchy. - - The handle kind is a string that defines a namespace so objects with the - same parent and kind will have unique keys. - - The handle key is a string uniquely identifying the object. No other objects - under the same parent and kind may have the same key. - """ - - def __init__(self, parent, kind, key): - if parent and not isinstance(parent, Handle): - parent = parent.handle - self._parent = parent - self._kind = kind - self._key = key - if parent: - if key: - self._path = f"{parent}/{kind}[{key}]" - else: - self._path = f"{parent}/{kind}" - else: - if key: - self._path = f"{kind}[{key}]" - else: - self._path = f"{kind}" - - def nest(self, kind, key): - return Handle(self, kind, key) - - def __hash__(self): - return hash((self.parent, self.kind, self.key)) - - def __eq__(self, other): - return (self.parent, self.kind, self.key) == (other.parent, other.kind, other.key) - - def __str__(self): - return self.path - - @property - def parent(self): - return self._parent - - @property - def kind(self): - return self._kind - - @property - def key(self): - return self._key - - @property - def path(self): - return self._path - - @classmethod - def from_path(cls, path): - handle = None - for pair in path.split("/"): - pair = pair.split("[") - good = False - if len(pair) == 1: - kind, key = pair[0], None - good = True - elif len(pair) == 2: - kind, key = pair - if key and key[-1] == ']': - key = key[:-1] - good = True - if not good: - raise RuntimeError("attempted to restore invalid handle path {path}") - handle = Handle(handle, kind, key) - return handle - - -class EventBase: - - def __init__(self, handle): - self.handle = handle - self.deferred = False - - def defer(self): - self.deferred = True - - def snapshot(self): - """Return the snapshot data that should be persisted. - - Subclasses must override to save any custom state. - """ - return None - - def restore(self, snapshot): - """Restore the value state from the given snapshot. - - Subclasses must override to restore their custom state. - """ - self.deferred = False - - -class EventSource: - """EventSource wraps an event type with a descriptor to facilitate observing and emitting. - - It is generally used as: - - class SomethingHappened(EventBase): - pass - - class SomeObject(Object): - something_happened = EventSource(SomethingHappened) - - With that, instances of that type will offer the someobj.something_happened - attribute which is a BoundEvent and may be used to emit and observe the event. - """ - - def __init__(self, event_type): - if not isinstance(event_type, type) or not issubclass(event_type, EventBase): - raise RuntimeError(f"Event requires a subclass of EventBase as an argument, got {event_type}") - self.event_type = event_type - self.event_kind = None - self.emitter_type = None - - def __set_name__(self, emitter_type, event_kind): - if self.event_kind is not None: - raise RuntimeError( - f'EventSource({self.event_type.__name__}) reused as ' - f'{self.emitter_type.__name__}.{self.event_kind} and ' - f'{emitter_type.__name__}.{event_kind}') - self.event_kind = event_kind - self.emitter_type = emitter_type - - def __get__(self, emitter, emitter_type=None): - if emitter is None: - return self - # Framework might not be available if accessed as CharmClass.on.event rather than charm_instance.on.event, - # but in that case it couldn't be emitted anyway, so there's no point to registering it. - framework = getattr(emitter, 'framework', None) - if framework is not None: - framework.register_type(self.event_type, emitter, self.event_kind) - return BoundEvent(emitter, self.event_type, self.event_kind) - - -class BoundEvent: - - def __repr__(self): - return (f'') - - def __init__(self, emitter, event_type, event_kind): - self.emitter = emitter - self.event_type = event_type - self.event_kind = event_kind - - def emit(self, *args, **kwargs): - """Emit event to all registered observers. - - The current storage state is committed before and after each observer is notified. - """ - framework = self.emitter.framework - key = framework._next_event_key() - event = self.event_type(Handle(self.emitter, self.event_kind, key), *args, **kwargs) - framework._emit(event) - - -class HandleKind: - """Helper descriptor to define the Object.handle_kind field. - - The handle_kind for an object defaults to its type name, but it may - be explicitly overridden if desired. - """ - - def __get__(self, obj, obj_type): - kind = obj_type.__dict__.get("handle_kind") - if kind: - return kind - return obj_type.__name__ - - -class Object: - - handle_kind = HandleKind() - - def __init__(self, parent, key): - kind = self.handle_kind - if isinstance(parent, Framework): - self.framework = parent - # Avoid Framework instances having a circular reference to themselves. - if self.framework is self: - self.framework = weakref.proxy(self.framework) - self.handle = Handle(None, kind, key) - else: - self.framework = parent.framework - self.handle = Handle(parent, kind, key) - self.framework._track(self) - - # TODO Detect conflicting handles here. - - @property - def model(self): - return self.framework.model - - @property - def meta(self): - return self.framework.meta - - @property - def charm_dir(self): - return self.framework.charm_dir - - -class EventsBase(Object): - """Convenience type to allow defining .on attributes at class level.""" - - handle_kind = "on" - - def __init__(self, parent=None, key=None): - if parent is not None: - super().__init__(parent, key) - else: - self._cache = weakref.WeakKeyDictionary() - - def __get__(self, emitter, emitter_type): - if emitter is None: - return self - instance = self._cache.get(emitter) - if instance is None: - # Same type, different instance, more data. Doing this unusual construct - # means people can subclass just this one class to have their own 'on'. - instance = self._cache[emitter] = type(self)(emitter) - return instance - - @classmethod - def define_event(cls, event_kind, event_type): - """Define an event on this type at runtime. - - cls -- a type to define an event on. - event_kind -- an attribute name that will be used to access the event. Must be a valid python identifier, not be a keyword or an existing attribute. - event_type -- a type of the event to define. - """ - if not event_kind.isidentifier(): - raise RuntimeError(f'unable to define an event with event_kind that is not a valid python identifier: {event_kind}') - elif keyword.iskeyword(event_kind): - raise RuntimeError(f'unable to define an event with event_kind that is a python keyword: {event_kind}') - try: - getattr(cls, event_kind) - raise RuntimeError(f'unable to define an event with event_kind that overlaps with an existing type {cls} attribute: {event_kind}') - except AttributeError: - pass - - event_descriptor = EventSource(event_type) - event_descriptor.__set_name__(cls, event_kind) - setattr(cls, event_kind, event_descriptor) - - def events(self): - """Return a mapping of event_kinds to bound_events for all available events. - """ - events_map = {} - # We have to iterate over the class rather than instance to allow for properties which - # might call this method (e.g., event views), leading to infinite recursion. - for attr_name, attr_value in inspect.getmembers(type(self)): - if isinstance(attr_value, EventSource): - # We actually care about the bound_event, however, since it - # provides the most info for users of this method. - event_kind = attr_name - bound_event = getattr(self, event_kind) - events_map[event_kind] = bound_event - return events_map - - def __getitem__(self, key): - return PrefixedEvents(self, key) - - -class PrefixedEvents: - - def __init__(self, emitter, key): - self._emitter = emitter - self._prefix = key.replace("-", "_") + '_' - - def __getattr__(self, name): - return getattr(self._emitter, self._prefix + name) - - -class PreCommitEvent(EventBase): - pass - - -class CommitEvent(EventBase): - pass - - -class FrameworkEvents(EventsBase): - pre_commit = EventSource(PreCommitEvent) - commit = EventSource(CommitEvent) - - -class NoSnapshotError(Exception): - - def __init__(self, handle_path): - self.handle_path = handle_path - - def __str__(self): - return f'no snapshot data found for {self.handle_path} object' - - -class NoTypeError(Exception): - - def __init__(self, handle_path): - self.handle_path = handle_path - - def __str__(self): - return f"cannot restore {self.handle_path} since no class was registered for it" - - -class SQLiteStorage: - - DB_LOCK_TIMEOUT = timedelta(hours=1) - - def __init__(self, filename): - # The isolation_level argument is set to None such that the implicit transaction management behavior of the sqlite3 module is disabled. - self._db = sqlite3.connect(str(filename), isolation_level=None, timeout=self.DB_LOCK_TIMEOUT.total_seconds()) - self._setup() - - def _setup(self): - # Make sure that the database is locked until the connection is closed, not until the transaction ends. - self._db.execute("PRAGMA locking_mode=EXCLUSIVE") - c = self._db.execute("BEGIN") - c.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='snapshot'") - if c.fetchone()[0] == 0: - # Keep in mind what might happen if the process dies somewhere below. - # The system must not be rendered permanently broken by that. - self._db.execute("CREATE TABLE snapshot (handle TEXT PRIMARY KEY, data BLOB)") - self._db.execute("CREATE TABLE notice (sequence INTEGER PRIMARY KEY AUTOINCREMENT, event_path TEXT, observer_path TEXT, method_name TEXT)") - self._db.commit() - - def close(self): - self._db.close() - - def commit(self): - self._db.commit() - - # There's commit but no rollback. For abort to be supported, we'll need logic that - # can rollback decisions made by third-party code in terms of the internal state - # of objects that have been snapshotted, and hooks to let them know about it and - # take the needed actions to undo their logic until the last snapshot. - # This is doable but will increase significantly the chances for mistakes. - - def save_snapshot(self, handle_path, snapshot_data): - self._db.execute("REPLACE INTO snapshot VALUES (?, ?)", (handle_path, snapshot_data)) - - def load_snapshot(self, handle_path): - c = self._db.cursor() - c.execute("SELECT data FROM snapshot WHERE handle=?", (handle_path,)) - row = c.fetchone() - if row: - return row[0] - return None - - def drop_snapshot(self, handle_path): - self._db.execute("DELETE FROM snapshot WHERE handle=?", (handle_path,)) - - def save_notice(self, event_path, observer_path, method_name): - self._db.execute("INSERT INTO notice VALUES (NULL, ?, ?, ?)", (event_path, observer_path, method_name)) - - def drop_notice(self, event_path, observer_path, method_name): - self._db.execute("DELETE FROM notice WHERE event_path=? AND observer_path=? AND method_name=?", (event_path, observer_path, method_name)) - - def notices(self, event_path): - if event_path: - c = self._db.execute("SELECT event_path, observer_path, method_name FROM notice WHERE event_path=? ORDER BY sequence", (event_path,)) - else: - c = self._db.execute("SELECT event_path, observer_path, method_name FROM notice ORDER BY sequence") - while True: - rows = c.fetchmany() - if not rows: - break - for row in rows: - yield tuple(row) - - -class Framework(Object): - - on = FrameworkEvents() - - # Override properties from Object so that we can set them in __init__. - model = None - meta = None - charm_dir = None - - def __init__(self, data_path, charm_dir, meta, model): - - super().__init__(self, None) - - self._data_path = data_path - self.charm_dir = charm_dir - self.meta = meta - self.model = model - self._observers = [] # [(observer_path, method_name, parent_path, event_key)] - self._observer = weakref.WeakValueDictionary() # {observer_path: observer} - self._objects = weakref.WeakValueDictionary() - self._type_registry = {} # {(parent_path, kind): cls} - self._type_known = set() # {cls} - - self._storage = SQLiteStorage(data_path) - - # We can't use the higher-level StoredState because it relies on events. - self.register_type(StoredStateData, None, StoredStateData.handle_kind) - stored_handle = Handle(None, StoredStateData.handle_kind, '_stored') - try: - self._stored = self.load_snapshot(stored_handle) - except NoSnapshotError: - self._stored = StoredStateData(self, '_stored') - self._stored['event_count'] = 0 - - def close(self): - self._storage.close() - - def _track(self, obj): - """Track object and ensure it is the only object created using its handle path.""" - if obj is self: - # Framework objects don't track themselves - return - if obj.handle.path in self.framework._objects: - raise RuntimeError(f"two objects claiming to be {obj.handle.path} have been created") - self._objects[obj.handle.path] = obj - - def _forget(self, obj): - """Stop tracking the given object. See also _track.""" - self._objects.pop(obj.handle.path, None) - - def commit(self): - # Give a chance for objects to persist data they want to before a commit is made. - self.on.pre_commit.emit() - # Make sure snapshots are saved by instances of StoredStateData. Any possible state - # modifications in on_commit handlers of instances of other classes will not be persisted. - self.on.commit.emit() - # Save our event count after all events have been emitted. - self.save_snapshot(self._stored) - self._storage.commit() - - def register_type(self, cls, parent, kind=None): - if parent and not isinstance(parent, Handle): - parent = parent.handle - if parent: - parent_path = parent.path - else: - parent_path = None - if not kind: - kind = cls.handle_kind - self._type_registry[(parent_path, kind)] = cls - self._type_known.add(cls) - - def save_snapshot(self, value): - """Save a persistent snapshot of the provided value. - - The provided value must implement the following interface: - - value.handle = Handle(...) - value.snapshot() => {...} # Simple builtin types only. - value.restore(snapshot) # Restore custom state from prior snapshot. - """ - if type(value) not in self._type_known: - raise RuntimeError(f"cannot save {type(value).__name__} values before registering that type") - data = value.snapshot() - # Use marshal as a validator, enforcing the use of simple types. - marshal.dumps(data) - # Use pickle for serialization, so the value remains portable. - raw_data = pickle.dumps(data) - self._storage.save_snapshot(value.handle.path, raw_data) - - def load_snapshot(self, handle): - parent_path = None - if handle.parent: - parent_path = handle.parent.path - cls = self._type_registry.get((parent_path, handle.kind)) - if not cls: - raise NoTypeError(handle.path) - raw_data = self._storage.load_snapshot(handle.path) - if not raw_data: - raise NoSnapshotError(handle.path) - data = pickle.loads(raw_data) - obj = cls.__new__(cls) - obj.framework = self - obj.handle = handle - obj.restore(data) - self._track(obj) - return obj - - def drop_snapshot(self, handle): - self._storage.drop_snapshot(handle.path) - - def observe(self, bound_event, observer): - """Register observer to be called when bound_event is emitted. - - The bound_event is generally provided as an attribute of the object that emits - the event, and is created in this style: - - class SomeObject: - something_happened = Event(SomethingHappened) - - That event may be observed as: - - framework.observe(someobj.something_happened, self.on_something_happened) - - If the method to be called follows the name convention "on_", it - may be omitted from the observe call. That means the above is equivalent to: - - framework.observe(someobj.something_happened, self) - - """ - if not isinstance(bound_event, BoundEvent): - raise RuntimeError(f'Framework.observe requires a BoundEvent as second parameter, got {bound_event}') - - event_type = bound_event.event_type - event_kind = bound_event.event_kind - emitter = bound_event.emitter - - self.register_type(event_type, emitter, event_kind) - - if hasattr(emitter, "handle"): - emitter_path = emitter.handle.path - else: - raise RuntimeError(f'event emitter {type(emitter).__name__} must have a "handle" attribute') - - method_name = None - if isinstance(observer, types.MethodType): - method_name = observer.__name__ - observer = observer.__self__ - else: - method_name = "on_" + event_kind - if not hasattr(observer, method_name): - raise RuntimeError(f'Observer method not provided explicitly and {type(observer).__name__} type has no "{method_name}" method') - - # Validate that the method has an acceptable call signature. - sig = inspect.signature(getattr(observer, method_name)) - # Self isn't included in the params list, so the first arg will be the event. - extra_params = list(sig.parameters.values())[1:] - if not sig.parameters: - raise TypeError(f'{type(observer).__name__}.{method_name} must accept event parameter') - elif any(param.default is inspect.Parameter.empty for param in extra_params): - # Allow for additional optional params, since there's no reason to exclude them, but - # required params will break. - raise TypeError(f'{type(observer).__name__}.{method_name} has extra required parameter') - - # TODO Prevent the exact same parameters from being registered more than once. - - self._observer[observer.handle.path] = observer - self._observers.append((observer.handle.path, method_name, emitter_path, event_kind)) - - def _next_event_key(self): - """Return the next event key that should be used, incrementing the internal counter.""" - # Increment the count first; this means the keys will start at 1, and 0 means no events have been emitted. - self._stored['event_count'] += 1 - return str(self._stored['event_count']) - - def _emit(self, event): - """See BoundEvent.emit for the public way to call this.""" - - # Save the event for all known observers before the first notification - # takes place, so that either everyone interested sees it, or nobody does. - self.save_snapshot(event) - event_path = event.handle.path - event_kind = event.handle.kind - parent_path = event.handle.parent.path - # TODO Track observers by (parent_path, event_kind) rather than as a list of all observers. Avoiding linear search through all observers for every event - for observer_path, method_name, _parent_path, _event_kind in self._observers: - if _parent_path != parent_path: - continue - if _event_kind and _event_kind != event_kind: - continue - # Again, only commit this after all notices are saved. - self._storage.save_notice(event_path, observer_path, method_name) - self._reemit(event_path) - - def reemit(self): - """Reemit previously deferred events to the observers that deferred them. - - Only the specific observers that have previously deferred the event will be - notified again. Observers that asked to be notified about events after it's - been first emitted won't be notified, as that would mean potentially observing - events out of order. - """ - self._reemit() - - def _reemit(self, single_event_path=None): - last_event_path = None - deferred = True - for event_path, observer_path, method_name in self._storage.notices(single_event_path): - event_handle = Handle.from_path(event_path) - - if last_event_path != event_path: - if not deferred: - self._storage.drop_snapshot(last_event_path) - last_event_path = event_path - deferred = False - - try: - event = self.load_snapshot(event_handle) - except NoTypeError: - self._storage.drop_notice(event_path, observer_path, method_name) - continue - - event.deferred = False - observer = self._observer.get(observer_path) - if observer: - custom_handler = getattr(observer, method_name, None) - if custom_handler: - custom_handler(event) - - if event.deferred: - deferred = True - else: - self._storage.drop_notice(event_path, observer_path, method_name) - # We intentionally consider this event to be dead and reload it from scratch in the next path. - self.framework._forget(event) - - if not deferred: - self._storage.drop_snapshot(last_event_path) - - -class StoredStateChanged(EventBase): - pass - - -class StoredStateEvents(EventsBase): - changed = EventSource(StoredStateChanged) - - -class StoredStateData(Object): - - on = StoredStateEvents() - - def __init__(self, parent, attr_name): - super().__init__(parent, attr_name) - self._cache = {} - self.dirty = False - - def __getitem__(self, key): - return self._cache.get(key) - - def __setitem__(self, key, value): - self._cache[key] = value - self.dirty = True - - def __contains__(self, key): - return key in self._cache - - def snapshot(self): - return self._cache - - def restore(self, snapshot): - self._cache = snapshot - self.dirty = False - - def on_commit(self, event): - if self.dirty: - self.framework.save_snapshot(self) - self.dirty = False - - -class BoundStoredState: - - def __init__(self, parent, attr_name): - parent.framework.register_type(StoredStateData, parent) - - handle = Handle(parent, StoredStateData.handle_kind, attr_name) - try: - data = parent.framework.load_snapshot(handle) - except NoSnapshotError: - data = StoredStateData(parent, attr_name) - - # __dict__ is used to avoid infinite recursion. - self.__dict__["_data"] = data - self.__dict__["_attr_name"] = attr_name - - parent.framework.observe(parent.framework.on.commit, self._data) - - def __getattr__(self, key): - # "on" is the only reserved key that can't be used in the data map. - if key == "on": - return self._data.on - if key not in self._data: - raise AttributeError(f"attribute '{key}' is not stored") - return _wrap_stored(self._data, self._data[key]) - - def __setattr__(self, key, value): - if key == "on": - raise AttributeError(f"attribute 'on' is reserved and cannot be set") - - value = _unwrap_stored(self._data, value) - - if not isinstance(value, (type(None), int, str, bytes, list, dict, set)): - raise AttributeError(f"attribute '{key}' cannot be set to {type(value).__name__}: must be int/dict/list/etc") - - self._data[key] = _unwrap_stored(self._data, value) - self.on.changed.emit() - - def set_default(self, **kwargs): - """"Set the value of any given key if it has not already been set""" - for k, v in kwargs.items(): - if k not in self._data: - self._data[k] = v - - -class StoredState: - - def __init__(self): - self.parent_type = None - self.attr_name = None - - def __get__(self, parent, parent_type=None): - if self.parent_type is None: - self.parent_type = parent_type - elif self.parent_type is not parent_type: - raise RuntimeError("StoredState shared by {} and {}".format(self.parent_type.__name__, parent_type.__name__)) - - if parent is None: - return self - - bound = parent.__dict__.get(self.attr_name) - if bound is None: - for attr_name, attr_value in parent_type.__dict__.items(): - if attr_value is self: - if self.attr_name and attr_name != self.attr_name: - parent_tname = parent_type.__name__ - raise RuntimeError(f"StoredState shared by {parent_tname}.{self.attr_name} and {parent_tname}.{attr_name}") - self.attr_name = attr_name - bound = BoundStoredState(parent, attr_name) - parent.__dict__[attr_name] = bound - break - else: - raise RuntimeError("cannot find StoredVariable attribute in type {}".format(parent_type.__name__)) - - return bound - - -def _wrap_stored(parent_data, value): - t = type(value) - if t is dict: - return StoredDict(parent_data, value) - if t is list: - return StoredList(parent_data, value) - if t is set: - return StoredSet(parent_data, value) - return value - - -def _unwrap_stored(parent_data, value): - t = type(value) - if t is StoredDict or t is StoredList or t is StoredSet: - return value._under - return value - - -class StoredDict(collections.abc.MutableMapping): - - def __init__(self, stored_data, under): - self._stored_data = stored_data - self._under = under - - def __getitem__(self, key): - return _wrap_stored(self._stored_data, self._under[key]) - - def __setitem__(self, key, value): - self._under[key] = _unwrap_stored(self._stored_data, value) - self._stored_data.dirty = True - - def __delitem__(self, key): - del self._under[key] - self._stored_data.dirty = True - - def __iter__(self): - return self._under.__iter__() - - def __len__(self): - return len(self._under) - - def __eq__(self, other): - if isinstance(other, StoredDict): - return self._under == other._under - elif isinstance(other, collections.abc.Mapping): - return self._under == other - else: - return NotImplemented - - -class StoredList(collections.abc.MutableSequence): - - def __init__(self, stored_data, under): - self._stored_data = stored_data - self._under = under - - def __getitem__(self, index): - return _wrap_stored(self._stored_data, self._under[index]) - - def __setitem__(self, index, value): - self._under[index] = _unwrap_stored(self._stored_data, value) - self._stored_data.dirty = True - - def __delitem__(self, index): - del self._under[index] - self._stored_data.dirty = True - - def __len__(self): - return len(self._under) - - def insert(self, index, value): - self._under.insert(index, value) - self._stored_data.dirty = True - - def append(self, value): - self._under.append(value) - self._stored_data.dirty = True - - def __eq__(self, other): - if isinstance(other, StoredList): - return self._under == other._under - elif isinstance(other, collections.abc.Sequence): - return self._under == other - else: - return NotImplemented - - def __lt__(self, other): - if isinstance(other, StoredList): - return self._under < other._under - elif isinstance(other, collections.abc.Sequence): - return self._under < other - else: - return NotImplemented - - def __le__(self, other): - if isinstance(other, StoredList): - return self._under <= other._under - elif isinstance(other, collections.abc.Sequence): - return self._under <= other - else: - return NotImplemented - - def __gt__(self, other): - if isinstance(other, StoredList): - return self._under > other._under - elif isinstance(other, collections.abc.Sequence): - return self._under > other - else: - return NotImplemented - - def __ge__(self, other): - if isinstance(other, StoredList): - return self._under >= other._under - elif isinstance(other, collections.abc.Sequence): - return self._under >= other - else: - return NotImplemented - - -class StoredSet(collections.abc.MutableSet): - - def __init__(self, stored_data, under): - self._stored_data = stored_data - self._under = under - - def add(self, key): - self._under.add(key) - self._stored_data.dirty = True - - def discard(self, key): - self._under.discard(key) - self._stored_data.dirty = True - - def __contains__(self, key): - return key in self._under - - def __iter__(self): - return self._under.__iter__() - - def __len__(self): - return len(self._under) - - @classmethod - def _from_iterable(cls, it): - """Construct an instance of the class from any iterable input. - - Per https://docs.python.org/3/library/collections.abc.html - if the Set mixin is being used in a class with a different constructor signature, - you will need to override _from_iterable() with a classmethod that can construct - new instances from an iterable argument. - """ - return set(it) - - def __le__(self, other): - if isinstance(other, StoredSet): - return self._under <= other._under - elif isinstance(other, collections.abc.Set): - return self._under <= other - else: - return NotImplemented - - def __ge__(self, other): - if isinstance(other, StoredSet): - return self._under >= other._under - elif isinstance(other, collections.abc.Set): - return self._under >= other - else: - return NotImplemented - - def __eq__(self, other): - if isinstance(other, StoredSet): - return self._under == other._under - elif isinstance(other, collections.abc.Set): - return self._under == other - else: - return NotImplemented diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/jujuversion.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/jujuversion.py deleted file mode 100755 index 5256f24ff8fb37a2cbe6162d824e77fb7dca1f45..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/jujuversion.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2020 Canonical Ltd. -# -# 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 re -from functools import total_ordering - - -@total_ordering -class JujuVersion: - - PATTERN = r'^(?P\d{1,9})\.(?P\d{1,9})((?:\.|-(?P[a-z]+))(?P\d{1,9}))?(\.(?P\d{1,9}))?$' - - def __init__(self, version): - m = re.match(self.PATTERN, version) - if not m: - raise RuntimeError(f'"{version}" is not a valid Juju version string') - - d = m.groupdict() - self.major = int(m.group('major')) - self.minor = int(m.group('minor')) - self.tag = d['tag'] or '' - self.patch = int(d['patch'] or 0) - self.build = int(d['build'] or 0) - - def __repr__(self): - if self.tag: - s = f'{self.major}.{self.minor}-{self.tag}{self.patch}' - else: - s = f'{self.major}.{self.minor}.{self.patch}' - if self.build > 0: - s += f'.{self.build}' - return s - - def __eq__(self, other): - if self is other: - return True - if isinstance(other, str): - other = type(self)(other) - elif not isinstance(other, JujuVersion): - raise RuntimeError(f'cannot compare Juju version "{self}" with "{other}"') - return self.major == other.major and self.minor == other.minor\ - and self.tag == other.tag and self.build == other.build and self.patch == other.patch - - def __lt__(self, other): - if self is other: - return False - if isinstance(other, str): - other = type(self)(other) - elif not isinstance(other, JujuVersion): - raise RuntimeError(f'cannot compare Juju version "{self}" with "{other}"') - - if self.major != other.major: - return self.major < other.major - elif self.minor != other.minor: - return self.minor < other.minor - elif self.tag != other.tag: - if not self.tag: - return False - elif not other.tag: - return True - return self.tag < other.tag - elif self.patch != other.patch: - return self.patch < other.patch - elif self.build != other.build: - return self.build < other.build - return False diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/main.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/main.py deleted file mode 100755 index c8d5da2adf1b4f6ccdb3a0b1292dcab8d51c0e9b..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/main.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2019 Canonical Ltd. -# -# 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 os -import sys -from pathlib import Path - -import yaml - -import ops.charm -import ops.framework -import ops.model - -CHARM_STATE_FILE = '.unit-state.db' - - -def debugf(format, *args, **kwargs): - pass - - -def _get_charm_dir(): - charm_dir = os.environ.get("JUJU_CHARM_DIR") - if charm_dir is None: - # Assume $JUJU_CHARM_DIR/lib/op/main.py structure. - charm_dir = Path(f'{__file__}/../../..').resolve() - else: - charm_dir = Path(charm_dir).resolve() - return charm_dir - - -def _load_metadata(charm_dir): - metadata = yaml.safe_load((charm_dir / 'metadata.yaml').read_text()) - - actions_meta = charm_dir / 'actions.yaml' - if actions_meta.exists(): - actions_metadata = yaml.safe_load(actions_meta.read_text()) - else: - actions_metadata = {} - return metadata, actions_metadata - - -def _create_event_link(charm, bound_event): - """Create a symlink for a particular event. - - charm -- A charm object. - bound_event -- An event for which to create a symlink. - """ - if issubclass(bound_event.event_type, ops.charm.HookEvent): - event_dir = charm.framework.charm_dir / 'hooks' - event_path = event_dir / bound_event.event_kind.replace('_', '-') - elif issubclass(bound_event.event_type, ops.charm.ActionEvent): - if not bound_event.event_kind.endswith("_action"): - raise RuntimeError(f"action event name {bound_event.event_kind} needs _action suffix") - event_dir = charm.framework.charm_dir / 'actions' - # The event_kind is suffixed with "_action" while the executable is not. - event_path = event_dir / bound_event.event_kind[:-len('_action')].replace('_', '-') - else: - raise RuntimeError(f'cannot create a symlink: unsupported event type {bound_event.event_type}') - - event_dir.mkdir(exist_ok=True) - if not event_path.exists(): - # CPython has different implementations for populating sys.argv[0] for Linux and Windows. For Windows - # it is always an absolute path (any symlinks are resolved) while for Linux it can be a relative path. - target_path = os.path.relpath(os.path.realpath(sys.argv[0]), event_dir) - - # Ignore the non-symlink files or directories assuming the charm author knows what they are doing. - debugf(f'Creating a new relative symlink at {event_path} pointing to {target_path}') - event_path.symlink_to(target_path) - - -def _setup_event_links(charm_dir, charm): - """Set up links for supported events that originate from Juju. - - Whether a charm can handle an event or not can be determined by - introspecting which events are defined on it. - - Hooks or actions are created as symlinks to the charm code file which is determined by inspecting - symlinks provided by the charm author at hooks/install or hooks/start. - - charm_dir -- A root directory of the charm. - charm -- An instance of the Charm class. - """ - for bound_event in charm.on.events().values(): - # Only events that originate from Juju need symlinks. - if issubclass(bound_event.event_type, (ops.charm.HookEvent, ops.charm.ActionEvent)): - _create_event_link(charm, bound_event) - - -def _emit_charm_event(charm, event_name): - """Emits a charm event based on a Juju event name. - - charm -- A charm instance to emit an event from. - event_name -- A Juju event name to emit on a charm. - """ - event_to_emit = None - try: - event_to_emit = getattr(charm.on, event_name) - except AttributeError: - debugf(f"event {event_name} not defined for {charm}") - - # If the event is not supported by the charm implementation, do - # not error out or try to emit it. This is to support rollbacks. - if event_to_emit is not None: - args, kwargs = _get_event_args(charm, event_to_emit) - debugf(f'Emitting Juju event {event_name}') - event_to_emit.emit(*args, **kwargs) - - -def _get_event_args(charm, bound_event): - event_type = bound_event.event_type - model = charm.framework.model - - if issubclass(event_type, ops.charm.RelationEvent): - relation_name = os.environ['JUJU_RELATION'] - relation_id = int(os.environ['JUJU_RELATION_ID'].split(':')[-1]) - relation = model.get_relation(relation_name, relation_id) - else: - relation = None - - remote_app_name = os.environ.get('JUJU_REMOTE_APP', '') - remote_unit_name = os.environ.get('JUJU_REMOTE_UNIT', '') - if remote_app_name or remote_unit_name: - if not remote_app_name: - if '/' not in remote_unit_name: - raise RuntimeError(f'invalid remote unit name: {remote_unit_name}') - remote_app_name = remote_unit_name.split('/')[0] - args = [relation, model.get_app(remote_app_name)] - if remote_unit_name: - args.append(model.get_unit(remote_unit_name)) - return args, {} - elif relation: - return [relation], {} - return [], {} - - -def main(charm_class): - """Setup the charm and dispatch the observed event. - - The event name is based on the way this executable was called (argv[0]). - """ - - charm_dir = _get_charm_dir() - - # Process the Juju event relevant to the current hook execution - # JUJU_HOOK_NAME, JUJU_FUNCTION_NAME, and JUJU_ACTION_NAME are not used - # in order to support simulation of events from debugging sessions. - # TODO: For Windows, when symlinks are used, this is not a valid method of getting an event name (see LP: #1854505). - juju_exec_path = Path(sys.argv[0]) - juju_event_name = juju_exec_path.name.replace('-', '_') - if juju_exec_path.parent.name == 'actions': - juju_event_name = f'{juju_event_name}_action' - - metadata, actions_metadata = _load_metadata(charm_dir) - meta = ops.charm.CharmMeta(metadata, actions_metadata) - unit_name = os.environ['JUJU_UNIT_NAME'] - model = ops.model.Model(unit_name, meta, ops.model.ModelBackend()) - - # TODO: If Juju unit agent crashes after exit(0) from the charm code - # the framework will commit the snapshot but Juju will not commit its - # operation. - charm_state_path = charm_dir / CHARM_STATE_FILE - framework = ops.framework.Framework(charm_state_path, charm_dir, meta, model) - try: - charm = charm_class(framework, None) - - # When a charm is force-upgraded and a unit is in an error state Juju does not run upgrade-charm and - # instead runs the failed hook followed by config-changed. Given the nature of force-upgrading - # the hook setup code is not triggered on config-changed. - # 'start' event is included as Juju does not fire the install event for K8s charms (see LP: #1854635). - if juju_event_name in ('install', 'start', 'upgrade_charm') or juju_event_name.endswith('_storage_attached'): - _setup_event_links(charm_dir, charm) - - framework.reemit() - - _emit_charm_event(charm, juju_event_name) - - framework.commit() - finally: - framework.close() diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/model.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/model.py deleted file mode 100644 index a12dcca2b1a85b3ed4a0983b88073f9f8c7bcb42..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/lib/ops/model.py +++ /dev/null @@ -1,679 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 json -import weakref -import os -import shutil -import tempfile -import time -import datetime - -from abc import ABC, abstractmethod -from collections.abc import Mapping, MutableMapping -from pathlib import Path -from subprocess import run, PIPE, CalledProcessError - - -class Model: - - def __init__(self, unit_name, meta, backend): - self._cache = ModelCache(backend) - self._backend = backend - self.unit = self.get_unit(unit_name) - self.app = self.unit.app - self.relations = RelationMapping(meta.relations, self.unit, self._backend, self._cache) - self.config = ConfigData(self._backend) - self.resources = Resources(list(meta.resources), self._backend) - self.pod = Pod(self._backend) - self.storages = StorageMapping(list(meta.storages), self._backend) - - def get_unit(self, unit_name): - return self._cache.get(Unit, unit_name) - - def get_app(self, app_name): - return self._cache.get(Application, app_name) - - def get_relation(self, relation_name, relation_id=None): - """Get a specific Relation instance. - - If relation_id is given, this will return that Relation instance. - - If relation_id is not given, this will return the Relation instance if the - relation is established only once or None if it is not established. If this - same relation is established multiple times the error TooManyRelatedAppsError is raised. - """ - return self.relations._get_unique(relation_name, relation_id) - - -class ModelCache: - - def __init__(self, backend): - self._backend = backend - self._weakrefs = weakref.WeakValueDictionary() - - def get(self, entity_type, *args): - key = (entity_type,) + args - entity = self._weakrefs.get(key) - if entity is None: - entity = entity_type(*args, backend=self._backend, cache=self) - self._weakrefs[key] = entity - return entity - - -class Application: - - def __init__(self, name, backend, cache): - self.name = name - self._backend = backend - self._cache = cache - self._is_our_app = self.name == self._backend.app_name - self._status = None - - @property - def status(self): - if not self._is_our_app: - return UnknownStatus() - - if not self._backend.is_leader(): - raise RuntimeError('cannot get application status as a non-leader unit') - - if self._status: - return self._status - - s = self._backend.status_get(is_app=True) - self._status = StatusBase.from_name(s['status'], s['message']) - return self._status - - @status.setter - def status(self, value): - if not isinstance(value, StatusBase): - raise InvalidStatusError(f'invalid value provided for application {self} status: {value}') - - if not self._is_our_app: - raise RuntimeError(f'cannot to set status for a remote application {self}') - - if not self._backend.is_leader(): - raise RuntimeError('cannot set application status as a non-leader unit') - - self._backend.status_set(value.name, value.message, is_app=True) - self._status = value - - def __repr__(self): - return f'<{type(self).__module__}.{type(self).__name__} {self.name}>' - - -class Unit: - - def __init__(self, name, backend, cache): - self.name = name - - app_name = name.split('/')[0] - self.app = cache.get(Application, app_name) - - self._backend = backend - self._cache = cache - self._is_our_unit = self.name == self._backend.unit_name - self._status = None - - @property - def status(self): - if not self._is_our_unit: - return UnknownStatus() - - if self._status: - return self._status - - s = self._backend.status_get(is_app=False) - self._status = StatusBase.from_name(s['status'], s['message']) - return self._status - - @status.setter - def status(self, value): - if not isinstance(value, StatusBase): - raise InvalidStatusError(f'invalid value provided for unit {self} status: {value}') - - if not self._is_our_unit: - raise RuntimeError(f'cannot set status for a remote unit {self}') - - self._backend.status_set(value.name, value.message, is_app=False) - self._status = value - - def __repr__(self): - return f'<{type(self).__module__}.{type(self).__name__} {self.name}>' - - def is_leader(self): - if self._is_our_unit: - # This value is not cached as it is not guaranteed to persist for the whole duration - # of a hook execution. - return self._backend.is_leader() - else: - raise RuntimeError(f"cannot determine leadership status for remote applications: {self}") - - -class LazyMapping(Mapping, ABC): - - _lazy_data = None - - @abstractmethod - def _load(self): - raise NotImplementedError() - - @property - def _data(self): - data = self._lazy_data - if data is None: - data = self._lazy_data = self._load() - return data - - def __contains__(self, key): - return key in self._data - - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(self._data) - - def __getitem__(self, key): - return self._data[key] - - -class RelationMapping(Mapping): - """Map of relation names to lists of Relation instances.""" - - def __init__(self, relations_meta, our_unit, backend, cache): - self._peers = set() - for name, relation_meta in relations_meta.items(): - if relation_meta.role == 'peers': - self._peers.add(name) - self._our_unit = our_unit - self._backend = backend - self._cache = cache - self._data = {relation_name: None for relation_name in relations_meta} - - def __contains__(self, key): - return key in self._data - - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(self._data) - - def __getitem__(self, relation_name): - is_peer = relation_name in self._peers - relation_list = self._data[relation_name] - if relation_list is None: - relation_list = self._data[relation_name] = [] - for rid in self._backend.relation_ids(relation_name): - relation = Relation(relation_name, rid, is_peer, self._our_unit, self._backend, self._cache) - relation_list.append(relation) - return relation_list - - def _get_unique(self, relation_name, relation_id=None): - if relation_id is not None: - if not isinstance(relation_id, int): - raise ModelError(f'relation name {relation_id} must be int or None not {type(relation_id).__name__}') - for relation in self[relation_name]: - if relation.id == relation_id: - return relation - else: - # The relation may be dead, but it is not forgotten. - is_peer = relation_name in self._peers - return Relation(relation_name, relation_id, is_peer, self._our_unit, self._backend, self._cache) - num_related = len(self[relation_name]) - if num_related == 0: - return None - elif num_related == 1: - return self[relation_name][0] - else: - # TODO: We need something in the framework to catch and gracefully handle - # errors, ideally integrating the error catching with Juju's mechanisms. - raise TooManyRelatedAppsError(relation_name, num_related, 1) - - -class Relation: - def __init__(self, relation_name, relation_id, is_peer, our_unit, backend, cache): - self.name = relation_name - self.id = relation_id - self.app = None - self.units = set() - - # For peer relations, both the remote and the local app are the same. - if is_peer: - self.app = our_unit.app - try: - for unit_name in backend.relation_list(self.id): - unit = cache.get(Unit, unit_name) - self.units.add(unit) - if self.app is None: - self.app = unit.app - except RelationNotFoundError: - # If the relation is dead, just treat it as if it has no remote units. - pass - self.data = RelationData(self, our_unit, backend) - - def __repr__(self): - return f'<{type(self).__module__}.{type(self).__name__} {self.name}:{self.id}>' - - -class RelationData(Mapping): - def __init__(self, relation, our_unit, backend): - self.relation = weakref.proxy(relation) - self._data = {our_unit: RelationDataContent(self.relation, our_unit, backend)} - self._data.update({our_unit.app: RelationDataContent(self.relation, our_unit.app, backend)}) - self._data.update({unit: RelationDataContent(self.relation, unit, backend) for unit in self.relation.units}) - # The relation might be dead so avoid a None key here. - if self.relation.app: - self._data.update({self.relation.app: RelationDataContent(self.relation, self.relation.app, backend)}) - - def __contains__(self, key): - return key in self._data - - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(self._data) - - def __getitem__(self, key): - return self._data[key] - - -# We mix in MutableMapping here to get some convenience implementations, but whether it's actually -# mutable or not is controlled by the flag. -class RelationDataContent(LazyMapping, MutableMapping): - - def __init__(self, relation, entity, backend): - self.relation = relation - self._entity = entity - self._backend = backend - self._is_app = isinstance(entity, Application) - - def _load(self): - try: - return self._backend.relation_get(self.relation.id, self._entity.name, self._is_app) - except RelationNotFoundError: - # Dead relations tell no tales (and have no data). - return {} - - def _is_mutable(self): - if self._is_app: - is_our_app = self._backend.app_name == self._entity.name - if not is_our_app: - return False - # Whether the application data bag is mutable or not depends on whether this unit is a leader or not, - # but this is not guaranteed to be always true during the same hook execution. - return self._backend.is_leader() - else: - is_our_unit = self._backend.unit_name == self._entity.name - if is_our_unit: - return True - return False - - def __setitem__(self, key, value): - if not self._is_mutable(): - raise RelationDataError(f'cannot set relation data for {self._entity.name}') - if not isinstance(value, str): - raise RelationDataError('relation data values must be strings') - - self._backend.relation_set(self.relation.id, key, value, self._is_app) - - # Don't load data unnecessarily if we're only updating. - if self._lazy_data is not None: - if value == '': - # Match the behavior of Juju, which is that setting the value to an empty string will - # remove the key entirely from the relation data. - del self._data[key] - else: - self._data[key] = value - - def __delitem__(self, key): - # Match the behavior of Juju, which is that setting the value to an empty string will - # remove the key entirely from the relation data. - self.__setitem__(key, '') - - -class ConfigData(LazyMapping): - - def __init__(self, backend): - self._backend = backend - - def _load(self): - return self._backend.config_get() - - -class StatusBase: - """Status values specific to applications and units.""" - - _statuses = {} - - def __init__(self, message): - self.message = message - - def __new__(cls, *args, **kwargs): - if cls is StatusBase: - raise TypeError("cannot instantiate a base class") - cls._statuses[cls.name] = cls - return super().__new__(cls) - - @classmethod - def from_name(cls, name, message): - return cls._statuses[name](message) - - -class ActiveStatus(StatusBase): - """The unit is ready. - - The unit believes it is correctly offering all the services it has been asked to offer. - """ - name = 'active' - - def __init__(self, message=None): - super().__init__(message or '') - - -class BlockedStatus(StatusBase): - """The unit requires manual intervention. - - An operator has to manually intervene to unblock the unit and let it proceed. - """ - name = 'blocked' - - -class MaintenanceStatus(StatusBase): - """The unit is performing maintenance tasks. - - The unit is not yet providing services, but is actively doing work in preparation for providing those services. - This is a "spinning" state, not an error state. It reflects activity on the unit itself, not on peers or related units. - """ - name = 'maintenance' - - -class UnknownStatus(StatusBase): - """The unit status is unknown. - - A unit-agent has finished calling install, config-changed and start, but the charm has not called status-set yet. - """ - name = 'unknown' - - def __init__(self): - # Unknown status cannot be set and does not have a message associated with it. - super().__init__('') - - -class WaitingStatus(StatusBase): - """A unit is unable to progress. - - The unit is unable to progress to an active state because an application to which it is related is not running. - """ - name = 'waiting' - - -class Resources: - """Object representing resources for the charm. - """ - - def __init__(self, names, backend): - self._backend = backend - self._paths = {name: None for name in names} - - def fetch(self, name): - """Fetch the resource from the controller or store. - - If successfully fetched, this returns a Path object to where the resource is stored - on disk, otherwise it raises a ModelError. - """ - if name not in self._paths: - raise RuntimeError(f'invalid resource name: {name}') - if self._paths[name] is None: - self._paths[name] = Path(self._backend.resource_get(name)) - return self._paths[name] - - -class Pod: - def __init__(self, backend): - self._backend = backend - - def set_spec(self, spec, k8s_resources=None): - if not self._backend.is_leader(): - raise ModelError('cannot set a pod spec as this unit is not a leader') - self._backend.pod_spec_set(spec, k8s_resources) - - -class StorageMapping(Mapping): - """Map of storage names to lists of Storage instances.""" - - def __init__(self, storage_names, backend): - self._backend = backend - self._storage_map = {storage_name: None for storage_name in storage_names} - - def __contains__(self, key): - return key in self._storage_map - - def __len__(self): - return len(self._storage_map) - - def __iter__(self): - return iter(self._storage_map) - - def __getitem__(self, storage_name): - storage_list = self._storage_map[storage_name] - if storage_list is None: - storage_list = self._storage_map[storage_name] = [] - for storage_id in self._backend.storage_list(storage_name): - storage_list.append(Storage(storage_name, storage_id, self._backend)) - return storage_list - - def request(self, storage_name, count=1): - """Requests new storage instances of a given name. - - Uses storage-add tool to request additional storage. Juju will notify the unit - via -storage-attached events when it becomes available. - """ - if storage_name not in self._storage_map: - raise ModelError(f'cannot add storage with {storage_name} as it is not present in the charm metadata') - self._backend.storage_add(storage_name, count) - - -class Storage: - - def __init__(self, storage_name, storage_id, backend): - self.name = storage_name - self.id = storage_id - self._backend = backend - self._location = None - - @property - def location(self): - if self._location is None: - self._location = Path(self._backend.storage_get(f'{self.name}/{self.id}', "location")) - return self._location - - -class ModelError(Exception): - pass - - -class TooManyRelatedAppsError(ModelError): - def __init__(self, relation_name, num_related, max_supported): - super().__init__(f'Too many remote applications on {relation_name} ({num_related} > {max_supported})') - self.relation_name = relation_name - self.num_related = num_related - self.max_supported = max_supported - - -class RelationDataError(ModelError): - pass - - -class RelationNotFoundError(ModelError): - pass - - -class InvalidStatusError(ModelError): - pass - - -class ModelBackend: - - LEASE_RENEWAL_PERIOD = datetime.timedelta(seconds=30) - - def __init__(self): - self.unit_name = os.environ['JUJU_UNIT_NAME'] - self.app_name = self.unit_name.split('/')[0] - - self._is_leader = None - self._leader_check_time = 0 - - def _run(self, *args, return_output=False, use_json=False): - kwargs = dict(stdout=PIPE, stderr=PIPE) - if use_json: - args += ('--format=json',) - try: - result = run(args, check=True, **kwargs) - except CalledProcessError as e: - raise ModelError(e.stderr) - if return_output: - if result.stdout is None: - return '' - else: - text = result.stdout.decode('utf8') - if use_json: - return json.loads(text) - else: - return text - - def relation_ids(self, relation_name): - relation_ids = self._run('relation-ids', relation_name, return_output=True, use_json=True) - return [int(relation_id.split(':')[-1]) for relation_id in relation_ids] - - def relation_list(self, relation_id): - try: - return self._run('relation-list', '-r', str(relation_id), return_output=True, use_json=True) - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise - - def relation_get(self, relation_id, member_name, is_app): - if not isinstance(is_app, bool): - raise TypeError('is_app parameter to relation_get must be a boolean') - - try: - return self._run('relation-get', '-r', str(relation_id), '-', member_name, f'--app={is_app}', return_output=True, use_json=True) - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise - - def relation_set(self, relation_id, key, value, is_app): - if not isinstance(is_app, bool): - raise TypeError('is_app parameter to relation_set must be a boolean') - - try: - return self._run('relation-set', '-r', str(relation_id), f'{key}={value}', f'--app={is_app}') - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise - - def config_get(self): - return self._run('config-get', return_output=True, use_json=True) - - def is_leader(self): - """Obtain the current leadership status for the unit the charm code is executing on. - - The value is cached for the duration of a lease which is 30s in Juju. - """ - now = time.monotonic() - time_since_check = datetime.timedelta(seconds=now - self._leader_check_time) - if time_since_check > self.LEASE_RENEWAL_PERIOD or self._is_leader is None: - # Current time MUST be saved before running is-leader to ensure the cache - # is only used inside the window that is-leader itself asserts. - self._leader_check_time = now - self._is_leader = self._run('is-leader', return_output=True, use_json=True) - - return self._is_leader - - def resource_get(self, resource_name): - return self._run('resource-get', resource_name, return_output=True).strip() - - def pod_spec_set(self, spec, k8s_resources): - tmpdir = Path(tempfile.mkdtemp('-pod-spec-set')) - try: - spec_path = tmpdir / 'spec.json' - spec_path.write_text(json.dumps(spec)) - args = ['--file', str(spec_path)] - if k8s_resources: - k8s_res_path = tmpdir / 'k8s-resources.json' - k8s_res_path.write_text(json.dumps(k8s_resources)) - args.extend(['--k8s-resources', str(k8s_res_path)]) - self._run('pod-spec-set', *args) - finally: - shutil.rmtree(tmpdir) - - def status_get(self, *, is_app=False): - """Get a status of a unit or an application. - app -- A boolean indicating whether the status should be retrieved for a unit or an application. - """ - return self._run('status-get', '--include-data', f'--application={is_app}') - - def status_set(self, status, message='', *, is_app=False): - """Set a status of a unit or an application. - app -- A boolean indicating whether the status should be set for a unit or an application. - """ - if not isinstance(is_app, bool): - raise TypeError('is_app parameter must be boolean') - return self._run('status-set', f'--application={is_app}', status, message) - - def storage_list(self, name): - return [int(s.split('/')[1]) for s in self._run('storage-list', name, return_output=True, use_json=True)] - - def storage_get(self, storage_name_id, attribute): - return self._run('storage-get', '-s', storage_name_id, attribute, return_output=True, use_json=True) - - def storage_add(self, name, count=1): - if not isinstance(count, int) or isinstance(count, bool): - raise TypeError(f'storage count must be integer, got: {count} ({type(count)})') - self._run('storage-add', f'{name}={count}') - - def action_get(self): - return self._run(f'action-get', return_output=True, use_json=True) - - def action_set(self, results): - self._run(f'action-set', *[f"{k}={v}" for k, v in results.items()]) - - def action_log(self, message): - self._run(f'action-log', f"{message}") - - def action_fail(self, message=''): - self._run(f'action-fail', f"{message}") - - def network_get(self, endpoint_name, relation_id=None): - """Return network info provided by network-get for a given endpoint. - - endpoint_name -- A name of an endpoint (relation name or extra-binding name). - relation_id -- An optional relation id to get network info for. - """ - cmd = ['network-get', endpoint_name] - if relation_id is not None: - cmd.extend(['-r', str(relation_id)]) - try: - return self._run(*cmd, return_output=True, use_json=True) - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/metadata.yaml b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/metadata.yaml deleted file mode 100644 index fb67ecbf117ecf6a48be11cf1985861aee60bb89..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/metadata.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: simple-k8s -summary: A simple example Kubernetes charm -description: | - Simple is an example charm used in OSM Hackfests -series: - - kubernetes -deployment: - mode: operator diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/LICENSE b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/LICENSE deleted file mode 100644 index 261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/README.md b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/README.md deleted file mode 100644 index a5d80722e22eaf984532d0f2ac208e3085c4f469..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# charms.osm -A Python library to aid the development of charms for Open Source Mano (OSM) - -## SSHProxy - -Example: - -```python -from charms.osm.sshproxy import SSHProxy - -# Check if SSH Proxy has key -if not SSHProxy.has_ssh_key(): - # Generate SSH Key - SSHProxy.generate_ssh_key() - -# Get generated public and private keys -SSHProxy.get_ssh_public_key() -SSHProxy.get_ssh_private_key() - -# Get Proxy -proxy = SSHProxy( - hostname=config["ssh-hostname"], - username=config["ssh-username"], - password=config["ssh-password"], -) - -# Verify credentials -verified = proxy.verify_credentials() - -if verified: - # Run commands in remote machine - proxy.run("touch /home/ubuntu/touch") -``` - -## Libansible - -```python -from charms.osm import libansible - -# Install ansible packages in the charm -libansible.install_ansible_support() - -result = libansible.execute_playbook( - "configure-remote.yaml", # Name of the playbook <-- Put the playbook in playbooks/ folder - config["ssh-hostname"], - config["ssh-username"], - config["ssh-password"], - dict_vars, # Dictionary with variables to populate in the playbook -) -``` - -## Usage - -Import submodules: - -```bash -git submodule add https://github.com/charmed-osm/charms.osm mod/charms.osm -git submodule add https://github.com/juju/charm-helpers.git mod/charm-helpers # Only for libansible -``` - -Add symlinks: - -```bash -mkdir -p lib/charms -ln -s ../mod/charms.osm/charms/osm lib/charms/osm -ln -s ../mod/charm-helpers/charmhelpers lib/charmhelpers # Only for libansible -``` diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/charms/osm/libansible.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/charms/osm/libansible.py deleted file mode 100644 index 32fd26ae7d63d42edef33982d5438669b191a361..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/charms/osm/libansible.py +++ /dev/null @@ -1,108 +0,0 @@ -## -# Copyright 2020 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 fnmatch -import os -import yaml -import subprocess -import sys - -sys.path.append("lib") -import charmhelpers.fetch - - -ansible_hosts_path = "/etc/ansible/hosts" - - -def install_ansible_support(from_ppa=True, ppa_location="ppa:ansible/ansible"): - """Installs the ansible package. - - By default it is installed from the `PPA`_ linked from - the ansible `website`_ or from a ppa specified by a charm config.. - - .. _PPA: https://launchpad.net/~rquillo/+archive/ansible - .. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu - - If from_ppa is empty, you must ensure that the package is available - from a configured repository. - """ - if from_ppa: - charmhelpers.fetch.add_source(ppa_location) - charmhelpers.fetch.apt_update(fatal=True) - charmhelpers.fetch.apt_install("ansible") - with open(ansible_hosts_path, "w+") as hosts_file: - hosts_file.write("localhost ansible_connection=local") - - -def create_hosts(hostname, username, password, hosts): - inventory_path = "/etc/ansible/hosts" - - with open(inventory_path, "w") as f: - f.write("[{}]\n".format(hosts)) - h1 = "host ansible_host={0} ansible_user={1} ansible_password={2}\n".format( - hostname, username, password - ) - f.write(h1) - - -def create_ansible_cfg(): - ansible_config_path = "/etc/ansible/ansible.cfg" - - with open(ansible_config_path, "w") as f: - f.write("[defaults]\n") - f.write("host_key_checking = False\n") - - -# 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 - - -def execute_playbook(playbook_file, hostname, user, password, vars_dict=None): - playbook_path = find(playbook_file, "/var/lib/juju/agents/") - - with open(playbook_path, "r") as f: - playbook_data = yaml.load(f) - - hosts = "all" - if "hosts" in playbook_data[0].keys() and playbook_data[0]["hosts"]: - hosts = playbook_data[0]["hosts"] - - create_ansible_cfg() - create_hosts(hostname, user, password, hosts) - - call = "ansible-playbook {} ".format(playbook_path) - - if vars_dict and isinstance(vars_dict, dict) and len(vars_dict) > 0: - call += "--extra-vars " - - string_var = "" - for k,v in vars_dict.items(): - string_var += "{}={} ".format(k, v) - - string_var = string_var.strip() - call += '"{}"'.format(string_var) - - call = call.strip() - result = subprocess.check_output(call, shell=True) - - return result diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/charms/osm/ns.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/charms/osm/ns.py deleted file mode 100644 index 25be4056282e48ce946632025be8b466557e3171..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/charms/osm/ns.py +++ /dev/null @@ -1,301 +0,0 @@ -# A prototype of a library to aid in the development and operation of -# OSM Network Service charms - -import asyncio -import logging -import os -import os.path -import re -import subprocess -import sys -import time -import yaml - -try: - import juju -except ImportError: - # Not all cloud images are created equal - if not os.path.exists("/usr/bin/python3") or not os.path.exists("/usr/bin/pip3"): - # Update the apt cache - subprocess.check_call(["apt-get", "update"]) - - # Install the Python3 package - subprocess.check_call(["apt-get", "install", "-y", "python3", "python3-pip"],) - - - # Install the libjuju build dependencies - subprocess.check_call(["apt-get", "install", "-y", "libffi-dev", "libssl-dev"],) - - subprocess.check_call( - [sys.executable, "-m", "pip", "install", "juju"], - ) - -from juju.controller import Controller - -# Quiet the debug logging -logging.getLogger('websockets.protocol').setLevel(logging.INFO) -logging.getLogger('juju.client.connection').setLevel(logging.WARN) -logging.getLogger('juju.model').setLevel(logging.WARN) -logging.getLogger('juju.machine').setLevel(logging.WARN) - - -class NetworkService: - """A lightweight interface to the Juju controller. - - This NetworkService client is specifically designed to allow a higher-level - "NS" charm to interoperate with "VNF" charms, allowing for the execution of - Primitives across other charms within the same model. - """ - endpoint = None - user = 'admin' - secret = None - port = 17070 - loop = None - client = None - model = None - cacert = None - - def __init__(self, user, secret, endpoint=None): - - self.user = user - self.secret = secret - if endpoint is None: - addresses = os.environ['JUJU_API_ADDRESSES'] - for address in addresses.split(' '): - self.endpoint = address - else: - self.endpoint = endpoint - - # Stash the name of the model - self.model = os.environ['JUJU_MODEL_NAME'] - - # Load the ca-cert from agent.conf - AGENT_PATH = os.path.dirname(os.environ['JUJU_CHARM_DIR']) - with open("{}/agent.conf".format(AGENT_PATH), "r") as f: - try: - y = yaml.safe_load(f) - self.cacert = y['cacert'] - except yaml.YAMLError as exc: - print("Unable to find Juju ca-cert.") - raise exc - - # Create our event loop - self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.loop) - - async def connect(self): - """Connect to the Juju controller.""" - controller = Controller() - - print( - "Connecting to controller... ws://{}:{} as {}/{}".format( - self.endpoint, - self.port, - self.user, - self.secret[-4:].rjust(len(self.secret), "*"), - ) - ) - await controller.connect( - endpoint=self.endpoint, - username=self.user, - password=self.secret, - cacert=self.cacert, - ) - - return controller - - def __del__(self): - self.logout() - - async def disconnect(self): - """Disconnect from the Juju controller.""" - if self.client: - print("Disconnecting Juju controller") - await self.client.disconnect() - - def login(self): - """Login to the Juju controller.""" - if not self.client: - # Connect to the Juju API server - self.client = self.loop.run_until_complete(self.connect()) - return self.client - - def logout(self): - """Logout of the Juju controller.""" - - if self.loop: - print("Disconnecting from API") - self.loop.run_until_complete(self.disconnect()) - - def FormatApplicationName(self, *args): - """ - Generate a Juju-compatible Application name - - :param args tuple: Positional arguments to be used to construct the - application name. - - Limitations:: - - Only accepts characters a-z and non-consequitive dashes (-) - - Application name should not exceed 50 characters - - Examples:: - - FormatApplicationName("ping_pong_ns", "ping_vnf", "a") - """ - appname = "" - for c in "-".join(list(args)): - if c.isdigit(): - c = chr(97 + int(c)) - elif not c.isalpha(): - c = "-" - appname += c - - return re.sub('-+', '-', appname.lower()) - - def GetApplicationName(self, nsr_name, vnf_name, vnf_member_index): - """Get the runtime application name of a VNF/VDU. - - This will generate an application name matching the name of the deployed charm, - given the right parameters. - - :param nsr_name str: The name of the running Network Service, as specified at instantiation. - :param vnf_name str: The name of the VNF or VDU - :param vnf_member_index: The vnf-member-index as specified in the descriptor - """ - - application_name = self.FormatApplicationName(nsr_name, vnf_member_index, vnf_name) - - # This matches the logic used by the LCM - application_name = application_name[0:48] - vca_index = int(vnf_member_index) - 1 - application_name += '-' + chr(97 + vca_index // 26) + chr(97 + vca_index % 26) - - return application_name - - def ExecutePrimitiveGetOutput(self, application, primitive, params={}, timeout=600): - """Execute a single primitive and return it's output. - - This is a blocking method that will execute a single primitive and wait - for its completion before return it's output. - - :param application str: The application name provided by `GetApplicationName`. - :param primitive str: The name of the primitive to execute. - :param params list: A list of parameters. - :param timeout int: A timeout, in seconds, to wait for the primitive to finish. Defaults to 600 seconds. - """ - uuid = self.ExecutePrimitive(application, primitive, params) - - status = None - output = None - - starttime = time.time() - while(time.time() < starttime + timeout): - status = self.GetPrimitiveStatus(uuid) - if status in ['completed', 'failed']: - break - time.sleep(10) - - # When the primitive is done, get the output - if status in ['completed', 'failed']: - output = self.GetPrimitiveOutput(uuid) - - return output - - def ExecutePrimitive(self, application, primitive, params={}): - """Execute a primitive. - - This is a non-blocking method to execute a primitive. It will return - the UUID of the queued primitive execution, which you can use - for subsequent calls to `GetPrimitiveStatus` and `GetPrimitiveOutput`. - - :param application string: The name of the application - :param primitive string: The name of the Primitive. - :param params list: A list of parameters. - - :returns uuid string: The UUID of the executed Primitive - """ - uuid = None - - if not self.client: - self.login() - - model = self.loop.run_until_complete( - self.client.get_model(self.model) - ) - - # Get the application - if application in model.applications: - app = model.applications[application] - - # Execute the primitive - unit = app.units[0] - if unit: - action = self.loop.run_until_complete( - unit.run_action(primitive, **params) - ) - uuid = action.id - print("Executing action: {}".format(uuid)) - self.loop.run_until_complete( - model.disconnect() - ) - else: - # Invalid mapping: application not found. Raise exception - raise Exception("Application not found: {}".format(application)) - - return uuid - - def GetPrimitiveStatus(self, uuid): - """Get the status of a Primitive execution. - - This will return one of the following strings: - - pending - - running - - completed - - failed - - :param uuid string: The UUID of the executed Primitive. - :returns: The status of the executed Primitive - """ - status = None - - if not self.client: - self.login() - - model = self.loop.run_until_complete( - self.client.get_model(self.model) - ) - - status = self.loop.run_until_complete( - model.get_action_status(uuid) - ) - - self.loop.run_until_complete( - model.disconnect() - ) - - return status[uuid] - - def GetPrimitiveOutput(self, uuid): - """Get the output of a completed Primitive execution. - - - :param uuid string: The UUID of the executed Primitive. - :returns: The output of the execution, or None if it's still running. - """ - result = None - if not self.client: - self.login() - - model = self.loop.run_until_complete( - self.client.get_model(self.model) - ) - - result = self.loop.run_until_complete( - model.get_action_output(uuid) - ) - - self.loop.run_until_complete( - model.disconnect() - ) - - return result diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/charms/osm/sshproxy.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/charms/osm/sshproxy.py deleted file mode 100644 index 724b98cfc8e879217ccaf8ba5d255eac1b0c49aa..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms.osm/charms/osm/sshproxy.py +++ /dev/null @@ -1,250 +0,0 @@ -"""Module to help with executing commands over SSH.""" -## -# Copyright 2016 Canonical Ltd. -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -## - -# from charmhelpers.core import unitdata -# from charmhelpers.core.hookenv import log - -import io -import ipaddress - -import os -import socket -import shlex -import traceback -import sys - -from subprocess import ( - check_call, - Popen, - CalledProcessError, - PIPE, -) - -def install_dependencies(): - # Make sure Python3 + PIP are available - if not os.path.exists("/usr/bin/python3") or not os.path.exists("/usr/bin/pip3"): - # This is needed when running as a k8s charm, as the ubuntu:latest - # image doesn't include either package. - - # Update the apt cache - check_call(["apt-get", "update"]) - - # Install the Python3 package - check_call(["apt-get", "install", "-y", "python3", "python3-pip"],) - - # Install the build dependencies for our requirements (paramiko) - check_call(["apt-get", "install", "-y", "libffi-dev", "libssl-dev"],) - - check_call( - [sys.executable, "-m", "pip", "install", "paramiko"], - ) - -try: - import paramiko -except Exception as ex: - install_dependencies() - import paramiko - -class SSHProxy: - private_key_path = "/root/.ssh/id_sshproxy" - public_key_path = "/root/.ssh/id_sshproxy.pub" - key_type = "rsa" - key_bits = 4096 - - def __init__(self, hostname: str, username: str, password: str = ""): - self.hostname = hostname - self.username = username - self.password = password - - @staticmethod - def generate_ssh_key(): - """Generate a 4096-bit rsa keypair.""" - if not os.path.exists(SSHProxy.private_key_path): - cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format( - SSHProxy.key_type, SSHProxy.key_bits, SSHProxy.private_key_path, - ) - - try: - check_call(cmd, shell=True) - except CalledProcessError: - return False - - return True - - @staticmethod - def write_ssh_keys(public, private): - """Write a 4096-bit rsa keypair.""" - with open(SSHProxy.public_key_path, "w") as f: - f.write(public) - f.close() - with open(SSHProxy.private_key_path, "w") as f: - f.write(private) - f.close() - - @staticmethod - def get_ssh_public_key(): - publickey = "" - if os.path.exists(SSHProxy.private_key_path): - with open(SSHProxy.public_key_path, "r") as f: - publickey = f.read() - return publickey - - @staticmethod - def get_ssh_private_key(): - privatekey = "" - if os.path.exists(SSHProxy.private_key_path): - with open(SSHProxy.private_key_path, "r") as f: - privatekey = f.read() - return privatekey - - @staticmethod - def has_ssh_key(): - return True if os.path.exists(SSHProxy.private_key_path) else False - - def run(self, cmd: str) -> (str, str): - """Run a command remotely via SSH. - - Note: The previous behavior was to run the command locally if SSH wasn't - configured, but that can lead to cases where execution succeeds when you'd - expect it not to. - """ - if isinstance(cmd, str): - cmd = shlex.split(cmd) - - host = self._get_hostname() - user = self.username - passwd = self.password - key = self.private_key_path - - # Make sure we have everything we need to connect - if host and user: - return self._ssh(cmd) - - raise Exception("Invalid SSH credentials.") - - def sftp(self, local, remote): - client = self._get_ssh_client() - - # Create an sftp connection from the underlying transport - sftp = paramiko.SFTPClient.from_transport(client.get_transport()) - sftp.put(local, remote) - client.close() - pass - - def verify_credentials(self): - """Verify the SSH credentials. - - :return (bool, str): Verified, Stderr - """ - try: - (stdout, stderr) = self.run("hostname") - except CalledProcessError as e: - stderr = "Command failed: {} ({})".format(" ".join(e.cmd), str(e.output)) - except paramiko.ssh_exception.AuthenticationException as e: - stderr = "{}.".format(e) - except paramiko.ssh_exception.BadAuthenticationType as e: - stderr = "{}".format(e.explanation) - except paramiko.ssh_exception.BadHostKeyException as e: - stderr = "Host key mismatch: expected {} but got {}.".format( - e.expected_key, e.got_key, - ) - except (TimeoutError, socket.timeout): - stderr = "Timeout attempting to reach {}".format(self._get_hostname()) - except Exception as error: - tb = traceback.format_exc() - stderr = "Unhandled exception: {}".format(tb) - - if len(stderr) == 0: - return True, stderr - return False, stderr - - ################### - # Private methods # - ################### - def _get_hostname(self): - """Get the hostname for the ssh target. - - HACK: This function was added to work around an issue where the - ssh-hostname was passed in the format of a.b.c.d;a.b.c.d, where the first - is the floating ip, and the second the non-floating ip, for an Openstack - instance. - """ - return self.hostname.split(";")[0] - - def _get_ssh_client(self): - """Return a connected Paramiko ssh object.""" - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - pkey = None - - # Otherwise, check for the auto-generated private key - if os.path.exists(self.private_key_path): - with open(self.private_key_path) as f: - pkey = paramiko.RSAKey.from_private_key(f) - - ########################################################################### - # There is a bug in some versions of OpenSSH 4.3 (CentOS/RHEL 5) where # - # the server may not send the SSH_MSG_USERAUTH_BANNER message except when # - # responding to an auth_none request. For example, paramiko will attempt # - # to use password authentication when a password is set, but the server # - # could deny that, instead requesting keyboard-interactive. The hack to # - # workaround this is to attempt a reconnect, which will receive the right # - # banner, and authentication can proceed. See the following for more info # - # https://github.com/paramiko/paramiko/issues/432 # - # https://github.com/paramiko/paramiko/pull/438 # - ########################################################################### - - try: - client.connect( - self.hostname, - port=22, - username=self.username, - password=self.password, - pkey=pkey, - ) - except paramiko.ssh_exception.SSHException as e: - if "Error reading SSH protocol banner" == str(e): - # Once more, with feeling - client.connect( - host, port=22, username=user, password=password, pkey=pkey - ) - else: - # Reraise the original exception - raise e - - return client - - def _ssh(self, cmd): - """Run an arbitrary command over SSH. - - Returns a tuple of (stdout, stderr) - """ - client = self._get_ssh_client() - - cmds = " ".join(cmd) - stdin, stdout, stderr = client.exec_command(cmds, get_pty=True) - retcode = stdout.channel.recv_exit_status() - client.close() # @TODO re-use connections - if retcode > 0: - output = stderr.read().strip() - raise CalledProcessError(returncode=retcode, cmd=cmd, output=output) - return ( - stdout.read().decode("utf-8").strip(), - stderr.read().decode("utf-8").strip(), - ) diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms/LICENSE b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms/LICENSE deleted file mode 100644 index 261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms/README.md b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms/README.md deleted file mode 100644 index a56670704b8333f968a78d6b22dcbad5f8873bd7..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# charms.requirementstxt - -A Python library, to aid the development of charms, that will automatically install Python dependencies as declared by a `requirements.txt` file in the root of the charm. - -## Usage - -Install the charms.requirementstxt library in your charm: - -```bash -git submodule add https://github.com/AdamIsrael/charms.osm mod/charms.osm -mkdir -p lib/charms -ln -s ../mod/charms.osm/charms/osm lib/charms/osm -``` - -Import the `charms.requirementstxt` library early, before any dependencies it may install. - -In `src/charm.py`: - -```python -#!/usr/bin/env python3 - -import sys - -sys.path.append("lib") - -import charms.requirementstxt -... - -``` \ No newline at end of file diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms/charms/requirementstxt.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms/charms/requirementstxt.py deleted file mode 100644 index 298d584530ba46f0d7941b88c870a4fe20b6c44f..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/charms/charms/requirementstxt.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -# Requirements.txt support - -import sys - -sys.path.append("lib") - -from ops.framework import StoredState - -import os -import subprocess -import sys -from remote_pdb import RemotePdb - -REQUIREMENTS_TXT = "{}/requirements.txt".format(os.environ["JUJU_CHARM_DIR"]) - - -def install_requirements(): - if os.path.exists(REQUIREMENTS_TXT): - - # First, make sure python3 and python3-pip are installed - if not os.path.exists("/usr/bin/python3") or not os.path.exists("/usr/bin/pip3"): - # Update the apt cache - subprocess.check_call(["apt-get", "update"]) - # Install the Python3 package - subprocess.check_call( - ["apt-get", "install", "-y", "python3", "python3-pip", "python3-paramiko"], - # Eat stdout so it's not returned in an action's stdout - # TODO: redirect to a file handle and log to juju log - # stdout=subprocess.DEVNULL, - ) - - # Lastly, install the python requirements - cmd = [sys.executable, "-m", "pip", "install", "-r", REQUIREMENTS_TXT] - # stdout = subprocess.check_output(cmd) - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) - - stdout, stderr = p.communicate() - - print(stdout) - print(stderr) - # subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", REQUIREMENTS_TXT], - # # Eat stdout so it's not returned in an action's stdout - # # TODO: redirect to a file handle and log to juju log - # # stdout=subprocess.DEVNULL, - # ) - - -# Use StoredState to make sure we're run exactly once automatically -# RemotePdb('127.0.0.1', 4444).set_trace() - -state = StoredState() - -installed = getattr(state, "requirements_txt_installed", None) -if not installed: - install_requirements() - state.requirements_txt_installed = True - diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/.flake8 b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/.flake8 deleted file mode 100644 index d0224f38cd598a23fef72c097330c9474e8e6f09..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -max-line-length = 160 -exclude = sandbox diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/.gitignore b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/.gitignore deleted file mode 100644 index cf0f37160ecf99377020ac5508e0db4746ea2608..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -__pycache__ -/sandbox -.idea diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/.travis.yml b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/.travis.yml deleted file mode 100644 index adfdcb93cc0086f67fe447515aaddad3dffcba05..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -dist: bionic - -language: python - -python: - - "3.6" - - "3.7" - -install: - - sudo apt update - - sudo apt install flake8 make - - pip3 install pyyaml autopep8 - -script: - - make test diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/LICENSE.txt b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/LICENSE.txt deleted file mode 100644 index d645695673349e3947e8e5ae42332d0ac3164cd7..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - 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/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/Makefile b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/Makefile deleted file mode 100644 index 3c82254068ae2be56cbdc0e90d6673a974845fc4..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/Makefile +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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. - -test: lint - @python3 -m unittest - -lint: quotelint check-copyright - @autopep8 -r --aggressive --diff --exit-code . - @flake8 --config=.flake8 - -quotelint: - @x=$$(grep -rnH --include \*.py "\\\\[\"']"); \ - if [ "$$x" ]; then \ - echo "Please fix the quoting to avoid spurious backslashes:"; \ - echo "$$x"; \ - exit 1; \ - fi >&2 - -check-copyright: - @x=$$(find . -name \*.py -not -empty -type f -print0 | xargs -0 grep -L "^# Copyright"); \ - if [ "$$x" ]; then \ - echo "Please add copyright headers to the following files:"; \ - echo "$$x"; \ - exit 1; \ - fi >&2 - - - - -.PHONY: lint test quotelint check-copyright diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/README.md b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/README.md deleted file mode 100644 index f684a8288c8e897990ee0b7aab2be38375464393..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# Operator Framework for Charms - -This framework is not yet stable and is subject to change, but is available -for early testing. - -## Getting Started - -The following overall structure for your charm directory is recommended: - -``` -. -+-- config.yaml -+-- metadata.yaml -+-- mod/ -+-- lib/ -| +-- ops -> ../mod/operator/ops -+-- src/ -| +-- charm.py -+-- hooks/ - +-- install -> ../src/charm.py - +-- start -> ../src/charm.py # for k8s charms per below -``` - -The `mod/` directory should contain the operator framework dependency as a git -submodule: - -``` -git submodule add https://github.com/canonical/operator mod/operator -``` - -Then symlink from the git submodule for the operator framework into the `lib/` -directory of your charm so it can be imported at run time: - -``` -ln -s ../mod/operator/ops lib/ops -``` - -Other dependencies included as git submodules can be added in the `mod/` -directory and symlinked into `lib/` as well. - -You can sync subsequent changes from the framework and other submodule -dependencies by running: - -``` -git submodule update -``` - -Those cloning and checking out the source for your charm for the first time -will need to run: - -``` -git submodule update --init -``` - -Your `src/charm.py` is the entry point for your charm logic. It should be set -to executable and use Python 3.6 or greater. At a minimum, it needs to define -a subclass of `CharmBase` and pass that into the framework's `main` function: - -```python -import sys -sys.path.append('lib') # noqa: E402 - -from ops.charm import CharmBase -from ops.main import main - - -class MyCharm(CharmBase): - pass - - -if __name__ == "__main__": - main(MyCharm) -``` - -This charm does nothing, because the `MyCharm` class passed to the operator -framework's `main` function is empty. Functionality can be added to the charm -by instructing it to observe particular Juju events when the `MyCharm` object -is initialized. For example, - -```python -class MyCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - self.framework.observe(self.on.start, self.on_start) - - def on_start(self, event): - # Handle the start event here. -``` - -Every standard event in Juju may be observed that way, and you can also easily -define your own events in your custom types. - -> The second argument to `observe` can be either the handler as a bound -> method, or the observer itself if the handler is a method of the observer -> that follows the conventional naming pattern. That is, in this case, we -> could have called just `self.framework.obseve(self.on.start, self)`. - -The `hooks/` directory must contain a symlink to your `src/charm.py` entry -point so that Juju can call it. You only need to set up the `hooks/install` link -(`hooks/start` for K8s charms, until [lp#1854635](https://bugs.launchpad.net/juju/+bug/1854635) -is resolved), and the framework will create all others at runtime. - -Once your charm is ready, upload it to the charm store and deploy it as -normal with: - -``` -# Replace ${CHARM} with the name of the charm. -charm push . cs:~${USER}/${CHARM} -# Replace ${VERSION} with the version created by `charm push`. -charm release cs:~${USER}/${CHARM}-${VERSION} -charm grant cs:~${USER}/${CHARM}-${VERSION} everyone -# And now deploy your charm. -juju deploy cs:~${USER}/$CHARM -``` - -Alternatively, to deploy directly from local disk, run: - -``` -juju deploy . -``` diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/__init__.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/charm.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/charm.py deleted file mode 100755 index 71472f963a82461a02fe36a0aa47260151f4cafe..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/charm.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 os - -import yaml - -from ops.framework import Object, EventSource, EventBase, EventsBase - - -class HookEvent(EventBase): - pass - - -class ActionEvent(EventBase): - - def defer(self): - raise RuntimeError('cannot defer action events') - - def restore(self, snapshot): - env_action_name = os.environ.get('JUJU_ACTION_NAME') - event_action_name = self.handle.kind[:-len('_action')].replace('_', '-') - if event_action_name != env_action_name: - # This could only happen if the dev manually emits the action, or from a bug. - raise RuntimeError('action event kind does not match current action') - # Params are loaded at restore rather than __init__ because the model is not available in __init__. - self.params = self.framework.model._backend.action_get() - - def set_results(self, results): - self.framework.model._backend.action_set(results) - - def log(self, message): - self.framework.model._backend.action_log(message) - - def fail(self, message=''): - self.framework.model._backend.action_fail(message) - - -class InstallEvent(HookEvent): - pass - - -class StartEvent(HookEvent): - pass - - -class StopEvent(HookEvent): - pass - - -class ConfigChangedEvent(HookEvent): - pass - - -class UpdateStatusEvent(HookEvent): - pass - - -class UpgradeCharmEvent(HookEvent): - pass - - -class PreSeriesUpgradeEvent(HookEvent): - pass - - -class PostSeriesUpgradeEvent(HookEvent): - pass - - -class LeaderElectedEvent(HookEvent): - pass - - -class LeaderSettingsChangedEvent(HookEvent): - pass - - -class RelationEvent(HookEvent): - def __init__(self, handle, relation, app=None, unit=None): - super().__init__(handle) - - if unit and unit.app != app: - raise RuntimeError(f'cannot create RelationEvent with application {app} and unit {unit}') - - self.relation = relation - self.app = app - self.unit = unit - - def snapshot(self): - snapshot = { - 'relation_name': self.relation.name, - 'relation_id': self.relation.id, - } - if self.app: - snapshot['app_name'] = self.app.name - if self.unit: - snapshot['unit_name'] = self.unit.name - return snapshot - - def restore(self, snapshot): - self.relation = self.framework.model.get_relation(snapshot['relation_name'], snapshot['relation_id']) - - app_name = snapshot.get('app_name') - if app_name: - self.app = self.framework.model.get_app(app_name) - else: - self.app = None - - unit_name = snapshot.get('unit_name') - if unit_name: - self.unit = self.framework.model.get_unit(unit_name) - else: - self.unit = None - - -class RelationJoinedEvent(RelationEvent): - pass - - -class RelationChangedEvent(RelationEvent): - pass - - -class RelationDepartedEvent(RelationEvent): - pass - - -class RelationBrokenEvent(RelationEvent): - pass - - -class StorageEvent(HookEvent): - pass - - -class StorageAttachedEvent(StorageEvent): - pass - - -class StorageDetachingEvent(StorageEvent): - pass - - -class CharmEvents(EventsBase): - - install = EventSource(InstallEvent) - start = EventSource(StartEvent) - stop = EventSource(StopEvent) - update_status = EventSource(UpdateStatusEvent) - config_changed = EventSource(ConfigChangedEvent) - upgrade_charm = EventSource(UpgradeCharmEvent) - pre_series_upgrade = EventSource(PreSeriesUpgradeEvent) - post_series_upgrade = EventSource(PostSeriesUpgradeEvent) - leader_elected = EventSource(LeaderElectedEvent) - leader_settings_changed = EventSource(LeaderSettingsChangedEvent) - - -class CharmBase(Object): - - on = CharmEvents() - - def __init__(self, framework, key): - super().__init__(framework, key) - - for relation_name in self.framework.meta.relations: - relation_name = relation_name.replace('-', '_') - self.on.define_event(f'{relation_name}_relation_joined', RelationJoinedEvent) - self.on.define_event(f'{relation_name}_relation_changed', RelationChangedEvent) - self.on.define_event(f'{relation_name}_relation_departed', RelationDepartedEvent) - self.on.define_event(f'{relation_name}_relation_broken', RelationBrokenEvent) - - for storage_name in self.framework.meta.storages: - storage_name = storage_name.replace('-', '_') - self.on.define_event(f'{storage_name}_storage_attached', StorageAttachedEvent) - self.on.define_event(f'{storage_name}_storage_detaching', StorageDetachingEvent) - - for action_name in self.framework.meta.actions: - action_name = action_name.replace('-', '_') - self.on.define_event(f'{action_name}_action', ActionEvent) - - -class CharmMeta: - """Object containing the metadata for the charm. - - The maintainers, tags, terms, series, and extra_bindings attributes are all - lists of strings. The requires, provides, peers, relations, storage, - resources, and payloads attributes are all mappings of names to instances - of the respective RelationMeta, StorageMeta, ResourceMeta, or PayloadMeta. - - The relations attribute is a convenience accessor which includes all of the - requires, provides, and peers RelationMeta items. If needed, the role of - the relation definition can be obtained from its role attribute. - """ - - def __init__(self, raw={}, actions_raw={}): - self.name = raw.get('name', '') - self.summary = raw.get('summary', '') - self.description = raw.get('description', '') - self.maintainers = [] - if 'maintainer' in raw: - self.maintainers.append(raw['maintainer']) - if 'maintainers' in raw: - self.maintainers.extend(raw['maintainers']) - self.tags = raw.get('tags', []) - self.terms = raw.get('terms', []) - self.series = raw.get('series', []) - self.subordinate = raw.get('subordinate', False) - self.min_juju_version = raw.get('min-juju-version') - self.requires = {name: RelationMeta('requires', name, rel) - for name, rel in raw.get('requires', {}).items()} - self.provides = {name: RelationMeta('provides', name, rel) - for name, rel in raw.get('provides', {}).items()} - self.peers = {name: RelationMeta('peers', name, rel) - for name, rel in raw.get('peers', {}).items()} - self.relations = {} - self.relations.update(self.requires) - self.relations.update(self.provides) - self.relations.update(self.peers) - self.storages = {name: StorageMeta(name, storage) - for name, storage in raw.get('storage', {}).items()} - self.resources = {name: ResourceMeta(name, res) - for name, res in raw.get('resources', {}).items()} - self.payloads = {name: PayloadMeta(name, payload) - for name, payload in raw.get('payloads', {}).items()} - self.extra_bindings = raw.get('extra-bindings', []) - self.actions = {name: ActionMeta(name, action) for name, action in actions_raw.items()} - - @classmethod - def from_yaml(cls, metadata, actions=None): - meta = yaml.safe_load(metadata) - raw_actions = {} - if actions is not None: - raw_actions = yaml.safe_load(actions) - return cls(meta, raw_actions) - - -class RelationMeta: - """Object containing metadata about a relation definition.""" - - def __init__(self, role, relation_name, raw): - self.role = role - self.relation_name = relation_name - self.interface_name = raw['interface'] - self.scope = raw.get('scope') - - -class StorageMeta: - """Object containing metadata about a storage definition.""" - - def __init__(self, name, raw): - self.storage_name = name - self.type = raw['type'] - self.description = raw.get('description', '') - self.shared = raw.get('shared', False) - self.read_only = raw.get('read-only', False) - self.minimum_size = raw.get('minimum-size') - self.location = raw.get('location') - self.multiple_range = None - if 'multiple' in raw: - range = raw['multiple']['range'] - if '-' not in range: - self.multiple_range = (int(range), int(range)) - else: - range = range.split('-') - self.multiple_range = (int(range[0]), int(range[1]) if range[1] else None) - - -class ResourceMeta: - """Object containing metadata about a resource definition.""" - - def __init__(self, name, raw): - self.resource_name = name - self.type = raw['type'] - self.filename = raw.get('filename', None) - self.description = raw.get('description', '') - - -class PayloadMeta: - """Object containing metadata about a payload definition.""" - - def __init__(self, name, raw): - self.payload_name = name - self.type = raw['type'] - - -class ActionMeta: - - def __init__(self, name, raw=None): - raw = raw or {} - self.name = name - self.title = raw.get('title', '') - self.description = raw.get('description', '') - self.parameters = raw.get('params', {}) # {: } - self.required = raw.get('required', []) # [, ...] diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/framework.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/framework.py deleted file mode 100755 index d95eb61fae1c8a0207687ab759f15ab6399ca4db..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/framework.py +++ /dev/null @@ -1,941 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 inspect -import pickle -import marshal -import types -import sqlite3 -import collections -import collections.abc -import keyword -import weakref -from datetime import timedelta - - -class Handle: - """Handle defines a name for an object in the form of a hierarchical path. - - The provided parent is the object (or that object's handle) that this handle - sits under, or None if the object identified by this handle stands by itself - as the root of its own hierarchy. - - The handle kind is a string that defines a namespace so objects with the - same parent and kind will have unique keys. - - The handle key is a string uniquely identifying the object. No other objects - under the same parent and kind may have the same key. - """ - - def __init__(self, parent, kind, key): - if parent and not isinstance(parent, Handle): - parent = parent.handle - self._parent = parent - self._kind = kind - self._key = key - if parent: - if key: - self._path = f"{parent}/{kind}[{key}]" - else: - self._path = f"{parent}/{kind}" - else: - if key: - self._path = f"{kind}[{key}]" - else: - self._path = f"{kind}" - - def nest(self, kind, key): - return Handle(self, kind, key) - - def __hash__(self): - return hash((self.parent, self.kind, self.key)) - - def __eq__(self, other): - return (self.parent, self.kind, self.key) == (other.parent, other.kind, other.key) - - def __str__(self): - return self.path - - @property - def parent(self): - return self._parent - - @property - def kind(self): - return self._kind - - @property - def key(self): - return self._key - - @property - def path(self): - return self._path - - @classmethod - def from_path(cls, path): - handle = None - for pair in path.split("/"): - pair = pair.split("[") - good = False - if len(pair) == 1: - kind, key = pair[0], None - good = True - elif len(pair) == 2: - kind, key = pair - if key and key[-1] == ']': - key = key[:-1] - good = True - if not good: - raise RuntimeError("attempted to restore invalid handle path {path}") - handle = Handle(handle, kind, key) - return handle - - -class EventBase: - - def __init__(self, handle): - self.handle = handle - self.deferred = False - - def defer(self): - self.deferred = True - - def snapshot(self): - """Return the snapshot data that should be persisted. - - Subclasses must override to save any custom state. - """ - return None - - def restore(self, snapshot): - """Restore the value state from the given snapshot. - - Subclasses must override to restore their custom state. - """ - self.deferred = False - - -class EventSource: - """EventSource wraps an event type with a descriptor to facilitate observing and emitting. - - It is generally used as: - - class SomethingHappened(EventBase): - pass - - class SomeObject(Object): - something_happened = EventSource(SomethingHappened) - - With that, instances of that type will offer the someobj.something_happened - attribute which is a BoundEvent and may be used to emit and observe the event. - """ - - def __init__(self, event_type): - if not isinstance(event_type, type) or not issubclass(event_type, EventBase): - raise RuntimeError(f"Event requires a subclass of EventBase as an argument, got {event_type}") - self.event_type = event_type - self.event_kind = None - self.emitter_type = None - - def __set_name__(self, emitter_type, event_kind): - if self.event_kind is not None: - raise RuntimeError( - f'EventSource({self.event_type.__name__}) reused as ' - f'{self.emitter_type.__name__}.{self.event_kind} and ' - f'{emitter_type.__name__}.{event_kind}') - self.event_kind = event_kind - self.emitter_type = emitter_type - - def __get__(self, emitter, emitter_type=None): - if emitter is None: - return self - # Framework might not be available if accessed as CharmClass.on.event rather than charm_instance.on.event, - # but in that case it couldn't be emitted anyway, so there's no point to registering it. - framework = getattr(emitter, 'framework', None) - if framework is not None: - framework.register_type(self.event_type, emitter, self.event_kind) - return BoundEvent(emitter, self.event_type, self.event_kind) - - -class BoundEvent: - - def __repr__(self): - return (f'') - - def __init__(self, emitter, event_type, event_kind): - self.emitter = emitter - self.event_type = event_type - self.event_kind = event_kind - - def emit(self, *args, **kwargs): - """Emit event to all registered observers. - - The current storage state is committed before and after each observer is notified. - """ - framework = self.emitter.framework - key = framework._next_event_key() - event = self.event_type(Handle(self.emitter, self.event_kind, key), *args, **kwargs) - framework._emit(event) - - -class HandleKind: - """Helper descriptor to define the Object.handle_kind field. - - The handle_kind for an object defaults to its type name, but it may - be explicitly overridden if desired. - """ - - def __get__(self, obj, obj_type): - kind = obj_type.__dict__.get("handle_kind") - if kind: - return kind - return obj_type.__name__ - - -class Object: - - handle_kind = HandleKind() - - def __init__(self, parent, key): - kind = self.handle_kind - if isinstance(parent, Framework): - self.framework = parent - # Avoid Framework instances having a circular reference to themselves. - if self.framework is self: - self.framework = weakref.proxy(self.framework) - self.handle = Handle(None, kind, key) - else: - self.framework = parent.framework - self.handle = Handle(parent, kind, key) - self.framework._track(self) - - # TODO Detect conflicting handles here. - - @property - def model(self): - return self.framework.model - - @property - def meta(self): - return self.framework.meta - - @property - def charm_dir(self): - return self.framework.charm_dir - - -class EventsBase(Object): - """Convenience type to allow defining .on attributes at class level.""" - - handle_kind = "on" - - def __init__(self, parent=None, key=None): - if parent is not None: - super().__init__(parent, key) - else: - self._cache = weakref.WeakKeyDictionary() - - def __get__(self, emitter, emitter_type): - if emitter is None: - return self - instance = self._cache.get(emitter) - if instance is None: - # Same type, different instance, more data. Doing this unusual construct - # means people can subclass just this one class to have their own 'on'. - instance = self._cache[emitter] = type(self)(emitter) - return instance - - @classmethod - def define_event(cls, event_kind, event_type): - """Define an event on this type at runtime. - - cls -- a type to define an event on. - event_kind -- an attribute name that will be used to access the event. Must be a valid python identifier, not be a keyword or an existing attribute. - event_type -- a type of the event to define. - """ - if not event_kind.isidentifier(): - raise RuntimeError(f'unable to define an event with event_kind that is not a valid python identifier: {event_kind}') - elif keyword.iskeyword(event_kind): - raise RuntimeError(f'unable to define an event with event_kind that is a python keyword: {event_kind}') - try: - getattr(cls, event_kind) - raise RuntimeError(f'unable to define an event with event_kind that overlaps with an existing type {cls} attribute: {event_kind}') - except AttributeError: - pass - - event_descriptor = EventSource(event_type) - event_descriptor.__set_name__(cls, event_kind) - setattr(cls, event_kind, event_descriptor) - - def events(self): - """Return a mapping of event_kinds to bound_events for all available events. - """ - events_map = {} - # We have to iterate over the class rather than instance to allow for properties which - # might call this method (e.g., event views), leading to infinite recursion. - for attr_name, attr_value in inspect.getmembers(type(self)): - if isinstance(attr_value, EventSource): - # We actually care about the bound_event, however, since it - # provides the most info for users of this method. - event_kind = attr_name - bound_event = getattr(self, event_kind) - events_map[event_kind] = bound_event - return events_map - - def __getitem__(self, key): - return PrefixedEvents(self, key) - - -class PrefixedEvents: - - def __init__(self, emitter, key): - self._emitter = emitter - self._prefix = key.replace("-", "_") + '_' - - def __getattr__(self, name): - return getattr(self._emitter, self._prefix + name) - - -class PreCommitEvent(EventBase): - pass - - -class CommitEvent(EventBase): - pass - - -class FrameworkEvents(EventsBase): - pre_commit = EventSource(PreCommitEvent) - commit = EventSource(CommitEvent) - - -class NoSnapshotError(Exception): - - def __init__(self, handle_path): - self.handle_path = handle_path - - def __str__(self): - return f'no snapshot data found for {self.handle_path} object' - - -class NoTypeError(Exception): - - def __init__(self, handle_path): - self.handle_path = handle_path - - def __str__(self): - return f"cannot restore {self.handle_path} since no class was registered for it" - - -class SQLiteStorage: - - DB_LOCK_TIMEOUT = timedelta(hours=1) - - def __init__(self, filename): - # The isolation_level argument is set to None such that the implicit transaction management behavior of the sqlite3 module is disabled. - self._db = sqlite3.connect(str(filename), isolation_level=None, timeout=self.DB_LOCK_TIMEOUT.total_seconds()) - self._setup() - - def _setup(self): - # Make sure that the database is locked until the connection is closed, not until the transaction ends. - self._db.execute("PRAGMA locking_mode=EXCLUSIVE") - c = self._db.execute("BEGIN") - c.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='snapshot'") - if c.fetchone()[0] == 0: - # Keep in mind what might happen if the process dies somewhere below. - # The system must not be rendered permanently broken by that. - self._db.execute("CREATE TABLE snapshot (handle TEXT PRIMARY KEY, data BLOB)") - self._db.execute("CREATE TABLE notice (sequence INTEGER PRIMARY KEY AUTOINCREMENT, event_path TEXT, observer_path TEXT, method_name TEXT)") - self._db.commit() - - def close(self): - self._db.close() - - def commit(self): - self._db.commit() - - # There's commit but no rollback. For abort to be supported, we'll need logic that - # can rollback decisions made by third-party code in terms of the internal state - # of objects that have been snapshotted, and hooks to let them know about it and - # take the needed actions to undo their logic until the last snapshot. - # This is doable but will increase significantly the chances for mistakes. - - def save_snapshot(self, handle_path, snapshot_data): - self._db.execute("REPLACE INTO snapshot VALUES (?, ?)", (handle_path, snapshot_data)) - - def load_snapshot(self, handle_path): - c = self._db.cursor() - c.execute("SELECT data FROM snapshot WHERE handle=?", (handle_path,)) - row = c.fetchone() - if row: - return row[0] - return None - - def drop_snapshot(self, handle_path): - self._db.execute("DELETE FROM snapshot WHERE handle=?", (handle_path,)) - - def save_notice(self, event_path, observer_path, method_name): - self._db.execute("INSERT INTO notice VALUES (NULL, ?, ?, ?)", (event_path, observer_path, method_name)) - - def drop_notice(self, event_path, observer_path, method_name): - self._db.execute("DELETE FROM notice WHERE event_path=? AND observer_path=? AND method_name=?", (event_path, observer_path, method_name)) - - def notices(self, event_path): - if event_path: - c = self._db.execute("SELECT event_path, observer_path, method_name FROM notice WHERE event_path=? ORDER BY sequence", (event_path,)) - else: - c = self._db.execute("SELECT event_path, observer_path, method_name FROM notice ORDER BY sequence") - while True: - rows = c.fetchmany() - if not rows: - break - for row in rows: - yield tuple(row) - - -class Framework(Object): - - on = FrameworkEvents() - - # Override properties from Object so that we can set them in __init__. - model = None - meta = None - charm_dir = None - - def __init__(self, data_path, charm_dir, meta, model): - - super().__init__(self, None) - - self._data_path = data_path - self.charm_dir = charm_dir - self.meta = meta - self.model = model - self._observers = [] # [(observer_path, method_name, parent_path, event_key)] - self._observer = weakref.WeakValueDictionary() # {observer_path: observer} - self._objects = weakref.WeakValueDictionary() - self._type_registry = {} # {(parent_path, kind): cls} - self._type_known = set() # {cls} - - self._storage = SQLiteStorage(data_path) - - # We can't use the higher-level StoredState because it relies on events. - self.register_type(StoredStateData, None, StoredStateData.handle_kind) - stored_handle = Handle(None, StoredStateData.handle_kind, '_stored') - try: - self._stored = self.load_snapshot(stored_handle) - except NoSnapshotError: - self._stored = StoredStateData(self, '_stored') - self._stored['event_count'] = 0 - - def close(self): - self._storage.close() - - def _track(self, obj): - """Track object and ensure it is the only object created using its handle path.""" - if obj is self: - # Framework objects don't track themselves - return - if obj.handle.path in self.framework._objects: - raise RuntimeError(f"two objects claiming to be {obj.handle.path} have been created") - self._objects[obj.handle.path] = obj - - def _forget(self, obj): - """Stop tracking the given object. See also _track.""" - self._objects.pop(obj.handle.path, None) - - def commit(self): - # Give a chance for objects to persist data they want to before a commit is made. - self.on.pre_commit.emit() - # Make sure snapshots are saved by instances of StoredStateData. Any possible state - # modifications in on_commit handlers of instances of other classes will not be persisted. - self.on.commit.emit() - # Save our event count after all events have been emitted. - self.save_snapshot(self._stored) - self._storage.commit() - - def register_type(self, cls, parent, kind=None): - if parent and not isinstance(parent, Handle): - parent = parent.handle - if parent: - parent_path = parent.path - else: - parent_path = None - if not kind: - kind = cls.handle_kind - self._type_registry[(parent_path, kind)] = cls - self._type_known.add(cls) - - def save_snapshot(self, value): - """Save a persistent snapshot of the provided value. - - The provided value must implement the following interface: - - value.handle = Handle(...) - value.snapshot() => {...} # Simple builtin types only. - value.restore(snapshot) # Restore custom state from prior snapshot. - """ - if type(value) not in self._type_known: - raise RuntimeError(f"cannot save {type(value).__name__} values before registering that type") - data = value.snapshot() - # Use marshal as a validator, enforcing the use of simple types. - marshal.dumps(data) - # Use pickle for serialization, so the value remains portable. - raw_data = pickle.dumps(data) - self._storage.save_snapshot(value.handle.path, raw_data) - - def load_snapshot(self, handle): - parent_path = None - if handle.parent: - parent_path = handle.parent.path - cls = self._type_registry.get((parent_path, handle.kind)) - if not cls: - raise NoTypeError(handle.path) - raw_data = self._storage.load_snapshot(handle.path) - if not raw_data: - raise NoSnapshotError(handle.path) - data = pickle.loads(raw_data) - obj = cls.__new__(cls) - obj.framework = self - obj.handle = handle - obj.restore(data) - self._track(obj) - return obj - - def drop_snapshot(self, handle): - self._storage.drop_snapshot(handle.path) - - def observe(self, bound_event, observer): - """Register observer to be called when bound_event is emitted. - - The bound_event is generally provided as an attribute of the object that emits - the event, and is created in this style: - - class SomeObject: - something_happened = Event(SomethingHappened) - - That event may be observed as: - - framework.observe(someobj.something_happened, self.on_something_happened) - - If the method to be called follows the name convention "on_", it - may be omitted from the observe call. That means the above is equivalent to: - - framework.observe(someobj.something_happened, self) - - """ - if not isinstance(bound_event, BoundEvent): - raise RuntimeError(f'Framework.observe requires a BoundEvent as second parameter, got {bound_event}') - - event_type = bound_event.event_type - event_kind = bound_event.event_kind - emitter = bound_event.emitter - - self.register_type(event_type, emitter, event_kind) - - if hasattr(emitter, "handle"): - emitter_path = emitter.handle.path - else: - raise RuntimeError(f'event emitter {type(emitter).__name__} must have a "handle" attribute') - - method_name = None - if isinstance(observer, types.MethodType): - method_name = observer.__name__ - observer = observer.__self__ - else: - method_name = "on_" + event_kind - if not hasattr(observer, method_name): - raise RuntimeError(f'Observer method not provided explicitly and {type(observer).__name__} type has no "{method_name}" method') - - # Validate that the method has an acceptable call signature. - sig = inspect.signature(getattr(observer, method_name)) - # Self isn't included in the params list, so the first arg will be the event. - extra_params = list(sig.parameters.values())[1:] - if not sig.parameters: - raise TypeError(f'{type(observer).__name__}.{method_name} must accept event parameter') - elif any(param.default is inspect.Parameter.empty for param in extra_params): - # Allow for additional optional params, since there's no reason to exclude them, but - # required params will break. - raise TypeError(f'{type(observer).__name__}.{method_name} has extra required parameter') - - # TODO Prevent the exact same parameters from being registered more than once. - - self._observer[observer.handle.path] = observer - self._observers.append((observer.handle.path, method_name, emitter_path, event_kind)) - - def _next_event_key(self): - """Return the next event key that should be used, incrementing the internal counter.""" - # Increment the count first; this means the keys will start at 1, and 0 means no events have been emitted. - self._stored['event_count'] += 1 - return str(self._stored['event_count']) - - def _emit(self, event): - """See BoundEvent.emit for the public way to call this.""" - - # Save the event for all known observers before the first notification - # takes place, so that either everyone interested sees it, or nobody does. - self.save_snapshot(event) - event_path = event.handle.path - event_kind = event.handle.kind - parent_path = event.handle.parent.path - # TODO Track observers by (parent_path, event_kind) rather than as a list of all observers. Avoiding linear search through all observers for every event - for observer_path, method_name, _parent_path, _event_kind in self._observers: - if _parent_path != parent_path: - continue - if _event_kind and _event_kind != event_kind: - continue - # Again, only commit this after all notices are saved. - self._storage.save_notice(event_path, observer_path, method_name) - self._reemit(event_path) - - def reemit(self): - """Reemit previously deferred events to the observers that deferred them. - - Only the specific observers that have previously deferred the event will be - notified again. Observers that asked to be notified about events after it's - been first emitted won't be notified, as that would mean potentially observing - events out of order. - """ - self._reemit() - - def _reemit(self, single_event_path=None): - last_event_path = None - deferred = True - for event_path, observer_path, method_name in self._storage.notices(single_event_path): - event_handle = Handle.from_path(event_path) - - if last_event_path != event_path: - if not deferred: - self._storage.drop_snapshot(last_event_path) - last_event_path = event_path - deferred = False - - try: - event = self.load_snapshot(event_handle) - except NoTypeError: - self._storage.drop_notice(event_path, observer_path, method_name) - continue - - event.deferred = False - observer = self._observer.get(observer_path) - if observer: - custom_handler = getattr(observer, method_name, None) - if custom_handler: - custom_handler(event) - - if event.deferred: - deferred = True - else: - self._storage.drop_notice(event_path, observer_path, method_name) - # We intentionally consider this event to be dead and reload it from scratch in the next path. - self.framework._forget(event) - - if not deferred: - self._storage.drop_snapshot(last_event_path) - - -class StoredStateChanged(EventBase): - pass - - -class StoredStateEvents(EventsBase): - changed = EventSource(StoredStateChanged) - - -class StoredStateData(Object): - - on = StoredStateEvents() - - def __init__(self, parent, attr_name): - super().__init__(parent, attr_name) - self._cache = {} - self.dirty = False - - def __getitem__(self, key): - return self._cache.get(key) - - def __setitem__(self, key, value): - self._cache[key] = value - self.dirty = True - - def __contains__(self, key): - return key in self._cache - - def snapshot(self): - return self._cache - - def restore(self, snapshot): - self._cache = snapshot - self.dirty = False - - def on_commit(self, event): - if self.dirty: - self.framework.save_snapshot(self) - self.dirty = False - - -class BoundStoredState: - - def __init__(self, parent, attr_name): - parent.framework.register_type(StoredStateData, parent) - - handle = Handle(parent, StoredStateData.handle_kind, attr_name) - try: - data = parent.framework.load_snapshot(handle) - except NoSnapshotError: - data = StoredStateData(parent, attr_name) - - # __dict__ is used to avoid infinite recursion. - self.__dict__["_data"] = data - self.__dict__["_attr_name"] = attr_name - - parent.framework.observe(parent.framework.on.commit, self._data) - - def __getattr__(self, key): - # "on" is the only reserved key that can't be used in the data map. - if key == "on": - return self._data.on - if key not in self._data: - raise AttributeError(f"attribute '{key}' is not stored") - return _wrap_stored(self._data, self._data[key]) - - def __setattr__(self, key, value): - if key == "on": - raise AttributeError(f"attribute 'on' is reserved and cannot be set") - - value = _unwrap_stored(self._data, value) - - if not isinstance(value, (type(None), int, str, bytes, list, dict, set)): - raise AttributeError(f"attribute '{key}' cannot be set to {type(value).__name__}: must be int/dict/list/etc") - - self._data[key] = _unwrap_stored(self._data, value) - self.on.changed.emit() - - def set_default(self, **kwargs): - """"Set the value of any given key if it has not already been set""" - for k, v in kwargs.items(): - if k not in self._data: - self._data[k] = v - - -class StoredState: - - def __init__(self): - self.parent_type = None - self.attr_name = None - - def __get__(self, parent, parent_type=None): - if self.parent_type is None: - self.parent_type = parent_type - elif self.parent_type is not parent_type: - raise RuntimeError("StoredState shared by {} and {}".format(self.parent_type.__name__, parent_type.__name__)) - - if parent is None: - return self - - bound = parent.__dict__.get(self.attr_name) - if bound is None: - for attr_name, attr_value in parent_type.__dict__.items(): - if attr_value is self: - if self.attr_name and attr_name != self.attr_name: - parent_tname = parent_type.__name__ - raise RuntimeError(f"StoredState shared by {parent_tname}.{self.attr_name} and {parent_tname}.{attr_name}") - self.attr_name = attr_name - bound = BoundStoredState(parent, attr_name) - parent.__dict__[attr_name] = bound - break - else: - raise RuntimeError("cannot find StoredVariable attribute in type {}".format(parent_type.__name__)) - - return bound - - -def _wrap_stored(parent_data, value): - t = type(value) - if t is dict: - return StoredDict(parent_data, value) - if t is list: - return StoredList(parent_data, value) - if t is set: - return StoredSet(parent_data, value) - return value - - -def _unwrap_stored(parent_data, value): - t = type(value) - if t is StoredDict or t is StoredList or t is StoredSet: - return value._under - return value - - -class StoredDict(collections.abc.MutableMapping): - - def __init__(self, stored_data, under): - self._stored_data = stored_data - self._under = under - - def __getitem__(self, key): - return _wrap_stored(self._stored_data, self._under[key]) - - def __setitem__(self, key, value): - self._under[key] = _unwrap_stored(self._stored_data, value) - self._stored_data.dirty = True - - def __delitem__(self, key): - del self._under[key] - self._stored_data.dirty = True - - def __iter__(self): - return self._under.__iter__() - - def __len__(self): - return len(self._under) - - def __eq__(self, other): - if isinstance(other, StoredDict): - return self._under == other._under - elif isinstance(other, collections.abc.Mapping): - return self._under == other - else: - return NotImplemented - - -class StoredList(collections.abc.MutableSequence): - - def __init__(self, stored_data, under): - self._stored_data = stored_data - self._under = under - - def __getitem__(self, index): - return _wrap_stored(self._stored_data, self._under[index]) - - def __setitem__(self, index, value): - self._under[index] = _unwrap_stored(self._stored_data, value) - self._stored_data.dirty = True - - def __delitem__(self, index): - del self._under[index] - self._stored_data.dirty = True - - def __len__(self): - return len(self._under) - - def insert(self, index, value): - self._under.insert(index, value) - self._stored_data.dirty = True - - def append(self, value): - self._under.append(value) - self._stored_data.dirty = True - - def __eq__(self, other): - if isinstance(other, StoredList): - return self._under == other._under - elif isinstance(other, collections.abc.Sequence): - return self._under == other - else: - return NotImplemented - - def __lt__(self, other): - if isinstance(other, StoredList): - return self._under < other._under - elif isinstance(other, collections.abc.Sequence): - return self._under < other - else: - return NotImplemented - - def __le__(self, other): - if isinstance(other, StoredList): - return self._under <= other._under - elif isinstance(other, collections.abc.Sequence): - return self._under <= other - else: - return NotImplemented - - def __gt__(self, other): - if isinstance(other, StoredList): - return self._under > other._under - elif isinstance(other, collections.abc.Sequence): - return self._under > other - else: - return NotImplemented - - def __ge__(self, other): - if isinstance(other, StoredList): - return self._under >= other._under - elif isinstance(other, collections.abc.Sequence): - return self._under >= other - else: - return NotImplemented - - -class StoredSet(collections.abc.MutableSet): - - def __init__(self, stored_data, under): - self._stored_data = stored_data - self._under = under - - def add(self, key): - self._under.add(key) - self._stored_data.dirty = True - - def discard(self, key): - self._under.discard(key) - self._stored_data.dirty = True - - def __contains__(self, key): - return key in self._under - - def __iter__(self): - return self._under.__iter__() - - def __len__(self): - return len(self._under) - - @classmethod - def _from_iterable(cls, it): - """Construct an instance of the class from any iterable input. - - Per https://docs.python.org/3/library/collections.abc.html - if the Set mixin is being used in a class with a different constructor signature, - you will need to override _from_iterable() with a classmethod that can construct - new instances from an iterable argument. - """ - return set(it) - - def __le__(self, other): - if isinstance(other, StoredSet): - return self._under <= other._under - elif isinstance(other, collections.abc.Set): - return self._under <= other - else: - return NotImplemented - - def __ge__(self, other): - if isinstance(other, StoredSet): - return self._under >= other._under - elif isinstance(other, collections.abc.Set): - return self._under >= other - else: - return NotImplemented - - def __eq__(self, other): - if isinstance(other, StoredSet): - return self._under == other._under - elif isinstance(other, collections.abc.Set): - return self._under == other - else: - return NotImplemented diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/jujuversion.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/jujuversion.py deleted file mode 100755 index 5256f24ff8fb37a2cbe6162d824e77fb7dca1f45..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/jujuversion.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2020 Canonical Ltd. -# -# 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 re -from functools import total_ordering - - -@total_ordering -class JujuVersion: - - PATTERN = r'^(?P\d{1,9})\.(?P\d{1,9})((?:\.|-(?P[a-z]+))(?P\d{1,9}))?(\.(?P\d{1,9}))?$' - - def __init__(self, version): - m = re.match(self.PATTERN, version) - if not m: - raise RuntimeError(f'"{version}" is not a valid Juju version string') - - d = m.groupdict() - self.major = int(m.group('major')) - self.minor = int(m.group('minor')) - self.tag = d['tag'] or '' - self.patch = int(d['patch'] or 0) - self.build = int(d['build'] or 0) - - def __repr__(self): - if self.tag: - s = f'{self.major}.{self.minor}-{self.tag}{self.patch}' - else: - s = f'{self.major}.{self.minor}.{self.patch}' - if self.build > 0: - s += f'.{self.build}' - return s - - def __eq__(self, other): - if self is other: - return True - if isinstance(other, str): - other = type(self)(other) - elif not isinstance(other, JujuVersion): - raise RuntimeError(f'cannot compare Juju version "{self}" with "{other}"') - return self.major == other.major and self.minor == other.minor\ - and self.tag == other.tag and self.build == other.build and self.patch == other.patch - - def __lt__(self, other): - if self is other: - return False - if isinstance(other, str): - other = type(self)(other) - elif not isinstance(other, JujuVersion): - raise RuntimeError(f'cannot compare Juju version "{self}" with "{other}"') - - if self.major != other.major: - return self.major < other.major - elif self.minor != other.minor: - return self.minor < other.minor - elif self.tag != other.tag: - if not self.tag: - return False - elif not other.tag: - return True - return self.tag < other.tag - elif self.patch != other.patch: - return self.patch < other.patch - elif self.build != other.build: - return self.build < other.build - return False diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/main.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/main.py deleted file mode 100755 index c8d5da2adf1b4f6ccdb3a0b1292dcab8d51c0e9b..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/main.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2019 Canonical Ltd. -# -# 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 os -import sys -from pathlib import Path - -import yaml - -import ops.charm -import ops.framework -import ops.model - -CHARM_STATE_FILE = '.unit-state.db' - - -def debugf(format, *args, **kwargs): - pass - - -def _get_charm_dir(): - charm_dir = os.environ.get("JUJU_CHARM_DIR") - if charm_dir is None: - # Assume $JUJU_CHARM_DIR/lib/op/main.py structure. - charm_dir = Path(f'{__file__}/../../..').resolve() - else: - charm_dir = Path(charm_dir).resolve() - return charm_dir - - -def _load_metadata(charm_dir): - metadata = yaml.safe_load((charm_dir / 'metadata.yaml').read_text()) - - actions_meta = charm_dir / 'actions.yaml' - if actions_meta.exists(): - actions_metadata = yaml.safe_load(actions_meta.read_text()) - else: - actions_metadata = {} - return metadata, actions_metadata - - -def _create_event_link(charm, bound_event): - """Create a symlink for a particular event. - - charm -- A charm object. - bound_event -- An event for which to create a symlink. - """ - if issubclass(bound_event.event_type, ops.charm.HookEvent): - event_dir = charm.framework.charm_dir / 'hooks' - event_path = event_dir / bound_event.event_kind.replace('_', '-') - elif issubclass(bound_event.event_type, ops.charm.ActionEvent): - if not bound_event.event_kind.endswith("_action"): - raise RuntimeError(f"action event name {bound_event.event_kind} needs _action suffix") - event_dir = charm.framework.charm_dir / 'actions' - # The event_kind is suffixed with "_action" while the executable is not. - event_path = event_dir / bound_event.event_kind[:-len('_action')].replace('_', '-') - else: - raise RuntimeError(f'cannot create a symlink: unsupported event type {bound_event.event_type}') - - event_dir.mkdir(exist_ok=True) - if not event_path.exists(): - # CPython has different implementations for populating sys.argv[0] for Linux and Windows. For Windows - # it is always an absolute path (any symlinks are resolved) while for Linux it can be a relative path. - target_path = os.path.relpath(os.path.realpath(sys.argv[0]), event_dir) - - # Ignore the non-symlink files or directories assuming the charm author knows what they are doing. - debugf(f'Creating a new relative symlink at {event_path} pointing to {target_path}') - event_path.symlink_to(target_path) - - -def _setup_event_links(charm_dir, charm): - """Set up links for supported events that originate from Juju. - - Whether a charm can handle an event or not can be determined by - introspecting which events are defined on it. - - Hooks or actions are created as symlinks to the charm code file which is determined by inspecting - symlinks provided by the charm author at hooks/install or hooks/start. - - charm_dir -- A root directory of the charm. - charm -- An instance of the Charm class. - """ - for bound_event in charm.on.events().values(): - # Only events that originate from Juju need symlinks. - if issubclass(bound_event.event_type, (ops.charm.HookEvent, ops.charm.ActionEvent)): - _create_event_link(charm, bound_event) - - -def _emit_charm_event(charm, event_name): - """Emits a charm event based on a Juju event name. - - charm -- A charm instance to emit an event from. - event_name -- A Juju event name to emit on a charm. - """ - event_to_emit = None - try: - event_to_emit = getattr(charm.on, event_name) - except AttributeError: - debugf(f"event {event_name} not defined for {charm}") - - # If the event is not supported by the charm implementation, do - # not error out or try to emit it. This is to support rollbacks. - if event_to_emit is not None: - args, kwargs = _get_event_args(charm, event_to_emit) - debugf(f'Emitting Juju event {event_name}') - event_to_emit.emit(*args, **kwargs) - - -def _get_event_args(charm, bound_event): - event_type = bound_event.event_type - model = charm.framework.model - - if issubclass(event_type, ops.charm.RelationEvent): - relation_name = os.environ['JUJU_RELATION'] - relation_id = int(os.environ['JUJU_RELATION_ID'].split(':')[-1]) - relation = model.get_relation(relation_name, relation_id) - else: - relation = None - - remote_app_name = os.environ.get('JUJU_REMOTE_APP', '') - remote_unit_name = os.environ.get('JUJU_REMOTE_UNIT', '') - if remote_app_name or remote_unit_name: - if not remote_app_name: - if '/' not in remote_unit_name: - raise RuntimeError(f'invalid remote unit name: {remote_unit_name}') - remote_app_name = remote_unit_name.split('/')[0] - args = [relation, model.get_app(remote_app_name)] - if remote_unit_name: - args.append(model.get_unit(remote_unit_name)) - return args, {} - elif relation: - return [relation], {} - return [], {} - - -def main(charm_class): - """Setup the charm and dispatch the observed event. - - The event name is based on the way this executable was called (argv[0]). - """ - - charm_dir = _get_charm_dir() - - # Process the Juju event relevant to the current hook execution - # JUJU_HOOK_NAME, JUJU_FUNCTION_NAME, and JUJU_ACTION_NAME are not used - # in order to support simulation of events from debugging sessions. - # TODO: For Windows, when symlinks are used, this is not a valid method of getting an event name (see LP: #1854505). - juju_exec_path = Path(sys.argv[0]) - juju_event_name = juju_exec_path.name.replace('-', '_') - if juju_exec_path.parent.name == 'actions': - juju_event_name = f'{juju_event_name}_action' - - metadata, actions_metadata = _load_metadata(charm_dir) - meta = ops.charm.CharmMeta(metadata, actions_metadata) - unit_name = os.environ['JUJU_UNIT_NAME'] - model = ops.model.Model(unit_name, meta, ops.model.ModelBackend()) - - # TODO: If Juju unit agent crashes after exit(0) from the charm code - # the framework will commit the snapshot but Juju will not commit its - # operation. - charm_state_path = charm_dir / CHARM_STATE_FILE - framework = ops.framework.Framework(charm_state_path, charm_dir, meta, model) - try: - charm = charm_class(framework, None) - - # When a charm is force-upgraded and a unit is in an error state Juju does not run upgrade-charm and - # instead runs the failed hook followed by config-changed. Given the nature of force-upgrading - # the hook setup code is not triggered on config-changed. - # 'start' event is included as Juju does not fire the install event for K8s charms (see LP: #1854635). - if juju_event_name in ('install', 'start', 'upgrade_charm') or juju_event_name.endswith('_storage_attached'): - _setup_event_links(charm_dir, charm) - - framework.reemit() - - _emit_charm_event(charm, juju_event_name) - - framework.commit() - finally: - framework.close() diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/model.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/model.py deleted file mode 100644 index a12dcca2b1a85b3ed4a0983b88073f9f8c7bcb42..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/ops/model.py +++ /dev/null @@ -1,679 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 json -import weakref -import os -import shutil -import tempfile -import time -import datetime - -from abc import ABC, abstractmethod -from collections.abc import Mapping, MutableMapping -from pathlib import Path -from subprocess import run, PIPE, CalledProcessError - - -class Model: - - def __init__(self, unit_name, meta, backend): - self._cache = ModelCache(backend) - self._backend = backend - self.unit = self.get_unit(unit_name) - self.app = self.unit.app - self.relations = RelationMapping(meta.relations, self.unit, self._backend, self._cache) - self.config = ConfigData(self._backend) - self.resources = Resources(list(meta.resources), self._backend) - self.pod = Pod(self._backend) - self.storages = StorageMapping(list(meta.storages), self._backend) - - def get_unit(self, unit_name): - return self._cache.get(Unit, unit_name) - - def get_app(self, app_name): - return self._cache.get(Application, app_name) - - def get_relation(self, relation_name, relation_id=None): - """Get a specific Relation instance. - - If relation_id is given, this will return that Relation instance. - - If relation_id is not given, this will return the Relation instance if the - relation is established only once or None if it is not established. If this - same relation is established multiple times the error TooManyRelatedAppsError is raised. - """ - return self.relations._get_unique(relation_name, relation_id) - - -class ModelCache: - - def __init__(self, backend): - self._backend = backend - self._weakrefs = weakref.WeakValueDictionary() - - def get(self, entity_type, *args): - key = (entity_type,) + args - entity = self._weakrefs.get(key) - if entity is None: - entity = entity_type(*args, backend=self._backend, cache=self) - self._weakrefs[key] = entity - return entity - - -class Application: - - def __init__(self, name, backend, cache): - self.name = name - self._backend = backend - self._cache = cache - self._is_our_app = self.name == self._backend.app_name - self._status = None - - @property - def status(self): - if not self._is_our_app: - return UnknownStatus() - - if not self._backend.is_leader(): - raise RuntimeError('cannot get application status as a non-leader unit') - - if self._status: - return self._status - - s = self._backend.status_get(is_app=True) - self._status = StatusBase.from_name(s['status'], s['message']) - return self._status - - @status.setter - def status(self, value): - if not isinstance(value, StatusBase): - raise InvalidStatusError(f'invalid value provided for application {self} status: {value}') - - if not self._is_our_app: - raise RuntimeError(f'cannot to set status for a remote application {self}') - - if not self._backend.is_leader(): - raise RuntimeError('cannot set application status as a non-leader unit') - - self._backend.status_set(value.name, value.message, is_app=True) - self._status = value - - def __repr__(self): - return f'<{type(self).__module__}.{type(self).__name__} {self.name}>' - - -class Unit: - - def __init__(self, name, backend, cache): - self.name = name - - app_name = name.split('/')[0] - self.app = cache.get(Application, app_name) - - self._backend = backend - self._cache = cache - self._is_our_unit = self.name == self._backend.unit_name - self._status = None - - @property - def status(self): - if not self._is_our_unit: - return UnknownStatus() - - if self._status: - return self._status - - s = self._backend.status_get(is_app=False) - self._status = StatusBase.from_name(s['status'], s['message']) - return self._status - - @status.setter - def status(self, value): - if not isinstance(value, StatusBase): - raise InvalidStatusError(f'invalid value provided for unit {self} status: {value}') - - if not self._is_our_unit: - raise RuntimeError(f'cannot set status for a remote unit {self}') - - self._backend.status_set(value.name, value.message, is_app=False) - self._status = value - - def __repr__(self): - return f'<{type(self).__module__}.{type(self).__name__} {self.name}>' - - def is_leader(self): - if self._is_our_unit: - # This value is not cached as it is not guaranteed to persist for the whole duration - # of a hook execution. - return self._backend.is_leader() - else: - raise RuntimeError(f"cannot determine leadership status for remote applications: {self}") - - -class LazyMapping(Mapping, ABC): - - _lazy_data = None - - @abstractmethod - def _load(self): - raise NotImplementedError() - - @property - def _data(self): - data = self._lazy_data - if data is None: - data = self._lazy_data = self._load() - return data - - def __contains__(self, key): - return key in self._data - - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(self._data) - - def __getitem__(self, key): - return self._data[key] - - -class RelationMapping(Mapping): - """Map of relation names to lists of Relation instances.""" - - def __init__(self, relations_meta, our_unit, backend, cache): - self._peers = set() - for name, relation_meta in relations_meta.items(): - if relation_meta.role == 'peers': - self._peers.add(name) - self._our_unit = our_unit - self._backend = backend - self._cache = cache - self._data = {relation_name: None for relation_name in relations_meta} - - def __contains__(self, key): - return key in self._data - - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(self._data) - - def __getitem__(self, relation_name): - is_peer = relation_name in self._peers - relation_list = self._data[relation_name] - if relation_list is None: - relation_list = self._data[relation_name] = [] - for rid in self._backend.relation_ids(relation_name): - relation = Relation(relation_name, rid, is_peer, self._our_unit, self._backend, self._cache) - relation_list.append(relation) - return relation_list - - def _get_unique(self, relation_name, relation_id=None): - if relation_id is not None: - if not isinstance(relation_id, int): - raise ModelError(f'relation name {relation_id} must be int or None not {type(relation_id).__name__}') - for relation in self[relation_name]: - if relation.id == relation_id: - return relation - else: - # The relation may be dead, but it is not forgotten. - is_peer = relation_name in self._peers - return Relation(relation_name, relation_id, is_peer, self._our_unit, self._backend, self._cache) - num_related = len(self[relation_name]) - if num_related == 0: - return None - elif num_related == 1: - return self[relation_name][0] - else: - # TODO: We need something in the framework to catch and gracefully handle - # errors, ideally integrating the error catching with Juju's mechanisms. - raise TooManyRelatedAppsError(relation_name, num_related, 1) - - -class Relation: - def __init__(self, relation_name, relation_id, is_peer, our_unit, backend, cache): - self.name = relation_name - self.id = relation_id - self.app = None - self.units = set() - - # For peer relations, both the remote and the local app are the same. - if is_peer: - self.app = our_unit.app - try: - for unit_name in backend.relation_list(self.id): - unit = cache.get(Unit, unit_name) - self.units.add(unit) - if self.app is None: - self.app = unit.app - except RelationNotFoundError: - # If the relation is dead, just treat it as if it has no remote units. - pass - self.data = RelationData(self, our_unit, backend) - - def __repr__(self): - return f'<{type(self).__module__}.{type(self).__name__} {self.name}:{self.id}>' - - -class RelationData(Mapping): - def __init__(self, relation, our_unit, backend): - self.relation = weakref.proxy(relation) - self._data = {our_unit: RelationDataContent(self.relation, our_unit, backend)} - self._data.update({our_unit.app: RelationDataContent(self.relation, our_unit.app, backend)}) - self._data.update({unit: RelationDataContent(self.relation, unit, backend) for unit in self.relation.units}) - # The relation might be dead so avoid a None key here. - if self.relation.app: - self._data.update({self.relation.app: RelationDataContent(self.relation, self.relation.app, backend)}) - - def __contains__(self, key): - return key in self._data - - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(self._data) - - def __getitem__(self, key): - return self._data[key] - - -# We mix in MutableMapping here to get some convenience implementations, but whether it's actually -# mutable or not is controlled by the flag. -class RelationDataContent(LazyMapping, MutableMapping): - - def __init__(self, relation, entity, backend): - self.relation = relation - self._entity = entity - self._backend = backend - self._is_app = isinstance(entity, Application) - - def _load(self): - try: - return self._backend.relation_get(self.relation.id, self._entity.name, self._is_app) - except RelationNotFoundError: - # Dead relations tell no tales (and have no data). - return {} - - def _is_mutable(self): - if self._is_app: - is_our_app = self._backend.app_name == self._entity.name - if not is_our_app: - return False - # Whether the application data bag is mutable or not depends on whether this unit is a leader or not, - # but this is not guaranteed to be always true during the same hook execution. - return self._backend.is_leader() - else: - is_our_unit = self._backend.unit_name == self._entity.name - if is_our_unit: - return True - return False - - def __setitem__(self, key, value): - if not self._is_mutable(): - raise RelationDataError(f'cannot set relation data for {self._entity.name}') - if not isinstance(value, str): - raise RelationDataError('relation data values must be strings') - - self._backend.relation_set(self.relation.id, key, value, self._is_app) - - # Don't load data unnecessarily if we're only updating. - if self._lazy_data is not None: - if value == '': - # Match the behavior of Juju, which is that setting the value to an empty string will - # remove the key entirely from the relation data. - del self._data[key] - else: - self._data[key] = value - - def __delitem__(self, key): - # Match the behavior of Juju, which is that setting the value to an empty string will - # remove the key entirely from the relation data. - self.__setitem__(key, '') - - -class ConfigData(LazyMapping): - - def __init__(self, backend): - self._backend = backend - - def _load(self): - return self._backend.config_get() - - -class StatusBase: - """Status values specific to applications and units.""" - - _statuses = {} - - def __init__(self, message): - self.message = message - - def __new__(cls, *args, **kwargs): - if cls is StatusBase: - raise TypeError("cannot instantiate a base class") - cls._statuses[cls.name] = cls - return super().__new__(cls) - - @classmethod - def from_name(cls, name, message): - return cls._statuses[name](message) - - -class ActiveStatus(StatusBase): - """The unit is ready. - - The unit believes it is correctly offering all the services it has been asked to offer. - """ - name = 'active' - - def __init__(self, message=None): - super().__init__(message or '') - - -class BlockedStatus(StatusBase): - """The unit requires manual intervention. - - An operator has to manually intervene to unblock the unit and let it proceed. - """ - name = 'blocked' - - -class MaintenanceStatus(StatusBase): - """The unit is performing maintenance tasks. - - The unit is not yet providing services, but is actively doing work in preparation for providing those services. - This is a "spinning" state, not an error state. It reflects activity on the unit itself, not on peers or related units. - """ - name = 'maintenance' - - -class UnknownStatus(StatusBase): - """The unit status is unknown. - - A unit-agent has finished calling install, config-changed and start, but the charm has not called status-set yet. - """ - name = 'unknown' - - def __init__(self): - # Unknown status cannot be set and does not have a message associated with it. - super().__init__('') - - -class WaitingStatus(StatusBase): - """A unit is unable to progress. - - The unit is unable to progress to an active state because an application to which it is related is not running. - """ - name = 'waiting' - - -class Resources: - """Object representing resources for the charm. - """ - - def __init__(self, names, backend): - self._backend = backend - self._paths = {name: None for name in names} - - def fetch(self, name): - """Fetch the resource from the controller or store. - - If successfully fetched, this returns a Path object to where the resource is stored - on disk, otherwise it raises a ModelError. - """ - if name not in self._paths: - raise RuntimeError(f'invalid resource name: {name}') - if self._paths[name] is None: - self._paths[name] = Path(self._backend.resource_get(name)) - return self._paths[name] - - -class Pod: - def __init__(self, backend): - self._backend = backend - - def set_spec(self, spec, k8s_resources=None): - if not self._backend.is_leader(): - raise ModelError('cannot set a pod spec as this unit is not a leader') - self._backend.pod_spec_set(spec, k8s_resources) - - -class StorageMapping(Mapping): - """Map of storage names to lists of Storage instances.""" - - def __init__(self, storage_names, backend): - self._backend = backend - self._storage_map = {storage_name: None for storage_name in storage_names} - - def __contains__(self, key): - return key in self._storage_map - - def __len__(self): - return len(self._storage_map) - - def __iter__(self): - return iter(self._storage_map) - - def __getitem__(self, storage_name): - storage_list = self._storage_map[storage_name] - if storage_list is None: - storage_list = self._storage_map[storage_name] = [] - for storage_id in self._backend.storage_list(storage_name): - storage_list.append(Storage(storage_name, storage_id, self._backend)) - return storage_list - - def request(self, storage_name, count=1): - """Requests new storage instances of a given name. - - Uses storage-add tool to request additional storage. Juju will notify the unit - via -storage-attached events when it becomes available. - """ - if storage_name not in self._storage_map: - raise ModelError(f'cannot add storage with {storage_name} as it is not present in the charm metadata') - self._backend.storage_add(storage_name, count) - - -class Storage: - - def __init__(self, storage_name, storage_id, backend): - self.name = storage_name - self.id = storage_id - self._backend = backend - self._location = None - - @property - def location(self): - if self._location is None: - self._location = Path(self._backend.storage_get(f'{self.name}/{self.id}', "location")) - return self._location - - -class ModelError(Exception): - pass - - -class TooManyRelatedAppsError(ModelError): - def __init__(self, relation_name, num_related, max_supported): - super().__init__(f'Too many remote applications on {relation_name} ({num_related} > {max_supported})') - self.relation_name = relation_name - self.num_related = num_related - self.max_supported = max_supported - - -class RelationDataError(ModelError): - pass - - -class RelationNotFoundError(ModelError): - pass - - -class InvalidStatusError(ModelError): - pass - - -class ModelBackend: - - LEASE_RENEWAL_PERIOD = datetime.timedelta(seconds=30) - - def __init__(self): - self.unit_name = os.environ['JUJU_UNIT_NAME'] - self.app_name = self.unit_name.split('/')[0] - - self._is_leader = None - self._leader_check_time = 0 - - def _run(self, *args, return_output=False, use_json=False): - kwargs = dict(stdout=PIPE, stderr=PIPE) - if use_json: - args += ('--format=json',) - try: - result = run(args, check=True, **kwargs) - except CalledProcessError as e: - raise ModelError(e.stderr) - if return_output: - if result.stdout is None: - return '' - else: - text = result.stdout.decode('utf8') - if use_json: - return json.loads(text) - else: - return text - - def relation_ids(self, relation_name): - relation_ids = self._run('relation-ids', relation_name, return_output=True, use_json=True) - return [int(relation_id.split(':')[-1]) for relation_id in relation_ids] - - def relation_list(self, relation_id): - try: - return self._run('relation-list', '-r', str(relation_id), return_output=True, use_json=True) - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise - - def relation_get(self, relation_id, member_name, is_app): - if not isinstance(is_app, bool): - raise TypeError('is_app parameter to relation_get must be a boolean') - - try: - return self._run('relation-get', '-r', str(relation_id), '-', member_name, f'--app={is_app}', return_output=True, use_json=True) - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise - - def relation_set(self, relation_id, key, value, is_app): - if not isinstance(is_app, bool): - raise TypeError('is_app parameter to relation_set must be a boolean') - - try: - return self._run('relation-set', '-r', str(relation_id), f'{key}={value}', f'--app={is_app}') - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise - - def config_get(self): - return self._run('config-get', return_output=True, use_json=True) - - def is_leader(self): - """Obtain the current leadership status for the unit the charm code is executing on. - - The value is cached for the duration of a lease which is 30s in Juju. - """ - now = time.monotonic() - time_since_check = datetime.timedelta(seconds=now - self._leader_check_time) - if time_since_check > self.LEASE_RENEWAL_PERIOD or self._is_leader is None: - # Current time MUST be saved before running is-leader to ensure the cache - # is only used inside the window that is-leader itself asserts. - self._leader_check_time = now - self._is_leader = self._run('is-leader', return_output=True, use_json=True) - - return self._is_leader - - def resource_get(self, resource_name): - return self._run('resource-get', resource_name, return_output=True).strip() - - def pod_spec_set(self, spec, k8s_resources): - tmpdir = Path(tempfile.mkdtemp('-pod-spec-set')) - try: - spec_path = tmpdir / 'spec.json' - spec_path.write_text(json.dumps(spec)) - args = ['--file', str(spec_path)] - if k8s_resources: - k8s_res_path = tmpdir / 'k8s-resources.json' - k8s_res_path.write_text(json.dumps(k8s_resources)) - args.extend(['--k8s-resources', str(k8s_res_path)]) - self._run('pod-spec-set', *args) - finally: - shutil.rmtree(tmpdir) - - def status_get(self, *, is_app=False): - """Get a status of a unit or an application. - app -- A boolean indicating whether the status should be retrieved for a unit or an application. - """ - return self._run('status-get', '--include-data', f'--application={is_app}') - - def status_set(self, status, message='', *, is_app=False): - """Set a status of a unit or an application. - app -- A boolean indicating whether the status should be set for a unit or an application. - """ - if not isinstance(is_app, bool): - raise TypeError('is_app parameter must be boolean') - return self._run('status-set', f'--application={is_app}', status, message) - - def storage_list(self, name): - return [int(s.split('/')[1]) for s in self._run('storage-list', name, return_output=True, use_json=True)] - - def storage_get(self, storage_name_id, attribute): - return self._run('storage-get', '-s', storage_name_id, attribute, return_output=True, use_json=True) - - def storage_add(self, name, count=1): - if not isinstance(count, int) or isinstance(count, bool): - raise TypeError(f'storage count must be integer, got: {count} ({type(count)})') - self._run('storage-add', f'{name}={count}') - - def action_get(self): - return self._run(f'action-get', return_output=True, use_json=True) - - def action_set(self, results): - self._run(f'action-set', *[f"{k}={v}" for k, v in results.items()]) - - def action_log(self, message): - self._run(f'action-log', f"{message}") - - def action_fail(self, message=''): - self._run(f'action-fail', f"{message}") - - def network_get(self, endpoint_name, relation_id=None): - """Return network info provided by network-get for a given endpoint. - - endpoint_name -- A name of an endpoint (relation name or extra-binding name). - relation_id -- An optional relation id to get network info for. - """ - cmd = ['network-get', endpoint_name] - if relation_id is not None: - cmd.extend(['-r', str(relation_id)]) - try: - return self._run(*cmd, return_output=True, use_json=True) - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/setup.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/setup.py deleted file mode 100644 index 44765e2066bc3ea7c7214974dea708a646662617..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/setup.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 setuptools import setup - -with open("README.md", "r") as fh: - long_description = fh.read() - -setup( - name="ops", - version="0.0.1", - description="The Python library behind great charms", - long_description=long_description, - long_description_content_type="text/markdown", - license="Apache-2.0", - url="https://github.com/canonical/operator", - packages=["ops"], - classifiers=[ - "Development Status :: 4 - Beta", - - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - - "License :: OSI Approved :: Apache Software License", - ], -) diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/__init__.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/bin/relation-ids b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/bin/relation-ids deleted file mode 100755 index a7e0ead2d3182713bd826696fc403b5a8c54faa6..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/bin/relation-ids +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -case $1 in - db) echo '["db:1"]' ;; - mon) echo '["mon:2"]' ;; - ha) echo '[]' ;; - db0) echo '[]' ;; - db1) echo '["db1:4"]' ;; - db2) echo '["db2:5", "db2:6"]' ;; - *) echo '[]' ;; -esac diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/bin/relation-list b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/bin/relation-list deleted file mode 100755 index 88490159775624108766a17a35a77599ddea8f03..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/bin/relation-list +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -fail_not_found() { - 1>&2 echo "ERROR invalid value \"$1\" for option -r: relation not found" - exit 2 -} - -case $2 in - 1) echo '["remote/0"]' ;; - 2) echo '["remote/0"]' ;; - 3) fail_not_found $2 ;; - 4) echo '["remoteapp1/0"]' ;; - 5) echo '["remoteapp1/0"]' ;; - 6) echo '["remoteapp2/0"]' ;; - *) fail_not_found $2 ;; -esac diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/config.yaml b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/config.yaml deleted file mode 100644 index ffc0186002391ca52273d39bebcc9c4261c47535..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/config.yaml +++ /dev/null @@ -1 +0,0 @@ -"options": {} diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/__init__.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/__init__.py deleted file mode 100755 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/__init__.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/charm.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/charm.py deleted file mode 100755 index 71472f963a82461a02fe36a0aa47260151f4cafe..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/charm.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 os - -import yaml - -from ops.framework import Object, EventSource, EventBase, EventsBase - - -class HookEvent(EventBase): - pass - - -class ActionEvent(EventBase): - - def defer(self): - raise RuntimeError('cannot defer action events') - - def restore(self, snapshot): - env_action_name = os.environ.get('JUJU_ACTION_NAME') - event_action_name = self.handle.kind[:-len('_action')].replace('_', '-') - if event_action_name != env_action_name: - # This could only happen if the dev manually emits the action, or from a bug. - raise RuntimeError('action event kind does not match current action') - # Params are loaded at restore rather than __init__ because the model is not available in __init__. - self.params = self.framework.model._backend.action_get() - - def set_results(self, results): - self.framework.model._backend.action_set(results) - - def log(self, message): - self.framework.model._backend.action_log(message) - - def fail(self, message=''): - self.framework.model._backend.action_fail(message) - - -class InstallEvent(HookEvent): - pass - - -class StartEvent(HookEvent): - pass - - -class StopEvent(HookEvent): - pass - - -class ConfigChangedEvent(HookEvent): - pass - - -class UpdateStatusEvent(HookEvent): - pass - - -class UpgradeCharmEvent(HookEvent): - pass - - -class PreSeriesUpgradeEvent(HookEvent): - pass - - -class PostSeriesUpgradeEvent(HookEvent): - pass - - -class LeaderElectedEvent(HookEvent): - pass - - -class LeaderSettingsChangedEvent(HookEvent): - pass - - -class RelationEvent(HookEvent): - def __init__(self, handle, relation, app=None, unit=None): - super().__init__(handle) - - if unit and unit.app != app: - raise RuntimeError(f'cannot create RelationEvent with application {app} and unit {unit}') - - self.relation = relation - self.app = app - self.unit = unit - - def snapshot(self): - snapshot = { - 'relation_name': self.relation.name, - 'relation_id': self.relation.id, - } - if self.app: - snapshot['app_name'] = self.app.name - if self.unit: - snapshot['unit_name'] = self.unit.name - return snapshot - - def restore(self, snapshot): - self.relation = self.framework.model.get_relation(snapshot['relation_name'], snapshot['relation_id']) - - app_name = snapshot.get('app_name') - if app_name: - self.app = self.framework.model.get_app(app_name) - else: - self.app = None - - unit_name = snapshot.get('unit_name') - if unit_name: - self.unit = self.framework.model.get_unit(unit_name) - else: - self.unit = None - - -class RelationJoinedEvent(RelationEvent): - pass - - -class RelationChangedEvent(RelationEvent): - pass - - -class RelationDepartedEvent(RelationEvent): - pass - - -class RelationBrokenEvent(RelationEvent): - pass - - -class StorageEvent(HookEvent): - pass - - -class StorageAttachedEvent(StorageEvent): - pass - - -class StorageDetachingEvent(StorageEvent): - pass - - -class CharmEvents(EventsBase): - - install = EventSource(InstallEvent) - start = EventSource(StartEvent) - stop = EventSource(StopEvent) - update_status = EventSource(UpdateStatusEvent) - config_changed = EventSource(ConfigChangedEvent) - upgrade_charm = EventSource(UpgradeCharmEvent) - pre_series_upgrade = EventSource(PreSeriesUpgradeEvent) - post_series_upgrade = EventSource(PostSeriesUpgradeEvent) - leader_elected = EventSource(LeaderElectedEvent) - leader_settings_changed = EventSource(LeaderSettingsChangedEvent) - - -class CharmBase(Object): - - on = CharmEvents() - - def __init__(self, framework, key): - super().__init__(framework, key) - - for relation_name in self.framework.meta.relations: - relation_name = relation_name.replace('-', '_') - self.on.define_event(f'{relation_name}_relation_joined', RelationJoinedEvent) - self.on.define_event(f'{relation_name}_relation_changed', RelationChangedEvent) - self.on.define_event(f'{relation_name}_relation_departed', RelationDepartedEvent) - self.on.define_event(f'{relation_name}_relation_broken', RelationBrokenEvent) - - for storage_name in self.framework.meta.storages: - storage_name = storage_name.replace('-', '_') - self.on.define_event(f'{storage_name}_storage_attached', StorageAttachedEvent) - self.on.define_event(f'{storage_name}_storage_detaching', StorageDetachingEvent) - - for action_name in self.framework.meta.actions: - action_name = action_name.replace('-', '_') - self.on.define_event(f'{action_name}_action', ActionEvent) - - -class CharmMeta: - """Object containing the metadata for the charm. - - The maintainers, tags, terms, series, and extra_bindings attributes are all - lists of strings. The requires, provides, peers, relations, storage, - resources, and payloads attributes are all mappings of names to instances - of the respective RelationMeta, StorageMeta, ResourceMeta, or PayloadMeta. - - The relations attribute is a convenience accessor which includes all of the - requires, provides, and peers RelationMeta items. If needed, the role of - the relation definition can be obtained from its role attribute. - """ - - def __init__(self, raw={}, actions_raw={}): - self.name = raw.get('name', '') - self.summary = raw.get('summary', '') - self.description = raw.get('description', '') - self.maintainers = [] - if 'maintainer' in raw: - self.maintainers.append(raw['maintainer']) - if 'maintainers' in raw: - self.maintainers.extend(raw['maintainers']) - self.tags = raw.get('tags', []) - self.terms = raw.get('terms', []) - self.series = raw.get('series', []) - self.subordinate = raw.get('subordinate', False) - self.min_juju_version = raw.get('min-juju-version') - self.requires = {name: RelationMeta('requires', name, rel) - for name, rel in raw.get('requires', {}).items()} - self.provides = {name: RelationMeta('provides', name, rel) - for name, rel in raw.get('provides', {}).items()} - self.peers = {name: RelationMeta('peers', name, rel) - for name, rel in raw.get('peers', {}).items()} - self.relations = {} - self.relations.update(self.requires) - self.relations.update(self.provides) - self.relations.update(self.peers) - self.storages = {name: StorageMeta(name, storage) - for name, storage in raw.get('storage', {}).items()} - self.resources = {name: ResourceMeta(name, res) - for name, res in raw.get('resources', {}).items()} - self.payloads = {name: PayloadMeta(name, payload) - for name, payload in raw.get('payloads', {}).items()} - self.extra_bindings = raw.get('extra-bindings', []) - self.actions = {name: ActionMeta(name, action) for name, action in actions_raw.items()} - - @classmethod - def from_yaml(cls, metadata, actions=None): - meta = yaml.safe_load(metadata) - raw_actions = {} - if actions is not None: - raw_actions = yaml.safe_load(actions) - return cls(meta, raw_actions) - - -class RelationMeta: - """Object containing metadata about a relation definition.""" - - def __init__(self, role, relation_name, raw): - self.role = role - self.relation_name = relation_name - self.interface_name = raw['interface'] - self.scope = raw.get('scope') - - -class StorageMeta: - """Object containing metadata about a storage definition.""" - - def __init__(self, name, raw): - self.storage_name = name - self.type = raw['type'] - self.description = raw.get('description', '') - self.shared = raw.get('shared', False) - self.read_only = raw.get('read-only', False) - self.minimum_size = raw.get('minimum-size') - self.location = raw.get('location') - self.multiple_range = None - if 'multiple' in raw: - range = raw['multiple']['range'] - if '-' not in range: - self.multiple_range = (int(range), int(range)) - else: - range = range.split('-') - self.multiple_range = (int(range[0]), int(range[1]) if range[1] else None) - - -class ResourceMeta: - """Object containing metadata about a resource definition.""" - - def __init__(self, name, raw): - self.resource_name = name - self.type = raw['type'] - self.filename = raw.get('filename', None) - self.description = raw.get('description', '') - - -class PayloadMeta: - """Object containing metadata about a payload definition.""" - - def __init__(self, name, raw): - self.payload_name = name - self.type = raw['type'] - - -class ActionMeta: - - def __init__(self, name, raw=None): - raw = raw or {} - self.name = name - self.title = raw.get('title', '') - self.description = raw.get('description', '') - self.parameters = raw.get('params', {}) # {: } - self.required = raw.get('required', []) # [, ...] diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/framework.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/framework.py deleted file mode 100755 index d95eb61fae1c8a0207687ab759f15ab6399ca4db..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/framework.py +++ /dev/null @@ -1,941 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 inspect -import pickle -import marshal -import types -import sqlite3 -import collections -import collections.abc -import keyword -import weakref -from datetime import timedelta - - -class Handle: - """Handle defines a name for an object in the form of a hierarchical path. - - The provided parent is the object (or that object's handle) that this handle - sits under, or None if the object identified by this handle stands by itself - as the root of its own hierarchy. - - The handle kind is a string that defines a namespace so objects with the - same parent and kind will have unique keys. - - The handle key is a string uniquely identifying the object. No other objects - under the same parent and kind may have the same key. - """ - - def __init__(self, parent, kind, key): - if parent and not isinstance(parent, Handle): - parent = parent.handle - self._parent = parent - self._kind = kind - self._key = key - if parent: - if key: - self._path = f"{parent}/{kind}[{key}]" - else: - self._path = f"{parent}/{kind}" - else: - if key: - self._path = f"{kind}[{key}]" - else: - self._path = f"{kind}" - - def nest(self, kind, key): - return Handle(self, kind, key) - - def __hash__(self): - return hash((self.parent, self.kind, self.key)) - - def __eq__(self, other): - return (self.parent, self.kind, self.key) == (other.parent, other.kind, other.key) - - def __str__(self): - return self.path - - @property - def parent(self): - return self._parent - - @property - def kind(self): - return self._kind - - @property - def key(self): - return self._key - - @property - def path(self): - return self._path - - @classmethod - def from_path(cls, path): - handle = None - for pair in path.split("/"): - pair = pair.split("[") - good = False - if len(pair) == 1: - kind, key = pair[0], None - good = True - elif len(pair) == 2: - kind, key = pair - if key and key[-1] == ']': - key = key[:-1] - good = True - if not good: - raise RuntimeError("attempted to restore invalid handle path {path}") - handle = Handle(handle, kind, key) - return handle - - -class EventBase: - - def __init__(self, handle): - self.handle = handle - self.deferred = False - - def defer(self): - self.deferred = True - - def snapshot(self): - """Return the snapshot data that should be persisted. - - Subclasses must override to save any custom state. - """ - return None - - def restore(self, snapshot): - """Restore the value state from the given snapshot. - - Subclasses must override to restore their custom state. - """ - self.deferred = False - - -class EventSource: - """EventSource wraps an event type with a descriptor to facilitate observing and emitting. - - It is generally used as: - - class SomethingHappened(EventBase): - pass - - class SomeObject(Object): - something_happened = EventSource(SomethingHappened) - - With that, instances of that type will offer the someobj.something_happened - attribute which is a BoundEvent and may be used to emit and observe the event. - """ - - def __init__(self, event_type): - if not isinstance(event_type, type) or not issubclass(event_type, EventBase): - raise RuntimeError(f"Event requires a subclass of EventBase as an argument, got {event_type}") - self.event_type = event_type - self.event_kind = None - self.emitter_type = None - - def __set_name__(self, emitter_type, event_kind): - if self.event_kind is not None: - raise RuntimeError( - f'EventSource({self.event_type.__name__}) reused as ' - f'{self.emitter_type.__name__}.{self.event_kind} and ' - f'{emitter_type.__name__}.{event_kind}') - self.event_kind = event_kind - self.emitter_type = emitter_type - - def __get__(self, emitter, emitter_type=None): - if emitter is None: - return self - # Framework might not be available if accessed as CharmClass.on.event rather than charm_instance.on.event, - # but in that case it couldn't be emitted anyway, so there's no point to registering it. - framework = getattr(emitter, 'framework', None) - if framework is not None: - framework.register_type(self.event_type, emitter, self.event_kind) - return BoundEvent(emitter, self.event_type, self.event_kind) - - -class BoundEvent: - - def __repr__(self): - return (f'') - - def __init__(self, emitter, event_type, event_kind): - self.emitter = emitter - self.event_type = event_type - self.event_kind = event_kind - - def emit(self, *args, **kwargs): - """Emit event to all registered observers. - - The current storage state is committed before and after each observer is notified. - """ - framework = self.emitter.framework - key = framework._next_event_key() - event = self.event_type(Handle(self.emitter, self.event_kind, key), *args, **kwargs) - framework._emit(event) - - -class HandleKind: - """Helper descriptor to define the Object.handle_kind field. - - The handle_kind for an object defaults to its type name, but it may - be explicitly overridden if desired. - """ - - def __get__(self, obj, obj_type): - kind = obj_type.__dict__.get("handle_kind") - if kind: - return kind - return obj_type.__name__ - - -class Object: - - handle_kind = HandleKind() - - def __init__(self, parent, key): - kind = self.handle_kind - if isinstance(parent, Framework): - self.framework = parent - # Avoid Framework instances having a circular reference to themselves. - if self.framework is self: - self.framework = weakref.proxy(self.framework) - self.handle = Handle(None, kind, key) - else: - self.framework = parent.framework - self.handle = Handle(parent, kind, key) - self.framework._track(self) - - # TODO Detect conflicting handles here. - - @property - def model(self): - return self.framework.model - - @property - def meta(self): - return self.framework.meta - - @property - def charm_dir(self): - return self.framework.charm_dir - - -class EventsBase(Object): - """Convenience type to allow defining .on attributes at class level.""" - - handle_kind = "on" - - def __init__(self, parent=None, key=None): - if parent is not None: - super().__init__(parent, key) - else: - self._cache = weakref.WeakKeyDictionary() - - def __get__(self, emitter, emitter_type): - if emitter is None: - return self - instance = self._cache.get(emitter) - if instance is None: - # Same type, different instance, more data. Doing this unusual construct - # means people can subclass just this one class to have their own 'on'. - instance = self._cache[emitter] = type(self)(emitter) - return instance - - @classmethod - def define_event(cls, event_kind, event_type): - """Define an event on this type at runtime. - - cls -- a type to define an event on. - event_kind -- an attribute name that will be used to access the event. Must be a valid python identifier, not be a keyword or an existing attribute. - event_type -- a type of the event to define. - """ - if not event_kind.isidentifier(): - raise RuntimeError(f'unable to define an event with event_kind that is not a valid python identifier: {event_kind}') - elif keyword.iskeyword(event_kind): - raise RuntimeError(f'unable to define an event with event_kind that is a python keyword: {event_kind}') - try: - getattr(cls, event_kind) - raise RuntimeError(f'unable to define an event with event_kind that overlaps with an existing type {cls} attribute: {event_kind}') - except AttributeError: - pass - - event_descriptor = EventSource(event_type) - event_descriptor.__set_name__(cls, event_kind) - setattr(cls, event_kind, event_descriptor) - - def events(self): - """Return a mapping of event_kinds to bound_events for all available events. - """ - events_map = {} - # We have to iterate over the class rather than instance to allow for properties which - # might call this method (e.g., event views), leading to infinite recursion. - for attr_name, attr_value in inspect.getmembers(type(self)): - if isinstance(attr_value, EventSource): - # We actually care about the bound_event, however, since it - # provides the most info for users of this method. - event_kind = attr_name - bound_event = getattr(self, event_kind) - events_map[event_kind] = bound_event - return events_map - - def __getitem__(self, key): - return PrefixedEvents(self, key) - - -class PrefixedEvents: - - def __init__(self, emitter, key): - self._emitter = emitter - self._prefix = key.replace("-", "_") + '_' - - def __getattr__(self, name): - return getattr(self._emitter, self._prefix + name) - - -class PreCommitEvent(EventBase): - pass - - -class CommitEvent(EventBase): - pass - - -class FrameworkEvents(EventsBase): - pre_commit = EventSource(PreCommitEvent) - commit = EventSource(CommitEvent) - - -class NoSnapshotError(Exception): - - def __init__(self, handle_path): - self.handle_path = handle_path - - def __str__(self): - return f'no snapshot data found for {self.handle_path} object' - - -class NoTypeError(Exception): - - def __init__(self, handle_path): - self.handle_path = handle_path - - def __str__(self): - return f"cannot restore {self.handle_path} since no class was registered for it" - - -class SQLiteStorage: - - DB_LOCK_TIMEOUT = timedelta(hours=1) - - def __init__(self, filename): - # The isolation_level argument is set to None such that the implicit transaction management behavior of the sqlite3 module is disabled. - self._db = sqlite3.connect(str(filename), isolation_level=None, timeout=self.DB_LOCK_TIMEOUT.total_seconds()) - self._setup() - - def _setup(self): - # Make sure that the database is locked until the connection is closed, not until the transaction ends. - self._db.execute("PRAGMA locking_mode=EXCLUSIVE") - c = self._db.execute("BEGIN") - c.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='snapshot'") - if c.fetchone()[0] == 0: - # Keep in mind what might happen if the process dies somewhere below. - # The system must not be rendered permanently broken by that. - self._db.execute("CREATE TABLE snapshot (handle TEXT PRIMARY KEY, data BLOB)") - self._db.execute("CREATE TABLE notice (sequence INTEGER PRIMARY KEY AUTOINCREMENT, event_path TEXT, observer_path TEXT, method_name TEXT)") - self._db.commit() - - def close(self): - self._db.close() - - def commit(self): - self._db.commit() - - # There's commit but no rollback. For abort to be supported, we'll need logic that - # can rollback decisions made by third-party code in terms of the internal state - # of objects that have been snapshotted, and hooks to let them know about it and - # take the needed actions to undo their logic until the last snapshot. - # This is doable but will increase significantly the chances for mistakes. - - def save_snapshot(self, handle_path, snapshot_data): - self._db.execute("REPLACE INTO snapshot VALUES (?, ?)", (handle_path, snapshot_data)) - - def load_snapshot(self, handle_path): - c = self._db.cursor() - c.execute("SELECT data FROM snapshot WHERE handle=?", (handle_path,)) - row = c.fetchone() - if row: - return row[0] - return None - - def drop_snapshot(self, handle_path): - self._db.execute("DELETE FROM snapshot WHERE handle=?", (handle_path,)) - - def save_notice(self, event_path, observer_path, method_name): - self._db.execute("INSERT INTO notice VALUES (NULL, ?, ?, ?)", (event_path, observer_path, method_name)) - - def drop_notice(self, event_path, observer_path, method_name): - self._db.execute("DELETE FROM notice WHERE event_path=? AND observer_path=? AND method_name=?", (event_path, observer_path, method_name)) - - def notices(self, event_path): - if event_path: - c = self._db.execute("SELECT event_path, observer_path, method_name FROM notice WHERE event_path=? ORDER BY sequence", (event_path,)) - else: - c = self._db.execute("SELECT event_path, observer_path, method_name FROM notice ORDER BY sequence") - while True: - rows = c.fetchmany() - if not rows: - break - for row in rows: - yield tuple(row) - - -class Framework(Object): - - on = FrameworkEvents() - - # Override properties from Object so that we can set them in __init__. - model = None - meta = None - charm_dir = None - - def __init__(self, data_path, charm_dir, meta, model): - - super().__init__(self, None) - - self._data_path = data_path - self.charm_dir = charm_dir - self.meta = meta - self.model = model - self._observers = [] # [(observer_path, method_name, parent_path, event_key)] - self._observer = weakref.WeakValueDictionary() # {observer_path: observer} - self._objects = weakref.WeakValueDictionary() - self._type_registry = {} # {(parent_path, kind): cls} - self._type_known = set() # {cls} - - self._storage = SQLiteStorage(data_path) - - # We can't use the higher-level StoredState because it relies on events. - self.register_type(StoredStateData, None, StoredStateData.handle_kind) - stored_handle = Handle(None, StoredStateData.handle_kind, '_stored') - try: - self._stored = self.load_snapshot(stored_handle) - except NoSnapshotError: - self._stored = StoredStateData(self, '_stored') - self._stored['event_count'] = 0 - - def close(self): - self._storage.close() - - def _track(self, obj): - """Track object and ensure it is the only object created using its handle path.""" - if obj is self: - # Framework objects don't track themselves - return - if obj.handle.path in self.framework._objects: - raise RuntimeError(f"two objects claiming to be {obj.handle.path} have been created") - self._objects[obj.handle.path] = obj - - def _forget(self, obj): - """Stop tracking the given object. See also _track.""" - self._objects.pop(obj.handle.path, None) - - def commit(self): - # Give a chance for objects to persist data they want to before a commit is made. - self.on.pre_commit.emit() - # Make sure snapshots are saved by instances of StoredStateData. Any possible state - # modifications in on_commit handlers of instances of other classes will not be persisted. - self.on.commit.emit() - # Save our event count after all events have been emitted. - self.save_snapshot(self._stored) - self._storage.commit() - - def register_type(self, cls, parent, kind=None): - if parent and not isinstance(parent, Handle): - parent = parent.handle - if parent: - parent_path = parent.path - else: - parent_path = None - if not kind: - kind = cls.handle_kind - self._type_registry[(parent_path, kind)] = cls - self._type_known.add(cls) - - def save_snapshot(self, value): - """Save a persistent snapshot of the provided value. - - The provided value must implement the following interface: - - value.handle = Handle(...) - value.snapshot() => {...} # Simple builtin types only. - value.restore(snapshot) # Restore custom state from prior snapshot. - """ - if type(value) not in self._type_known: - raise RuntimeError(f"cannot save {type(value).__name__} values before registering that type") - data = value.snapshot() - # Use marshal as a validator, enforcing the use of simple types. - marshal.dumps(data) - # Use pickle for serialization, so the value remains portable. - raw_data = pickle.dumps(data) - self._storage.save_snapshot(value.handle.path, raw_data) - - def load_snapshot(self, handle): - parent_path = None - if handle.parent: - parent_path = handle.parent.path - cls = self._type_registry.get((parent_path, handle.kind)) - if not cls: - raise NoTypeError(handle.path) - raw_data = self._storage.load_snapshot(handle.path) - if not raw_data: - raise NoSnapshotError(handle.path) - data = pickle.loads(raw_data) - obj = cls.__new__(cls) - obj.framework = self - obj.handle = handle - obj.restore(data) - self._track(obj) - return obj - - def drop_snapshot(self, handle): - self._storage.drop_snapshot(handle.path) - - def observe(self, bound_event, observer): - """Register observer to be called when bound_event is emitted. - - The bound_event is generally provided as an attribute of the object that emits - the event, and is created in this style: - - class SomeObject: - something_happened = Event(SomethingHappened) - - That event may be observed as: - - framework.observe(someobj.something_happened, self.on_something_happened) - - If the method to be called follows the name convention "on_", it - may be omitted from the observe call. That means the above is equivalent to: - - framework.observe(someobj.something_happened, self) - - """ - if not isinstance(bound_event, BoundEvent): - raise RuntimeError(f'Framework.observe requires a BoundEvent as second parameter, got {bound_event}') - - event_type = bound_event.event_type - event_kind = bound_event.event_kind - emitter = bound_event.emitter - - self.register_type(event_type, emitter, event_kind) - - if hasattr(emitter, "handle"): - emitter_path = emitter.handle.path - else: - raise RuntimeError(f'event emitter {type(emitter).__name__} must have a "handle" attribute') - - method_name = None - if isinstance(observer, types.MethodType): - method_name = observer.__name__ - observer = observer.__self__ - else: - method_name = "on_" + event_kind - if not hasattr(observer, method_name): - raise RuntimeError(f'Observer method not provided explicitly and {type(observer).__name__} type has no "{method_name}" method') - - # Validate that the method has an acceptable call signature. - sig = inspect.signature(getattr(observer, method_name)) - # Self isn't included in the params list, so the first arg will be the event. - extra_params = list(sig.parameters.values())[1:] - if not sig.parameters: - raise TypeError(f'{type(observer).__name__}.{method_name} must accept event parameter') - elif any(param.default is inspect.Parameter.empty for param in extra_params): - # Allow for additional optional params, since there's no reason to exclude them, but - # required params will break. - raise TypeError(f'{type(observer).__name__}.{method_name} has extra required parameter') - - # TODO Prevent the exact same parameters from being registered more than once. - - self._observer[observer.handle.path] = observer - self._observers.append((observer.handle.path, method_name, emitter_path, event_kind)) - - def _next_event_key(self): - """Return the next event key that should be used, incrementing the internal counter.""" - # Increment the count first; this means the keys will start at 1, and 0 means no events have been emitted. - self._stored['event_count'] += 1 - return str(self._stored['event_count']) - - def _emit(self, event): - """See BoundEvent.emit for the public way to call this.""" - - # Save the event for all known observers before the first notification - # takes place, so that either everyone interested sees it, or nobody does. - self.save_snapshot(event) - event_path = event.handle.path - event_kind = event.handle.kind - parent_path = event.handle.parent.path - # TODO Track observers by (parent_path, event_kind) rather than as a list of all observers. Avoiding linear search through all observers for every event - for observer_path, method_name, _parent_path, _event_kind in self._observers: - if _parent_path != parent_path: - continue - if _event_kind and _event_kind != event_kind: - continue - # Again, only commit this after all notices are saved. - self._storage.save_notice(event_path, observer_path, method_name) - self._reemit(event_path) - - def reemit(self): - """Reemit previously deferred events to the observers that deferred them. - - Only the specific observers that have previously deferred the event will be - notified again. Observers that asked to be notified about events after it's - been first emitted won't be notified, as that would mean potentially observing - events out of order. - """ - self._reemit() - - def _reemit(self, single_event_path=None): - last_event_path = None - deferred = True - for event_path, observer_path, method_name in self._storage.notices(single_event_path): - event_handle = Handle.from_path(event_path) - - if last_event_path != event_path: - if not deferred: - self._storage.drop_snapshot(last_event_path) - last_event_path = event_path - deferred = False - - try: - event = self.load_snapshot(event_handle) - except NoTypeError: - self._storage.drop_notice(event_path, observer_path, method_name) - continue - - event.deferred = False - observer = self._observer.get(observer_path) - if observer: - custom_handler = getattr(observer, method_name, None) - if custom_handler: - custom_handler(event) - - if event.deferred: - deferred = True - else: - self._storage.drop_notice(event_path, observer_path, method_name) - # We intentionally consider this event to be dead and reload it from scratch in the next path. - self.framework._forget(event) - - if not deferred: - self._storage.drop_snapshot(last_event_path) - - -class StoredStateChanged(EventBase): - pass - - -class StoredStateEvents(EventsBase): - changed = EventSource(StoredStateChanged) - - -class StoredStateData(Object): - - on = StoredStateEvents() - - def __init__(self, parent, attr_name): - super().__init__(parent, attr_name) - self._cache = {} - self.dirty = False - - def __getitem__(self, key): - return self._cache.get(key) - - def __setitem__(self, key, value): - self._cache[key] = value - self.dirty = True - - def __contains__(self, key): - return key in self._cache - - def snapshot(self): - return self._cache - - def restore(self, snapshot): - self._cache = snapshot - self.dirty = False - - def on_commit(self, event): - if self.dirty: - self.framework.save_snapshot(self) - self.dirty = False - - -class BoundStoredState: - - def __init__(self, parent, attr_name): - parent.framework.register_type(StoredStateData, parent) - - handle = Handle(parent, StoredStateData.handle_kind, attr_name) - try: - data = parent.framework.load_snapshot(handle) - except NoSnapshotError: - data = StoredStateData(parent, attr_name) - - # __dict__ is used to avoid infinite recursion. - self.__dict__["_data"] = data - self.__dict__["_attr_name"] = attr_name - - parent.framework.observe(parent.framework.on.commit, self._data) - - def __getattr__(self, key): - # "on" is the only reserved key that can't be used in the data map. - if key == "on": - return self._data.on - if key not in self._data: - raise AttributeError(f"attribute '{key}' is not stored") - return _wrap_stored(self._data, self._data[key]) - - def __setattr__(self, key, value): - if key == "on": - raise AttributeError(f"attribute 'on' is reserved and cannot be set") - - value = _unwrap_stored(self._data, value) - - if not isinstance(value, (type(None), int, str, bytes, list, dict, set)): - raise AttributeError(f"attribute '{key}' cannot be set to {type(value).__name__}: must be int/dict/list/etc") - - self._data[key] = _unwrap_stored(self._data, value) - self.on.changed.emit() - - def set_default(self, **kwargs): - """"Set the value of any given key if it has not already been set""" - for k, v in kwargs.items(): - if k not in self._data: - self._data[k] = v - - -class StoredState: - - def __init__(self): - self.parent_type = None - self.attr_name = None - - def __get__(self, parent, parent_type=None): - if self.parent_type is None: - self.parent_type = parent_type - elif self.parent_type is not parent_type: - raise RuntimeError("StoredState shared by {} and {}".format(self.parent_type.__name__, parent_type.__name__)) - - if parent is None: - return self - - bound = parent.__dict__.get(self.attr_name) - if bound is None: - for attr_name, attr_value in parent_type.__dict__.items(): - if attr_value is self: - if self.attr_name and attr_name != self.attr_name: - parent_tname = parent_type.__name__ - raise RuntimeError(f"StoredState shared by {parent_tname}.{self.attr_name} and {parent_tname}.{attr_name}") - self.attr_name = attr_name - bound = BoundStoredState(parent, attr_name) - parent.__dict__[attr_name] = bound - break - else: - raise RuntimeError("cannot find StoredVariable attribute in type {}".format(parent_type.__name__)) - - return bound - - -def _wrap_stored(parent_data, value): - t = type(value) - if t is dict: - return StoredDict(parent_data, value) - if t is list: - return StoredList(parent_data, value) - if t is set: - return StoredSet(parent_data, value) - return value - - -def _unwrap_stored(parent_data, value): - t = type(value) - if t is StoredDict or t is StoredList or t is StoredSet: - return value._under - return value - - -class StoredDict(collections.abc.MutableMapping): - - def __init__(self, stored_data, under): - self._stored_data = stored_data - self._under = under - - def __getitem__(self, key): - return _wrap_stored(self._stored_data, self._under[key]) - - def __setitem__(self, key, value): - self._under[key] = _unwrap_stored(self._stored_data, value) - self._stored_data.dirty = True - - def __delitem__(self, key): - del self._under[key] - self._stored_data.dirty = True - - def __iter__(self): - return self._under.__iter__() - - def __len__(self): - return len(self._under) - - def __eq__(self, other): - if isinstance(other, StoredDict): - return self._under == other._under - elif isinstance(other, collections.abc.Mapping): - return self._under == other - else: - return NotImplemented - - -class StoredList(collections.abc.MutableSequence): - - def __init__(self, stored_data, under): - self._stored_data = stored_data - self._under = under - - def __getitem__(self, index): - return _wrap_stored(self._stored_data, self._under[index]) - - def __setitem__(self, index, value): - self._under[index] = _unwrap_stored(self._stored_data, value) - self._stored_data.dirty = True - - def __delitem__(self, index): - del self._under[index] - self._stored_data.dirty = True - - def __len__(self): - return len(self._under) - - def insert(self, index, value): - self._under.insert(index, value) - self._stored_data.dirty = True - - def append(self, value): - self._under.append(value) - self._stored_data.dirty = True - - def __eq__(self, other): - if isinstance(other, StoredList): - return self._under == other._under - elif isinstance(other, collections.abc.Sequence): - return self._under == other - else: - return NotImplemented - - def __lt__(self, other): - if isinstance(other, StoredList): - return self._under < other._under - elif isinstance(other, collections.abc.Sequence): - return self._under < other - else: - return NotImplemented - - def __le__(self, other): - if isinstance(other, StoredList): - return self._under <= other._under - elif isinstance(other, collections.abc.Sequence): - return self._under <= other - else: - return NotImplemented - - def __gt__(self, other): - if isinstance(other, StoredList): - return self._under > other._under - elif isinstance(other, collections.abc.Sequence): - return self._under > other - else: - return NotImplemented - - def __ge__(self, other): - if isinstance(other, StoredList): - return self._under >= other._under - elif isinstance(other, collections.abc.Sequence): - return self._under >= other - else: - return NotImplemented - - -class StoredSet(collections.abc.MutableSet): - - def __init__(self, stored_data, under): - self._stored_data = stored_data - self._under = under - - def add(self, key): - self._under.add(key) - self._stored_data.dirty = True - - def discard(self, key): - self._under.discard(key) - self._stored_data.dirty = True - - def __contains__(self, key): - return key in self._under - - def __iter__(self): - return self._under.__iter__() - - def __len__(self): - return len(self._under) - - @classmethod - def _from_iterable(cls, it): - """Construct an instance of the class from any iterable input. - - Per https://docs.python.org/3/library/collections.abc.html - if the Set mixin is being used in a class with a different constructor signature, - you will need to override _from_iterable() with a classmethod that can construct - new instances from an iterable argument. - """ - return set(it) - - def __le__(self, other): - if isinstance(other, StoredSet): - return self._under <= other._under - elif isinstance(other, collections.abc.Set): - return self._under <= other - else: - return NotImplemented - - def __ge__(self, other): - if isinstance(other, StoredSet): - return self._under >= other._under - elif isinstance(other, collections.abc.Set): - return self._under >= other - else: - return NotImplemented - - def __eq__(self, other): - if isinstance(other, StoredSet): - return self._under == other._under - elif isinstance(other, collections.abc.Set): - return self._under == other - else: - return NotImplemented diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/jujuversion.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/jujuversion.py deleted file mode 100755 index 5256f24ff8fb37a2cbe6162d824e77fb7dca1f45..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/jujuversion.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2020 Canonical Ltd. -# -# 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 re -from functools import total_ordering - - -@total_ordering -class JujuVersion: - - PATTERN = r'^(?P\d{1,9})\.(?P\d{1,9})((?:\.|-(?P[a-z]+))(?P\d{1,9}))?(\.(?P\d{1,9}))?$' - - def __init__(self, version): - m = re.match(self.PATTERN, version) - if not m: - raise RuntimeError(f'"{version}" is not a valid Juju version string') - - d = m.groupdict() - self.major = int(m.group('major')) - self.minor = int(m.group('minor')) - self.tag = d['tag'] or '' - self.patch = int(d['patch'] or 0) - self.build = int(d['build'] or 0) - - def __repr__(self): - if self.tag: - s = f'{self.major}.{self.minor}-{self.tag}{self.patch}' - else: - s = f'{self.major}.{self.minor}.{self.patch}' - if self.build > 0: - s += f'.{self.build}' - return s - - def __eq__(self, other): - if self is other: - return True - if isinstance(other, str): - other = type(self)(other) - elif not isinstance(other, JujuVersion): - raise RuntimeError(f'cannot compare Juju version "{self}" with "{other}"') - return self.major == other.major and self.minor == other.minor\ - and self.tag == other.tag and self.build == other.build and self.patch == other.patch - - def __lt__(self, other): - if self is other: - return False - if isinstance(other, str): - other = type(self)(other) - elif not isinstance(other, JujuVersion): - raise RuntimeError(f'cannot compare Juju version "{self}" with "{other}"') - - if self.major != other.major: - return self.major < other.major - elif self.minor != other.minor: - return self.minor < other.minor - elif self.tag != other.tag: - if not self.tag: - return False - elif not other.tag: - return True - return self.tag < other.tag - elif self.patch != other.patch: - return self.patch < other.patch - elif self.build != other.build: - return self.build < other.build - return False diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/main.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/main.py deleted file mode 100755 index c8d5da2adf1b4f6ccdb3a0b1292dcab8d51c0e9b..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/main.py +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2019 Canonical Ltd. -# -# 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 os -import sys -from pathlib import Path - -import yaml - -import ops.charm -import ops.framework -import ops.model - -CHARM_STATE_FILE = '.unit-state.db' - - -def debugf(format, *args, **kwargs): - pass - - -def _get_charm_dir(): - charm_dir = os.environ.get("JUJU_CHARM_DIR") - if charm_dir is None: - # Assume $JUJU_CHARM_DIR/lib/op/main.py structure. - charm_dir = Path(f'{__file__}/../../..').resolve() - else: - charm_dir = Path(charm_dir).resolve() - return charm_dir - - -def _load_metadata(charm_dir): - metadata = yaml.safe_load((charm_dir / 'metadata.yaml').read_text()) - - actions_meta = charm_dir / 'actions.yaml' - if actions_meta.exists(): - actions_metadata = yaml.safe_load(actions_meta.read_text()) - else: - actions_metadata = {} - return metadata, actions_metadata - - -def _create_event_link(charm, bound_event): - """Create a symlink for a particular event. - - charm -- A charm object. - bound_event -- An event for which to create a symlink. - """ - if issubclass(bound_event.event_type, ops.charm.HookEvent): - event_dir = charm.framework.charm_dir / 'hooks' - event_path = event_dir / bound_event.event_kind.replace('_', '-') - elif issubclass(bound_event.event_type, ops.charm.ActionEvent): - if not bound_event.event_kind.endswith("_action"): - raise RuntimeError(f"action event name {bound_event.event_kind} needs _action suffix") - event_dir = charm.framework.charm_dir / 'actions' - # The event_kind is suffixed with "_action" while the executable is not. - event_path = event_dir / bound_event.event_kind[:-len('_action')].replace('_', '-') - else: - raise RuntimeError(f'cannot create a symlink: unsupported event type {bound_event.event_type}') - - event_dir.mkdir(exist_ok=True) - if not event_path.exists(): - # CPython has different implementations for populating sys.argv[0] for Linux and Windows. For Windows - # it is always an absolute path (any symlinks are resolved) while for Linux it can be a relative path. - target_path = os.path.relpath(os.path.realpath(sys.argv[0]), event_dir) - - # Ignore the non-symlink files or directories assuming the charm author knows what they are doing. - debugf(f'Creating a new relative symlink at {event_path} pointing to {target_path}') - event_path.symlink_to(target_path) - - -def _setup_event_links(charm_dir, charm): - """Set up links for supported events that originate from Juju. - - Whether a charm can handle an event or not can be determined by - introspecting which events are defined on it. - - Hooks or actions are created as symlinks to the charm code file which is determined by inspecting - symlinks provided by the charm author at hooks/install or hooks/start. - - charm_dir -- A root directory of the charm. - charm -- An instance of the Charm class. - """ - for bound_event in charm.on.events().values(): - # Only events that originate from Juju need symlinks. - if issubclass(bound_event.event_type, (ops.charm.HookEvent, ops.charm.ActionEvent)): - _create_event_link(charm, bound_event) - - -def _emit_charm_event(charm, event_name): - """Emits a charm event based on a Juju event name. - - charm -- A charm instance to emit an event from. - event_name -- A Juju event name to emit on a charm. - """ - event_to_emit = None - try: - event_to_emit = getattr(charm.on, event_name) - except AttributeError: - debugf(f"event {event_name} not defined for {charm}") - - # If the event is not supported by the charm implementation, do - # not error out or try to emit it. This is to support rollbacks. - if event_to_emit is not None: - args, kwargs = _get_event_args(charm, event_to_emit) - debugf(f'Emitting Juju event {event_name}') - event_to_emit.emit(*args, **kwargs) - - -def _get_event_args(charm, bound_event): - event_type = bound_event.event_type - model = charm.framework.model - - if issubclass(event_type, ops.charm.RelationEvent): - relation_name = os.environ['JUJU_RELATION'] - relation_id = int(os.environ['JUJU_RELATION_ID'].split(':')[-1]) - relation = model.get_relation(relation_name, relation_id) - else: - relation = None - - remote_app_name = os.environ.get('JUJU_REMOTE_APP', '') - remote_unit_name = os.environ.get('JUJU_REMOTE_UNIT', '') - if remote_app_name or remote_unit_name: - if not remote_app_name: - if '/' not in remote_unit_name: - raise RuntimeError(f'invalid remote unit name: {remote_unit_name}') - remote_app_name = remote_unit_name.split('/')[0] - args = [relation, model.get_app(remote_app_name)] - if remote_unit_name: - args.append(model.get_unit(remote_unit_name)) - return args, {} - elif relation: - return [relation], {} - return [], {} - - -def main(charm_class): - """Setup the charm and dispatch the observed event. - - The event name is based on the way this executable was called (argv[0]). - """ - - charm_dir = _get_charm_dir() - - # Process the Juju event relevant to the current hook execution - # JUJU_HOOK_NAME, JUJU_FUNCTION_NAME, and JUJU_ACTION_NAME are not used - # in order to support simulation of events from debugging sessions. - # TODO: For Windows, when symlinks are used, this is not a valid method of getting an event name (see LP: #1854505). - juju_exec_path = Path(sys.argv[0]) - juju_event_name = juju_exec_path.name.replace('-', '_') - if juju_exec_path.parent.name == 'actions': - juju_event_name = f'{juju_event_name}_action' - - metadata, actions_metadata = _load_metadata(charm_dir) - meta = ops.charm.CharmMeta(metadata, actions_metadata) - unit_name = os.environ['JUJU_UNIT_NAME'] - model = ops.model.Model(unit_name, meta, ops.model.ModelBackend()) - - # TODO: If Juju unit agent crashes after exit(0) from the charm code - # the framework will commit the snapshot but Juju will not commit its - # operation. - charm_state_path = charm_dir / CHARM_STATE_FILE - framework = ops.framework.Framework(charm_state_path, charm_dir, meta, model) - try: - charm = charm_class(framework, None) - - # When a charm is force-upgraded and a unit is in an error state Juju does not run upgrade-charm and - # instead runs the failed hook followed by config-changed. Given the nature of force-upgrading - # the hook setup code is not triggered on config-changed. - # 'start' event is included as Juju does not fire the install event for K8s charms (see LP: #1854635). - if juju_event_name in ('install', 'start', 'upgrade_charm') or juju_event_name.endswith('_storage_attached'): - _setup_event_links(charm_dir, charm) - - framework.reemit() - - _emit_charm_event(charm, juju_event_name) - - framework.commit() - finally: - framework.close() diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/model.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/model.py deleted file mode 100644 index a12dcca2b1a85b3ed4a0983b88073f9f8c7bcb42..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/lib/ops/model.py +++ /dev/null @@ -1,679 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 json -import weakref -import os -import shutil -import tempfile -import time -import datetime - -from abc import ABC, abstractmethod -from collections.abc import Mapping, MutableMapping -from pathlib import Path -from subprocess import run, PIPE, CalledProcessError - - -class Model: - - def __init__(self, unit_name, meta, backend): - self._cache = ModelCache(backend) - self._backend = backend - self.unit = self.get_unit(unit_name) - self.app = self.unit.app - self.relations = RelationMapping(meta.relations, self.unit, self._backend, self._cache) - self.config = ConfigData(self._backend) - self.resources = Resources(list(meta.resources), self._backend) - self.pod = Pod(self._backend) - self.storages = StorageMapping(list(meta.storages), self._backend) - - def get_unit(self, unit_name): - return self._cache.get(Unit, unit_name) - - def get_app(self, app_name): - return self._cache.get(Application, app_name) - - def get_relation(self, relation_name, relation_id=None): - """Get a specific Relation instance. - - If relation_id is given, this will return that Relation instance. - - If relation_id is not given, this will return the Relation instance if the - relation is established only once or None if it is not established. If this - same relation is established multiple times the error TooManyRelatedAppsError is raised. - """ - return self.relations._get_unique(relation_name, relation_id) - - -class ModelCache: - - def __init__(self, backend): - self._backend = backend - self._weakrefs = weakref.WeakValueDictionary() - - def get(self, entity_type, *args): - key = (entity_type,) + args - entity = self._weakrefs.get(key) - if entity is None: - entity = entity_type(*args, backend=self._backend, cache=self) - self._weakrefs[key] = entity - return entity - - -class Application: - - def __init__(self, name, backend, cache): - self.name = name - self._backend = backend - self._cache = cache - self._is_our_app = self.name == self._backend.app_name - self._status = None - - @property - def status(self): - if not self._is_our_app: - return UnknownStatus() - - if not self._backend.is_leader(): - raise RuntimeError('cannot get application status as a non-leader unit') - - if self._status: - return self._status - - s = self._backend.status_get(is_app=True) - self._status = StatusBase.from_name(s['status'], s['message']) - return self._status - - @status.setter - def status(self, value): - if not isinstance(value, StatusBase): - raise InvalidStatusError(f'invalid value provided for application {self} status: {value}') - - if not self._is_our_app: - raise RuntimeError(f'cannot to set status for a remote application {self}') - - if not self._backend.is_leader(): - raise RuntimeError('cannot set application status as a non-leader unit') - - self._backend.status_set(value.name, value.message, is_app=True) - self._status = value - - def __repr__(self): - return f'<{type(self).__module__}.{type(self).__name__} {self.name}>' - - -class Unit: - - def __init__(self, name, backend, cache): - self.name = name - - app_name = name.split('/')[0] - self.app = cache.get(Application, app_name) - - self._backend = backend - self._cache = cache - self._is_our_unit = self.name == self._backend.unit_name - self._status = None - - @property - def status(self): - if not self._is_our_unit: - return UnknownStatus() - - if self._status: - return self._status - - s = self._backend.status_get(is_app=False) - self._status = StatusBase.from_name(s['status'], s['message']) - return self._status - - @status.setter - def status(self, value): - if not isinstance(value, StatusBase): - raise InvalidStatusError(f'invalid value provided for unit {self} status: {value}') - - if not self._is_our_unit: - raise RuntimeError(f'cannot set status for a remote unit {self}') - - self._backend.status_set(value.name, value.message, is_app=False) - self._status = value - - def __repr__(self): - return f'<{type(self).__module__}.{type(self).__name__} {self.name}>' - - def is_leader(self): - if self._is_our_unit: - # This value is not cached as it is not guaranteed to persist for the whole duration - # of a hook execution. - return self._backend.is_leader() - else: - raise RuntimeError(f"cannot determine leadership status for remote applications: {self}") - - -class LazyMapping(Mapping, ABC): - - _lazy_data = None - - @abstractmethod - def _load(self): - raise NotImplementedError() - - @property - def _data(self): - data = self._lazy_data - if data is None: - data = self._lazy_data = self._load() - return data - - def __contains__(self, key): - return key in self._data - - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(self._data) - - def __getitem__(self, key): - return self._data[key] - - -class RelationMapping(Mapping): - """Map of relation names to lists of Relation instances.""" - - def __init__(self, relations_meta, our_unit, backend, cache): - self._peers = set() - for name, relation_meta in relations_meta.items(): - if relation_meta.role == 'peers': - self._peers.add(name) - self._our_unit = our_unit - self._backend = backend - self._cache = cache - self._data = {relation_name: None for relation_name in relations_meta} - - def __contains__(self, key): - return key in self._data - - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(self._data) - - def __getitem__(self, relation_name): - is_peer = relation_name in self._peers - relation_list = self._data[relation_name] - if relation_list is None: - relation_list = self._data[relation_name] = [] - for rid in self._backend.relation_ids(relation_name): - relation = Relation(relation_name, rid, is_peer, self._our_unit, self._backend, self._cache) - relation_list.append(relation) - return relation_list - - def _get_unique(self, relation_name, relation_id=None): - if relation_id is not None: - if not isinstance(relation_id, int): - raise ModelError(f'relation name {relation_id} must be int or None not {type(relation_id).__name__}') - for relation in self[relation_name]: - if relation.id == relation_id: - return relation - else: - # The relation may be dead, but it is not forgotten. - is_peer = relation_name in self._peers - return Relation(relation_name, relation_id, is_peer, self._our_unit, self._backend, self._cache) - num_related = len(self[relation_name]) - if num_related == 0: - return None - elif num_related == 1: - return self[relation_name][0] - else: - # TODO: We need something in the framework to catch and gracefully handle - # errors, ideally integrating the error catching with Juju's mechanisms. - raise TooManyRelatedAppsError(relation_name, num_related, 1) - - -class Relation: - def __init__(self, relation_name, relation_id, is_peer, our_unit, backend, cache): - self.name = relation_name - self.id = relation_id - self.app = None - self.units = set() - - # For peer relations, both the remote and the local app are the same. - if is_peer: - self.app = our_unit.app - try: - for unit_name in backend.relation_list(self.id): - unit = cache.get(Unit, unit_name) - self.units.add(unit) - if self.app is None: - self.app = unit.app - except RelationNotFoundError: - # If the relation is dead, just treat it as if it has no remote units. - pass - self.data = RelationData(self, our_unit, backend) - - def __repr__(self): - return f'<{type(self).__module__}.{type(self).__name__} {self.name}:{self.id}>' - - -class RelationData(Mapping): - def __init__(self, relation, our_unit, backend): - self.relation = weakref.proxy(relation) - self._data = {our_unit: RelationDataContent(self.relation, our_unit, backend)} - self._data.update({our_unit.app: RelationDataContent(self.relation, our_unit.app, backend)}) - self._data.update({unit: RelationDataContent(self.relation, unit, backend) for unit in self.relation.units}) - # The relation might be dead so avoid a None key here. - if self.relation.app: - self._data.update({self.relation.app: RelationDataContent(self.relation, self.relation.app, backend)}) - - def __contains__(self, key): - return key in self._data - - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(self._data) - - def __getitem__(self, key): - return self._data[key] - - -# We mix in MutableMapping here to get some convenience implementations, but whether it's actually -# mutable or not is controlled by the flag. -class RelationDataContent(LazyMapping, MutableMapping): - - def __init__(self, relation, entity, backend): - self.relation = relation - self._entity = entity - self._backend = backend - self._is_app = isinstance(entity, Application) - - def _load(self): - try: - return self._backend.relation_get(self.relation.id, self._entity.name, self._is_app) - except RelationNotFoundError: - # Dead relations tell no tales (and have no data). - return {} - - def _is_mutable(self): - if self._is_app: - is_our_app = self._backend.app_name == self._entity.name - if not is_our_app: - return False - # Whether the application data bag is mutable or not depends on whether this unit is a leader or not, - # but this is not guaranteed to be always true during the same hook execution. - return self._backend.is_leader() - else: - is_our_unit = self._backend.unit_name == self._entity.name - if is_our_unit: - return True - return False - - def __setitem__(self, key, value): - if not self._is_mutable(): - raise RelationDataError(f'cannot set relation data for {self._entity.name}') - if not isinstance(value, str): - raise RelationDataError('relation data values must be strings') - - self._backend.relation_set(self.relation.id, key, value, self._is_app) - - # Don't load data unnecessarily if we're only updating. - if self._lazy_data is not None: - if value == '': - # Match the behavior of Juju, which is that setting the value to an empty string will - # remove the key entirely from the relation data. - del self._data[key] - else: - self._data[key] = value - - def __delitem__(self, key): - # Match the behavior of Juju, which is that setting the value to an empty string will - # remove the key entirely from the relation data. - self.__setitem__(key, '') - - -class ConfigData(LazyMapping): - - def __init__(self, backend): - self._backend = backend - - def _load(self): - return self._backend.config_get() - - -class StatusBase: - """Status values specific to applications and units.""" - - _statuses = {} - - def __init__(self, message): - self.message = message - - def __new__(cls, *args, **kwargs): - if cls is StatusBase: - raise TypeError("cannot instantiate a base class") - cls._statuses[cls.name] = cls - return super().__new__(cls) - - @classmethod - def from_name(cls, name, message): - return cls._statuses[name](message) - - -class ActiveStatus(StatusBase): - """The unit is ready. - - The unit believes it is correctly offering all the services it has been asked to offer. - """ - name = 'active' - - def __init__(self, message=None): - super().__init__(message or '') - - -class BlockedStatus(StatusBase): - """The unit requires manual intervention. - - An operator has to manually intervene to unblock the unit and let it proceed. - """ - name = 'blocked' - - -class MaintenanceStatus(StatusBase): - """The unit is performing maintenance tasks. - - The unit is not yet providing services, but is actively doing work in preparation for providing those services. - This is a "spinning" state, not an error state. It reflects activity on the unit itself, not on peers or related units. - """ - name = 'maintenance' - - -class UnknownStatus(StatusBase): - """The unit status is unknown. - - A unit-agent has finished calling install, config-changed and start, but the charm has not called status-set yet. - """ - name = 'unknown' - - def __init__(self): - # Unknown status cannot be set and does not have a message associated with it. - super().__init__('') - - -class WaitingStatus(StatusBase): - """A unit is unable to progress. - - The unit is unable to progress to an active state because an application to which it is related is not running. - """ - name = 'waiting' - - -class Resources: - """Object representing resources for the charm. - """ - - def __init__(self, names, backend): - self._backend = backend - self._paths = {name: None for name in names} - - def fetch(self, name): - """Fetch the resource from the controller or store. - - If successfully fetched, this returns a Path object to where the resource is stored - on disk, otherwise it raises a ModelError. - """ - if name not in self._paths: - raise RuntimeError(f'invalid resource name: {name}') - if self._paths[name] is None: - self._paths[name] = Path(self._backend.resource_get(name)) - return self._paths[name] - - -class Pod: - def __init__(self, backend): - self._backend = backend - - def set_spec(self, spec, k8s_resources=None): - if not self._backend.is_leader(): - raise ModelError('cannot set a pod spec as this unit is not a leader') - self._backend.pod_spec_set(spec, k8s_resources) - - -class StorageMapping(Mapping): - """Map of storage names to lists of Storage instances.""" - - def __init__(self, storage_names, backend): - self._backend = backend - self._storage_map = {storage_name: None for storage_name in storage_names} - - def __contains__(self, key): - return key in self._storage_map - - def __len__(self): - return len(self._storage_map) - - def __iter__(self): - return iter(self._storage_map) - - def __getitem__(self, storage_name): - storage_list = self._storage_map[storage_name] - if storage_list is None: - storage_list = self._storage_map[storage_name] = [] - for storage_id in self._backend.storage_list(storage_name): - storage_list.append(Storage(storage_name, storage_id, self._backend)) - return storage_list - - def request(self, storage_name, count=1): - """Requests new storage instances of a given name. - - Uses storage-add tool to request additional storage. Juju will notify the unit - via -storage-attached events when it becomes available. - """ - if storage_name not in self._storage_map: - raise ModelError(f'cannot add storage with {storage_name} as it is not present in the charm metadata') - self._backend.storage_add(storage_name, count) - - -class Storage: - - def __init__(self, storage_name, storage_id, backend): - self.name = storage_name - self.id = storage_id - self._backend = backend - self._location = None - - @property - def location(self): - if self._location is None: - self._location = Path(self._backend.storage_get(f'{self.name}/{self.id}', "location")) - return self._location - - -class ModelError(Exception): - pass - - -class TooManyRelatedAppsError(ModelError): - def __init__(self, relation_name, num_related, max_supported): - super().__init__(f'Too many remote applications on {relation_name} ({num_related} > {max_supported})') - self.relation_name = relation_name - self.num_related = num_related - self.max_supported = max_supported - - -class RelationDataError(ModelError): - pass - - -class RelationNotFoundError(ModelError): - pass - - -class InvalidStatusError(ModelError): - pass - - -class ModelBackend: - - LEASE_RENEWAL_PERIOD = datetime.timedelta(seconds=30) - - def __init__(self): - self.unit_name = os.environ['JUJU_UNIT_NAME'] - self.app_name = self.unit_name.split('/')[0] - - self._is_leader = None - self._leader_check_time = 0 - - def _run(self, *args, return_output=False, use_json=False): - kwargs = dict(stdout=PIPE, stderr=PIPE) - if use_json: - args += ('--format=json',) - try: - result = run(args, check=True, **kwargs) - except CalledProcessError as e: - raise ModelError(e.stderr) - if return_output: - if result.stdout is None: - return '' - else: - text = result.stdout.decode('utf8') - if use_json: - return json.loads(text) - else: - return text - - def relation_ids(self, relation_name): - relation_ids = self._run('relation-ids', relation_name, return_output=True, use_json=True) - return [int(relation_id.split(':')[-1]) for relation_id in relation_ids] - - def relation_list(self, relation_id): - try: - return self._run('relation-list', '-r', str(relation_id), return_output=True, use_json=True) - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise - - def relation_get(self, relation_id, member_name, is_app): - if not isinstance(is_app, bool): - raise TypeError('is_app parameter to relation_get must be a boolean') - - try: - return self._run('relation-get', '-r', str(relation_id), '-', member_name, f'--app={is_app}', return_output=True, use_json=True) - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise - - def relation_set(self, relation_id, key, value, is_app): - if not isinstance(is_app, bool): - raise TypeError('is_app parameter to relation_set must be a boolean') - - try: - return self._run('relation-set', '-r', str(relation_id), f'{key}={value}', f'--app={is_app}') - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise - - def config_get(self): - return self._run('config-get', return_output=True, use_json=True) - - def is_leader(self): - """Obtain the current leadership status for the unit the charm code is executing on. - - The value is cached for the duration of a lease which is 30s in Juju. - """ - now = time.monotonic() - time_since_check = datetime.timedelta(seconds=now - self._leader_check_time) - if time_since_check > self.LEASE_RENEWAL_PERIOD or self._is_leader is None: - # Current time MUST be saved before running is-leader to ensure the cache - # is only used inside the window that is-leader itself asserts. - self._leader_check_time = now - self._is_leader = self._run('is-leader', return_output=True, use_json=True) - - return self._is_leader - - def resource_get(self, resource_name): - return self._run('resource-get', resource_name, return_output=True).strip() - - def pod_spec_set(self, spec, k8s_resources): - tmpdir = Path(tempfile.mkdtemp('-pod-spec-set')) - try: - spec_path = tmpdir / 'spec.json' - spec_path.write_text(json.dumps(spec)) - args = ['--file', str(spec_path)] - if k8s_resources: - k8s_res_path = tmpdir / 'k8s-resources.json' - k8s_res_path.write_text(json.dumps(k8s_resources)) - args.extend(['--k8s-resources', str(k8s_res_path)]) - self._run('pod-spec-set', *args) - finally: - shutil.rmtree(tmpdir) - - def status_get(self, *, is_app=False): - """Get a status of a unit or an application. - app -- A boolean indicating whether the status should be retrieved for a unit or an application. - """ - return self._run('status-get', '--include-data', f'--application={is_app}') - - def status_set(self, status, message='', *, is_app=False): - """Set a status of a unit or an application. - app -- A boolean indicating whether the status should be set for a unit or an application. - """ - if not isinstance(is_app, bool): - raise TypeError('is_app parameter must be boolean') - return self._run('status-set', f'--application={is_app}', status, message) - - def storage_list(self, name): - return [int(s.split('/')[1]) for s in self._run('storage-list', name, return_output=True, use_json=True)] - - def storage_get(self, storage_name_id, attribute): - return self._run('storage-get', '-s', storage_name_id, attribute, return_output=True, use_json=True) - - def storage_add(self, name, count=1): - if not isinstance(count, int) or isinstance(count, bool): - raise TypeError(f'storage count must be integer, got: {count} ({type(count)})') - self._run('storage-add', f'{name}={count}') - - def action_get(self): - return self._run(f'action-get', return_output=True, use_json=True) - - def action_set(self, results): - self._run(f'action-set', *[f"{k}={v}" for k, v in results.items()]) - - def action_log(self, message): - self._run(f'action-log', f"{message}") - - def action_fail(self, message=''): - self._run(f'action-fail', f"{message}") - - def network_get(self, endpoint_name, relation_id=None): - """Return network info provided by network-get for a given endpoint. - - endpoint_name -- A name of an endpoint (relation name or extra-binding name). - relation_id -- An optional relation id to get network info for. - """ - cmd = ['network-get', endpoint_name] - if relation_id is not None: - cmd.extend(['-r', str(relation_id)]) - try: - return self._run(*cmd, return_output=True, use_json=True) - except ModelError as e: - if 'relation not found' in str(e): - raise RelationNotFoundError() from e - raise diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/metadata.yaml b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/metadata.yaml deleted file mode 100644 index 3b3aed87e96121224c63916b04009daf40fcab35..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/metadata.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: main -summary: A charm used for testing the basic operation of the entrypoint code. -maintainer: Dmitrii Shcherbakov -description: A charm used for testing the basic operation of the entrypoint code. -tags: - - misc -series: - - bionic - - cosmic - - disco -min-juju-version: 2.7.1 -provides: - db: - interface: db -requires: - mon: - interface: monitoring -peers: - ha: - interface: cluster -subordinate: false -storage: - disks: - type: block - multiple: - range: 0- diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/src/charm.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/src/charm.py deleted file mode 100755 index c20ae783ded4945b344e7602165e10af0e19c5b0..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/charms/test_main/src/charm.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2019 Canonical Ltd. -# -# 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 os -import base64 -import pickle -import sys -sys.path.append('lib') # noqa - -from ops.charm import CharmBase -from ops.main import main - -import logging - -logger = logging.getLogger() - - -class Charm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - - # This environment variable controls the test charm behavior. - charm_config = os.environ.get('CHARM_CONFIG') - if charm_config is not None: - self._charm_config = pickle.loads(base64.b64decode(charm_config)) - else: - self._charm_config = {} - - self._state_file = self._charm_config.get('STATE_FILE') - self._state = {} - - self._state['on_install'] = [] - self._state['on_start'] = [] - self._state['on_config_changed'] = [] - self._state['on_update_status'] = [] - self._state['on_leader_settings_changed'] = [] - self._state['on_db_relation_joined'] = [] - self._state['on_mon_relation_changed'] = [] - self._state['on_mon_relation_departed'] = [] - self._state['on_ha_relation_broken'] = [] - self._state['on_foo_bar_action'] = [] - self._state['on_start_action'] = [] - - # Observed event types per invocation. A list is used to preserve the order in which charm handlers have observed the events. - self._state['observed_event_types'] = [] - - self.framework.observe(self.on.install, self) - self.framework.observe(self.on.start, self) - self.framework.observe(self.on.config_changed, self) - self.framework.observe(self.on.update_status, self) - self.framework.observe(self.on.leader_settings_changed, self) - # Test relation events with endpoints from different - # sections (provides, requires, peers) as well. - self.framework.observe(self.on.db_relation_joined, self) - self.framework.observe(self.on.mon_relation_changed, self) - self.framework.observe(self.on.mon_relation_departed, self) - self.framework.observe(self.on.ha_relation_broken, self) - - if self._charm_config.get('USE_ACTIONS'): - self.framework.observe(self.on.start_action, self) - self.framework.observe(self.on.foo_bar_action, self) - - def _write_state(self): - """Write state variables so that the parent process can read them. - - Each invocation will override the previous state which is intentional. - """ - if self._state_file is not None: - with open(self._state_file, 'wb') as f: - pickle.dump(self._state, f) - - def on_install(self, event): - self._state['on_install'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - self._write_state() - - def on_start(self, event): - self._state['on_start'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - self._write_state() - - def on_config_changed(self, event): - self._state['on_config_changed'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - event.defer() - self._write_state() - - def on_update_status(self, event): - self._state['on_update_status'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - self._write_state() - - def on_leader_settings_changed(self, event): - self._state['on_leader_settings_changed'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - self._write_state() - - def on_db_relation_joined(self, event): - assert event.app is not None, 'application name cannot be None for a relation-joined event' - self._state['on_db_relation_joined'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - self._state['db_relation_joined_data'] = event.snapshot() - self._write_state() - - def on_mon_relation_changed(self, event): - assert event.app is not None, 'application name cannot be None for a relation-changed event' - if os.environ.get('JUJU_REMOTE_UNIT'): - assert event.unit is not None, 'a unit name cannot be None for a relation-changed event associated with a remote unit' - self._state['on_mon_relation_changed'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - self._state['mon_relation_changed_data'] = event.snapshot() - self._write_state() - - def on_mon_relation_departed(self, event): - assert event.app is not None, 'application name cannot be None for a relation-departed event' - self._state['on_mon_relation_departed'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - self._state['mon_relation_departed_data'] = event.snapshot() - self._write_state() - - def on_ha_relation_broken(self, event): - assert event.app is None, 'relation-broken events cannot have a reference to a remote application' - assert event.unit is None, 'relation broken events cannot have a reference to a remote unit' - self._state['on_ha_relation_broken'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - self._state['ha_relation_broken_data'] = event.snapshot() - self._write_state() - - def on_start_action(self, event): - assert event.handle.kind == 'start_action', 'event action name cannot be different from the one being handled' - self._state['on_start_action'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - self._write_state() - - def on_foo_bar_action(self, event): - assert event.handle.kind == 'foo_bar_action', 'event action name cannot be different from the one being handled' - self._state['on_foo_bar_action'].append(type(event)) - self._state['observed_event_types'].append(type(event)) - self._write_state() - - -if __name__ == '__main__': - main(Charm) diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_charm.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_charm.py deleted file mode 100755 index 26a943c4c0d0235dcc6f13629832dc046087510e..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_charm.py +++ /dev/null @@ -1,311 +0,0 @@ -#!/usr/bin/python3 -# Copyright 2019 Canonical Ltd. -# -# 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 os -import unittest -import tempfile -import shutil - -from pathlib import Path - -from ops.charm import ( - CharmBase, - CharmMeta, - CharmEvents, -) -from ops.framework import Framework, EventSource, EventBase -from ops.model import Model, ModelBackend - -from .test_helpers import fake_script, fake_script_calls - - -class TestCharm(unittest.TestCase): - - def setUp(self): - def restore_env(env): - os.environ.clear() - os.environ.update(env) - self.addCleanup(restore_env, os.environ.copy()) - - os.environ['PATH'] = f"{str(Path(__file__).parent / 'bin')}:{os.environ['PATH']}" - os.environ['JUJU_UNIT_NAME'] = 'local/0' - - self.tmpdir = Path(tempfile.mkdtemp()) - self.addCleanup(shutil.rmtree, self.tmpdir) - self.meta = CharmMeta() - - class CustomEvent(EventBase): - pass - - class TestCharmEvents(CharmEvents): - custom = EventSource(CustomEvent) - - # Relations events are defined dynamically and modify the class attributes. - # We use a subclass temporarily to prevent these side effects from leaking. - CharmBase.on = TestCharmEvents() - - def cleanup(): - CharmBase.on = CharmEvents() - self.addCleanup(cleanup) - - def create_framework(self): - model = Model('local/0', self.meta, ModelBackend()) - framework = Framework(self.tmpdir / "framework.data", self.tmpdir, self.meta, model) - self.addCleanup(framework.close) - return framework - - def test_basic(self): - - class MyCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - - self.started = False - framework.observe(self.on.start, self) - - def on_start(self, event): - self.started = True - - events = list(MyCharm.on.events()) - self.assertIn('install', events) - self.assertIn('custom', events) - - framework = self.create_framework() - charm = MyCharm(framework, None) - charm.on.start.emit() - - self.assertEqual(charm.started, True) - - def test_relation_events(self): - - class MyCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - self.seen = [] - for rel in ('req1', 'req-2', 'pro1', 'pro-2', 'peer1', 'peer-2'): - # Hook up relation events to generic handler. - self.framework.observe(self.on[rel].relation_joined, self.on_any_relation) - self.framework.observe(self.on[rel].relation_changed, self.on_any_relation) - self.framework.observe(self.on[rel].relation_departed, self.on_any_relation) - self.framework.observe(self.on[rel].relation_broken, self.on_any_relation) - - def on_any_relation(self, event): - assert event.relation.name == 'req1' - assert event.relation.app.name == 'remote' - self.seen.append(type(event).__name__) - - # language=YAML - self.meta = CharmMeta.from_yaml(metadata=''' -name: my-charm -requires: - req1: - interface: req1 - req-2: - interface: req2 -provides: - pro1: - interface: pro1 - pro-2: - interface: pro2 -peers: - peer1: - interface: peer1 - peer-2: - interface: peer2 -''') - - charm = MyCharm(self.create_framework(), None) - - rel = charm.framework.model.get_relation('req1', 1) - unit = charm.framework.model.get_unit('remote/0') - charm.on['req1'].relation_joined.emit(rel, unit) - charm.on['req1'].relation_changed.emit(rel, unit) - charm.on['req-2'].relation_changed.emit(rel, unit) - charm.on['pro1'].relation_departed.emit(rel, unit) - charm.on['pro-2'].relation_departed.emit(rel, unit) - charm.on['peer1'].relation_broken.emit(rel) - charm.on['peer-2'].relation_broken.emit(rel) - - self.assertEqual(charm.seen, [ - 'RelationJoinedEvent', - 'RelationChangedEvent', - 'RelationChangedEvent', - 'RelationDepartedEvent', - 'RelationDepartedEvent', - 'RelationBrokenEvent', - 'RelationBrokenEvent', - ]) - - def test_storage_events(self): - - class MyCharm(CharmBase): - def __init__(self, *args): - super().__init__(*args) - self.seen = [] - self.framework.observe(self.on['stor1'].storage_attached, self) - self.framework.observe(self.on['stor2'].storage_detaching, self) - self.framework.observe(self.on['stor3'].storage_attached, self) - self.framework.observe(self.on['stor-4'].storage_attached, self) - - def on_stor1_storage_attached(self, event): - self.seen.append(f'{type(event).__name__}') - - def on_stor2_storage_detaching(self, event): - self.seen.append(f'{type(event).__name__}') - - def on_stor3_storage_attached(self, event): - self.seen.append(f'{type(event).__name__}') - - def on_stor_4_storage_attached(self, event): - self.seen.append(f'{type(event).__name__}') - - # language=YAML - self.meta = CharmMeta.from_yaml(''' -name: my-charm -storage: - stor-4: - multiple: - range: 2-4 - type: filesystem - stor1: - type: filesystem - stor2: - multiple: - range: "2" - type: filesystem - stor3: - multiple: - range: 2- - type: filesystem -''') - - self.assertIsNone(self.meta.storages['stor1'].multiple_range) - self.assertEqual(self.meta.storages['stor2'].multiple_range, (2, 2)) - self.assertEqual(self.meta.storages['stor3'].multiple_range, (2, None)) - self.assertEqual(self.meta.storages['stor-4'].multiple_range, (2, 4)) - - charm = MyCharm(self.create_framework(), None) - - charm.on['stor1'].storage_attached.emit() - charm.on['stor2'].storage_detaching.emit() - charm.on['stor3'].storage_attached.emit() - charm.on['stor-4'].storage_attached.emit() - - self.assertEqual(charm.seen, [ - 'StorageAttachedEvent', - 'StorageDetachingEvent', - 'StorageAttachedEvent', - 'StorageAttachedEvent', - ]) - - @classmethod - def _get_action_test_meta(cls): - # language=YAML - return CharmMeta.from_yaml(metadata=''' -name: my-charm -''', actions=''' -foo-bar: - description: "Foos the bar." - params: - foo-name: - description: "A foo name to bar" - type: string - silent: - default: false - description: "" - type: boolean - required: foo-bar - title: foo-bar -start: - description: "Start the unit." -''') - - def _test_action_events(self, cmd_type): - - class MyCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - framework.observe(self.on.foo_bar_action, self) - framework.observe(self.on.start_action, self) - - def on_foo_bar_action(self, event): - self.seen_action_params = event.params - event.log('test-log') - event.set_results({'res': 'val with spaces'}) - event.fail('test-fail') - - def on_start_action(self, event): - pass - - fake_script(self, f'{cmd_type}-get', """echo '{"foo-name": "name", "silent": true}'""") - fake_script(self, f'{cmd_type}-set', "") - fake_script(self, f'{cmd_type}-log', "") - fake_script(self, f'{cmd_type}-fail', "") - self.meta = self._get_action_test_meta() - - os.environ[f'JUJU_{cmd_type.upper()}_NAME'] = 'foo-bar' - framework = self.create_framework() - charm = MyCharm(framework, None) - - events = list(MyCharm.on.events()) - self.assertIn('foo_bar_action', events) - self.assertIn('start_action', events) - - charm.on.foo_bar_action.emit() - self.assertEqual(charm.seen_action_params, {"foo-name": "name", "silent": True}) - self.assertEqual(fake_script_calls(self), [ - [f'{cmd_type}-get', '--format=json'], - [f'{cmd_type}-log', "test-log"], - [f'{cmd_type}-set', "res=val with spaces"], - [f'{cmd_type}-fail', "test-fail"], - ]) - - # Make sure that action events that do not match the current context are - # not possible to emit by hand. - with self.assertRaises(RuntimeError): - charm.on.start_action.emit() - - def test_action_events(self): - self._test_action_events('action') - - def _test_action_event_defer_fails(self, cmd_type): - - class MyCharm(CharmBase): - - def __init__(self, *args): - super().__init__(*args) - framework.observe(self.on.start_action, self) - - def on_start_action(self, event): - event.defer() - - fake_script(self, f'{cmd_type}-get', """echo '{"foo-name": "name", "silent": true}'""") - self.meta = self._get_action_test_meta() - - os.environ[f'JUJU_{cmd_type.upper()}_NAME'] = 'start' - framework = self.create_framework() - charm = MyCharm(framework, None) - - with self.assertRaises(RuntimeError): - charm.on.start_action.emit() - - def test_action_event_defer_fails(self): - self._test_action_event_defer_fails('action') - - -if __name__ == "__main__": - unittest.main() diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_framework.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_framework.py deleted file mode 100755 index fc364ea84049e544bb36f24b78150e7db86ba334..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_framework.py +++ /dev/null @@ -1,1200 +0,0 @@ -#!/usr/bin/python3 -# Copyright 2019 Canonical Ltd. -# -# 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 unittest -import tempfile -import shutil -import gc -import datetime - -from pathlib import Path - -from ops.framework import ( - Framework, Handle, EventSource, EventsBase, EventBase, Object, PreCommitEvent, CommitEvent, - NoSnapshotError, StoredState, StoredList, BoundStoredState, StoredStateData, SQLiteStorage -) - - -class TestFramework(unittest.TestCase): - - def setUp(self): - self.tmpdir = Path(tempfile.mkdtemp()) - self.addCleanup(shutil.rmtree, self.tmpdir) - default_timeout = SQLiteStorage.DB_LOCK_TIMEOUT - - def timeout_cleanup(): - SQLiteStorage.DB_LOCK_TIMEOUT = default_timeout - SQLiteStorage.DB_LOCK_TIMEOUT = datetime.timedelta(0) - self.addCleanup(timeout_cleanup) - - def create_framework(self): - framework = Framework(self.tmpdir / "framework.data", self.tmpdir, None, None) - self.addCleanup(framework.close) - return framework - - def test_handle_path(self): - cases = [ - (Handle(None, "root", None), "root"), - (Handle(None, "root", "1"), "root[1]"), - (Handle(Handle(None, "root", None), "child", None), "root/child"), - (Handle(Handle(None, "root", "1"), "child", "2"), "root[1]/child[2]"), - ] - for handle, path in cases: - self.assertEqual(str(handle), path) - self.assertEqual(Handle.from_path(path), handle) - - def test_handle_attrs_readonly(self): - handle = Handle(None, 'kind', 'key') - with self.assertRaises(AttributeError): - handle.parent = 'foo' - with self.assertRaises(AttributeError): - handle.kind = 'foo' - with self.assertRaises(AttributeError): - handle.key = 'foo' - with self.assertRaises(AttributeError): - handle.path = 'foo' - - def test_restore_unknown(self): - framework = self.create_framework() - - class Foo(Object): - pass - - handle = Handle(None, "a_foo", "some_key") - - framework.register_type(Foo, None, handle.kind) - - try: - framework.load_snapshot(handle) - except NoSnapshotError as e: - self.assertEqual(e.handle_path, str(handle)) - self.assertEqual(str(e), "no snapshot data found for a_foo[some_key] object") - else: - self.fail("exception NoSnapshotError not raised") - - def test_snapshot_roundtrip(self): - class Foo: - def __init__(self, handle, n): - self.handle = handle - self.my_n = n - - def snapshot(self): - return {"My N!": self.my_n} - - def restore(self, snapshot): - self.my_n = snapshot["My N!"] + 1 - - handle = Handle(None, "a_foo", "some_key") - event = Foo(handle, 1) - - framework1 = self.create_framework() - framework1.register_type(Foo, None, handle.kind) - framework1.save_snapshot(event) - framework1.commit() - framework1.close() - - framework2 = self.create_framework() - framework2.register_type(Foo, None, handle.kind) - event2 = framework2.load_snapshot(handle) - self.assertEqual(event2.my_n, 2) - - framework2.save_snapshot(event2) - del event2 - gc.collect() - event3 = framework2.load_snapshot(handle) - self.assertEqual(event3.my_n, 3) - - framework2.drop_snapshot(event.handle) - framework2.commit() - framework2.close() - - framework3 = self.create_framework() - framework3.register_type(Foo, None, handle.kind) - - self.assertRaises(NoSnapshotError, framework3.load_snapshot, handle) - - def test_simple_event_observer(self): - framework = self.create_framework() - - class MyEvent(EventBase): - pass - - class MyNotifier(Object): - foo = EventSource(MyEvent) - bar = EventSource(MyEvent) - baz = EventSource(MyEvent) - - class MyObserver(Object): - def __init__(self, parent, key): - super().__init__(parent, key) - self.seen = [] - - def on_any(self, event): - self.seen.append("on_any:" + event.handle.kind) - - def on_foo(self, event): - self.seen.append("on_foo:" + event.handle.kind) - - pub = MyNotifier(framework, "1") - obs = MyObserver(framework, "1") - - framework.observe(pub.foo, obs.on_any) - framework.observe(pub.bar, obs.on_any) - framework.observe(pub.foo, obs) # Method name defaults to on_. - - try: - framework.observe(pub.baz, obs) - except RuntimeError as e: - self.assertEqual(str(e), 'Observer method not provided explicitly and MyObserver type has no "on_baz" method') - else: - self.fail("RuntimeError not raised") - - pub.foo.emit() - pub.bar.emit() - - self.assertEqual(obs.seen, ["on_any:foo", "on_foo:foo", "on_any:bar"]) - - def test_bad_sig_observer(self): - - class MyEvent(EventBase): - pass - - class MyNotifier(Object): - foo = EventSource(MyEvent) - bar = EventSource(MyEvent) - baz = EventSource(MyEvent) - qux = EventSource(MyEvent) - - class MyObserver(Object): - def on_foo(self): - assert False, 'should not be reached' - - def on_bar(self, event, extra): - assert False, 'should not be reached' - - def on_baz(self, event, extra=None, *, k): - assert False, 'should not be reached' - - def on_qux(self, event, extra=None): - assert False, 'should not be reached' - - framework = self.create_framework() - pub = MyNotifier(framework, "pub") - obs = MyObserver(framework, "obs") - - with self.assertRaises(TypeError): - framework.observe(pub.foo, obs) - with self.assertRaises(TypeError): - framework.observe(pub.bar, obs) - with self.assertRaises(TypeError): - framework.observe(pub.baz, obs) - framework.observe(pub.qux, obs) - - def test_on_pre_commit_emitted(self): - framework = self.create_framework() - - class PreCommitObserver(Object): - - state = StoredState() - - def __init__(self, parent, key): - super().__init__(parent, key) - self.seen = [] - self.state.myinitdata = 40 - - def on_pre_commit(self, event): - self.state.myinitdata = 41 - self.state.mydata = 42 - self.seen.append(type(event)) - - def on_commit(self, event): - # Modifications made here will not be persisted. - self.state.myinitdata = 42 - self.state.mydata = 43 - self.state.myotherdata = 43 - self.seen.append(type(event)) - - obs = PreCommitObserver(framework, None) - - framework.observe(framework.on.pre_commit, obs.on_pre_commit) - - framework.commit() - - self.assertEqual(obs.state.myinitdata, 41) - self.assertEqual(obs.state.mydata, 42) - self.assertTrue(obs.seen, [PreCommitEvent, CommitEvent]) - framework.close() - - other_framework = self.create_framework() - - new_obs = PreCommitObserver(other_framework, None) - - self.assertEqual(obs.state.myinitdata, 41) - self.assertEqual(new_obs.state.mydata, 42) - - with self.assertRaises(AttributeError): - new_obs.state.myotherdata - - def test_defer_and_reemit(self): - framework = self.create_framework() - - class MyEvent(EventBase): - pass - - class MyNotifier1(Object): - a = EventSource(MyEvent) - b = EventSource(MyEvent) - - class MyNotifier2(Object): - c = EventSource(MyEvent) - - class MyObserver(Object): - def __init__(self, parent, key): - super().__init__(parent, key) - self.seen = [] - self.done = {} - - def on_any(self, event): - self.seen.append(event.handle.kind) - if not self.done.get(event.handle.kind): - event.defer() - - pub1 = MyNotifier1(framework, "1") - pub2 = MyNotifier2(framework, "1") - obs1 = MyObserver(framework, "1") - obs2 = MyObserver(framework, "2") - - framework.observe(pub1.a, obs1.on_any) - framework.observe(pub1.b, obs1.on_any) - framework.observe(pub1.a, obs2.on_any) - framework.observe(pub1.b, obs2.on_any) - framework.observe(pub2.c, obs2.on_any) - - pub1.a.emit() - pub1.b.emit() - pub2.c.emit() - - # Events remain stored because they were deferred. - ev_a_handle = Handle(pub1, "a", "1") - framework.load_snapshot(ev_a_handle) - ev_b_handle = Handle(pub1, "b", "2") - framework.load_snapshot(ev_b_handle) - ev_c_handle = Handle(pub2, "c", "3") - framework.load_snapshot(ev_c_handle) - # make sure the objects are gone before we reemit them - gc.collect() - - framework.reemit() - obs1.done["a"] = True - obs2.done["b"] = True - framework.reemit() - framework.reemit() - obs1.done["b"] = True - obs2.done["a"] = True - framework.reemit() - obs2.done["c"] = True - framework.reemit() - framework.reemit() - framework.reemit() - - self.assertEqual(" ".join(obs1.seen), "a b a b a b b b") - self.assertEqual(" ".join(obs2.seen), "a b c a b c a b c a c a c c") - - # Now the event objects must all be gone from storage. - self.assertRaises(NoSnapshotError, framework.load_snapshot, ev_a_handle) - self.assertRaises(NoSnapshotError, framework.load_snapshot, ev_b_handle) - self.assertRaises(NoSnapshotError, framework.load_snapshot, ev_c_handle) - - def test_custom_event_data(self): - framework = self.create_framework() - - class MyEvent(EventBase): - def __init__(self, handle, n): - super().__init__(handle) - self.my_n = n - - def snapshot(self): - return {"My N!": self.my_n} - - def restore(self, snapshot): - super().restore(snapshot) - self.my_n = snapshot["My N!"] + 1 - - class MyNotifier(Object): - foo = EventSource(MyEvent) - - class MyObserver(Object): - def __init__(self, parent, key): - super().__init__(parent, key) - self.seen = [] - - def on_foo(self, event): - self.seen.append(f"on_foo:{event.handle.kind}={event.my_n}") - event.defer() - - pub = MyNotifier(framework, "1") - obs = MyObserver(framework, "1") - - framework.observe(pub.foo, obs) - - pub.foo.emit(1) - - framework.reemit() - - # Two things being checked here: - # - # 1. There's a restore roundtrip before the event is first observed. - # That means the data is safe before it's ever seen, and the - # roundtrip logic is tested under normal circumstances. - # - # 2. The renotification restores from the pristine event, not - # from the one modified during the first restore (otherwise - # we'd get a foo=3). - # - self.assertEqual(obs.seen, ["on_foo:foo=2", "on_foo:foo=2"]) - - def test_weak_observer(self): - framework = self.create_framework() - - observed_events = [] - - class MyEvent(EventBase): - pass - - class MyEvents(EventsBase): - foo = EventSource(MyEvent) - - class MyNotifier(Object): - on = MyEvents() - - class MyObserver(Object): - def on_foo(self, event): - observed_events.append("foo") - - pub = MyNotifier(framework, "1") - obs = MyObserver(framework, "2") - - framework.observe(pub.on.foo, obs) - pub.on.foo.emit() - self.assertEqual(observed_events, ["foo"]) - # Now delete the observer, and note that when we emit the event, it - # doesn't update the local slice again - del obs - gc.collect() - pub.on.foo.emit() - self.assertEqual(observed_events, ["foo"]) - - def test_forget_and_multiple_objects(self): - framework = self.create_framework() - - class MyObject(Object): - pass - - o1 = MyObject(framework, "path") - # Creating a second object at the same path should fail with RuntimeError - with self.assertRaises(RuntimeError): - o2 = MyObject(framework, "path") - # Unless we _forget the object first - framework._forget(o1) - o2 = MyObject(framework, "path") - self.assertEqual(o1.handle.path, o2.handle.path) - # Deleting the tracked object should also work - del o2 - gc.collect() - o3 = MyObject(framework, "path") - self.assertEqual(o1.handle.path, o3.handle.path) - framework.close() - # Or using a second framework - framework_copy = self.create_framework() - o_copy = MyObject(framework_copy, "path") - self.assertEqual(o1.handle.path, o_copy.handle.path) - - def test_forget_and_multiple_objects_with_load_snapshot(self): - framework = self.create_framework() - - class MyObject(Object): - def __init__(self, parent, name): - super().__init__(parent, name) - self.value = name - - def snapshot(self): - return self.value - - def restore(self, value): - self.value = value - - framework.register_type(MyObject, None, MyObject.handle_kind) - o1 = MyObject(framework, "path") - framework.save_snapshot(o1) - framework.commit() - o_handle = o1.handle - del o1 - gc.collect() - o2 = framework.load_snapshot(o_handle) - # Trying to load_snapshot a second object at the same path should fail with RuntimeError - with self.assertRaises(RuntimeError): - framework.load_snapshot(o_handle) - # Unless we _forget the object first - framework._forget(o2) - o3 = framework.load_snapshot(o_handle) - self.assertEqual(o2.value, o3.value) - # A loaded object also prevents direct creation of an object - with self.assertRaises(RuntimeError): - MyObject(framework, "path") - framework.close() - # But we can create an object, or load a snapshot in a copy of the framework - framework_copy1 = self.create_framework() - o_copy1 = MyObject(framework_copy1, "path") - self.assertEqual(o_copy1.value, "path") - framework_copy1.close() - framework_copy2 = self.create_framework() - framework_copy2.register_type(MyObject, None, MyObject.handle_kind) - o_copy2 = framework_copy2.load_snapshot(o_handle) - self.assertEqual(o_copy2.value, "path") - - def test_events_base(self): - framework = self.create_framework() - - class MyEvent(EventBase): - pass - - class MyEvents(EventsBase): - foo = EventSource(MyEvent) - bar = EventSource(MyEvent) - - class MyNotifier(Object): - on = MyEvents() - - class MyObserver(Object): - def __init__(self, parent, key): - super().__init__(parent, key) - self.seen = [] - - def on_foo(self, event): - self.seen.append(f"on_foo:{event.handle.kind}") - event.defer() - - def on_bar(self, event): - self.seen.append(f"on_bar:{event.handle.kind}") - - pub = MyNotifier(framework, "1") - obs = MyObserver(framework, "1") - - # Confirm that temporary persistence of BoundEvents doesn't cause errors, - # and that events can be observed. - for bound_event in [pub.on.foo, pub.on.bar]: - framework.observe(bound_event, obs) - - # Confirm that events can be emitted and seen. - pub.on.foo.emit() - - self.assertEqual(obs.seen, ["on_foo:foo"]) - - def test_conflicting_event_attributes(self): - class MyEvent(EventBase): - pass - - event = EventSource(MyEvent) - - class MyEvents(EventsBase): - foo = event - - with self.assertRaises(RuntimeError) as cm: - class OtherEvents(EventsBase): - foo = event - self.assertEqual( - str(cm.exception.__cause__), - "EventSource(MyEvent) reused as MyEvents.foo and OtherEvents.foo") - - with self.assertRaises(RuntimeError) as cm: - class MyNotifier(Object): - on = MyEvents() - bar = event - self.assertEqual( - str(cm.exception.__cause__), - "EventSource(MyEvent) reused as MyEvents.foo and MyNotifier.bar") - - def test_reemit_ignores_unknown_event_type(self): - # The event type may have been gone for good, and nobody cares, - # so this shouldn't be an error scenario. - - framework = self.create_framework() - - class MyEvent(EventBase): - pass - - class MyNotifier(Object): - foo = EventSource(MyEvent) - - class MyObserver(Object): - def __init__(self, parent, key): - super().__init__(parent, key) - self.seen = [] - - def on_foo(self, event): - self.seen.append(event.handle) - event.defer() - - pub = MyNotifier(framework, "1") - obs = MyObserver(framework, "1") - - framework.observe(pub.foo, obs) - pub.foo.emit() - - event_handle = obs.seen[0] - self.assertEqual(event_handle.kind, "foo") - - framework.commit() - framework.close() - - framework_copy = self.create_framework() - - # No errors on missing event types here. - framework_copy.reemit() - - # Register the type and check that the event is gone from storage. - framework_copy.register_type(MyEvent, event_handle.parent, event_handle.kind) - self.assertRaises(NoSnapshotError, framework_copy.load_snapshot, event_handle) - - def test_auto_register_event_types(self): - framework = self.create_framework() - - class MyFoo(EventBase): - pass - - class MyBar(EventBase): - pass - - class MyEvents(EventsBase): - foo = EventSource(MyFoo) - - class MyNotifier(Object): - on = MyEvents() - bar = EventSource(MyBar) - - class MyObserver(Object): - def __init__(self, parent, key): - super().__init__(parent, key) - self.seen = [] - - def on_foo(self, event): - self.seen.append(f"on_foo:{type(event).__name__}:{event.handle.kind}") - event.defer() - - def on_bar(self, event): - self.seen.append(f"on_bar:{type(event).__name__}:{event.handle.kind}") - event.defer() - - pub = MyNotifier(framework, "1") - obs = MyObserver(framework, "1") - - pub.on.foo.emit() - pub.bar.emit() - - framework.observe(pub.on.foo, obs) - framework.observe(pub.bar, obs) - - pub.on.foo.emit() - pub.bar.emit() - - self.assertEqual(obs.seen, ["on_foo:MyFoo:foo", "on_bar:MyBar:bar"]) - - def test_dynamic_event_types(self): - framework = self.create_framework() - - class MyEventsA(EventsBase): - handle_kind = 'on_a' - - class MyEventsB(EventsBase): - handle_kind = 'on_b' - - class MyNotifier(Object): - on_a = MyEventsA() - on_b = MyEventsB() - - class MyObserver(Object): - def __init__(self, parent, key): - super().__init__(parent, key) - self.seen = [] - - def on_foo(self, event): - self.seen.append(f"on_foo:{type(event).__name__}:{event.handle.kind}") - event.defer() - - def on_bar(self, event): - self.seen.append(f"on_bar:{type(event).__name__}:{event.handle.kind}") - event.defer() - - pub = MyNotifier(framework, "1") - obs = MyObserver(framework, "1") - - class MyFoo(EventBase): - pass - - class MyBar(EventBase): - pass - - class DeadBeefEvent(EventBase): - pass - - class NoneEvent(EventBase): - pass - - pub.on_a.define_event("foo", MyFoo) - pub.on_b.define_event("bar", MyBar) - - framework.observe(pub.on_a.foo, obs) - framework.observe(pub.on_b.bar, obs) - - pub.on_a.foo.emit() - pub.on_b.bar.emit() - - self.assertEqual(obs.seen, ["on_foo:MyFoo:foo", "on_bar:MyBar:bar"]) - - # Definitions remained local to the specific type. - self.assertRaises(AttributeError, lambda: pub.on_a.bar) - self.assertRaises(AttributeError, lambda: pub.on_b.foo) - - # Try to use an event name which is not a valid python identifier. - with self.assertRaises(RuntimeError): - pub.on_a.define_event("dead-beef", DeadBeefEvent) - - # Try to use a python keyword for an event name. - with self.assertRaises(RuntimeError): - pub.on_a.define_event("None", NoneEvent) - - # Try to override an existing attribute. - with self.assertRaises(RuntimeError): - pub.on_a.define_event("foo", MyFoo) - - def test_event_key_roundtrip(self): - class MyEvent(EventBase): - def __init__(self, handle, value): - super().__init__(handle) - self.value = value - - def snapshot(self): - return self.value - - def restore(self, value): - self.value = value - - class MyNotifier(Object): - foo = EventSource(MyEvent) - - class MyObserver(Object): - has_deferred = False - - def __init__(self, parent, key): - super().__init__(parent, key) - self.seen = [] - - def on_foo(self, event): - self.seen.append((event.handle.key, event.value)) - # Only defer the first event and once. - if not MyObserver.has_deferred: - event.defer() - MyObserver.has_deferred = True - - framework1 = self.create_framework() - pub1 = MyNotifier(framework1, "pub") - obs1 = MyObserver(framework1, "obs") - framework1.observe(pub1.foo, obs1) - pub1.foo.emit('first') - self.assertEqual(obs1.seen, [('1', 'first')]) - - framework1.commit() - framework1.close() - del framework1 - - framework2 = self.create_framework() - pub2 = MyNotifier(framework2, "pub") - obs2 = MyObserver(framework2, "obs") - framework2.observe(pub2.foo, obs2) - pub2.foo.emit('second') - framework2.reemit() - - # First observer didn't get updated, since framework it was bound to is gone. - self.assertEqual(obs1.seen, [('1', 'first')]) - # Second observer saw the new event plus the reemit of the first event. - # (The event key goes up by 2 due to the pre-commit and commit events.) - self.assertEqual(obs2.seen, [('4', 'second'), ('1', 'first')]) - - def test_helper_properties(self): - framework = self.create_framework() - framework.model = 'test-model' - framework.meta = 'test-meta' - - my_obj = Object(framework, 'my_obj') - self.assertEqual(my_obj.model, framework.model) - self.assertEqual(my_obj.meta, framework.meta) - self.assertEqual(my_obj.charm_dir, framework.charm_dir) - - def test_ban_concurrent_frameworks(self): - f = self.create_framework() - with self.assertRaises(Exception) as cm: - self.create_framework() - self.assertIn('database is locked', str(cm.exception)) - f.close() - - -class TestStoredState(unittest.TestCase): - - def setUp(self): - self.tmpdir = Path(tempfile.mkdtemp()) - self.addCleanup(shutil.rmtree, self.tmpdir) - - def create_framework(self, cls=Framework): - framework = cls(self.tmpdir / "framework.data", self.tmpdir, None, None) - self.addCleanup(framework.close) - return framework - - def test_basic_state_storage(self): - framework = self.create_framework() - - class SomeObject(Object): - state = StoredState() - - obj = SomeObject(framework, "1") - - try: - obj.state.foo - except AttributeError as e: - self.assertEqual(str(e), "attribute 'foo' is not stored") - else: - self.fail("AttributeError not raised") - - try: - obj.state.on = "nonono" - except AttributeError as e: - self.assertEqual(str(e), "attribute 'on' is reserved and cannot be set") - else: - self.fail("AttributeError not raised") - - obj.state.foo = 41 - obj.state.foo = 42 - obj.state.bar = "s" - - self.assertEqual(obj.state.foo, 42) - - framework.commit() - - # This won't be committed, and should not be seen. - obj.state.foo = 43 - - framework.close() - - # Since this has the same absolute object handle, it will get its state back. - framework_copy = self.create_framework() - obj_copy = SomeObject(framework_copy, "1") - self.assertEqual(obj_copy.state.foo, 42) - self.assertEqual(obj_copy.state.bar, "s") - - def test_mutable_types_invalid(self): - framework = self.create_framework() - - class SomeObject(Object): - state = StoredState() - - obj = SomeObject(framework, '1') - try: - class CustomObject: - pass - obj.state.foo = CustomObject() - except AttributeError as e: - self.assertEqual(str(e), "attribute 'foo' cannot be set to CustomObject: must be int/dict/list/etc") - else: - self.fail('AttributeError not raised') - - framework.commit() - - def test_mutable_types(self): - # Test and validation functions in a list of 2-tuples. - # Assignment and keywords like del are not supported in lambdas so functions are used instead. - test_operations = [( - lambda: {}, # Operand A. - None, # Operand B. - {}, # Expected result. - lambda a, b: None, # Operation to perform. - lambda res, expected_res: self.assertEqual(res, expected_res) # Validation to perform. - ), ( - lambda: {}, - {'a': {}}, - {'a': {}}, - lambda a, b: a.update(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: {'a': {}}, - {'b': 'c'}, - {'a': {'b': 'c'}}, - lambda a, b: a['a'].update(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: {'a': {'b': 'c'}}, - {'d': 'e'}, - {'a': {'b': 'c', 'd': 'e'}}, - lambda a, b: a['a'].update(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: {'a': {'b': 'c', 'd': 'e'}}, - 'd', - {'a': {'b': 'c'}}, - lambda a, b: a['a'].pop(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: {'s': set()}, - 'a', - {'s': {'a'}}, - lambda a, b: a['s'].add(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: {'s': {'a'}}, - 'a', - {'s': set()}, - lambda a, b: a['s'].discard(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: [], - None, - [], - lambda a, b: None, - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: [], - 'a', - ['a'], - lambda a, b: a.append(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: ['a'], - ['c'], - ['a', ['c']], - lambda a, b: a.append(b), - lambda res, expected_res: ( - self.assertEqual(res, expected_res), - self.assertIsInstance(res[1], StoredList), - ) - ), ( - lambda: ['a', ['c']], - 'b', - ['b', 'a', ['c']], - lambda a, b: a.insert(0, b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: ['b', 'a', ['c']], - ['d'], - ['b', ['d'], 'a', ['c']], - lambda a, b: a.insert(1, b), - lambda res, expected_res: ( - self.assertEqual(res, expected_res), - self.assertIsInstance(res[1], StoredList) - ), - ), ( - lambda: ['b', 'a', ['c']], - ['d'], - ['b', ['d'], ['c']], - # a[1] = b - lambda a, b: a.__setitem__(1, b), - lambda res, expected_res: ( - self.assertEqual(res, expected_res), - self.assertIsInstance(res[1], StoredList) - ), - ), ( - lambda: ['b', ['d'], 'a', ['c']], - 0, - [['d'], 'a', ['c']], - lambda a, b: a.pop(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: [['d'], 'a', ['c']], - ['d'], - ['a', ['c']], - lambda a, b: a.remove(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: ['a', ['c']], - 'd', - ['a', ['c', 'd']], - lambda a, b: a[1].append(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: ['a', ['c', 'd']], - 1, - ['a', ['c']], - lambda a, b: a[1].pop(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: ['a', ['c']], - 'd', - ['a', ['c', 'd']], - lambda a, b: a[1].insert(1, b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: ['a', ['c', 'd']], - 'd', - ['a', ['c']], - lambda a, b: a[1].remove(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: set(), - None, - set(), - lambda a, b: None, - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: set(), - 'a', - set(['a']), - lambda a, b: a.add(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: set(['a']), - 'a', - set(), - lambda a, b: a.discard(b), - lambda res, expected_res: self.assertEqual(res, expected_res) - ), ( - lambda: set(), - {'a'}, - set(), - # Nested sets are not allowed as sets themselves are not hashable. - lambda a, b: self.assertRaises(TypeError, a.add, b), - lambda res, expected_res: self.assertEqual(res, expected_res) - )] - - class SomeObject(Object): - state = StoredState() - - class WrappedFramework(Framework): - def __init__(self, data_path, charm_dir, meta, model): - super().__init__(data_path, charm_dir, meta, model) - self.snapshots = [] - - def save_snapshot(self, value): - if value.handle.path == 'SomeObject[1]/StoredStateData[state]': - self.snapshots.append((type(value), value.snapshot())) - return super().save_snapshot(value) - - # Validate correctness of modification operations. - for get_a, b, expected_res, op, validate_op in test_operations: - framework = self.create_framework(cls=WrappedFramework) - obj = SomeObject(framework, '1') - - obj.state.a = get_a() - self.assertTrue(isinstance(obj.state, BoundStoredState)) - - op(obj.state.a, b) - validate_op(obj.state.a, expected_res) - - obj.state.a = get_a() - framework.commit() - # We should see an update for initializing a - self.assertEqual(framework.snapshots, [ - (StoredStateData, {'a': get_a()}), - ]) - del obj - gc.collect() - obj_copy1 = SomeObject(framework, '1') - self.assertEqual(obj_copy1.state.a, get_a()) - - op(obj_copy1.state.a, b) - validate_op(obj_copy1.state.a, expected_res) - framework.commit() - framework.close() - - framework_copy = self.create_framework(cls=WrappedFramework) - - obj_copy2 = SomeObject(framework_copy, '1') - - validate_op(obj_copy2.state.a, expected_res) - - # Commit saves the pre-commit and commit events, and the framework event counter, but shouldn't update the stored state of my object - framework.snapshots.clear() - framework_copy.commit() - self.assertEqual(framework_copy.snapshots, []) - framework_copy.close() - - def test_comparison_operations(self): - test_operations = [( - {"1"}, # Operand A. - {"1", "2"}, # Operand B. - lambda a, b: a < b, # Operation to test. - True, # Result of op(A, B). - False, # Result of op(B, A). - ), ( - {"1"}, - {"1", "2"}, - lambda a, b: a > b, - False, - True - ), ( - # Empty set comparison. - set(), - set(), - lambda a, b: a == b, - True, - True - ), ( - {"a", "c"}, - {"c", "a"}, - lambda a, b: a == b, - True, - True - ), ( - dict(), - dict(), - lambda a, b: a == b, - True, - True - ), ( - {"1": "2"}, - {"1": "2"}, - lambda a, b: a == b, - True, - True - ), ( - {"1": "2"}, - {"1": "3"}, - lambda a, b: a == b, - False, - False - ), ( - [], - [], - lambda a, b: a == b, - True, - True - ), ( - [1, 2], - [1, 2], - lambda a, b: a == b, - True, - True - ), ( - [1, 2, 5, 6], - [1, 2, 5, 8, 10], - lambda a, b: a <= b, - True, - False - ), ( - [1, 2, 5, 6], - [1, 2, 5, 8, 10], - lambda a, b: a < b, - True, - False - ), ( - [1, 2, 5, 8], - [1, 2, 5, 6, 10], - lambda a, b: a > b, - True, - False - ), ( - [1, 2, 5, 8], - [1, 2, 5, 6, 10], - lambda a, b: a >= b, - True, - False - )] - - class SomeObject(Object): - state = StoredState() - - framework = self.create_framework() - - for i, (a, b, op, op_ab, op_ba) in enumerate(test_operations): - obj = SomeObject(framework, str(i)) - obj.state.a = a - self.assertEqual(op(obj.state.a, b), op_ab) - self.assertEqual(op(b, obj.state.a), op_ba) - - def test_set_operations(self): - test_operations = [( - {"1"}, # A set to test an operation against (other_set). - lambda a, b: a | b, # An operation to test. - {"1", "a", "b"}, # The expected result of operation(obj.state.set, other_set). - {"1", "a", "b"} # The expected result of operation(other_set, obj.state.set). - ), ( - {"a", "c"}, - lambda a, b: a - b, - {"b"}, - {"c"} - ), ( - {"a", "c"}, - lambda a, b: a & b, - {"a"}, - {"a"} - ), ( - {"a", "c", "d"}, - lambda a, b: a ^ b, - {"b", "c", "d"}, - {"b", "c", "d"} - ), ( - set(), - lambda a, b: set(a), - {"a", "b"}, - set() - )] - - class SomeObject(Object): - state = StoredState() - - framework = self.create_framework() - - # Validate that operations between StoredSet and built-in sets only result in built-in sets being returned. - # Make sure that commutativity is preserved and that the original sets are not changed or used as a result. - for i, (variable_operand, operation, ab_res, ba_res) in enumerate(test_operations): - obj = SomeObject(framework, str(i)) - obj.state.set = {"a", "b"} - - for a, b, expected in [(obj.state.set, variable_operand, ab_res), (variable_operand, obj.state.set, ba_res)]: - old_a = set(a) - old_b = set(b) - - result = operation(a, b) - self.assertEqual(result, expected) - - # Common sanity checks - self.assertIsNot(obj.state.set._under, result) - self.assertIsNot(result, a) - self.assertIsNot(result, b) - self.assertEqual(a, old_a) - self.assertEqual(b, old_b) - - def test_set_default(self): - framework = self.create_framework() - - class StatefulObject(Object): - state = StoredState() - parent = StatefulObject(framework, 'key') - parent.state.set_default(foo=1) - self.assertEqual(parent.state.foo, 1) - parent.state.set_default(foo=2) - # foo was already set, so it doesn't get replaced - self.assertEqual(parent.state.foo, 1) - parent.state.set_default(foo=3, bar=4) - self.assertEqual(parent.state.foo, 1) - self.assertEqual(parent.state.bar, 4) - # reloading the state still leaves things at the default values - framework.commit() - del parent - parent = StatefulObject(framework, 'key') - parent.state.set_default(foo=5, bar=6) - self.assertEqual(parent.state.foo, 1) - self.assertEqual(parent.state.bar, 4) - # TODO(jam) 2020-01-30: is there a clean way to tell that parent.state._data.dirty is False? - - -if __name__ == "__main__": - unittest.main() diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_helpers.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_helpers.py deleted file mode 100644 index 7d7379fec7481a081d7de5fcfe40ecb8a5186dbd..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_helpers.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright 2019 Canonical Ltd. -# -# 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 os -import pathlib -import subprocess -import shutil -import tempfile -import unittest - - -def fake_script(test_case, name, content): - if not hasattr(test_case, 'fake_script_path'): - fake_script_path = tempfile.mkdtemp('-fake_script') - os.environ['PATH'] = f'{fake_script_path}:{os.environ["PATH"]}' - - def cleanup(): - shutil.rmtree(fake_script_path) - os.environ['PATH'] = os.environ['PATH'].replace(fake_script_path + ':', '') - - test_case.addCleanup(cleanup) - test_case.fake_script_path = pathlib.Path(fake_script_path) - - with open(test_case.fake_script_path / name, "w") as f: - # Before executing the provided script, dump the provided arguments in calls.txt. - f.write('#!/bin/bash\n{ echo -n $(basename $0); for s in "$@"; do echo -n \\;$s; done; echo; } >> $(dirname $0)/calls.txt\n' + content) - os.chmod(test_case.fake_script_path / name, 0o755) - - -def fake_script_calls(test_case, clear=False): - with open(test_case.fake_script_path / 'calls.txt', 'r+') as f: - calls = [line.split(';') for line in f.read().splitlines()] - if clear: - f.truncate(0) - return calls - - -class FakeScriptTest(unittest.TestCase): - - def test_fake_script_works(self): - fake_script(self, 'foo', 'echo foo runs') - fake_script(self, 'bar', 'echo bar runs') - output = subprocess.getoutput('foo a "b c"; bar "d e" f') - self.assertEqual(output, 'foo runs\nbar runs') - self.assertEqual(fake_script_calls(self), [ - ['foo', 'a', 'b c'], - ['bar', 'd e', 'f'], - ]) - - def test_fake_script_clear(self): - fake_script(self, 'foo', 'echo foo runs') - - output = subprocess.getoutput('foo a "b c"') - self.assertEqual(output, 'foo runs') - - self.assertEqual(fake_script_calls(self, clear=True), [['foo', 'a', 'b c']]) - - fake_script(self, 'bar', 'echo bar runs') - - output = subprocess.getoutput('bar "d e" f') - self.assertEqual(output, 'bar runs') - - self.assertEqual(fake_script_calls(self, clear=True), [['bar', 'd e', 'f']]) - - self.assertEqual(fake_script_calls(self, clear=True), []) diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_jujuversion.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_jujuversion.py deleted file mode 100755 index d19fd60045800c61378bcb0496fc79926bc71110..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_jujuversion.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2019 Canonical Ltd. -# -# 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 unittest - -from ops.jujuversion import JujuVersion - - -class TestJujuVersion(unittest.TestCase): - - def test_parsing(self): - test_cases = [ - ("0.0.0", 0, 0, '', 0, 0), - ("0.0.2", 0, 0, '', 2, 0), - ("0.1.0", 0, 1, '', 0, 0), - ("0.2.3", 0, 2, '', 3, 0), - ("10.234.3456", 10, 234, '', 3456, 0), - ("10.234.3456.1", 10, 234, '', 3456, 1), - ("1.21-alpha12", 1, 21, 'alpha', 12, 0), - ("1.21-alpha1.34", 1, 21, 'alpha', 1, 34), - ("2.7", 2, 7, '', 0, 0) - ] - - for vs, major, minor, tag, patch, build in test_cases: - v = JujuVersion(vs) - self.assertEqual(v.major, major) - self.assertEqual(v.minor, minor) - self.assertEqual(v.tag, tag) - self.assertEqual(v.patch, patch) - self.assertEqual(v.build, build) - - def test_parsing_errors(self): - invalid_versions = [ - "xyz", - "foo.bar", - "foo.bar.baz", - "dead.beef.ca.fe", - "1234567890.2.1", # The major version is too long. - "0.2..1", # Two periods next to each other. - "1.21.alpha1", # Tag comes after period. - "1.21-alpha", # No patch number but a tag is present. - "1.21-alpha1beta", # Non-numeric string after the patch number. - "1.21-alpha-dev", # Tag duplication. - "1.21-alpha_dev3", # Underscore in a tag. - "1.21-alpha123dev3", # Non-numeric string after the patch number. - ] - for v in invalid_versions: - with self.assertRaises(RuntimeError): - JujuVersion(v) - - def test_equality(self): - test_cases = [ - ("1.0.0", "1.0.0", True), - ("01.0.0", "1.0.0", True), - ("10.0.0", "9.0.0", False), - ("1.0.0", "1.0.1", False), - ("1.0.1", "1.0.0", False), - ("1.0.0", "1.1.0", False), - ("1.1.0", "1.0.0", False), - ("1.0.0", "2.0.0", False), - ("1.2-alpha1", "1.2.0", False), - ("1.2-alpha2", "1.2-alpha1", False), - ("1.2-alpha2.1", "1.2-alpha2", False), - ("1.2-alpha2.2", "1.2-alpha2.1", False), - ("1.2-beta1", "1.2-alpha1", False), - ("1.2-beta1", "1.2-alpha2.1", False), - ("1.2-beta1", "1.2.0", False), - ("1.2.1", "1.2.0", False), - ("2.0.0", "1.0.0", False), - ("2.0.0.0", "2.0.0", True), - ("2.0.0.0", "2.0.0.0", True), - ("2.0.0.1", "2.0.0.0", False), - ("2.0.1.10", "2.0.0.0", False), - ] - - for a, b, expected in test_cases: - self.assertEqual(JujuVersion(a) == JujuVersion(b), expected) - self.assertEqual(JujuVersion(a) == b, expected) - - def test_comparison(self): - test_cases = [ - ("1.0.0", "1.0.0", False, True), - ("01.0.0", "1.0.0", False, True), - ("10.0.0", "9.0.0", False, False), - ("1.0.0", "1.0.1", True, True), - ("1.0.1", "1.0.0", False, False), - ("1.0.0", "1.1.0", True, True), - ("1.1.0", "1.0.0", False, False), - ("1.0.0", "2.0.0", True, True), - ("1.2-alpha1", "1.2.0", True, True), - ("1.2-alpha2", "1.2-alpha1", False, False), - ("1.2-alpha2.1", "1.2-alpha2", False, False), - ("1.2-alpha2.2", "1.2-alpha2.1", False, False), - ("1.2-beta1", "1.2-alpha1", False, False), - ("1.2-beta1", "1.2-alpha2.1", False, False), - ("1.2-beta1", "1.2.0", True, True), - ("1.2.1", "1.2.0", False, False), - ("2.0.0", "1.0.0", False, False), - ("2.0.0.0", "2.0.0", False, True), - ("2.0.0.0", "2.0.0.0", False, True), - ("2.0.0.1", "2.0.0.0", False, False), - ("2.0.1.10", "2.0.0.0", False, False), - ] - - for a, b, expected_strict, expected_weak in test_cases: - self.assertEqual(JujuVersion(a) < JujuVersion(b), expected_strict) - self.assertEqual(JujuVersion(a) <= JujuVersion(b), expected_weak) - self.assertEqual(JujuVersion(b) > JujuVersion(a), expected_strict) - self.assertEqual(JujuVersion(b) >= JujuVersion(a), expected_weak) - # Implicit conversion. - self.assertEqual(JujuVersion(a) < b, expected_strict) - self.assertEqual(JujuVersion(a) <= b, expected_weak) - self.assertEqual(b > JujuVersion(a), expected_strict) - self.assertEqual(b >= JujuVersion(a), expected_weak) - - -if __name__ == "__main__": - unittest.main() diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_main.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_main.py deleted file mode 100755 index a4ced9485f62ecaa3fa12a9cdbc06bd2327d4a81..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_main.py +++ /dev/null @@ -1,362 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2019 Canonical Ltd. -# -# 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 unittest -import logging -import os -import sys -import subprocess -import pickle -import base64 -import tempfile -import shutil - -import importlib.util - -from pathlib import Path - -from ops.charm import ( - CharmBase, - CharmEvents, - HookEvent, - InstallEvent, - StartEvent, - ConfigChangedEvent, - UpgradeCharmEvent, - UpdateStatusEvent, - LeaderSettingsChangedEvent, - RelationJoinedEvent, - RelationChangedEvent, - RelationDepartedEvent, - RelationBrokenEvent, - RelationEvent, - StorageAttachedEvent, - ActionEvent, -) - -from .test_helpers import fake_script - -# This relies on the expected repository structure to find a path to source of the charm under test. -TEST_CHARM_DIR = Path(f'{__file__}/../charms/test_main').resolve() - -logger = logging.getLogger(__name__) - - -class SymlinkTargetError(Exception): - pass - - -class EventSpec: - def __init__(self, event_type, event_name, env_var=None, relation_id=None, remote_app=None, remote_unit=None, - charm_config=None): - self.event_type = event_type - self.event_name = event_name - self.env_var = env_var - self.relation_id = relation_id - self.remote_app = remote_app - self.remote_unit = remote_unit - self.charm_config = charm_config - - -class TestMain(unittest.TestCase): - - def setUp(self): - self._setup_charm_dir() - - _, tmp_file = tempfile.mkstemp() - self._state_file = Path(tmp_file) - self.addCleanup(self._state_file.unlink) - - # Relations events are defined dynamically and modify the class attributes. - # We use a subclass temporarily to prevent these side effects from leaking. - class TestCharmEvents(CharmEvents): - pass - CharmBase.on = TestCharmEvents() - - def cleanup(): - shutil.rmtree(self.JUJU_CHARM_DIR) - CharmBase.on = CharmEvents() - self.addCleanup(cleanup) - - def _setup_charm_dir(self): - self.JUJU_CHARM_DIR = Path(tempfile.mkdtemp()) / 'test_main' - self.hooks_dir = self.JUJU_CHARM_DIR / 'hooks' - self.charm_exec_path = os.path.relpath(self.JUJU_CHARM_DIR / 'src/charm.py', self.hooks_dir) - shutil.copytree(TEST_CHARM_DIR, self.JUJU_CHARM_DIR) - - charm_spec = importlib.util.spec_from_file_location("charm", str(self.JUJU_CHARM_DIR / 'src/charm.py')) - self.charm_module = importlib.util.module_from_spec(charm_spec) - charm_spec.loader.exec_module(self.charm_module) - - self._prepare_initial_hooks() - - def _prepare_initial_hooks(self): - initial_hooks = ('install', 'start', 'upgrade-charm', 'disks-storage-attached') - self.hooks_dir.mkdir() - for hook in initial_hooks: - hook_path = self.hooks_dir / hook - hook_path.symlink_to(self.charm_exec_path) - - def _prepare_actions(self): - actions_meta = ''' -foo-bar: - description: Foos the bar. - title: foo-bar - params: - foo-name: - type: string - description: A foo name to bar. - silent: - type: boolean - description: - default: false - required: - - foo-name -start: - description: Start the unit.''' - actions_dir_name = 'actions' - actions_meta_file = 'actions.yaml' - - with open(self.JUJU_CHARM_DIR / actions_meta_file, 'w+') as f: - f.write(actions_meta) - actions_dir = self.JUJU_CHARM_DIR / actions_dir_name - actions_dir.mkdir() - for action_name in ('start', 'foo-bar'): - action_path = actions_dir / action_name - action_path.symlink_to(self.charm_exec_path) - - def _read_and_clear_state(self): - state = None - if self._state_file.stat().st_size: - with open(self._state_file, 'r+b') as state_file: - state = pickle.load(state_file) - state_file.truncate() - return state - - def _simulate_event(self, event_spec): - env = { - 'PATH': f"{str(Path(__file__).parent / 'bin')}:{os.environ['PATH']}", - 'JUJU_CHARM_DIR': self.JUJU_CHARM_DIR, - 'JUJU_UNIT_NAME': 'test_main/0', - 'CHARM_CONFIG': event_spec.charm_config, - } - if issubclass(event_spec.event_type, RelationEvent): - rel_name = event_spec.event_name.split('_')[0] - env.update({ - 'JUJU_RELATION': rel_name, - 'JUJU_RELATION_ID': str(event_spec.relation_id), - }) - remote_app = event_spec.remote_app - # For juju < 2.7 app name is extracted from JUJU_REMOTE_UNIT. - if remote_app is not None: - env['JUJU_REMOTE_APP'] = remote_app - - remote_unit = event_spec.remote_unit - if remote_unit is None: - remote_unit = '' - - env['JUJU_REMOTE_UNIT'] = remote_unit - else: - env.update({ - 'JUJU_REMOTE_UNIT': '', - 'JUJU_REMOTE_APP': '', - }) - if issubclass(event_spec.event_type, ActionEvent): - event_filename = event_spec.event_name[:-len('_action')].replace('_', '-') - env.update({ - event_spec.env_var: event_filename, - }) - if event_spec.env_var == 'JUJU_ACTION_NAME': - event_dir = 'actions' - else: - raise RuntimeError('invalid envar name specified for a action event') - else: - event_filename = event_spec.event_name.replace('_', '-') - event_dir = 'hooks' - event_file = self.JUJU_CHARM_DIR / event_dir / event_filename - # Note that sys.executable is used to make sure we are using the same - # interpreter for the child process to support virtual environments. - subprocess.check_call([sys.executable, event_file], env=env, cwd=self.JUJU_CHARM_DIR) - return self._read_and_clear_state() - - def test_event_reemitted(self): - # base64 encoding is used to avoid null bytes. - charm_config = base64.b64encode(pickle.dumps({ - 'STATE_FILE': self._state_file, - })) - - # First run "install" to make sure all hooks are set up. - state = self._simulate_event(EventSpec(InstallEvent, 'install', charm_config=charm_config)) - self.assertEqual(state['observed_event_types'], [InstallEvent]) - - state = self._simulate_event(EventSpec(ConfigChangedEvent, 'config-changed', charm_config=charm_config)) - self.assertEqual(state['observed_event_types'], [ConfigChangedEvent]) - - # Re-emit should pick the deferred config-changed. - state = self._simulate_event(EventSpec(UpdateStatusEvent, 'update-status', charm_config=charm_config)) - self.assertEqual(state['observed_event_types'], [ConfigChangedEvent, UpdateStatusEvent]) - - def test_multiple_events_handled(self): - self._prepare_actions() - - charm_config = base64.b64encode(pickle.dumps({ - 'STATE_FILE': self._state_file, - })) - actions_charm_config = base64.b64encode(pickle.dumps({ - 'STATE_FILE': self._state_file, - 'USE_ACTIONS': True, - })) - - fake_script(self, 'action-get', "echo '{}'") - - # Sample events with a different amount of dashes used - # and with endpoints from different sections of metadata.yaml - events_under_test = [( - EventSpec(InstallEvent, 'install', charm_config=charm_config), - {}, - ), ( - EventSpec(StartEvent, 'start', charm_config=charm_config), - {}, - ), ( - EventSpec(UpdateStatusEvent, 'update_status', charm_config=charm_config), - {}, - ), ( - EventSpec(LeaderSettingsChangedEvent, 'leader_settings_changed', charm_config=charm_config), - {}, - ), ( - EventSpec(RelationJoinedEvent, 'db_relation_joined', relation_id=1, - remote_app='remote', remote_unit='remote/0', charm_config=charm_config), - {'relation_name': 'db', 'relation_id': 1, 'app_name': 'remote', 'unit_name': 'remote/0'}, - ), ( - EventSpec(RelationChangedEvent, 'mon_relation_changed', relation_id=2, - remote_app='remote', remote_unit='remote/0', charm_config=charm_config), - {'relation_name': 'mon', 'relation_id': 2, 'app_name': 'remote', 'unit_name': 'remote/0'}, - ), ( - EventSpec(RelationChangedEvent, 'mon_relation_changed', relation_id=2, - remote_app='remote', remote_unit=None, charm_config=charm_config), - {'relation_name': 'mon', 'relation_id': 2, 'app_name': 'remote', 'unit_name': None}, - ), ( - EventSpec(RelationDepartedEvent, 'mon_relation_departed', relation_id=2, - remote_app='remote', remote_unit='remote/0', charm_config=charm_config), - {'relation_name': 'mon', 'relation_id': 2, 'app_name': 'remote', 'unit_name': 'remote/0'}, - ), ( - EventSpec(RelationBrokenEvent, 'ha_relation_broken', relation_id=3, - charm_config=charm_config), - {'relation_name': 'ha', 'relation_id': 3}, - ), ( - # Events without a remote app specified (for Juju < 2.7). - EventSpec(RelationJoinedEvent, 'db_relation_joined', relation_id=1, - remote_unit='remote/0', charm_config=charm_config), - {'relation_name': 'db', 'relation_id': 1, 'app_name': 'remote', 'unit_name': 'remote/0'}, - ), ( - EventSpec(RelationChangedEvent, 'mon_relation_changed', relation_id=2, - remote_unit='remote/0', charm_config=charm_config), - {'relation_name': 'mon', 'relation_id': 2, 'app_name': 'remote', 'unit_name': 'remote/0'}, - ), ( - EventSpec(RelationDepartedEvent, 'mon_relation_departed', relation_id=2, - remote_unit='remote/0', charm_config=charm_config), - {'relation_name': 'mon', 'relation_id': 2, 'app_name': 'remote', 'unit_name': 'remote/0'}, - ), ( - EventSpec(ActionEvent, 'start_action', env_var='JUJU_ACTION_NAME', charm_config=actions_charm_config), - {}, - ), ( - EventSpec(ActionEvent, 'foo_bar_action', env_var='JUJU_ACTION_NAME', charm_config=actions_charm_config), - {}, - )] - - logger.debug(f'Expected events {events_under_test}') - - # First run "install" to make sure all hooks are set up. - self._simulate_event(EventSpec(InstallEvent, 'install', charm_config=charm_config)) - - # Simulate hook executions for every event. - for event_spec, expected_event_data in events_under_test: - state = self._simulate_event(event_spec) - - state_key = f'on_{event_spec.event_name}' - handled_events = state.get(state_key, []) - - # Make sure that a handler for that event was called once. - self.assertEqual(len(handled_events), 1) - # Make sure the event handled by the Charm has the right type. - handled_event_type = handled_events[0] - self.assertEqual(handled_event_type, event_spec.event_type) - - self.assertEqual(state['observed_event_types'], [event_spec.event_type]) - - if event_spec.event_name in expected_event_data: - self.assertEqual(state[f'{event_spec.event_name}_data'], expected_event_data[event_spec.event_name]) - - def test_event_not_implemented(self): - """Make sure events without implementation do not cause non-zero exit. - """ - charm_config = base64.b64encode(pickle.dumps({ - 'STATE_FILE': self._state_file, - })) - - # Simulate a scenario where there is a symlink for an event that - # a charm does not know how to handle. - hook_path = self.JUJU_CHARM_DIR / 'hooks/not-implemented-event' - # This will be cleared up in tearDown. - hook_path.symlink_to('install') - - try: - self._simulate_event(EventSpec(HookEvent, 'not-implemented-event', charm_config=charm_config)) - except subprocess.CalledProcessError: - self.fail('Event simulation for an unsupported event' - ' results in a non-zero exit code returned') - - def test_setup_event_links(self): - """Test auto-creation of symlinks caused by initial events. - """ - all_event_hooks = [f'hooks/{e.replace("_", "-")}' for e in self.charm_module.Charm.on.events().keys()] - charm_config = base64.b64encode(pickle.dumps({ - 'STATE_FILE': self._state_file, - })) - initial_events = { - EventSpec(InstallEvent, 'install', charm_config=charm_config), - EventSpec(StorageAttachedEvent, 'disks-storage-attached', charm_config=charm_config), - EventSpec(StartEvent, 'start', charm_config=charm_config), - EventSpec(UpgradeCharmEvent, 'upgrade-charm', charm_config=charm_config), - } - - def _assess_event_links(event_spec): - self.assertTrue(self.hooks_dir / event_spec.event_name in self.hooks_dir.iterdir()) - for event_hook in all_event_hooks: - self.assertTrue((self.JUJU_CHARM_DIR / event_hook).exists(), f'Missing hook: {event_hook}') - self.assertEqual(os.readlink(self.JUJU_CHARM_DIR / event_hook), self.charm_exec_path) - - for initial_event in initial_events: - self._setup_charm_dir() - - self._simulate_event(initial_event) - _assess_event_links(initial_event) - # Make sure it is idempotent. - self._simulate_event(initial_event) - _assess_event_links(initial_event) - - def test_setup_action_links(self): - charm_config = base64.b64encode(pickle.dumps({ - 'STATE_FILE': self._state_file, - })) - actions_yaml = self.JUJU_CHARM_DIR / 'actions.yaml' - actions_yaml.write_text('test: {}') - self._simulate_event(EventSpec(InstallEvent, 'install', charm_config=charm_config)) - action_hook = self.JUJU_CHARM_DIR / 'actions' / 'test' - self.assertTrue(action_hook.exists()) - - -if __name__ == "__main__": - unittest.main() diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_model.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_model.py deleted file mode 100755 index 544f4ff5532e664cfa1ed5f4d1e24b81d7975cab..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/mod/operator/test/test_model.py +++ /dev/null @@ -1,868 +0,0 @@ -#!/usr/bin/python3 -# Copyright 2019 Canonical Ltd. -# -# 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 os -import pathlib -import unittest -import time -import re -import json - -import ops.model -import ops.charm -from ops.charm import RelationMeta - -from test.test_helpers import fake_script, fake_script_calls - - -class TestModel(unittest.TestCase): - - def setUp(self): - def restore_env(env): - os.environ.clear() - os.environ.update(env) - self.addCleanup(restore_env, os.environ.copy()) - - os.environ['JUJU_UNIT_NAME'] = 'myapp/0' - - self.backend = ops.model.ModelBackend() - meta = ops.charm.CharmMeta() - meta.relations = { - 'db0': RelationMeta('provides', 'db0', {'interface': 'db0', 'scope': 'global'}), - 'db1': RelationMeta('requires', 'db1', {'interface': 'db1', 'scope': 'global'}), - 'db2': RelationMeta('peers', 'db2', {'interface': 'db2', 'scope': 'global'}), - } - self.model = ops.model.Model('myapp/0', meta, self.backend) - - def test_model(self): - self.assertIs(self.model.app, self.model.unit.app) - - def test_relations_keys(self): - fake_script(self, 'relation-ids', - """[ "$1" = db2 ] && echo '["db2:5", "db2:6"]' || echo '[]'""") - fake_script(self, 'relation-list', - """([ "$2" = 5 ] && echo '["remoteapp1/0", "remoteapp1/1"]') || ([ "$2" = 6 ] && echo '["remoteapp2/0"]') || exit 2""") - - for relation in self.model.relations['db2']: - self.assertIn(self.model.unit, relation.data) - unit_from_rel = next(filter(lambda u: u.name == 'myapp/0', relation.data.keys())) - self.assertIs(self.model.unit, unit_from_rel) - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db2', '--format=json'], - ['relation-list', '-r', '5', '--format=json'], - ['relation-list', '-r', '6', '--format=json'] - ]) - - def test_get_relation(self): - err_msg = 'ERROR invalid value "$2" for option -r: relation not found' - - fake_script(self, 'relation-ids', - """([ "$1" = db1 ] && echo '["db1:4"]') || ([ "$1" = db2 ] && echo '["db2:5", "db2:6"]') || echo '[]'""") - fake_script(self, 'relation-list', - f"""([ "$2" = 4 ] && echo '["remoteapp1/0"]') || (echo {err_msg} >&2 ; exit 2)""") - fake_script(self, 'relation-get', - f"""echo {err_msg} >&2 ; exit 2""") - - with self.assertRaises(ops.model.ModelError): - self.model.get_relation('db1', 'db1:4') - db1_4 = self.model.get_relation('db1', 4) - self.assertIsInstance(db1_4, ops.model.Relation) - dead_rel = self.model.get_relation('db1', 7) - self.assertIsInstance(dead_rel, ops.model.Relation) - self.assertEqual(list(dead_rel.data.keys()), [self.model.unit, self.model.unit.app]) - self.assertEqual(dead_rel.data[self.model.unit], {}) - self.assertIsNone(self.model.get_relation('db0')) - self.assertIs(self.model.get_relation('db1'), db1_4) - with self.assertRaises(ops.model.TooManyRelatedAppsError): - self.model.get_relation('db2') - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ['relation-list', '-r', '7', '--format=json'], - ['relation-get', '-r', '7', '-', 'myapp/0', '--app=False', '--format=json'], - ['relation-ids', 'db0', '--format=json'], - ['relation-ids', 'db2', '--format=json'], - ['relation-list', '-r', '5', '--format=json'], - ['relation-list', '-r', '6', '--format=json'] - ]) - - def test_peer_relation_app(self): - meta = ops.charm.CharmMeta() - meta.relations = {'dbpeer': RelationMeta('peers', 'dbpeer', {'interface': 'dbpeer', 'scope': 'global'})} - self.model = ops.model.Model('myapp/0', meta, self.backend) - - err_msg = 'ERROR invalid value "$2" for option -r: relation not found' - fake_script(self, 'relation-ids', - '''([ "$1" = dbpeer ] && echo '["dbpeer:0"]') || echo "[]"''') - fake_script(self, 'relation-list', - f'''([ "$2" = 0 ] && echo "[]") || (echo {err_msg} >&2 ; exit 2)''') - - db1_4 = self.model.get_relation('dbpeer') - self.assertIs(db1_4.app, self.model.app) - - def test_remote_units_is_our(self): - fake_script(self, 'relation-ids', - """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', - """[ "$2" = 4 ] && echo '["remoteapp1/0", "remoteapp1/1"]' || exit 2""") - - for u in self.model.get_relation('db1').units: - self.assertFalse(u._is_our_unit) - self.assertFalse(u.app._is_our_app) - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'] - ]) - - def test_our_unit_is_our(self): - self.assertTrue(self.model.unit._is_our_unit) - self.assertTrue(self.model.unit.app._is_our_app) - - def test_unit_relation_data(self): - fake_script(self, 'relation-ids', """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', """[ "$2" = 4 ] && echo '["remoteapp1/0"]' || exit 2""") - fake_script(self, 'relation-get', """([ "$2" = 4 ] && [ "$4" = "remoteapp1/0" ]) && echo '{"host": "remoteapp1-0"}' || exit 2""") - - random_unit = self.model._cache.get(ops.model.Unit, 'randomunit/0') - with self.assertRaises(KeyError): - self.model.get_relation('db1').data[random_unit] - remoteapp1_0 = next(filter(lambda u: u.name == 'remoteapp1/0', self.model.get_relation('db1').units)) - self.assertEqual(self.model.get_relation('db1').data[remoteapp1_0], {'host': 'remoteapp1-0'}) - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ['relation-get', '-r', '4', '-', 'remoteapp1/0', '--app=False', '--format=json'] - ]) - - def test_remote_app_relation_data(self): - fake_script(self, 'relation-ids', """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', """[ "$2" = 4 ] && echo '["remoteapp1/0", "remoteapp1/1"]' || exit 2""") - fake_script(self, 'relation-get', """[ "$2" = 4 ] && [ "$4" = remoteapp1 ] && echo '{"secret": "cafedeadbeef"}' || exit 2""") - - # Try to get relation data for an invalid remote application. - random_app = self.model._cache.get(ops.model.Application, 'randomapp') - with self.assertRaises(KeyError): - self.model.get_relation('db1').data[random_app] - - remoteapp1 = self.model.get_relation('db1').app - self.assertEqual(self.model.get_relation('db1').data[remoteapp1], {'secret': 'cafedeadbeef'}) - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ['relation-get', '-r', '4', '-', 'remoteapp1', '--app=True', '--format=json'], - ]) - - def test_relation_data_modify_remote(self): - fake_script(self, 'relation-ids', """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', """[ "$2" = 4 ] && echo '["remoteapp1/0"]' || exit 2""") - fake_script(self, 'relation-get', """([ "$2" = 4 ] && [ "$4" = "remoteapp1/0" ]) && echo '{"host": "remoteapp1-0"}' || exit 2""") - - rel_db1 = self.model.get_relation('db1') - remoteapp1_0 = next(filter(lambda u: u.name == 'remoteapp1/0', self.model.get_relation('db1').units)) - # Force memory cache to be loaded. - self.assertIn('host', rel_db1.data[remoteapp1_0]) - with self.assertRaises(ops.model.RelationDataError): - rel_db1.data[remoteapp1_0]['foo'] = 'bar' - self.assertNotIn('foo', rel_db1.data[remoteapp1_0]) - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ['relation-get', '-r', '4', '-', 'remoteapp1/0', '--app=False', '--format=json'] - ]) - - def test_relation_data_modify_our(self): - fake_script(self, 'relation-ids', """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', """[ "$2" = 4 ] && echo '["remoteapp1/0"]' || exit 2""") - fake_script(self, 'relation-set', '''[ "$2" = 4 ] && exit 0 || exit 2''') - fake_script(self, 'relation-get', """([ "$2" = 4 ] && [ "$4" = "myapp/0" ]) && echo '{"host": "bar"}' || exit 2""") - - rel_db1 = self.model.get_relation('db1') - # Force memory cache to be loaded. - self.assertIn('host', rel_db1.data[self.model.unit]) - rel_db1.data[self.model.unit]['host'] = 'bar' - self.assertEqual(rel_db1.data[self.model.unit]['host'], 'bar') - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ['relation-get', '-r', '4', '-', 'myapp/0', '--app=False', '--format=json'], - ['relation-set', '-r', '4', 'host=bar', '--app=False'] - ]) - - def test_app_relation_data_modify_local_as_leader(self): - fake_script(self, 'relation-ids', """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', """[ "$2" = 4 ] && echo '["remoteapp1/0", "remoteapp1/1"]' || exit 2""") - fake_script(self, 'relation-get', """[ "$2" = 4 ] && [ "$4" = myapp ] && echo '{"password": "deadbeefcafe"}' || exit 2""") - fake_script(self, 'relation-set', """[ "$2" = 4 ] && exit 0 || exit 2""") - fake_script(self, 'is-leader', 'echo true') - - local_app = self.model.unit.app - - rel_db1 = self.model.get_relation('db1') - self.assertEqual(rel_db1.data[local_app], {'password': 'deadbeefcafe'}) - - rel_db1.data[local_app]['password'] = 'foo' - - self.assertEqual(rel_db1.data[local_app]['password'], 'foo') - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ['relation-get', '-r', '4', '-', 'myapp', '--app=True', '--format=json'], - ['is-leader', '--format=json'], - ['relation-set', '-r', '4', 'password=foo', '--app=True'], - ]) - - def test_app_relation_data_modify_local_as_minion(self): - fake_script(self, 'relation-ids', """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', """[ "$2" = 4 ] && echo '["remoteapp1/0", "remoteapp1/1"]' || exit 2""") - fake_script(self, 'relation-get', """[ "$2" = 4 ] && [ "$4" = myapp ] && echo '{"password": "deadbeefcafe"}' || exit 2""") - fake_script(self, 'is-leader', 'echo false') - - local_app = self.model.unit.app - - rel_db1 = self.model.get_relation('db1') - self.assertEqual(rel_db1.data[local_app], {'password': 'deadbeefcafe'}) - - with self.assertRaises(ops.model.RelationDataError): - rel_db1.data[local_app]['password'] = 'foobar' - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ['relation-get', '-r', '4', '-', 'myapp', '--app=True', '--format=json'], - ['is-leader', '--format=json'], - ]) - - def test_relation_data_del_key(self): - fake_script(self, 'relation-ids', """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', """[ "$2" = 4 ] && echo '["remoteapp1/0"]' || exit 2""") - fake_script(self, 'relation-set', '''[ "$2" = 4 ] && exit 0 || exit 2''') - fake_script(self, 'relation-get', """([ "$2" = 4 ] && [ "$4" = "myapp/0" ]) && echo '{"host": "bar"}' || exit 2""") - - rel_db1 = self.model.get_relation('db1') - # Force memory cache to be loaded. - self.assertIn('host', rel_db1.data[self.model.unit]) - del rel_db1.data[self.model.unit]['host'] - fake_script(self, 'relation-get', """([ "$2" = 4 ] && [ "$4" = "myapp/0" ]) && echo '{}' || exit 2""") - self.assertNotIn('host', rel_db1.data[self.model.unit]) - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ['relation-get', '-r', '4', '-', 'myapp/0', '--app=False', '--format=json'], - ['relation-set', '-r', '4', 'host=', '--app=False'] - ]) - - def test_relation_set_fail(self): - fake_script(self, 'relation-ids', """[ "$1" = db2 ] && echo '["db2:5"]' || echo '[]'""") - fake_script(self, 'relation-list', - """[ "$2" = 5 ] && echo '["remoteapp1/0"]' || exit 2""") - fake_script(self, 'relation-get', """([ "$2" = 5 ] && [ "$4" = "myapp/0" ]) && echo '{"host": "myapp-0"}' || exit 2""") - fake_script(self, 'relation-set', 'exit 2') - - rel_db2 = self.model.relations['db2'][0] - # Force memory cache to be loaded. - self.assertIn('host', rel_db2.data[self.model.unit]) - with self.assertRaises(ops.model.ModelError): - rel_db2.data[self.model.unit]['host'] = 'bar' - self.assertEqual(rel_db2.data[self.model.unit]['host'], 'myapp-0') - with self.assertRaises(ops.model.ModelError): - del rel_db2.data[self.model.unit]['host'] - self.assertIn('host', rel_db2.data[self.model.unit]) - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db2', '--format=json'], - ['relation-list', '-r', '5', '--format=json'], - ['relation-get', '-r', '5', '-', 'myapp/0', '--app=False', '--format=json'], - ['relation-set', '-r', '5', 'host=bar', '--app=False'], - ['relation-set', '-r', '5', 'host=', '--app=False'] - ]) - - def test_relation_get_set_is_app_arg(self): - self.backend = ops.model.ModelBackend() - - # No is_app provided. - with self.assertRaises(TypeError): - self.backend.relation_set(1, 'fookey', 'barval') - - with self.assertRaises(TypeError): - self.backend.relation_get(1, 'fooentity') - - # Invalid types for is_app. - for is_app_v in [None, 1, 2.0, 'a', b'beef']: - with self.assertRaises(TypeError): - self.backend.relation_set(1, 'fookey', 'barval', is_app=is_app_v) - - with self.assertRaises(TypeError): - self.backend.relation_get(1, 'fooentity', is_app=is_app_v) - - def test_relation_data_type_check(self): - fake_script(self, 'relation-ids', """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', - """[ "$2" = 4 ] && echo '["remoteapp1/0"]' || exit 2""") - fake_script(self, 'relation-get', """([ "$2" = 4 ] && [ "$4" = "myapp/0" ]) && echo '{"host": "myapp-0"}' || exit 2""") - - rel_db1 = self.model.get_relation('db1') - with self.assertRaises(ops.model.RelationDataError): - rel_db1.data[self.model.unit]['foo'] = 1 - with self.assertRaises(ops.model.RelationDataError): - rel_db1.data[self.model.unit]['foo'] = {'foo': 'bar'} - with self.assertRaises(ops.model.RelationDataError): - rel_db1.data[self.model.unit]['foo'] = None - - self.assertEqual(fake_script_calls(self), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'] - ]) - - def test_config(self): - fake_script(self, 'config-get', """echo '{"foo":"foo","bar":1,"qux":true}'""") - self.assertEqual(self.model.config, { - 'foo': 'foo', - 'bar': 1, - 'qux': True, - }) - with self.assertRaises(TypeError): - # Confirm that we cannot modify config values. - self.model.config['foo'] = 'bar' - - self.assertEqual(fake_script_calls(self), [['config-get', '--format=json']]) - - def test_is_leader(self): - def check_remote_units(): - fake_script(self, 'relation-ids', - """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - - fake_script(self, 'relation-list', - """[ "$2" = 4 ] && echo '["remoteapp1/0", "remoteapp1/1"]' || exit 2""") - - # Cannot determine leadership for remote units. - for u in self.model.get_relation('db1').units: - with self.assertRaises(RuntimeError): - u.is_leader() - - fake_script(self, 'is-leader', 'echo true') - self.assertTrue(self.model.unit.is_leader()) - - check_remote_units() - - # Create a new model and backend to drop a cached is-leader output. - self.backend = ops.model.ModelBackend() - meta = ops.charm.CharmMeta() - meta.relations = { - 'db0': RelationMeta('provides', 'db0', {'interface': 'db0', 'scope': 'global'}), - 'db1': RelationMeta('requires', 'db1', {'interface': 'db1', 'scope': 'global'}), - 'db2': RelationMeta('peers', 'db2', {'interface': 'db2', 'scope': 'global'}), - } - self.model = ops.model.Model('myapp/0', meta, self.backend) - - fake_script(self, 'is-leader', 'echo false') - self.assertFalse(self.model.unit.is_leader()) - - check_remote_units() - - self.assertEqual(fake_script_calls(self), [ - ['is-leader', '--format=json'], - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ['is-leader', '--format=json'], - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ]) - - def test_is_leader_refresh(self): - # A sanity check. - self.assertGreater(time.monotonic(), ops.model.ModelBackend.LEASE_RENEWAL_PERIOD.total_seconds()) - - fake_script(self, 'is-leader', 'echo false') - self.assertFalse(self.model.unit.is_leader()) - - # Change the leadership status and force a recheck. - fake_script(self, 'is-leader', 'echo true') - self.backend._leader_check_time = 0 - self.assertTrue(self.model.unit.is_leader()) - - # Force a recheck without changing the leadership status. - fake_script(self, 'is-leader', 'echo true') - self.backend._leader_check_time = 0 - self.assertTrue(self.model.unit.is_leader()) - - def test_resources(self): - meta = ops.charm.CharmMeta() - meta.resources = {'foo': None, 'bar': None} - model = ops.model.Model('myapp/0', meta, self.backend) - - with self.assertRaises(RuntimeError): - model.resources.fetch('qux') - - fake_script(self, 'resource-get', 'exit 1') - with self.assertRaises(ops.model.ModelError): - model.resources.fetch('foo') - - fake_script(self, 'resource-get', 'echo /var/lib/juju/agents/unit-test-0/resources/$1/$1.tgz') - self.assertEqual(model.resources.fetch('foo').name, 'foo.tgz') - self.assertEqual(model.resources.fetch('bar').name, 'bar.tgz') - - def test_pod_spec(self): - fake_script(self, 'pod-spec-set', """ - cat $2 > $(dirname $0)/spec.json - [[ -n $4 ]] && cat $4 > $(dirname $0)/k8s_res.json || true - """) - fake_script(self, 'is-leader', 'echo true') - spec_path = self.fake_script_path / 'spec.json' - k8s_res_path = self.fake_script_path / 'k8s_res.json' - - def check_calls(calls): - # There may 1 or 2 calls because of is-leader. - self.assertLessEqual(len(fake_calls), 2) - pod_spec_call = next(filter(lambda c: c[0] == 'pod-spec-set', calls)) - self.assertEqual(pod_spec_call[:2], ['pod-spec-set', '--file']) - # 8 bytes are used as of python 3.4.0, see Python bug #12015. - # Other characters are from POSIX 3.282 (Portable Filename Character Set) a subset of which Python's mkdtemp uses. - self.assertTrue(re.match('/tmp/tmp[A-Za-z0-9._-]{8}-pod-spec-set', pod_spec_call[2])) - - self.model.pod.set_spec({'foo': 'bar'}) - self.assertEqual(spec_path.read_text(), '{"foo": "bar"}') - self.assertFalse(k8s_res_path.exists()) - - fake_calls = fake_script_calls(self, clear=True) - check_calls(fake_calls) - - self.model.pod.set_spec({'bar': 'foo'}, {'qux': 'baz'}) - self.assertEqual(spec_path.read_text(), '{"bar": "foo"}') - self.assertEqual(k8s_res_path.read_text(), '{"qux": "baz"}') - - fake_calls = fake_script_calls(self, clear=True) - check_calls(fake_calls) - - # Create a new model to drop is-leader caching result. - self.backend = ops.model.ModelBackend() - meta = ops.charm.CharmMeta() - self.model = ops.model.Model('myapp/0', meta, self.backend) - fake_script(self, 'is-leader', 'echo false') - with self.assertRaises(ops.model.ModelError): - self.model.pod.set_spec({'foo': 'bar'}) - - def test_base_status_instance_raises(self): - with self.assertRaises(TypeError): - ops.model.StatusBase('test') - - def test_active_message_default(self): - self.assertEqual(ops.model.ActiveStatus().message, '') - - def test_local_set_valid_unit_status(self): - test_cases = [( - ops.model.ActiveStatus('Green'), - lambda: fake_script(self, 'status-set', 'exit 0'), - lambda: self.assertEqual(fake_script_calls(self, True), [['status-set', '--application=False', 'active', 'Green']]), - ), ( - ops.model.MaintenanceStatus('Yellow'), - lambda: fake_script(self, 'status-set', 'exit 0'), - lambda: self.assertEqual(fake_script_calls(self, True), [['status-set', '--application=False', 'maintenance', 'Yellow']]), - ), ( - ops.model.BlockedStatus('Red'), - lambda: fake_script(self, 'status-set', 'exit 0'), - lambda: self.assertEqual(fake_script_calls(self, True), [['status-set', '--application=False', 'blocked', 'Red']]), - ), ( - ops.model.WaitingStatus('White'), - lambda: fake_script(self, 'status-set', 'exit 0'), - lambda: self.assertEqual(fake_script_calls(self, True), [['status-set', '--application=False', 'waiting', 'White']]), - )] - - for target_status, setup_tools, check_tool_calls in test_cases: - setup_tools() - - self.model.unit.status = target_status - - self.assertEqual(self.model.unit.status, target_status) - - check_tool_calls() - - def test_local_set_valid_app_status(self): - fake_script(self, 'is-leader', 'echo true') - test_cases = [( - ops.model.ActiveStatus('Green'), - lambda: fake_script(self, 'status-set', 'exit 0'), - lambda: self.assertIn(['status-set', '--application=True', 'active', 'Green'], fake_script_calls(self, True)), - ), ( - ops.model.MaintenanceStatus('Yellow'), - lambda: fake_script(self, 'status-set', 'exit 0'), - lambda: self.assertIn(['status-set', '--application=True', 'maintenance', 'Yellow'], fake_script_calls(self, True)), - ), ( - ops.model.BlockedStatus('Red'), - lambda: fake_script(self, 'status-set', 'exit 0'), - lambda: self.assertIn(['status-set', '--application=True', 'blocked', 'Red'], fake_script_calls(self, True)), - ), ( - ops.model.WaitingStatus('White'), - lambda: fake_script(self, 'status-set', 'exit 0'), - lambda: self.assertIn(['status-set', '--application=True', 'waiting', 'White'], fake_script_calls(self, True)), - )] - - for target_status, setup_tools, check_tool_calls in test_cases: - setup_tools() - - self.model.app.status = target_status - - self.assertEqual(self.model.app.status, target_status) - - check_tool_calls() - - def test_set_app_status_non_leader_raises(self): - fake_script(self, 'is-leader', 'echo false') - - with self.assertRaises(RuntimeError): - self.model.app.status - - with self.assertRaises(RuntimeError): - self.model.app.status = ops.model.ActiveStatus() - - def test_local_set_invalid_status(self): - fake_script(self, 'status-set', 'exit 1') - fake_script(self, 'is-leader', 'echo true') - - with self.assertRaises(ops.model.ModelError): - self.model.unit.status = ops.model.UnknownStatus() - - self.assertEqual(fake_script_calls(self, True), [ - ['status-set', '--application=False', 'unknown', ''], - ]) - - with self.assertRaises(ops.model.ModelError): - self.model.app.status = ops.model.UnknownStatus() - - # A leadership check is needed for application status. - self.assertEqual(fake_script_calls(self, True), [ - ['is-leader', '--format=json'], - ['status-set', '--application=True', 'unknown', ''], - ]) - - def test_status_set_is_app_not_bool_raises(self): - self.backend = ops.model.ModelBackend() - - for is_app_v in [None, 1, 2.0, 'a', b'beef', object]: - with self.assertRaises(TypeError): - self.backend.status_set(ops.model.ActiveStatus, is_app=is_app_v) - - def test_remote_unit_status(self): - fake_script(self, 'relation-ids', """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', """[ "$2" = 4 ] && echo '["remoteapp1/0", "remoteapp1/1"]' || exit 2""") - - remote_unit = next(filter(lambda u: u.name == 'remoteapp1/0', self.model.get_relation('db1').units)) - - test_statuses = ( - ops.model.UnknownStatus(), - ops.model.ActiveStatus('Green'), - ops.model.MaintenanceStatus('Yellow'), - ops.model.BlockedStatus('Red'), - ops.model.WaitingStatus('White'), - ) - - for target_status in test_statuses: - with self.assertRaises(RuntimeError): - remote_unit.status = target_status - - def test_remote_app_status(self): - fake_script(self, 'relation-ids', """[ "$1" = db1 ] && echo '["db1:4"]' || echo '[]'""") - fake_script(self, 'relation-list', """[ "$2" = 4 ] && echo '["remoteapp1/0", "remoteapp1/1"]' || exit 2""") - - remoteapp1 = self.model.get_relation('db1').app - - # Remote application status is always unknown. - self.assertIsInstance(remoteapp1.status, ops.model.UnknownStatus) - - test_statuses = ( - ops.model.UnknownStatus(), - ops.model.ActiveStatus(), - ops.model.MaintenanceStatus('Upgrading software'), - ops.model.BlockedStatus('Awaiting manual resolution'), - ops.model.WaitingStatus('Awaiting related app updates'), - ) - for target_status in test_statuses: - with self.assertRaises(RuntimeError): - remoteapp1.status = target_status - - self.assertEqual(fake_script_calls(self, clear=True), [ - ['relation-ids', 'db1', '--format=json'], - ['relation-list', '-r', '4', '--format=json'], - ]) - - def test_storage(self): - meta = ops.charm.CharmMeta() - meta.storages = {'disks': None, 'data': None} - self.model = ops.model.Model('myapp/0', meta, self.backend) - - fake_script(self, 'storage-list', """[ "$1" = disks ] && echo '["disks/0", "disks/1"]' || echo '[]'""") - fake_script(self, 'storage-get', - """ - if [ "$2" = disks/0 ]; then - echo '"/var/srv/disks/0"' - elif [ "$2" = disks/1 ]; then - echo '"/var/srv/disks/1"' - else - exit 2 - fi - """) - fake_script(self, 'storage-add', '') - - self.assertEqual(len(self.model.storages), 2) - self.assertEqual(self.model.storages.keys(), meta.storages.keys()) - self.assertIn('disks', self.model.storages) - test_cases = { - 0: {'name': 'disks', 'location': pathlib.Path('/var/srv/disks/0')}, - 1: {'name': 'disks', 'location': pathlib.Path('/var/srv/disks/1')}, - } - for storage in self.model.storages['disks']: - self.assertEqual(storage.name, 'disks') - self.assertIn(storage.id, test_cases) - self.assertEqual(storage.name, test_cases[storage.id]['name']) - self.assertEqual(storage.location, test_cases[storage.id]['location']) - - self.assertEqual(fake_script_calls(self, clear=True), [ - ['storage-list', 'disks', '--format=json'], - ['storage-get', '-s', 'disks/0', 'location', '--format=json'], - ['storage-get', '-s', 'disks/1', 'location', '--format=json'], - ]) - - self.assertSequenceEqual(self.model.storages['data'], []) - self.model.storages.request('data', count=3) - self.assertEqual(fake_script_calls(self), [ - ['storage-list', 'data', '--format=json'], - ['storage-add', 'data=3'], - ]) - - # Try to add storage not present in charm metadata. - with self.assertRaises(ops.model.ModelError): - self.model.storages.request('deadbeef') - - # Invalid count parameter types. - for count_v in [None, False, 2.0, 'a', b'beef', object]: - with self.assertRaises(TypeError): - self.model.storages.request('data', count_v) - - -class TestModelBackend(unittest.TestCase): - - def setUp(self): - os.environ['JUJU_UNIT_NAME'] = 'myapp/0' - self.addCleanup(os.environ.pop, 'JUJU_UNIT_NAME') - - self._backend = None - - @property - def backend(self): - if self._backend is None: - self._backend = ops.model.ModelBackend() - return self._backend - - def test_relation_tool_errors(self): - err_msg = 'ERROR invalid value "$2" for option -r: relation not found' - - test_cases = [( - lambda: fake_script(self, 'relation-list', f'echo fooerror >&2 ; exit 1'), - lambda: self.backend.relation_list(3), - ops.model.ModelError, - [['relation-list', '-r', '3', '--format=json']], - ), ( - lambda: fake_script(self, 'relation-list', f'echo {err_msg} >&2 ; exit 2'), - lambda: self.backend.relation_list(3), - ops.model.RelationNotFoundError, - [['relation-list', '-r', '3', '--format=json']], - ), ( - lambda: fake_script(self, 'relation-set', f'echo fooerror >&2 ; exit 1'), - lambda: self.backend.relation_set(3, 'foo', 'bar', is_app=False), - ops.model.ModelError, - [['relation-set', '-r', '3', 'foo=bar', '--app=False']], - ), ( - lambda: fake_script(self, 'relation-set', f'echo {err_msg} >&2 ; exit 2'), - lambda: self.backend.relation_set(3, 'foo', 'bar', is_app=False), - ops.model.RelationNotFoundError, - [['relation-set', '-r', '3', 'foo=bar', '--app=False']], - ), ( - lambda: fake_script(self, 'relation-get', f'echo fooerror >&2 ; exit 1'), - lambda: self.backend.relation_get(3, 'remote/0', is_app=False), - ops.model.ModelError, - [['relation-get', '-r', '3', '-', 'remote/0', '--app=False', '--format=json']], - ), ( - lambda: fake_script(self, 'relation-get', f'echo {err_msg} >&2 ; exit 2'), - lambda: self.backend.relation_get(3, 'remote/0', is_app=False), - ops.model.RelationNotFoundError, - [['relation-get', '-r', '3', '-', 'remote/0', '--app=False', '--format=json']], - )] - - for do_fake, run, exception, calls in test_cases: - do_fake() - with self.assertRaises(exception): - run() - self.assertEqual(fake_script_calls(self, clear=True), calls) - - def test_status_is_app_forced_kwargs(self): - fake_script(self, 'status-get', 'exit 1') - fake_script(self, 'status-set', 'exit 1') - - test_cases = ( - lambda: self.backend.status_get(False), - lambda: self.backend.status_get(True), - lambda: self.backend.status_set('active', '', False), - lambda: self.backend.status_set('active', '', True), - ) - - for case in test_cases: - with self.assertRaises(TypeError): - case() - - def test_storage_tool_errors(self): - test_cases = [( - lambda: fake_script(self, 'storage-list', f'echo fooerror >&2 ; exit 1'), - lambda: self.backend.storage_list('foobar'), - ops.model.ModelError, - [['storage-list', 'foobar', '--format=json']], - ), ( - lambda: fake_script(self, 'storage-get', f'echo fooerror >&2 ; exit 1'), - lambda: self.backend.storage_get('foobar', 'someattr'), - ops.model.ModelError, - [['storage-get', '-s', 'foobar', 'someattr', '--format=json']], - ), ( - lambda: fake_script(self, 'storage-add', f'echo fooerror >&2 ; exit 1'), - lambda: self.backend.storage_add('foobar', count=2), - ops.model.ModelError, - [['storage-add', 'foobar=2']], - ), ( - lambda: fake_script(self, 'storage-add', f'echo fooerror >&2 ; exit 1'), - lambda: self.backend.storage_add('foobar', count=object), - TypeError, - [], - ), ( - lambda: fake_script(self, 'storage-add', f'echo fooerror >&2 ; exit 1'), - lambda: self.backend.storage_add('foobar', count=True), - TypeError, - [], - )] - for do_fake, run, exception, calls in test_cases: - do_fake() - with self.assertRaises(exception): - run() - self.assertEqual(fake_script_calls(self, clear=True), calls) - - def test_network_get(self): - network_get_out = '''{ - "bind-addresses": [ - { - "mac-address": "", - "interface-name": "", - "addresses": [ - { - "hostname": "", - "value": "192.0.2.2", - "cidr": "" - } - ] - } - ], - "egress-subnets": [ - "192.0.2.2/32" - ], - "ingress-addresses": [ - "192.0.2.2" - ] -}''' - fake_script(self, 'network-get', f'''[ "$1" = deadbeef ] && echo '{network_get_out}' || exit 1''') - network_info = self.backend.network_get('deadbeef') - self.assertEqual(network_info, json.loads(network_get_out)) - self.assertEqual(fake_script_calls(self, clear=True), [['network-get', 'deadbeef', '--format=json']]) - - network_info = self.backend.network_get('deadbeef', 1) - self.assertEqual(network_info, json.loads(network_get_out)) - self.assertEqual(fake_script_calls(self, clear=True), [['network-get', 'deadbeef', '-r', '1', '--format=json']]) - - def test_network_get_errors(self): - err_no_endpoint = 'ERROR no network config found for binding "$2"' - err_no_rel = 'ERROR invalid value "$3" for option -r: relation not found' - - test_cases = [( - lambda: fake_script(self, 'network-get', f'echo {err_no_endpoint} >&2 ; exit 1'), - lambda: self.backend.network_get("deadbeef"), - ops.model.ModelError, - [['network-get', 'deadbeef', '--format=json']], - ), ( - lambda: fake_script(self, 'network-get', f'echo {err_no_rel} >&2 ; exit 2'), - lambda: self.backend.network_get("deadbeef", 3), - ops.model.RelationNotFoundError, - [['network-get', 'deadbeef', '-r', '3', '--format=json']], - )] - for do_fake, run, exception, calls in test_cases: - do_fake() - with self.assertRaises(exception): - run() - self.assertEqual(fake_script_calls(self, clear=True), calls) - - def test_action_get_error(self): - fake_script(self, 'action-get', '') - fake_script(self, 'action-get', f'echo fooerror >&2 ; exit 1') - with self.assertRaises(ops.model.ModelError): - self.backend.action_get() - calls = [['action-get', '--format=json']] - self.assertEqual(fake_script_calls(self, clear=True), calls) - - def test_action_set_error(self): - fake_script(self, 'action-get', '') - fake_script(self, 'action-set', f'echo fooerror >&2 ; exit 1') - with self.assertRaises(ops.model.ModelError): - self.backend.action_set({'foo': 'bar', 'dead': 'beef cafe'}) - calls = [["action-set", "foo=bar", "dead=beef cafe"]] - self.assertEqual(fake_script_calls(self, clear=True), calls) - - def test_action_log_error(self): - fake_script(self, 'action-get', '') - fake_script(self, 'action-log', f'echo fooerror >&2 ; exit 1') - with self.assertRaises(ops.model.ModelError): - self.backend.action_log('log-message') - calls = [["action-log", "log-message"]] - self.assertEqual(fake_script_calls(self, clear=True), calls) - - def test_action_get(self): - fake_script(self, 'action-get', """echo '{"foo-name": "bar", "silent": false}'""") - params = self.backend.action_get() - self.assertEqual(params['foo-name'], 'bar') - self.assertEqual(params['silent'], False) - self.assertEqual(fake_script_calls(self), [['action-get', '--format=json']]) - - def test_action_set(self): - fake_script(self, 'action-get', 'exit 1') - fake_script(self, 'action-set', 'exit 0') - self.backend.action_set({'x': 'dead beef', 'y': 1}) - self.assertEqual(fake_script_calls(self), [['action-set', 'x=dead beef', 'y=1']]) - - def test_action_fail(self): - fake_script(self, 'action-get', 'exit 1') - fake_script(self, 'action-fail', 'exit 0') - self.backend.action_fail('error 42') - self.assertEqual(fake_script_calls(self), [['action-fail', 'error 42']]) - - def test_action_log(self): - fake_script(self, 'action-get', 'exit 1') - fake_script(self, 'action-log', 'exit 0') - self.backend.action_log('progress: 42%') - self.assertEqual(fake_script_calls(self), [['action-log', 'progress: 42%']]) - - -if __name__ == "__main__": - unittest.main() diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/requirements.txt b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/requirements.txt deleted file mode 100644 index 8608c1b021f8e1b01ba7e0d6698102a06214a452..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -paramiko diff --git a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/src/charm.py b/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/src/charm.py deleted file mode 100755 index eb61f692e46b5fe7b6793f5d9a300718980e2f27..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/charms/charm-simple-k8s/src/charm.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -sys.path.append("lib") - -from ops.charm import CharmBase -from ops.framework import StoredState -from ops.main import main -from ops.model import ( - ActiveStatus, - BlockedStatus, - MaintenanceStatus, - WaitingStatus, - ModelError, -) -import os -import subprocess - - -def install_dependencies(): - # Make sure Python3 + PIP are available - if not os.path.exists("/usr/bin/python3") or not os.path.exists("/usr/bin/pip3"): - # This is needed when running as a k8s charm, as the ubuntu:latest - # image doesn't include either package. - - # Update the apt cache - subprocess.check_call(["apt-get", "update"]) - - # Install the Python3 package - subprocess.check_call(["apt-get", "install", "-y", "python3", "python3-pip"],) - - - # Install the build dependencies for our requirements (paramiko) - subprocess.check_call(["apt-get", "install", "-y", "libffi-dev", "libssl-dev"],) - - REQUIREMENTS_TXT = "{}/requirements.txt".format(os.environ["JUJU_CHARM_DIR"]) - if os.path.exists(REQUIREMENTS_TXT): - subprocess.check_call( - ["apt-get", "install", "-y", "python3-paramiko", "openssh-client"], - ) - - -try: - from charms.osm.sshproxy import SSHProxy -except Exception as ex: - install_dependencies() - from charms.osm.sshproxy import SSHProxy - - -class SimpleProxyCharm(CharmBase): - state = StoredState() - - def __init__(self, *args): - super().__init__(*args) - - # An example of setting charm state - # that's persistent across events - self.state.set_default(is_started=False) - - if not self.state.is_started: - self.state.is_started = True - - # Register all of the events we want to observe - for event in ( - # Charm events - self.on.config_changed, - self.on.start, - self.on.upgrade_charm, - # Charm actions (primitives) - self.on.touch_action, - # OSM actions (primitives) - self.on.start_action, - self.on.stop_action, - self.on.restart_action, - self.on.reboot_action, - self.on.upgrade_action, - # SSH Proxy actions (primitives) - self.on.generate_ssh_key_action, - self.on.get_ssh_public_key_action, - self.on.run_action, - self.on.verify_ssh_credentials_action, - ): - self.framework.observe(event, self) - - def get_ssh_proxy(self): - """Get the SSHProxy instance""" - proxy = SSHProxy( - hostname=self.model.config["ssh-hostname"], - username=self.model.config["ssh-username"], - password=self.model.config["ssh-password"], - ) - return proxy - - def on_config_changed(self, event): - """Handle changes in configuration""" - unit = self.model.unit - - # Unit should go into a waiting state until verify_ssh_credentials is successful - unit.status = WaitingStatus("Waiting for SSH credentials") - proxy = self.get_ssh_proxy() - - verified = proxy.verify_credentials() - if verified: - unit.status = ActiveStatus() - else: - unit.status = BlockedStatus("Invalid SSH credentials.") - - def on_start(self, event): - """Called when the charm is being started""" - unit = self.model.unit - - if not SSHProxy.has_ssh_key(): - unit.status = MaintenanceStatus("Generating SSH keys...") - - print("Generating SSH Keys") - SSHProxy.generate_ssh_key() - - unit.status = ActiveStatus() - - def on_touch_action(self, event): - """Touch a file.""" - try: - filename = event.params["filename"] - proxy = self.get_ssh_proxy() - - stdout, stderr = proxy.run("touch {}".format(filename)) - event.set_results({"output": stdout}) - except Exception as ex: - event.fail(ex) - - def on_upgrade_charm(self, event): - """Upgrade the charm.""" - unit = self.model.unit - - # Mark the unit as under Maintenance. - unit.status = MaintenanceStatus("Upgrading charm") - - self.on_install(event) - - # When maintenance is done, return to an Active state - unit.status = ActiveStatus() - - ############### - # OSM methods # - ############### - def on_start_action(self, event): - """Start the VNF service on the VM.""" - pass - - def on_stop_action(self, event): - """Stop the VNF service on the VM.""" - pass - - def on_restart_action(self, event): - """Restart the VNF service on the VM.""" - pass - - def on_reboot_action(self, event): - """Reboot the VM.""" - proxy = self.get_ssh_proxy() - stdout, stderr = proxy.run("sudo reboot") - - if len(stderr): - event.fail(stderr) - - def on_upgrade_action(self, event): - """Upgrade the VNF service on the VM.""" - pass - - ##################### - # SSH Proxy methods # - ##################### - def on_generate_ssh_key_action(self, event): - """Generate a new SSH keypair for this unit.""" - - if not SSHProxy.generate_ssh_key(): - event.fail("Unable to generate ssh key") - - def on_get_ssh_public_key_action(self, event): - """Get the SSH public key for this unit.""" - - pubkey = SSHProxy.get_ssh_public_key() - - event.set_results({"pubkey": SSHProxy.get_ssh_public_key()}) - - def on_run_action(self, event): - """Run an arbitrary command on the remote host.""" - - cmd = event.params["command"] - - proxy = self.get_ssh_proxy() - stdout, stderr = proxy.run(cmd) - - event.set_results({"output": stdout}) - - if len(stderr): - event.fail(stderr) - - def on_verify_ssh_credentials_action(self, event): - """Verify the SSH credentials for this unit.""" - - proxy = self.get_ssh_proxy() - - verified, stderr = proxy.verify_credentials() - if verified: - print("Verified!") - event.set_results({"verified": True}) - else: - print("Verification failed!") - event.set_results({"verified": False}) - event.fail(stderr) - - -if __name__ == "__main__": - main(SimpleProxyCharm) diff --git a/hackfest_k8sproxycharm_vnf/cloud_init/cloud-config.txt b/hackfest_k8sproxycharm_vnf/cloud_init/cloud-config.txt deleted file mode 100755 index 36c8d1bf2cdebbc4e50d1e8348003f64f419cd0b..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/cloud_init/cloud-config.txt +++ /dev/null @@ -1,12 +0,0 @@ -#cloud-config -password: osm4u -chpasswd: { expire: False } -ssh_pwauth: True - -write_files: -- content: | - # My new helloworld file - - owner: root:root - permissions: '0644' - path: /root/helloworld.txt diff --git a/hackfest_k8sproxycharm_vnf/hackfest_k8sproxycharm_vnfd.yaml b/hackfest_k8sproxycharm_vnf/hackfest_k8sproxycharm_vnfd.yaml deleted file mode 100644 index 6ed2d81a73d5e01f194f005f7de6553e002818e7..0000000000000000000000000000000000000000 --- a/hackfest_k8sproxycharm_vnf/hackfest_k8sproxycharm_vnfd.yaml +++ /dev/null @@ -1,95 +0,0 @@ -vnfd: - description: A VNF consisting of 1 VDU connected to two external VL, and one for - data and another one for management - df: - - id: default-df - instantiation-level: - - id: default-instantiation-level - vdu-level: - - number-of-instances: 1 - vdu-id: mgmtVM - vdu-profile: - - id: mgmtVM - min-number-of-instances: 1 - lcm-operations-configuration: - operate-vnf-op-config: - day1-2: - - config-primitive: - - name: touch - execution-environment-ref: simple-k8s-ee - parameter: - - data-type: STRING - default-value: /home/ubuntu/touched - name: filename - id: hackfest_k8sproxycharm-vnf - execution-environment-list: - - id: simple-k8s-ee - juju: - charm: charm-simple-k8s - cloud: k8s - proxy: true - initial-config-primitive: - - name: config - execution-environment-ref: simple-k8s-ee - parameter: - - name: ssh-hostname - value: - - name: ssh-username - value: ubuntu - - name: ssh-password - value: osm4u - seq: 1 - - name: touch - execution-environment-ref: simple-k8s-ee - parameter: - - data-type: STRING - name: filename - value: /home/ubuntu/first-touch - seq: 2 - ext-cpd: - - id: vnf-mgmt-ext - int-cpd: - cpd: mgmtVM-eth0-int - vdu-id: mgmtVM - - id: vnf-data-ext - int-cpd: - cpd: dataVM-xe0-int - vdu-id: mgmtVM - id: hackfest_k8sproxycharm-vnf - mgmt-cp: vnf-mgmt-ext - product-name: hackfest_k8sproxycharm-vnf - sw-image-desc: - - id: bionic - image: bionic - name: bionic - vdu: - - cloud-init-file: cloud-config.txt - id: mgmtVM - int-cpd: - - id: mgmtVM-eth0-int - virtual-network-interface-requirement: - - name: mgmtVM-eth0 - position: 1 - virtual-interface: - type: VIRTIO - - id: dataVM-xe0-int - virtual-network-interface-requirement: - - name: dataVM-xe0 - position: 2 - virtual-interface: - type: VIRTIO - name: mgmtVM - sw-image-desc: bionic - virtual-compute-desc: mgmtVM-compute - virtual-storage-desc: - - mgmtVM-storage - version: 1.0 - virtual-compute-desc: - - id: mgmtVM-compute - virtual-cpu: - num-virtual-cpu: 1 - virtual-memory: - size: 1.0 - virtual-storage-desc: - - id: mgmtVM-storage - size-of-storage: 10 diff --git a/hackfest_k8sproxycharm_vnf/icons/osm.png b/hackfest_k8sproxycharm_vnf/icons/osm.png deleted file mode 100644 index 62012d2a2b491bdcd536d62c3c3c863c0d8c1b33..0000000000000000000000000000000000000000 Binary files a/hackfest_k8sproxycharm_vnf/icons/osm.png and /dev/null differ diff --git a/hackfest_proxycharm_ns/hackfest_simplecharm_nsd.yaml b/hackfest_proxycharm_ns/hackfest_simplecharm_nsd.yaml deleted file mode 100644 index 615576c67f8ba638fc098f24a056a835cd5e70c5..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_ns/hackfest_simplecharm_nsd.yaml +++ /dev/null @@ -1,37 +0,0 @@ -nsd: - nsd: - - description: NS with 2 VNFs with cloudinit connected by datanet and mgmtnet VLs - df: - - id: default-df - vnf-profile: - - id: vnf1 - virtual-link-connectivity: - - constituent-cpd-id: - - constituent-base-element-id: vnf1 - constituent-cpd-id: vnf-mgmt-ext - virtual-link-profile-id: mgmtnet - - constituent-cpd-id: - - constituent-base-element-id: vnf1 - constituent-cpd-id: vnf-data-ext - virtual-link-profile-id: datanet - vnfd-id: hackfest_proxycharm-vnf - - id: vnf2 - virtual-link-connectivity: - - constituent-cpd-id: - - constituent-base-element-id: vnf2 - constituent-cpd-id: vnf-mgmt-ext - virtual-link-profile-id: mgmtnet - - constituent-cpd-id: - - constituent-base-element-id: vnf2 - constituent-cpd-id: vnf-data-ext - virtual-link-profile-id: datanet - vnfd-id: hackfest_proxycharm-vnf - id: hackfest_proxycharm-ns - name: hackfest_proxycharm-ns - version: 1.0 - virtual-link-desc: - - id: mgmtnet - mgmt-network: true - - id: datanet - vnfd-id: - - hackfest_proxycharm-vnf diff --git a/hackfest_proxycharm_ns/icons/osm.png b/hackfest_proxycharm_ns/icons/osm.png deleted file mode 100644 index 62012d2a2b491bdcd536d62c3c3c863c0d8c1b33..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_ns/icons/osm.png and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/.build.manifest b/hackfest_proxycharm_vnf/charms/simple/.build.manifest deleted file mode 100644 index 7a223f0e16cc2d8f86cdc5cc4c6a9195395f1854..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/.build.manifest +++ /dev/null @@ -1,356 +0,0 @@ -{ - "layers": [ - { - "rev": "fcdcea4e5de3e1556c24e6704607862d0ba00a56", - "url": "layer:options" - }, - { - "rev": "1e25cf7d63285f222207efb4b189f8c076b78b2d", - "url": "layer:basic" - }, - { - "rev": "c1425086885b011da0d37a51e16c07998b656fe6", - "url": "layer:sshproxy" - }, - { - "rev": "3301247d823a3fe266fb9e435e47d14a9da93779", - "url": "layer:vnfproxy" - }, - { - "rev": "525d4cb2b5b1db8a56ff3cf3132e101d03dc93b5", - "url": "simple" - } - ], - "signatures": { - ".build.manifest": [ - "build", - "dynamic", - "unchecked" - ], - ".gitignore": [ - "layer:sshproxy", - "static", - "17526a7f7312e7eefb932d1c514b7bc8425fab5bd1ade149e106ecf8bff67358" - ], - "LICENSE": [ - "layer:basic", - "static", - "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" - ], - "Makefile": [ - "layer:basic", - "static", - "b7ab3a34e5faf79b96a8632039a0ad0aa87f2a9b5f0ba604e007cafb22190301" - ], - "README.ex": [ - "simple", - "static", - "eb25333fb6ee2abc2d5dde329dfd6b4b2a6dd5516094e75354fb6cd84010afad" - ], - "README.md": [ - "layer:vnfproxy", - "static", - "84c9a4232bd1ff8672b5633d329349533dbb3d9c64b2f0ca13ffafd7a3add55b" - ], - "actions.yaml": [ - "simple", - "dynamic", - "ac75f1585b99b536feb6edb0866a5575b8e6390b2cbd7b767b1c573dd83051bd" - ], - "actions/generate-ssh-key": [ - "layer:sshproxy", - "static", - "46bf4be02078a0f5ac60ab81609372a8f9943885b7a4999ce5332ea2f4857036" - ], - "actions/get-ssh-public-key": [ - "layer:sshproxy", - "static", - "46bf4be02078a0f5ac60ab81609372a8f9943885b7a4999ce5332ea2f4857036" - ], - "actions/reboot": [ - "layer:vnfproxy", - "static", - "afc0df4409fb6eb7b7e6595490913c1efe329b393d4860ae226d8aaccc01fcf2" - ], - "actions/restart": [ - "layer:vnfproxy", - "static", - "afc0df4409fb6eb7b7e6595490913c1efe329b393d4860ae226d8aaccc01fcf2" - ], - "actions/run": [ - "layer:sshproxy", - "static", - "46bf4be02078a0f5ac60ab81609372a8f9943885b7a4999ce5332ea2f4857036" - ], - "actions/start": [ - "layer:vnfproxy", - "static", - "afc0df4409fb6eb7b7e6595490913c1efe329b393d4860ae226d8aaccc01fcf2" - ], - "actions/stop": [ - "layer:vnfproxy", - "static", - "afc0df4409fb6eb7b7e6595490913c1efe329b393d4860ae226d8aaccc01fcf2" - ], - "actions/touch": [ - "simple", - "static", - "d88c053583ba9f9f11051bd8162b970e48850c62f5a489f07a631cf4a19da503" - ], - "actions/upgrade": [ - "layer:vnfproxy", - "static", - "afc0df4409fb6eb7b7e6595490913c1efe329b393d4860ae226d8aaccc01fcf2" - ], - "actions/verify-ssh-credentials": [ - "layer:sshproxy", - "static", - "46bf4be02078a0f5ac60ab81609372a8f9943885b7a4999ce5332ea2f4857036" - ], - "bin/charm-env": [ - "layer:basic", - "static", - "75907c1ab3fe6d369c33ee776d32bcb4563948600f0fd4ca01d91193dc3eee50" - ], - "bin/layer_option": [ - "layer:options", - "static", - "e959bf29da4c5edff28b2602c24113c4df9e25cdc9f2aa3b5d46c8577b2a40cc" - ], - "config.yaml": [ - "simple", - "dynamic", - "38d42471f3d0fcaca02aa2f97566542437c7e58fb1b65551afb93598eba19d0d" - ], - "copyright": [ - "layer:basic", - "static", - "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" - ], - "copyright.layer-options": [ - "layer:options", - "static", - "f6740d66fd60b60f2533d9fcb53907078d1e20920a0219afce7182e2a1c97629" - ], - "hooks/collect-metrics": [ - "layer:vnfproxy", - "static", - "6b8e6016f80e401650b07301dc30cdc3d2928aa5f2532a8a92a55b906fa9ef19" - ], - "hooks/config-changed": [ - "layer:basic", - "dynamic", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "hooks/hook.template": [ - "layer:basic", - "static", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "hooks/install": [ - "layer:basic", - "dynamic", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "hooks/leader-elected": [ - "layer:basic", - "dynamic", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "hooks/leader-settings-changed": [ - "layer:basic", - "dynamic", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "hooks/post-series-upgrade": [ - "layer:basic", - "dynamic", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "hooks/pre-series-upgrade": [ - "layer:basic", - "dynamic", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "hooks/start": [ - "layer:basic", - "dynamic", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "hooks/stop": [ - "layer:basic", - "dynamic", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "hooks/update-status": [ - "layer:basic", - "dynamic", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "hooks/upgrade-charm": [ - "layer:basic", - "dynamic", - "2b693cb2a11594a80cc91235c2dc219a0a6303ae62bee8aa87eb35781f7158f7" - ], - "icon.svg": [ - "simple", - "static", - "085f367baa9c4ec724561b6e0d5d167c14c4295d6311b719a57729a17ac025c7" - ], - "layer.yaml": [ - "simple", - "dynamic", - "05443235de5b2744bb5ee2e10663d1a091336c7d1b6390f6e08ac3f578f3947d" - ], - "lib/charms/layer/__init__.py": [ - "layer:basic", - "static", - "dfe0d26c6bf409767de6e2546bc648f150e1b396243619bad3aa0553ab7e0e6f" - ], - "lib/charms/layer/basic.py": [ - "layer:basic", - "static", - "c3a84ecabc893ebdb2178b08daad29596ea8a0dbf6508fd2e45fe4b182e29c1d" - ], - "lib/charms/layer/execd.py": [ - "layer:basic", - "static", - "fda8bd491032db1db8ddaf4e99e7cc878c6fb5432efe1f91cadb5b34765d076d" - ], - "lib/charms/layer/options.py": [ - "layer:options", - "static", - "8ae7a07d22542fc964f2d2bee8219d1c78a68dace70a1b38d36d4aea47b1c3b2" - ], - "lib/charms/sshproxy.py": [ - "layer:sshproxy", - "static", - "d2060cbb756e094f44329bd0a404d47222af304aa75c508001ce0465a3c05974" - ], - "metadata.yaml": [ - "simple", - "dynamic", - "3f2e76c52df7a5323284bf57fca6b4eb5c1fe9b9d89e2183a0552d8f85505e71" - ], - "reactive/__init__.py": [ - "layer:basic", - "static", - "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - ], - "reactive/simple.py": [ - "simple", - "static", - "babb568b4e161a96e4040be28cd18bd4075879382b22326f14d58f7919e89ab5" - ], - "reactive/sshproxy.py": [ - "layer:sshproxy", - "static", - "2a47918df20f5d110b156bd9422ece72a6d54317e6167fef596c76c34bbe745f" - ], - "reactive/vnfproxy.py": [ - "layer:vnfproxy", - "static", - "8e4101a72f02832e5c233b731981ff483675be608321b8eb9743d605b3f7d77a" - ], - "requirements.txt": [ - "layer:basic", - "static", - "0f1c70d27e26005a96d66ad54482877ae20f7737693c833e29dd72bd6ac24892" - ], - "tests/00-setup": [ - "simple", - "static", - "111c079b81d260bbcd716dcf41672372a4cf4aaa14154b6c3055deeedae37a06" - ], - "tests/10-deploy": [ - "simple", - "static", - "3f06110a106f4c911fcf9f4d7a0299e648ead2a49046e78339e995d4e936962e" - ], - "tox.ini": [ - "layer:basic", - "static", - "0350315801b7fc2bd38a1f43a7e31d40d5a3bcfe5f6c8dac2bdf293833bc6d21" - ], - "version": [ - "simple", - "dynamic", - "6b3e4b3738acbb87f8a9a5d3e1276d7b34d9c1c1c416b6680fb02516cfb05438" - ], - "wheelhouse/Jinja2-2.10.1.tar.gz": [ - "layer:basic", - "dynamic", - "065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013" - ], - "wheelhouse/MarkupSafe-1.1.1.tar.gz": [ - "layer:basic", - "dynamic", - "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b" - ], - "wheelhouse/PyYAML-5.1.tar.gz": [ - "layer:basic", - "dynamic", - "436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95" - ], - "wheelhouse/Tempita-0.5.2.tar.gz": [ - "layer:basic", - "dynamic", - "cacecf0baa674d356641f1d406b8bff1d756d739c46b869a54de515d08e6fc9c" - ], - "wheelhouse/charmhelpers-0.19.13.tar.gz": [ - "layer:basic", - "dynamic", - "aaaf472ceaf03bec041b7a22ce9f43314e43bb673354942ef70d6708bbcb875a" - ], - "wheelhouse/charms.reactive-1.2.1.tar.gz": [ - "layer:basic", - "dynamic", - "011cc5141d2c1c488c1e54b8b3d56f91d48f72b7c3202f1027ab3d0666242503" - ], - "wheelhouse/ecdsa-0.13.2.tar.gz": [ - "layer:sshproxy", - "dynamic", - "5c034ffa23413ac923541ceb3ac14ec15a0d2530690413bff58c12b80e56d884" - ], - "wheelhouse/netaddr-0.7.19.tar.gz": [ - "layer:basic", - "dynamic", - "38aeec7cdd035081d3a4c306394b19d677623bf76fa0913f6695127c7753aefd" - ], - "wheelhouse/paramiko-1.16.3.tar.gz": [ - "layer:sshproxy", - "dynamic", - "97d932fdb4fec9aadf6bea368123f3ee15b92199f92eb62666370c7fed62d072" - ], - "wheelhouse/pip-8.1.2.tar.gz": [ - "layer:basic", - "dynamic", - "4d24b03ffa67638a3fa931c09fd9e0273ffa904e95ebebe7d4b1a54c93d7b732" - ], - "wheelhouse/pyaml-19.4.1.tar.gz": [ - "layer:basic", - "dynamic", - "c79ae98ececda136a034115ca178ee8bf3aa7df236c488c2f55d12f177b88f1e" - ], - "wheelhouse/pycrypto-2.6.1.tar.gz": [ - "layer:sshproxy", - "dynamic", - "f2ce1e989b272cfcb677616763e0a2e7ec659effa67a88aa92b3a65528f60a3c" - ], - "wheelhouse/setuptools-39.0.1.zip": [ - "layer:basic", - "dynamic", - "bec7badf0f60e7fc8153fac47836edc41b74e5d541d7692e614e635720d6a7c7" - ], - "wheelhouse/setuptools_scm-1.17.0.tar.gz": [ - "layer:basic", - "dynamic", - "70a4cf5584e966ae92f54a764e6437af992ba42ac4bca7eb37cc5d02b98ec40a" - ], - "wheelhouse/six-1.12.0.tar.gz": [ - "layer:basic", - "dynamic", - "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" - ] - } -} \ No newline at end of file diff --git a/hackfest_proxycharm_vnf/charms/simple/.gitignore b/hackfest_proxycharm_vnf/charms/simple/.gitignore deleted file mode 100644 index b8e7ba3a229bda75224369127d7454019fea641f..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/.gitignore +++ /dev/null @@ -1 +0,0 @@ -trusty/ diff --git a/hackfest_proxycharm_vnf/charms/simple/LICENSE b/hackfest_proxycharm_vnf/charms/simple/LICENSE deleted file mode 100644 index d645695673349e3947e8e5ae42332d0ac3164cd7..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - 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/hackfest_proxycharm_vnf/charms/simple/Makefile b/hackfest_proxycharm_vnf/charms/simple/Makefile deleted file mode 100644 index a1ad3a5cd27751144f6bd1a0a7db50f1e11b18eb..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -#!/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/hackfest_proxycharm_vnf/charms/simple/README.ex b/hackfest_proxycharm_vnf/charms/simple/README.ex deleted file mode 100644 index b6816b22ade2fa36326d7e1f612b6f6203768949..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/README.ex +++ /dev/null @@ -1,65 +0,0 @@ -# Overview - -Describe the intended usage of this charm and anything unique about how this -charm relates to others here. - -This README will be displayed in the Charm Store, it should be either Markdown -or RST. Ideal READMEs include instructions on how to use the charm, expected -usage, and charm features that your audience might be interested in. For an -example of a well written README check out Hadoop: -http://jujucharms.com/charms/precise/hadoop - -Use this as a Markdown reference if you need help with the formatting of this -README: http://askubuntu.com/editing-help - -This charm provides [service][]. Add a description here of what the service -itself actually does. - -Also remember to check the [icon guidelines][] so that your charm looks good -in the Juju GUI. - -# Usage - -Step by step instructions on using the charm: - -juju deploy servicename - -and so on. If you're providing a web service or something that the end user -needs to go to, tell them here, especially if you're deploying a service that -might listen to a non-default port. - -You can then browse to http://ip-address to configure the service. - -## Scale out Usage - -If the charm has any recommendations for running at scale, outline them in -examples here. For example if you have a memcached relation that improves -performance, mention it here. - -## 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 - -Though this will be listed in the charm store itself don't assume a user will -know that, so include that information here: - -## 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/hackfest_proxycharm_vnf/charms/simple/README.md b/hackfest_proxycharm_vnf/charms/simple/README.md deleted file mode 100644 index e4d801cfaf4fd723eea247e6a383c6333ca8e1a3..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/README.md +++ /dev/null @@ -1,267 +0,0 @@ -# vnfproxy - -A [Juju](https://jujucharms.com/) charm layer. See [how it works](https://jujucharms.com/how-it-works) and the [Getting Started](https://jujucharms.com/docs/stable/developer-getting-started) page for more information about Juju and Charms. - -[OSM](https://osm.etsi.org/) is an [ETSI](http://www.etsi.org/)-hosted project to develop an Open Source NFV Management and Orchestration (MANO) software stack aligned with ETSI NFV. - -## Overview - -The vnfproxy [layer](https://jujucharms.com/docs/stable/developer-layers) is intended for use by vendors who wish to integrate a VNF with OSM. The current release of OSM only supports a lightweight version of Juju charms, which we refer to as VNF Configuration or "proxy" charms. - -This document will describe the steps necessary to create a charm for your VNF. - -First, consider the diagram below: - -``` -+---------------------+ +---------------------+ -| <----+ | -| Resource | | Service | -| Orchestrator (RO) +----> Orchestrator (SO) | -| | | | -+------------------+--+ +-------+----^--------+ - | | | - | | | - | | | - +-----v-----+ +-v----+--+ - | <-------+ | - | Virtual | | Proxy | - | Machine | | Charm | - | +-------> | - +-----------+ +---------+ -``` - -The Virtual Machine (VM) is created by the Resource Orchestrator (RO), at the -request of the Service Orchestrator (SO). Once the VM has been created, a -"proxy charm" is deployed in order to facilitate operations between the SO and -your service running within the VM. - -As such, a proxy charm will expose a number of actions -- also known as service primitives -- that are run by the SO. By default, the following actions are exposed: - -```bash -actions -├── reboot -├── restart -├── run -├── start -├── stop -└── upgrade -``` - -Some actions, such as `run` and `reboot`, do not require any additional configuration. The rest, however, require you to implement the command(s) required to interact with your VNF. - -A charm is composed of multiple layers of code. The layer you create for your VNF is the topmost layer. It will include the basic layer, which provides the framework for building reactive charms, and the vnfproxy layer, which adds functionality specific to operating and configuring VNFs. Finally, these layers are combined to form the charm you'll place inside your VNF Descriptor Package. - -## Step 1: Create the layer for your proxy charm: - -Create a new charm layer, substituting "myvnf" with the name of your VNF. - -```bash -$ charm create myvnf -$ cd myvnf -``` - -Modify `layer.yaml` to the following: -```yaml -includes: - - layer:basic - - layer:vnfproxy -``` - -The `metadata.yaml` describes your service. It should look similar to the following: - -```yaml -name: myvnf -summary: My VNF provides a specific virtualized network function. -maintainer: Adam Israel -description: | - A longer description of your VNF and what it provides to users. -series: - - trusty - - xenial -tags: - - osm - - vnf -subordinate: false -``` - -### Actions (Service Primitives) - -In Juju, Service Primitives are referred to as Actions. These are commands that will be executed on the VNF on request of the Service Orchestrator. - -#### Defining Actions - -Actions are defined through a yaml file called `actions.yaml` in the root directory of your charm. This file describes the action and the parameters it requires in order to execute. - -```yaml -configure-server: - description: "Configure a thing" - params: - polling-interval: - type: int - description: "The interval, in seconds, to poll a thing." - default: 30 - -``` -#### Implementing your Actions - -We use executable files in the charm's `actions/` directory to invoke a reactive handler for your VNF logic. Each file should have the same name as the action you defined in `actions.yaml`. - -Cut and paste the following into `actions/configure-server` and `chmod +x actions/configure-server`: - -```bash -#!/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, 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)) -``` - -Next, we'll add the code that will be executed inside your VNF via the ssh credentials. Open `reactive/myvnf.py` and add the following reactive handler code: - -```python -# Change configure-server to match the name of the action you want to execute. -@when('actions.configure-server') -def configure_server(): - err = '' - try: - # Put the code here that you want to execute - cmd = "" - result, err = charms.sshproxy._run(cmd) - except: - action_fail('command failed:' + err) - else: - action_set({'output': result}) - finally: - remove_flag('actions.start') - -``` - -#### Default Actions (Service Primitives) - -The vnfproxy layer defines several default actions that you may implement. If you choose not to implement these, the actions will do nothing. - -Add the following code to `reactive/myvnf.py` and fill in the `cmd` variable with the command to be run on your VNF: - -```python -@when('actions.start') -def start(): - err = '' - try: - # Put the code here that you want to execute - cmd = "" - result, err = charms.sshproxy._run(cmd) - except: - action_fail('command failed:' + err) - else: - action_set({'output': result}) - finally: - remove_flag('actions.start') - - -@when('actions.stop') -def stop(): - err = '' - try: - # Enter the command to stop your service(s) - cmd = "service myname stop" - result, err = charms.sshproxy._run(cmd) - except: - action_fail('command failed:' + err) - else: - action_set({'output': result}) - finally: - remove_flag('actions.stop') - - -@when('actions.restart') -def restart(): - err = '' - try: - # Enter the command to restart your service(s) - cmd = "service myname restart" - result, err = charms.sshproxy._run(cmd) - except: - action_fail('command failed:' + err) - else: - action_set({'output': result}) - finally: - remove_flag('actions.restart') - - @when('actions.upgrade') - def upgrade_vnf(): - """Upgrade the software on the VNF. - - This action is intended to be used to perform software upgrades on a running VNF. - """ - err = '' - try: - # Enter the command (s) to upgrade your VNF software - cmd = "" - result, err = charms.sshproxy._run(cmd) - except: - action_fail('command failed:' + err) - else: - action_set({'output': result}) - finally: - remove_flag('actions.upgrade') -``` - -Rename `README.ex` to `README.md` and describe your application and its usage. - -### Configuration - -Charms support immutable configuration, defined by the `config.yaml` file. In the case of OSM, it's configuration is primarily driven through service primitives. Feel free to delete `config.yaml`. - -### Metrics - -Juju supports the polling of metrics. To do this, create the `metrics.yaml` file in the root directory of your charm, following the example below. The command specified will be executed inside your VNF; it should return a positive decimal number. The collected metrics will be made available to OSM, beginning with Release 3. - -```yaml -metrics: - uptime: - type: gauge - description: "Seconds since the machine was rebooted." - command: awk '{print $1}' /proc/uptime - -``` - -### Building your VNF charm - -Once you've implemented your actions, you need to compile the various charm layers. From the charm's root directory: -```bash -$ charm build -``` - -This will combine all of the layers required by your VNF layer into a single charm, in the builds/ directory. - -### VNF Descriptor Package -Copy the combined charm into the `charm` directory of your [VNF package]: - -``` -├── charm -│ └── myvnf -├── cloud_init -│ └── myvnf_cloud_init.cfg -├── icons -│ └── myvnf_logo.png -└── myvnf_vnfd.yaml -``` - -## Contact - -Send an email to the OSM_TECH@list.etsi.org mailing list. - -[VNF package]: https://osm.etsi.org/wikipub/index.php/Creating_your_own_VNF_package_(Release_TWO) diff --git a/hackfest_proxycharm_vnf/charms/simple/actions.yaml b/hackfest_proxycharm_vnf/charms/simple/actions.yaml deleted file mode 100644 index 14f8b3f33ffd908d03b3e8263edfcb10b7c317b3..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions.yaml +++ /dev/null @@ -1,53 +0,0 @@ -## -# Copyright 2016 Canonical Ltd. -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -## - -"run": - "description": "Run an arbitrary command" - "params": - "command": - "description": "The command to execute." - "type": "string" - "default": "" - "required": - - "command" -"generate-ssh-key": - "description": "Generate a new SSH keypair for this unit. This will replace any\ - \ existing previously generated keypair." -"verify-ssh-credentials": - "description": "Verify that this unit can authenticate with server specified by\ - \ ssh-hostname and ssh-username." -"get-ssh-public-key": - "description": "Get the public SSH key for this unit." -"start": - "description": "Stop the service on the VNF." -"stop": - "description": "Stop the service on the VNF." -"restart": - "description": "Stop the service on the VNF." -"reboot": - "description": "Reboot the VNF virtual machine." -"upgrade": - "description": "Upgrade the software on the VNF." -"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/hackfest_proxycharm_vnf/charms/simple/actions/generate-ssh-key b/hackfest_proxycharm_vnf/charms/simple/actions/generate-ssh-key deleted file mode 100755 index 7e30af4cfa084120a9343bf71c421b69ec3f2967..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions/generate-ssh-key +++ /dev/null @@ -1,33 +0,0 @@ -#!/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/hackfest_proxycharm_vnf/charms/simple/actions/get-ssh-public-key b/hackfest_proxycharm_vnf/charms/simple/actions/get-ssh-public-key deleted file mode 100755 index 7e30af4cfa084120a9343bf71c421b69ec3f2967..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions/get-ssh-public-key +++ /dev/null @@ -1,33 +0,0 @@ -#!/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/hackfest_proxycharm_vnf/charms/simple/actions/reboot b/hackfest_proxycharm_vnf/charms/simple/actions/reboot deleted file mode 100755 index 9a2ba24b9efdfa3d21457469c503c050196fb170..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions/reboot +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -import sys -sys.path.append('lib') -from charms.reactive import main, set_flag -from charmhelpers.core.hookenv import action_fail, action_name - -set_flag('actions.{}'.format(action_name())) - -try: - main() -except Exception as e: - action_fail(repr(e)) diff --git a/hackfest_proxycharm_vnf/charms/simple/actions/restart b/hackfest_proxycharm_vnf/charms/simple/actions/restart deleted file mode 100755 index 9a2ba24b9efdfa3d21457469c503c050196fb170..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions/restart +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -import sys -sys.path.append('lib') -from charms.reactive import main, set_flag -from charmhelpers.core.hookenv import action_fail, action_name - -set_flag('actions.{}'.format(action_name())) - -try: - main() -except Exception as e: - action_fail(repr(e)) diff --git a/hackfest_proxycharm_vnf/charms/simple/actions/run b/hackfest_proxycharm_vnf/charms/simple/actions/run deleted file mode 100755 index 7e30af4cfa084120a9343bf71c421b69ec3f2967..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions/run +++ /dev/null @@ -1,33 +0,0 @@ -#!/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/hackfest_proxycharm_vnf/charms/simple/actions/start b/hackfest_proxycharm_vnf/charms/simple/actions/start deleted file mode 100755 index 9a2ba24b9efdfa3d21457469c503c050196fb170..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions/start +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -import sys -sys.path.append('lib') -from charms.reactive import main, set_flag -from charmhelpers.core.hookenv import action_fail, action_name - -set_flag('actions.{}'.format(action_name())) - -try: - main() -except Exception as e: - action_fail(repr(e)) diff --git a/hackfest_proxycharm_vnf/charms/simple/actions/stop b/hackfest_proxycharm_vnf/charms/simple/actions/stop deleted file mode 100755 index 9a2ba24b9efdfa3d21457469c503c050196fb170..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions/stop +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -import sys -sys.path.append('lib') -from charms.reactive import main, set_flag -from charmhelpers.core.hookenv import action_fail, action_name - -set_flag('actions.{}'.format(action_name())) - -try: - main() -except Exception as e: - action_fail(repr(e)) diff --git a/hackfest_proxycharm_vnf/charms/simple/actions/touch b/hackfest_proxycharm_vnf/charms/simple/actions/touch deleted file mode 100755 index d85d3facc597b486ac5b5cfa26ee9d74089c90cb..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions/touch +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -## -# Copyright 2016 Canonical Ltd. -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -## -import sys -sys.path.append('lib') - -from charms.reactive import main -from charms.reactive import set_state -from charmhelpers.core.hookenv import action_fail, action_name - -""" -`set_state` only works here because it's flushed to disk inside the `main()` -loop. remove_state will need to be called inside the action method. -""" -set_state('actions.{}'.format(action_name())) - -try: - main() -except Exception as e: - action_fail(repr(e)) diff --git a/hackfest_proxycharm_vnf/charms/simple/actions/upgrade b/hackfest_proxycharm_vnf/charms/simple/actions/upgrade deleted file mode 100755 index 9a2ba24b9efdfa3d21457469c503c050196fb170..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions/upgrade +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -import sys -sys.path.append('lib') -from charms.reactive import main, set_flag -from charmhelpers.core.hookenv import action_fail, action_name - -set_flag('actions.{}'.format(action_name())) - -try: - main() -except Exception as e: - action_fail(repr(e)) diff --git a/hackfest_proxycharm_vnf/charms/simple/actions/verify-ssh-credentials b/hackfest_proxycharm_vnf/charms/simple/actions/verify-ssh-credentials deleted file mode 100755 index 7e30af4cfa084120a9343bf71c421b69ec3f2967..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/actions/verify-ssh-credentials +++ /dev/null @@ -1,33 +0,0 @@ -#!/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/hackfest_proxycharm_vnf/charms/simple/bin/charm-env b/hackfest_proxycharm_vnf/charms/simple/bin/charm-env deleted file mode 100755 index c2adc7fbff30d0e062603cc13a1ae30ad05a092b..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/bin/charm-env +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/bash - -find_charm_dirs() { - # Hopefully, $JUJU_CHARM_DIR is set so which venv to use in unambiguous. - if [[ -n "$JUJU_CHARM_DIR" || -n "$CHARM_DIR" ]]; then - if [[ -z "$JUJU_CHARM_DIR" ]]; then - # accept $CHARM_DIR to be more forgiving - export JUJU_CHARM_DIR="$CHARM_DIR" - fi - if [[ -z "$CHARM_DIR" ]]; then - # set CHARM_DIR as well to help with backwards compatibility - export CHARM_DIR="$JUJU_CHARM_DIR" - fi - return - fi - # Try to guess the value for JUJU_CHARM_DIR by looking for a non-subordinate - # (because there's got to be at least one principle) charm directory; - # if there are several, pick the first by alpha order. - agents_dir="/var/lib/juju/agents" - if [[ -d "$agents_dir" ]]; then - desired_charm="$1" - found_charm_dir="" - if [[ -n "$desired_charm" ]]; then - for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do - charm_name="$(JUJU_CHARM_DIR="$charm_dir" charm-env python3 -c 'from charmhelpers.core.hookenv import charm_name; print(charm_name())')" - if [[ "$charm_name" == "$desired_charm" ]]; then - if [[ -n "$found_charm_dir" ]]; then - >&2 echo "Ambiguous possibilities for JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" - exit 1 - fi - found_charm_dir="$charm_dir" - fi - done - if [[ -z "$found_charm_dir" ]]; then - >&2 echo "Unable to determine JUJU_CHARM_DIR matching '$desired_charm'; please run within a Juju hook context" - exit 1 - fi - export JUJU_CHARM_DIR="$found_charm_dir" - export CHARM_DIR="$found_charm_dir" - return - fi - # shellcheck disable=SC2126 - non_subordinates="$(grep -L 'subordinate:.*true' "$agents_dir"/unit-*/charm/metadata.yaml | wc -l)" - if [[ "$non_subordinates" -gt 1 ]]; then - >&2 echo 'Ambiguous possibilities for JUJU_CHARM_DIR; please use --charm or run within a Juju hook context' - exit 1 - elif [[ "$non_subordinates" -eq 1 ]]; then - for charm_dir in $(/bin/ls -d "$agents_dir"/unit-*/charm); do - if grep -q 'subordinate:.*true' "$charm_dir/metadata.yaml"; then - continue - fi - export JUJU_CHARM_DIR="$charm_dir" - export CHARM_DIR="$charm_dir" - return - done - fi - fi - >&2 echo 'Unable to determine JUJU_CHARM_DIR; please run within a Juju hook context' - exit 1 -} - -try_activate_venv() { - if [[ -d "$JUJU_CHARM_DIR/../.venv" ]]; then - . "$JUJU_CHARM_DIR/../.venv/bin/activate" - fi -} - -find_wrapped() { - PATH="${PATH/\/usr\/local\/sbin:}" which "$(basename "$0")" -} - - -# allow --charm option to hint which JUJU_CHARM_DIR to choose when ambiguous -# NB: --charm option must come first -# NB: option must be processed outside find_charm_dirs to modify $@ -charm_name="" -if [[ "$1" == "--charm" ]]; then - charm_name="$2" - shift; shift -fi - -find_charm_dirs "$charm_name" -try_activate_venv -export PYTHONPATH="$JUJU_CHARM_DIR/lib:$PYTHONPATH" - -if [[ "$(basename "$0")" == "charm-env" ]]; then - # being used as a shebang - exec "$@" -elif [[ "$0" == "$BASH_SOURCE" ]]; then - # being invoked as a symlink wrapping something to find in the venv - exec "$(find_wrapped)" "$@" -elif [[ "$(basename "$BASH_SOURCE")" == "charm-env" ]]; then - # being sourced directly; do nothing - /bin/true -else - # being sourced for wrapped bash helpers - . "$(find_wrapped)" -fi diff --git a/hackfest_proxycharm_vnf/charms/simple/bin/layer_option b/hackfest_proxycharm_vnf/charms/simple/bin/layer_option deleted file mode 100755 index 3253ef8aadb95807f26e5ebd8486adde67e5cbc2..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/bin/layer_option +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import argparse -from charms import layer - - -parser = argparse.ArgumentParser(description='Access layer options.') -parser.add_argument('section', - help='the section, or layer, the option is from') -parser.add_argument('option', - help='the option to access') - -args = parser.parse_args() -value = layer.options.get(args.section, args.option) -if isinstance(value, bool): - sys.exit(0 if value else 1) -elif isinstance(value, list): - for val in value: - print(val) -else: - print(value) diff --git a/hackfest_proxycharm_vnf/charms/simple/config.yaml b/hackfest_proxycharm_vnf/charms/simple/config.yaml deleted file mode 100644 index e6e8edde939969f15948c60661da9ed8da2f0f05..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/config.yaml +++ /dev/null @@ -1,41 +0,0 @@ -"options": - "ssh-hostname": - "type": "string" - "default": "" - "description": "The hostname or IP address of the machine to" - "ssh-username": - "type": "string" - "default": "" - "description": "The username to login as." - "ssh-password": - "type": "string" - "default": "" - "description": "The password used to authenticate." - "ssh-private-key": - "type": "string" - "default": "" - "description": "DEPRECATED. The private ssh key to be used to authenticate." - "ssh-public-key": - "type": "string" - "default": "" - "description": "The public key of this unit." - "ssh-key-type": - "type": "string" - "default": "rsa" - "description": "The type of encryption to use for the SSH key." - "ssh-key-bits": - "type": "int" - "default": !!int "4096" - "description": "The number of bits to use for the SSH key." - "string-option": - "type": "string" - "default": "Default Value" - "description": "A short description of the configuration option" - "boolean-option": - "type": "boolean" - "default": !!bool "false" - "description": "A short description of the configuration option" - "int-option": - "type": "int" - "default": !!int "9001" - "description": "A short description of the configuration option" diff --git a/hackfest_proxycharm_vnf/charms/simple/copyright b/hackfest_proxycharm_vnf/charms/simple/copyright deleted file mode 100644 index d4fdd18281c632d030a301d26d45b4dabdb308ef..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/copyright +++ /dev/null @@ -1,16 +0,0 @@ -Format: http://dep.debian.net/deps/dep5/ - -Files: * -Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. -License: Apache License 2.0 - 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/hackfest_proxycharm_vnf/charms/simple/copyright.layer-options b/hackfest_proxycharm_vnf/charms/simple/copyright.layer-options deleted file mode 100644 index d4fdd18281c632d030a301d26d45b4dabdb308ef..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/copyright.layer-options +++ /dev/null @@ -1,16 +0,0 @@ -Format: http://dep.debian.net/deps/dep5/ - -Files: * -Copyright: Copyright 2015-2017, Canonical Ltd., All Rights Reserved. -License: Apache License 2.0 - 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/hackfest_proxycharm_vnf/charms/simple/hooks/collect-metrics b/hackfest_proxycharm_vnf/charms/simple/hooks/collect-metrics deleted file mode 100755 index ed69a2e98c7606eac29ddc80ad02e04fb2a6addb..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/collect-metrics +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -"""Handle the collect-metrics hook via proxy. - -The normal metrics layer will only execute metrics collection against the local -machine. This hook implements the same approach, but runs the collection code -against the configured ssh proxy. - -Because the metrics hook runs in a restricted context, it can't be run as a -normal reactive event, nor can it access things like config. -""" - -import os -import shlex -from subprocess import check_call, CalledProcessError -import sys -import yaml - -# Load modules from $CHARM_DIR/lib -sys.path.append('lib') -import charms.sshproxy - - -def build_command(doc): - """Build the commands to report metrics. - - Build a list of `add-metric` commands to report the current metrics - back to the Juju controller. - """ - values = {} - metrics = doc.get("metrics", {}) - for metric, mdoc in metrics.items(): - cmd = mdoc.get("command") - if cmd: - try: - value, err = charms.sshproxy._run( - # The command may contain quotes that need to be preserved, - # i.e., `awk '{print $1}' /proc/uptime` - shlex.split(cmd, posix=False) - ) - except Exception as e: - # Ignore all errors - with open("metrics.log", "a") as f: - f.write("{}".format(e)) - continue - - value = value.strip() - if value: - values[metric] = value - - if not values: - return None - command = ["add-metric"] - for metric, value in values.items(): - command.append("%s=%s" % (metric, value)) - return command - - -if __name__ == '__main__': - charm_dir = os.path.dirname(os.path.abspath(os.path.join(__file__, ".."))) - metrics_yaml = os.path.join(charm_dir, "metrics.yaml") - with open(metrics_yaml) as f: - doc = yaml.load(f) - command = build_command(doc) - if command: - check_call(command) diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/config-changed b/hackfest_proxycharm_vnf/charms/simple/hooks/config-changed deleted file mode 100755 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/config-changed +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/hook.template b/hackfest_proxycharm_vnf/charms/simple/hooks/hook.template deleted file mode 100644 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/hook.template +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/install b/hackfest_proxycharm_vnf/charms/simple/hooks/install deleted file mode 100755 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/install +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/leader-elected b/hackfest_proxycharm_vnf/charms/simple/hooks/leader-elected deleted file mode 100755 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/leader-elected +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/leader-settings-changed b/hackfest_proxycharm_vnf/charms/simple/hooks/leader-settings-changed deleted file mode 100755 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/leader-settings-changed +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/post-series-upgrade b/hackfest_proxycharm_vnf/charms/simple/hooks/post-series-upgrade deleted file mode 100755 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/post-series-upgrade +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/pre-series-upgrade b/hackfest_proxycharm_vnf/charms/simple/hooks/pre-series-upgrade deleted file mode 100755 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/pre-series-upgrade +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/start b/hackfest_proxycharm_vnf/charms/simple/hooks/start deleted file mode 100755 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/start +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/stop b/hackfest_proxycharm_vnf/charms/simple/hooks/stop deleted file mode 100755 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/stop +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/update-status b/hackfest_proxycharm_vnf/charms/simple/hooks/update-status deleted file mode 100755 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/update-status +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/hooks/upgrade-charm b/hackfest_proxycharm_vnf/charms/simple/hooks/upgrade-charm deleted file mode 100755 index 9858c6be15b8a682778ab4727835151e1b693801..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/hooks/upgrade-charm +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# Load modules from $JUJU_CHARM_DIR/lib -import sys -sys.path.append('lib') - -from charms.layer import basic # noqa -basic.bootstrap_charm_deps() - -from charmhelpers.core import hookenv # noqa -hookenv.atstart(basic.init_config_states) -hookenv.atexit(basic.clear_config_states) - - -# This will load and run the appropriate @hook and other decorated -# handlers from $JUJU_CHARM_DIR/reactive, $JUJU_CHARM_DIR/hooks/reactive, -# and $JUJU_CHARM_DIR/hooks/relations. -# -# See https://jujucharms.com/docs/stable/authors-charm-building -# for more information on this pattern. -from charms.reactive import main # noqa -main() diff --git a/hackfest_proxycharm_vnf/charms/simple/icon.svg b/hackfest_proxycharm_vnf/charms/simple/icon.svg deleted file mode 100644 index e092eef75dc91ec1c1d85ddfcc3ca157a9c02a27..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/icon.svg +++ /dev/null @@ -1,279 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - diff --git a/hackfest_proxycharm_vnf/charms/simple/layer.yaml b/hackfest_proxycharm_vnf/charms/simple/layer.yaml deleted file mode 100644 index c851bc323d21f497e85c52ff0dc74f792c6df2b3..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/layer.yaml +++ /dev/null @@ -1,15 +0,0 @@ -"includes": -- "layer:options" -- "layer:basic" -- "layer:sshproxy" -- "layer:vnfproxy" -"options": - "basic": - "use_venv": !!bool "false" - "packages": [] - "python_packages": [] - "include_system_packages": !!bool "true" - "simple": {} - "sshproxy": {} - "vnfproxy": {} -"is": "simple" diff --git a/hackfest_proxycharm_vnf/charms/simple/lib/charms/layer/__init__.py b/hackfest_proxycharm_vnf/charms/simple/lib/charms/layer/__init__.py deleted file mode 100644 index a8e0c640642f44cac85df60f3d9b7f73b0bb18bb..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/lib/charms/layer/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -import sys -from importlib import import_module -from pathlib import Path - - -def import_layer_libs(): - """ - Ensure that all layer libraries are imported. - - This makes it possible to do the following: - - from charms import layer - - layer.foo.do_foo_thing() - - Note: This function must be called after bootstrap. - """ - for module_file in Path('lib/charms/layer').glob('*'): - module_name = module_file.stem - if module_name in ('__init__', 'basic', 'execd') or not ( - module_file.suffix == '.py' or module_file.is_dir() - ): - continue - import_module('charms.layer.{}'.format(module_name)) - - -# Terrible hack to support the old terrible interface. -# Try to get people to call layer.options.get() instead so -# that we can remove this garbage. -# Cribbed from https://stackoverfLow.com/a/48100440/4941864 -class OptionsBackwardsCompatibilityHack(sys.modules[__name__].__class__): - def __call__(self, section=None, layer_file=None): - if layer_file is None: - return self.get(section=section) - else: - return self.get(section=section, - layer_file=Path(layer_file)) - - -def patch_options_interface(): - from charms.layer import options - if sys.version_info.minor >= 5: - options.__class__ = OptionsBackwardsCompatibilityHack - else: - # Py 3.4 doesn't support changing the __class__, so we have to do it - # another way. The last line is needed because we already have a - # reference that doesn't get updated with sys.modules. - name = options.__name__ - hack = OptionsBackwardsCompatibilityHack(name) - hack.get = options.get - sys.modules[name] = hack - sys.modules[__name__].options = hack - - -try: - patch_options_interface() -except ImportError: - # This may fail if pyyaml hasn't been installed yet. But in that - # case, the bootstrap logic will try it again once it has. - pass diff --git a/hackfest_proxycharm_vnf/charms/simple/lib/charms/layer/basic.py b/hackfest_proxycharm_vnf/charms/simple/lib/charms/layer/basic.py deleted file mode 100644 index 1a6ea9fc620de16c0eb13113158aacc241317aab..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/lib/charms/layer/basic.py +++ /dev/null @@ -1,259 +0,0 @@ -import os -import sys -import shutil -from glob import glob -from subprocess import check_call, CalledProcessError -from time import sleep - -from charms import layer -from charms.layer.execd import execd_preinstall - - -def lsb_release(): - """Return /etc/lsb-release in a dict""" - d = {} - with open('/etc/lsb-release', 'r') as lsb: - for l in lsb: - k, v = l.split('=') - d[k.strip()] = v.strip() - return d - - -def bootstrap_charm_deps(): - """ - Set up the base charm dependencies so that the reactive system can run. - """ - # execd must happen first, before any attempt to install packages or - # access the network, because sites use this hook to do bespoke - # configuration and install secrets so the rest of this bootstrap - # and the charm itself can actually succeed. This call does nothing - # unless the operator has created and populated $JUJU_CHARM_DIR/exec.d. - execd_preinstall() - # ensure that $JUJU_CHARM_DIR/bin is on the path, for helper scripts - charm_dir = os.environ['JUJU_CHARM_DIR'] - os.environ['PATH'] += ':%s' % os.path.join(charm_dir, 'bin') - venv = os.path.abspath('../.venv') - vbin = os.path.join(venv, 'bin') - vpip = os.path.join(vbin, 'pip') - vpy = os.path.join(vbin, 'python') - hook_name = os.path.basename(sys.argv[0]) - is_bootstrapped = os.path.exists('wheelhouse/.bootstrapped') - is_charm_upgrade = hook_name == 'upgrade-charm' - is_series_upgrade = hook_name == 'post-series-upgrade' - post_upgrade = os.path.exists('wheelhouse/.upgrade') - is_upgrade = not post_upgrade and (is_charm_upgrade or is_series_upgrade) - if is_bootstrapped and not is_upgrade: - activate_venv() - # the .upgrade file prevents us from getting stuck in a loop - # when re-execing to activate the venv; at this point, we've - # activated the venv, so it's safe to clear it - if post_upgrade: - os.unlink('wheelhouse/.upgrade') - return - if is_series_upgrade and os.path.exists(venv): - # series upgrade should do a full clear of the venv, rather than just - # updating it, to bring in updates to Python itself - shutil.rmtree(venv) - if is_upgrade: - if os.path.exists('wheelhouse/.bootstrapped'): - os.unlink('wheelhouse/.bootstrapped') - open('wheelhouse/.upgrade', 'w').close() - # bootstrap wheelhouse - if os.path.exists('wheelhouse'): - with open('/root/.pydistutils.cfg', 'w') as fp: - # make sure that easy_install also only uses the wheelhouse - # (see https://github.com/pypa/pip/issues/410) - fp.writelines([ - "[easy_install]\n", - "allow_hosts = ''\n", - "find_links = file://{}/wheelhouse/\n".format(charm_dir), - ]) - apt_install([ - 'python3-pip', - 'python3-setuptools', - 'python3-yaml', - 'python3-dev', - 'python3-wheel', - 'build-essential', - ]) - from charms.layer import options - cfg = options.get('basic') - # include packages defined in layer.yaml - apt_install(cfg.get('packages', [])) - # if we're using a venv, set it up - if cfg.get('use_venv'): - if not os.path.exists(venv): - series = lsb_release()['DISTRIB_CODENAME'] - if series in ('precise', 'trusty'): - apt_install(['python-virtualenv']) - else: - apt_install(['virtualenv']) - cmd = ['virtualenv', '-ppython3', '--never-download', venv] - if cfg.get('include_system_packages'): - cmd.append('--system-site-packages') - check_call(cmd) - os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) - pip = vpip - else: - pip = 'pip3' - # save a copy of system pip to prevent `pip3 install -U pip` - # from changing it - if os.path.exists('/usr/bin/pip'): - shutil.copy2('/usr/bin/pip', '/usr/bin/pip.save') - # need newer pip, to fix spurious Double Requirement error: - # https://github.com/pypa/pip/issues/56 - check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', - 'pip']) - # per https://github.com/juju-solutions/layer-basic/issues/110 - # this replaces the setuptools that was copied over from the system on - # venv create with latest setuptools and adds setuptools_scm - check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse', - 'setuptools', 'setuptools-scm']) - # install the rest of the wheelhouse deps - check_call([pip, 'install', '-U', '--no-index', '-f', 'wheelhouse'] + - glob('wheelhouse/*')) - # re-enable installation from pypi - os.remove('/root/.pydistutils.cfg') - # install python packages from layer options - if cfg.get('python_packages'): - check_call([pip, 'install', '-U'] + cfg.get('python_packages')) - if not cfg.get('use_venv'): - # restore system pip to prevent `pip3 install -U pip` - # from changing it - if os.path.exists('/usr/bin/pip.save'): - shutil.copy2('/usr/bin/pip.save', '/usr/bin/pip') - os.remove('/usr/bin/pip.save') - # setup wrappers to ensure envs are used for scripts - shutil.copy2('bin/charm-env', '/usr/local/sbin/') - for wrapper in ('charms.reactive', 'charms.reactive.sh', - 'chlp', 'layer_option'): - src = os.path.join('/usr/local/sbin', 'charm-env') - dst = os.path.join('/usr/local/sbin', wrapper) - if not os.path.exists(dst): - os.symlink(src, dst) - if cfg.get('use_venv'): - shutil.copy2('bin/layer_option', vbin) - else: - shutil.copy2('bin/layer_option', '/usr/local/bin/') - # re-link the charm copy to the wrapper in case charms - # call bin/layer_option directly (as was the old pattern) - os.remove('bin/layer_option') - os.symlink('/usr/local/sbin/layer_option', 'bin/layer_option') - # flag us as having already bootstrapped so we don't do it again - open('wheelhouse/.bootstrapped', 'w').close() - # Ensure that the newly bootstrapped libs are available. - # Note: this only seems to be an issue with namespace packages. - # Non-namespace-package libs (e.g., charmhelpers) are available - # without having to reload the interpreter. :/ - reload_interpreter(vpy if cfg.get('use_venv') else sys.argv[0]) - - -def activate_venv(): - """ - Activate the venv if enabled in ``layer.yaml``. - - This is handled automatically for normal hooks, but actions might - need to invoke this manually, using something like: - - # Load modules from $JUJU_CHARM_DIR/lib - import sys - sys.path.append('lib') - - from charms.layer.basic import activate_venv - activate_venv() - - This will ensure that modules installed in the charm's - virtual environment are available to the action. - """ - from charms.layer import options - venv = os.path.abspath('../.venv') - vbin = os.path.join(venv, 'bin') - vpy = os.path.join(vbin, 'python') - use_venv = options.get('basic', 'use_venv') - if use_venv and '.venv' not in sys.executable: - # activate the venv - os.environ['PATH'] = ':'.join([vbin, os.environ['PATH']]) - reload_interpreter(vpy) - layer.patch_options_interface() - layer.import_layer_libs() - - -def reload_interpreter(python): - """ - Reload the python interpreter to ensure that all deps are available. - - Newly installed modules in namespace packages sometimes seemt to - not be picked up by Python 3. - """ - os.execve(python, [python] + list(sys.argv), os.environ) - - -def apt_install(packages): - """ - Install apt packages. - - This ensures a consistent set of options that are often missed but - should really be set. - """ - if isinstance(packages, (str, bytes)): - packages = [packages] - - env = os.environ.copy() - - if 'DEBIAN_FRONTEND' not in env: - env['DEBIAN_FRONTEND'] = 'noninteractive' - - cmd = ['apt-get', - '--option=Dpkg::Options::=--force-confold', - '--assume-yes', - 'install'] - for attempt in range(3): - try: - check_call(cmd + packages, env=env) - except CalledProcessError: - if attempt == 2: # third attempt - raise - try: - # sometimes apt-get update needs to be run - check_call(['apt-get', 'update']) - except CalledProcessError: - # sometimes it's a dpkg lock issue - pass - sleep(5) - else: - break - - -def init_config_states(): - import yaml - from charmhelpers.core import hookenv - from charms.reactive import set_state - from charms.reactive import toggle_state - config = hookenv.config() - config_defaults = {} - config_defs = {} - config_yaml = os.path.join(hookenv.charm_dir(), 'config.yaml') - if os.path.exists(config_yaml): - with open(config_yaml) as fp: - config_defs = yaml.safe_load(fp).get('options', {}) - config_defaults = {key: value.get('default') - for key, value in config_defs.items()} - for opt in config_defs.keys(): - if config.changed(opt): - set_state('config.changed') - set_state('config.changed.{}'.format(opt)) - toggle_state('config.set.{}'.format(opt), config.get(opt)) - toggle_state('config.default.{}'.format(opt), - config.get(opt) == config_defaults[opt]) - - -def clear_config_states(): - from charmhelpers.core import hookenv, unitdata - from charms.reactive import remove_state - config = hookenv.config() - remove_state('config.changed') - for opt in config.keys(): - remove_state('config.changed.{}'.format(opt)) - remove_state('config.set.{}'.format(opt)) - remove_state('config.default.{}'.format(opt)) - unitdata.kv().flush() diff --git a/hackfest_proxycharm_vnf/charms/simple/lib/charms/layer/execd.py b/hackfest_proxycharm_vnf/charms/simple/lib/charms/layer/execd.py deleted file mode 100644 index 438d9a1bc90042fd8b20517d5ecf358dfbf08afc..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/lib/charms/layer/execd.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright 2014-2016 Canonical Limited. -# -# This file is part of layer-basic, the reactive base layer for Juju. -# -# charm-helpers is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3 as -# published by the Free Software Foundation. -# -# charm-helpers is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with charm-helpers. If not, see . - -# This module may only import from the Python standard library. -import os -import sys -import subprocess -import time - -''' -execd/preinstall - -Read the layer-basic docs for more info on how to use this feature. -https://charmsreactive.readthedocs.io/en/latest/layer-basic.html#exec-d-support -''' - - -def default_execd_dir(): - return os.path.join(os.environ['JUJU_CHARM_DIR'], 'exec.d') - - -def execd_module_paths(execd_dir=None): - """Generate a list of full paths to modules within execd_dir.""" - if not execd_dir: - execd_dir = default_execd_dir() - - if not os.path.exists(execd_dir): - return - - for subpath in os.listdir(execd_dir): - module = os.path.join(execd_dir, subpath) - if os.path.isdir(module): - yield module - - -def execd_submodule_paths(command, execd_dir=None): - """Generate a list of full paths to the specified command within exec_dir. - """ - for module_path in execd_module_paths(execd_dir): - path = os.path.join(module_path, command) - if os.access(path, os.X_OK) and os.path.isfile(path): - yield path - - -def execd_sentinel_path(submodule_path): - module_path = os.path.dirname(submodule_path) - execd_path = os.path.dirname(module_path) - module_name = os.path.basename(module_path) - submodule_name = os.path.basename(submodule_path) - return os.path.join(execd_path, - '.{}_{}.done'.format(module_name, submodule_name)) - - -def execd_run(command, execd_dir=None, stop_on_error=True, stderr=None): - """Run command for each module within execd_dir which defines it.""" - if stderr is None: - stderr = sys.stdout - for submodule_path in execd_submodule_paths(command, execd_dir): - # Only run each execd once. We cannot simply run them in the - # install hook, as potentially storage hooks are run before that. - # We cannot rely on them being idempotent. - sentinel = execd_sentinel_path(submodule_path) - if os.path.exists(sentinel): - continue - - try: - subprocess.check_call([submodule_path], stderr=stderr, - universal_newlines=True) - with open(sentinel, 'w') as f: - f.write('{} ran successfully {}\n'.format(submodule_path, - time.ctime())) - f.write('Removing this file will cause it to be run again\n') - except subprocess.CalledProcessError as e: - # Logs get the details. We can't use juju-log, as the - # output may be substantial and exceed command line - # length limits. - print("ERROR ({}) running {}".format(e.returncode, e.cmd), - file=stderr) - print("STDOUT< 0: - raise CalledProcessError(returncode=retcode, - cmd=cmd, - output=stderr.decode("utf-8").strip()) - return (stdout.decode('utf-8').strip(), stderr.decode('utf-8').strip()) - - -def _run(cmd, env=None): - """Run a command remotely via SSH. - - Note: The previous behavior was to run the command locally if SSH wasn't - configured, but that can lead to cases where execution succeeds when you'd - expect it not to. - """ - if isinstance(cmd, str): - cmd = shlex.split(cmd) - - if type(cmd) is not list: - cmd = [cmd] - - cfg = get_config() - - if cfg: - if all(k in cfg for k in ['ssh-hostname', 'ssh-username', - 'ssh-password', 'ssh-private-key']): - host = get_host_ip() - user = cfg['ssh-username'] - passwd = cfg['ssh-password'] - key = cfg['ssh-private-key'] # DEPRECATED - - if host and user: - return ssh(cmd, host, user, passwd, key) - - raise Exception("Invalid SSH credentials.") - - -def get_ssh_client(host, user, password=None, key=None): - """Return a connected Paramiko ssh object.""" - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - pkey = None - - # Check for the DEPRECATED private-key - if key: - f = io.StringIO(key) - pkey = paramiko.RSAKey.from_private_key(f) - else: - # Otherwise, check for the auto-generated private key - if os.path.exists('/root/.ssh/id_juju_sshproxy'): - with open('/root/.ssh/id_juju_sshproxy', 'r') as f: - pkey = paramiko.RSAKey.from_private_key(f) - - ########################################################################### - # There is a bug in some versions of OpenSSH 4.3 (CentOS/RHEL 5) where # - # the server may not send the SSH_MSG_USERAUTH_BANNER message except when # - # responding to an auth_none request. For example, paramiko will attempt # - # to use password authentication when a password is set, but the server # - # could deny that, instead requesting keyboard-interactive. The hack to # - # workaround this is to attempt a reconnect, which will receive the right # - # banner, and authentication can proceed. See the following for more info # - # https://github.com/paramiko/paramiko/issues/432 # - # https://github.com/paramiko/paramiko/pull/438 # - ########################################################################### - - try: - client.connect(host, port=22, username=user, - password=password, pkey=pkey) - except paramiko.ssh_exception.SSHException as e: - if 'Error reading SSH protocol banner' == str(e): - # Once more, with feeling - client.connect(host, port=22, username=user, - password=password, pkey=pkey) - else: - # Reraise the original exception - raise e - - return client - - -def sftp(local_file, remote_file, host, user, password=None, key=None): - """Copy a local file to a remote host.""" - client = get_ssh_client(host, user, password, key) - - # Create an sftp connection from the underlying transport - sftp = paramiko.SFTPClient.from_transport(client.get_transport()) - sftp.put(local_file, remote_file) - client.close() - - -def ssh(cmd, host, user, password=None, key=None): - """Run an arbitrary command over SSH.""" - client = get_ssh_client(host, user, password, key) - - cmds = ' '.join(cmd) - stdin, stdout, stderr = client.exec_command(cmds, get_pty=True) - retcode = stdout.channel.recv_exit_status() - client.close() # @TODO re-use connections - if retcode > 0: - output = stderr.read().strip() - raise CalledProcessError(returncode=retcode, cmd=cmd, - output=output) - return ( - stdout.read().decode('utf-8').strip(), - stderr.read().decode('utf-8').strip() - ) diff --git a/hackfest_proxycharm_vnf/charms/simple/metadata.yaml b/hackfest_proxycharm_vnf/charms/simple/metadata.yaml deleted file mode 100644 index f55fdd650c3aeb9c17c197c5f98d8d7b392751a8..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/metadata.yaml +++ /dev/null @@ -1,16 +0,0 @@ -"name": "simple" -"summary": "A simple VNF proxy charm" -"maintainer": "Adam Israel " -"description": | - VNF "proxy" charms are a lightweight version of a charm that, rather than - installing software on the same machine, execute commands over an ssh channel. -"tags": - # Replace "misc" with one or more whitelisted tags from this list: - # https://jujucharms.com/docs/stable/authors-charm-metadata - - "misc" - - "osm" - - "vnf" -"series": -- "xenial" -- "trusty" -"subordinate": !!bool "false" diff --git a/hackfest_proxycharm_vnf/charms/simple/reactive/__init__.py b/hackfest_proxycharm_vnf/charms/simple/reactive/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/hackfest_proxycharm_vnf/charms/simple/reactive/simple.py b/hackfest_proxycharm_vnf/charms/simple/reactive/simple.py deleted file mode 100644 index 21e3ff9175516ff66fa3eeac0cd3d4229a8ed9e8..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/reactive/simple.py +++ /dev/null @@ -1,35 +0,0 @@ -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(): - 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({'outout': result}) - finally: - clear_flag('actions.touch') diff --git a/hackfest_proxycharm_vnf/charms/simple/reactive/sshproxy.py b/hackfest_proxycharm_vnf/charms/simple/reactive/sshproxy.py deleted file mode 100644 index cde43bc2193c1c410363d2c81873017d725ad7aa..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/reactive/sshproxy.py +++ /dev/null @@ -1,209 +0,0 @@ -## -# Copyright 2016 Canonical Ltd. -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -## - -from charmhelpers.core import unitdata -from charmhelpers.core.hookenv import ( - action_fail, - action_get, - action_set, - config, - log, - status_set, - DEBUG, -) - -from charms.reactive.flags import register_trigger - -from charms.reactive import ( - clear_flag, - set_flag, - when, - when_not, - when_any, -) -import charms.sshproxy -import os -import subprocess - -# Register a trigger so that we can respond to config.changed, even if -# it's being cleared by another handler -register_trigger(when='config.changed', - set_flag='sshproxy.reconfigure') - - -@when_any('config.changed', 'sshproxy.reconfigure') -def ssh_configured(): - """Check if charm is properly configured. - - Check to see if the charm is configured with SSH credentials. If so, - set a state flag that can be used to execute ssh-only actions. - - For example: - - @when('sshproxy.configured') - def run_remote_command(cmd): - ... - - @when_not('sshproxy.configured') - def run_local_command(cmd): - ... - """ - log("Checking sshproxy configuration", DEBUG) - cfg = config() - ssh_keys = ['ssh-hostname', 'ssh-username', - 'ssh-password', 'ssh-private-key'] - - if all(k in cfg for k in ssh_keys): - - # Store config in unitdata so it's accessible to sshproxy - db = unitdata.kv() - db.set('config', cfg) - - # Explicitly flush the kv so it's immediately available - db.flush() - - log("Verifying ssh credentials...", DEBUG) - (verified, output) = charms.sshproxy.verify_ssh_credentials() - if verified: - log("SSH credentials verified.", DEBUG) - set_flag('sshproxy.configured') - status_set('active', 'Ready!') - else: - clear_flag('sshproxy.configured') - status_set('blocked', "Verification failed: {}".format(output)) - else: - log("No ssh credentials configured", DEBUG) - clear_flag('sshproxy.configured') - status_set('blocked', 'Invalid SSH credentials.') - - -def generate_ssh_key(): - """Generate a new 4096-bit rsa keypair. - - If there is an existing keypair for this unit, it will be overwritten. - """ - cfg = config() - if all(k in cfg for k in ['ssh-key-type', 'ssh-key-bits']): - keytype = cfg['ssh-key-type'] - bits = str(cfg['ssh-key-bits']) - privatekey = '/root/.ssh/id_juju_sshproxy' - publickey = "{}.pub".format(privatekey) - - if os.path.exists(privatekey): - os.remove(privatekey) - if os.path.exists(publickey): - os.remove(publickey) - - cmd = "ssh-keygen -t {} -b {} -N '' -f {}".format( - keytype, - bits, - privatekey - ) - - output, err = charms.sshproxy.run_local([cmd]) - if len(err) == 0: - return True - return False - - -@when('actions.generate-ssh-key') -def action_generate_ssh_key(): - """Generate a new 4096-bit rsa keypair. - - If there is an existing keypair for this unit, it will be overwritten. - """ - try: - if not generate_ssh_key(): - action_fail('Unable to generate ssh key.') - except subprocess.CalledProcessError as e: - action_fail('Command failed: %s (%s)' % - (' '.join(e.cmd), str(e.output))) - finally: - clear_flag('actions.generate-ssh-key') - - -def get_ssh_public_key(): - """Get the public SSH key of this unit.""" - publickey_path = '/root/.ssh/id_juju_sshproxy.pub' - publickey = None - if os.path.exists(publickey_path): - with open(publickey_path, 'r') as f: - publickey = f.read() - - return publickey - - -@when('actions.get-ssh-public-key') -def action_get_ssh_public_key(): - """Get the public SSH key of this unit.""" - try: - action_set({'pubkey': get_ssh_public_key()}) - except subprocess.CalledProcessError as e: - action_fail('Command failed: %s (%s)' % - (' '.join(e.cmd), str(e.output))) - finally: - clear_flag('actions.get-ssh-public-key') - - -@when('actions.verify-ssh-credentials') -def action_verify_ssh_credentials(): - """Verify the ssh credentials have been installed to the VNF. - - Attempts to run a stock command - `hostname` on the remote host. - """ - try: - (verified, output) = charms.sshproxy.verify_ssh_credentials() - action_set({ - 'output': output, - 'verified': verified, - }) - if not verified: - action_fail("Verification failed: {}".format( - output, - )) - finally: - clear_flag('actions.verify-ssh-credentials') - - -@when('actions.run') -def run_command(): - """Run an arbitrary command. - - Run an arbitrary command, either locally or over SSH with the configured - credentials. - """ - try: - cmd = action_get('command') - output, err = charms.sshproxy._run(cmd) - if len(err): - action_fail("Command '{}' returned error code {}".format(cmd, err)) - else: - action_set({'output': output}) - except subprocess.CalledProcessError as e: - action_fail('Command failed: %s (%s)' % - (' '.join(e.cmd), str(e.output))) - finally: - clear_flag('actions.run') - - -@when_not('sshproxy.installed') -def install_vnf_ubuntu_proxy(): - """Install and Configure SSH Proxy.""" - - log("Generating SSH key...", DEBUG) - generate_ssh_key() - set_flag('sshproxy.installed') diff --git a/hackfest_proxycharm_vnf/charms/simple/reactive/vnfproxy.py b/hackfest_proxycharm_vnf/charms/simple/reactive/vnfproxy.py deleted file mode 100644 index 6616991df245a920116b9301b52602f15ba7a9bc..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/reactive/vnfproxy.py +++ /dev/null @@ -1,89 +0,0 @@ -from charmhelpers.core.hookenv import ( - action_fail, - action_set, -) - -from charms.reactive import ( - when, - clear_flag, -) -import charms.sshproxy - - -@when('actions.reboot') -def reboot(): - err = '' - try: - result, err = charms.sshproxy._run("reboot") - except: - action_fail('command failed:' + err) - else: - action_set({'outout': result}) - finally: - clear_flag('actions.reboot') - - -############################################################################### -# Below is an example implementation of the start/stop/restart actions. # -# To use this, copy the below code into your layer and add the appropriate # -# command(s) necessary to perform the action. # -############################################################################### - -# @when('actions.start') -# def start(): -# err = '' -# try: -# cmd = "service myname start" -# result, err = charms.sshproxy._run(cmd) -# except: -# action_fail('command failed:' + err) -# else: -# action_set({'outout': result}) -# finally: -# clear_flag('actions.start') -# -# -# @when('actions.stop') -# def stop(): -# err = '' -# try: -# # Enter the command to stop your service(s) -# cmd = "service myname stop" -# result, err = charms.sshproxy._run(cmd) -# except: -# action_fail('command failed:' + err) -# else: -# action_set({'outout': result}) -# finally: -# clear_flag('actions.stop') -# -# -# @when('actions.restart') -# def restart(): -# err = '' -# try: -# # Enter the command to restart your service(s) -# cmd = "service myname restart" -# result, err = charms.sshproxy._run(cmd) -# except: -# action_fail('command failed:' + err) -# else: -# action_set({'outout': result}) -# finally: -# clear_flag('actions.restart') -# -# -# @when('actions.upgrade') -# def upgrade_vnf(): -# err = '' -# try: -# # Add the command(s) to perform a VNF software upgrade -# cmd = '' -# result, err = charms.sshproxy._run(cmd) -# except: -# action_fail('command failed:' + err) -# else: -# action_set({'outout': result}) -# finally: -# clear_flag('actions.upgrade') -# diff --git a/hackfest_proxycharm_vnf/charms/simple/requirements.txt b/hackfest_proxycharm_vnf/charms/simple/requirements.txt deleted file mode 100644 index 28ecacab6029381bd43d65f8dcfc0cc704870f71..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -flake8 -pytest diff --git a/hackfest_proxycharm_vnf/charms/simple/tests/00-setup b/hackfest_proxycharm_vnf/charms/simple/tests/00-setup deleted file mode 100755 index f0616a560a811bae78d7872dd433d612909c73cd..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/tests/00-setup +++ /dev/null @@ -1,5 +0,0 @@ -#!/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/hackfest_proxycharm_vnf/charms/simple/tests/10-deploy b/hackfest_proxycharm_vnf/charms/simple/tests/10-deploy deleted file mode 100755 index 9a26117089498423ebba7dda3f7e3acb50ba54e7..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/tests/10-deploy +++ /dev/null @@ -1,35 +0,0 @@ -#!/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/hackfest_proxycharm_vnf/charms/simple/tox.ini b/hackfest_proxycharm_vnf/charms/simple/tox.ini deleted file mode 100644 index 6f0fa11dee3187367ecd1637bef45533f289fd9e..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/tox.ini +++ /dev/null @@ -1,15 +0,0 @@ -[tox] -skipsdist=True -envlist = py34, py35 -skip_missing_interpreters = True - -[testenv] -# This is not pretty, but pytest will return 0 if all tests worked and 5 if no tests are found. -# We want to consider no tests as an indication of success. -commands = /bin/bash -c 'py.test -v || if [[ $? == 5 ]]; then true; else false; fi' - -deps = - -r{toxinidir}/requirements.txt - -[flake8] -exclude=docs diff --git a/hackfest_proxycharm_vnf/charms/simple/version b/hackfest_proxycharm_vnf/charms/simple/version deleted file mode 100644 index e13a57015aedd85e122d4cc2f4074c614fd6eb68..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/charms/simple/version +++ /dev/null @@ -1 +0,0 @@ -525d4cb \ No newline at end of file diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/Jinja2-2.10.1.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/Jinja2-2.10.1.tar.gz deleted file mode 100644 index ffd10546db30d3b8594aa231dcfca1b010d95860..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/Jinja2-2.10.1.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/MarkupSafe-1.1.1.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/MarkupSafe-1.1.1.tar.gz deleted file mode 100644 index a6dad8e8548c37cd3a2888fad72eeed547c0997e..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/MarkupSafe-1.1.1.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/PyYAML-5.1.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/PyYAML-5.1.tar.gz deleted file mode 100644 index 91ba249905136e08cb348378f20950bc3b6e77a9..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/PyYAML-5.1.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/Tempita-0.5.2.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/Tempita-0.5.2.tar.gz deleted file mode 100644 index 755befcd6ff3194844557aa12662d50ed56567af..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/Tempita-0.5.2.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/charmhelpers-0.19.13.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/charmhelpers-0.19.13.tar.gz deleted file mode 100644 index 188360d10d714bf03d7852147197c31219e09a0e..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/charmhelpers-0.19.13.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/charms.reactive-1.2.1.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/charms.reactive-1.2.1.tar.gz deleted file mode 100644 index 2115b37c814bc957ac580097ec39895711279e1c..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/charms.reactive-1.2.1.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/ecdsa-0.13.2.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/ecdsa-0.13.2.tar.gz deleted file mode 100644 index f2d8e59202974967d9f32e001e65e0db2e6c4077..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/ecdsa-0.13.2.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/netaddr-0.7.19.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/netaddr-0.7.19.tar.gz deleted file mode 100644 index cc31d9dfb66310f119ab8f1375f872645cc5205e..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/netaddr-0.7.19.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/paramiko-1.16.3.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/paramiko-1.16.3.tar.gz deleted file mode 100644 index b76f01275ff02bc20da0065ee9a956ec4800f947..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/paramiko-1.16.3.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/pip-8.1.2.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/pip-8.1.2.tar.gz deleted file mode 100644 index e7a1a3cc53050c4a69e6ec06a1e9f0863515d08a..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/pip-8.1.2.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/pyaml-19.4.1.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/pyaml-19.4.1.tar.gz deleted file mode 100644 index 2d12c9d7e63f8caa938b9e91f46f3ccaf0a3217a..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/pyaml-19.4.1.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/pycrypto-2.6.1.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/pycrypto-2.6.1.tar.gz deleted file mode 100644 index e6bf62ccd7761023333ec623390cb713efadaf8d..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/pycrypto-2.6.1.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/setuptools-39.0.1.zip b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/setuptools-39.0.1.zip deleted file mode 100644 index e4b0aaca167d76e92f2ef499e206799366799017..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/setuptools-39.0.1.zip and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/setuptools_scm-1.17.0.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/setuptools_scm-1.17.0.tar.gz deleted file mode 100644 index 43b16c75a0208b8f00670ecc5198589155e2645f..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/setuptools_scm-1.17.0.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/six-1.12.0.tar.gz b/hackfest_proxycharm_vnf/charms/simple/wheelhouse/six-1.12.0.tar.gz deleted file mode 100644 index 02c0a14c853df2369cffd9d61daaf64b4ebbfba3..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/charms/simple/wheelhouse/six-1.12.0.tar.gz and /dev/null differ diff --git a/hackfest_proxycharm_vnf/cloud_init/cloud-config.txt b/hackfest_proxycharm_vnf/cloud_init/cloud-config.txt deleted file mode 100755 index 36c8d1bf2cdebbc4e50d1e8348003f64f419cd0b..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/cloud_init/cloud-config.txt +++ /dev/null @@ -1,12 +0,0 @@ -#cloud-config -password: osm4u -chpasswd: { expire: False } -ssh_pwauth: True - -write_files: -- content: | - # My new helloworld file - - owner: root:root - permissions: '0644' - path: /root/helloworld.txt diff --git a/hackfest_proxycharm_vnf/hackfest_proxycharm_vnfd.yaml b/hackfest_proxycharm_vnf/hackfest_proxycharm_vnfd.yaml deleted file mode 100644 index b724abb628a819382014792d191dc5268fd5b869..0000000000000000000000000000000000000000 --- a/hackfest_proxycharm_vnf/hackfest_proxycharm_vnfd.yaml +++ /dev/null @@ -1,97 +0,0 @@ -vnfd: - description: A VNF consisting of 1 VDU connected to two external VL, and one for - data and another one for management - df: - - id: default-df - instantiation-level: - - id: default-instantiation-level - vdu-level: - - number-of-instances: 1 - vdu-id: mgmtVM - vdu-profile: - - id: mgmtVM - min-number-of-instances: 1 - lcm-operations-configuration: - operate-vnf-op-config: - day1-2: - - config-primitive: - - name: touch - execution-environment-ref: simple-ee - parameter: - - data-type: STRING - default-value: /home/ubuntu/touched - name: filename - id: hackfest_proxycharm-vnf - execution-environment-list: - - id: simple-ee - juju: - charm: simple - config-access: - ssh-access: - default-user: ubuntu - required: true - initial-config-primitive: - - name: config - execution-environment-ref: simple-ee - parameter: - - name: ssh-hostname - value: - - name: ssh-username - value: ubuntu - - name: ssh-password - value: osm4u - seq: 1 - - name: touch - execution-environment-ref: simple-ee - parameter: - - data-type: STRING - name: filename - value: /home/ubuntu/first-touch - seq: 2 - ext-cpd: - - id: vnf-mgmt-ext - int-cpd: - cpd: mgmtVM-eth0-int - vdu-id: mgmtVM - - id: vnf-data-ext - int-cpd: - cpd: dataVM-xe0-int - vdu-id: mgmtVM - id: hackfest_proxycharm-vnf - mgmt-cp: vnf-mgmt-ext - product-name: hackfest_proxycharm-vnf - sw-image-desc: - - id: ubuntu18.04 - image: ubuntu18.04 - name: ubuntu18.04 - vdu: - - cloud-init-file: cloud-config.txt - id: mgmtVM - int-cpd: - - id: mgmtVM-eth0-int - virtual-network-interface-requirement: - - name: mgmtVM-eth0 - position: 1 - virtual-interface: - type: PARAVIRT - - id: dataVM-xe0-int - virtual-network-interface-requirement: - - name: dataVM-xe0 - position: 2 - virtual-interface: - type: PARAVIRT - name: mgmtVM - sw-image-desc: ubuntu18.04 - virtual-compute-desc: mgmtVM-compute - virtual-storage-desc: - - mgmtVM-storage - version: 1.0 - virtual-compute-desc: - - id: mgmtVM-compute - virtual-cpu: - num-virtual-cpu: 1 - virtual-memory: - size: 1.0 - virtual-storage-desc: - - id: mgmtVM-storage - size-of-storage: 10 diff --git a/hackfest_proxycharm_vnf/icons/osm.png b/hackfest_proxycharm_vnf/icons/osm.png deleted file mode 100644 index 62012d2a2b491bdcd536d62c3c3c863c0d8c1b33..0000000000000000000000000000000000000000 Binary files a/hackfest_proxycharm_vnf/icons/osm.png and /dev/null differ