Commit 7d364cb2 authored by Mark Beierl's avatar Mark Beierl
Browse files

Updating PNF Session



Removes the unnecessary src and build steps from
PNF package.

Adds some scripts for participants
Signed-off-by: Mark Beierl's avatarMark Beierl <mark.beierl@canonical.com>
parent 7a0cde63
Pipeline #7696 failed with stage
in 2 minutes and 49 seconds
#!/bin/bash
#!/bin/bash
HFID=$(echo $HOSTNAME | cut -f2 -d-)
IP=$(expr 100 + $HFID)
cd ~/osm-packages
if [ ! -d hackfest_firewall_pnf ] ; then
echo "It does not look like we are in the osm-packages directory, exiting"
exit 1
fi
echo "========================================================================"
echo "Building operator charms"
echo "========================================================================"
cd hackfest_firewall_pnf/charms/vyos-config-src
#virtualenv -p python3 venv
#source venv/bin/activate
#pip install -r requirements-dev.txt
#pip install charmcraft
#./venv/bin/charmcraft build
rm -rf venv
charmcraft build
cd -
cd hackfest_firewall_pnf/charms
mkdir -p vyos-config/
rm -rf vyos-config/*
cp -r vyos-config-src/build/* vyos-config/
cd -
echo "========================================================================"
echo "Cleaning out any prior versions of the descriptors from OSM"
echo "========================================================================"
osm nsd-delete hackfest_firewall_pnf_ns
osm vnfd-delete hackfest_firewall_pnf
osm pdu-delete router01
rm -v hackfest_firewall_pnf*.tar.gz
echo "========================================================================"
echo "Building packages"
echo "========================================================================"
osm package-build hackfest_firewall_pnf
osm package-build hackfest_firewall_pnf_ns
osm nsd-delete hackfest_firewall_pnf_ns | grep -v "not found"
osm vnfd-delete hackfest_firewall_pnf | grep -v "not found"
osm pdu-delete router01 | grep -v "not found"
echo "========================================================================"
echo "Uploading packages"
echo "========================================================================"
osm upload-package hackfest_firewall_pnf.tar.gz
osm upload-package hackfest_firewall_pnf_ns.tar.gz
osm upload-package hackfest_firewall_pnf
osm upload-package hackfest_firewall_pnf_ns
VIMID=`osm vim-list | grep osm_ | awk '{ print $4 }'`
echo "========================================================================"
echo "Registering PDU 172.21.19.${HFID} with $VIMID"
echo "Registering PDU 172.21.19.${IP} with OSM"
echo "========================================================================"
cat << EOF > firewall-pdu.yaml
......@@ -55,7 +32,7 @@ type: gateway
shared: false
interfaces:
- name: gateway_public
ip-address: 172.21.19.${HFID}
ip-address: 172.21.19.${IP}
mgmt: true
vim-network-name: osm-ext
- name: vnf_internal
......@@ -64,9 +41,7 @@ interfaces:
vim-network-name: private
EOF
osm pdu-create --descriptor_file firewall-pdu.yaml \
--vim_account $VIMID
osm pdu-create --descriptor_file firewall-pdu.yaml --vim_account openstack
echo "========================================================================"
echo "Done"
echo "========================================================================"
#!/bin/bash
VIMID=`osm vim-list | grep osm_ | awk '{ print $4 }'`
VIMID=openstack
echo "========================================================================"
echo "Launching network service with VIMID ${VIMID}"
echo "========================================================================"
......
#!/bin/bash
echo "========================================================================"
echo "Launching a workload behind the firewall"
echo "========================================================================"
openstack server create --image=ubuntu20.04 --flavor=m1.medium \
--network private --key-name hackfest workload
......@@ -6,9 +6,7 @@ echo "========================================================================"
cat << 'EOF'
DESKTOP_IP=`osm ns-show virtual-desktop --literal | yq e '.vcaStatus.*.machines.0.network_interfaces.ens3.ip_addresses.0' -`
osm ns-action firewall --vnf_name VYOS-PNF --action_name add-port-forward --params "{ruleNumber: '10', sourcePort: '3389', destinationAddress: \"${DESKTOP_IP}\", destinationPort: '3389'}"
osm ns-action firewall --vnf_name VYOS-PNF --action_name add-port-forward --params "{ruleNumber: '10', sourcePort: '5022', destinationAddress: '192.168.239.NN', destinationPort: '22'}"
osm ns-action firewall --vnf_name VYOS-PNF --action_name remove-port-forward --params '{ruleNumber: "10"}'
EOF 
#!/bin/bash
echo "========================================================================"
echo "Enabling Debug Mode for LCM"
echo "========================================================================"
cat << 'EOF'
juju config lcm debug-mode=true
juju run-action lcm/0 get-debug-mode-information --wait
Once the debugger has started, go to n2vc/n2vc_juju_conn.py #L 1051
EOF 
export OS_AUTH_TYPE=password
export OS_AUTH_URL=https://keystone.pc1.canonical.com:5000/v3
export OS_CACERT=/home/mark/git/osm/files/pc-cacert.pem
export OS_CACERT=/home/ubuntu/pc-cacert.pem
export OS_DOMAIN_NAME=admin_domain
export OS_IDENTITY_API_VERSION=3
export OS_INTERFACE=public
......
......@@ -35,17 +35,16 @@ if [ "${PROJECT_ID}" != "" ]; then
echo "Removing Networks"
openstack --os-username=$OPENSTACK_USER --os-password=$PASSWORD --os-project-id=$PROJECT_ID network list -f value -c ID| xargs openstack --os-username=$OPENSTACK_USER --os-password=$PASSWORD --os-project-id=$PROJECT_ID network delete
. ./admin-credentials.rc
for RBAC in `openstack network rbac list -f value -c ID`; do
openstack network rbac show $RBAC -f value | grep $PROJECT_ID 2> /dev/null
if [ $? -eq 0 ] ; then
echo "Deleting RBAC policy $RBAC"
openstack network rbac delete $RBAC &
fi
done
wait
#for RBAC in `openstack network rbac list -f value -c ID`; do
# openstack network rbac show $RBAC -f value | grep $PROJECT_ID 2> /dev/null
# if [ $? -eq 0 ] ; then
# echo "Deleting RBAC policy $RBAC"
# openstack network rbac delete $RBAC &
# fi
#done
echo "Deleting OpenStack project: $PROJECT"
. ./admin-credentials.rc
openstack project purge --project ${PROJECT_ID}
fi
......
......@@ -7,7 +7,8 @@ mkdir -p logs/
for PARTICIPANT in `seq ${START} ${MAX}` ; do
./create-openstack-user-and-project.sh ${PARTICIPANT} 2>&1 | tee -a logs/create-openstack-user-and-project-${PARTICIPANT}.log &
sleep 60
done
wait
echo $0 $@ complete at $(date)
\ No newline at end of file
echo $0 $@ complete at $(date)
......@@ -11,9 +11,9 @@ echo "Acquire::https::Proxy \"http://172.21.18.3:3142\";" | sudo tee -a /etc/apt
sudo apt update
sudo apt full-upgrade -y
sudo apt install -y gnome-session xrdp
sudo apt install -y firefox gnome-session xrdp
sudo snap install code --classic
sudo snap install firefox openstackclients yq jq
sudo snap install openstackclients yq jq
# Update so buttons show up
gsettings set org.gnome.desktop.wm.preferences button-layout :minimize,maximize,close
......@@ -41,4 +41,4 @@ sudo chmod +x /etc/rc.local
git clone --recurse-submodules -j8 https://osm.etsi.org/gitlab/vnf-onboarding/osm-packages.git
echo $0 $@ complete at $(date)
\ No newline at end of file
echo $0 $@ complete at $(date)
# Vyos-config
This is a proxy charm used by Open Source Mano (OSM) to configure Vyos Router PNF, written in the [Python Operator Framwork](https://github.com/canonical/operator)
# VyOS Action
add-port-forward:
description: "Adds a port forwarding rule"
params:
ruleNumber:
description: "Rule number, must be unique and needed to remove the rule later"
type: "string"
default: "10"
sourcePort:
description: "Source port to listen on"
type: "string"
destinationPort:
description: "Target port number on remote host to forward"
type: "string"
destinationAddress:
description: "Target host or IP address to forward traffic"
type: "string"
required:
- sourcePort
- destinationPort
- destinationAddress
remove-port-forward:
description: "Removes a port forwarding rule by number"
params:
ruleNumber:
description: "Rule number to remove"
type: "string"
default: "10"
# 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."
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-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."
# Copyright 2014-2015 Canonical Limited.
#
# 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.
# Bootstrap charm-helpers, installing its dependencies if necessary using
# only standard libraries.
from __future__ import print_function
from __future__ import absolute_import
import functools
import inspect
import subprocess
import sys
try:
import six # NOQA:F401
except ImportError:
if sys.version_info.major == 2:
subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
else:
subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
import six # NOQA:F401
try:
import yaml # NOQA:F401
except ImportError:
if sys.version_info.major == 2:
subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
else:
subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
import yaml # NOQA:F401
# Holds a list of mapping of mangled function names that have been deprecated
# using the @deprecate decorator below. This is so that the warning is only
# printed once for each usage of the function.
__deprecated_functions = {}
def deprecate(warning, date=None, log=None):
"""Add a deprecation warning the first time the function is used.
The date, which is a string in semi-ISO8660 format indicate the year-month
that the function is officially going to be removed.
usage:
@deprecate('use core/fetch/add_source() instead', '2017-04')
def contributed_add_source_thing(...):
...
And it then prints to the log ONCE that the function is deprecated.
The reason for passing the logging function (log) is so that hookenv.log
can be used for a charm if needed.
:param warning: String to indicat where it has moved ot.
:param date: optional sting, in YYYY-MM format to indicate when the
function will definitely (probably) be removed.
:param log: The log function to call to log. If not, logs to stdout
"""
def wrap(f):
@functools.wraps(f)
def wrapped_f(*args, **kwargs):
try:
module = inspect.getmodule(f)
file = inspect.getsourcefile(f)
lines = inspect.getsourcelines(f)
f_name = "{}-{}-{}..{}-{}".format(
module.__name__, file, lines[0], lines[-1], f.__name__)
except (IOError, TypeError):
# assume it was local, so just use the name of the function
f_name = f.__name__
if f_name not in __deprecated_functions:
__deprecated_functions[f_name] = True
s = "DEPRECATION WARNING: Function {} is being removed".format(
f.__name__)
if date:
s = "{} on/around {}".format(s, date)
if warning:
s = "{} : {}".format(s, warning)
if log:
log(s)
else:
print(s)
return f(*args, **kwargs)
return wrapped_f
return wrap
==========
Commandant
==========
-----------------------------------------------------
Automatic command-line interfaces to Python functions
-----------------------------------------------------
One of the benefits of ``libvirt`` is the uniformity of the interface: the C API (as well as the bindings in other languages) is a set of functions that accept parameters that are nearly identical to the command-line arguments. If you run ``virsh``, you get an interactive command prompt that supports all of the same commands that your shell scripts use as ``virsh`` subcommands.
Command execution and stdio manipulation is the greatest common factor across all development systems in the POSIX environment. By exposing your functions as commands that manipulate streams of text, you can make life easier for all the Ruby and Erlang and Go programmers in your life.
Goals
=====
* Single decorator to expose a function as a command.
* now two decorators - one "automatic" and one that allows authors to manipulate the arguments for fine-grained control.(MW)
* Automatic analysis of function signature through ``inspect.getargspec()``
* Command argument parser built automatically with ``argparse``
* Interactive interpreter loop object made with ``Cmd``
* Options to output structured return value data via ``pprint``, ``yaml`` or ``json`` dumps.
Other Important Features that need writing
------------------------------------------
* Help and Usage documentation can be automatically generated, but it will be important to let users override this behaviour
* The decorator should allow specifying further parameters to the parser's add_argument() calls, to specify types or to make arguments behave as boolean flags, etc.
- Filename arguments are important, as good practice is for functions to accept file objects as parameters.
- choices arguments help to limit bad input before the function is called
* Some automatic behaviour could make for better defaults, once the user can override them.
- We could automatically detect arguments that default to False or True, and automatically support --no-foo for foo=True.
- We could automatically support hyphens as alternates for underscores
- Arguments defaulting to sequence types could support the ``append`` action.
-----------------------------------------------------
Implementing subcommands
-----------------------------------------------------
(WIP)
So as to avoid dependencies on the cli module, subcommands should be defined separately from their implementations. The recommmendation would be to place definitions into separate modules near the implementations which they expose.
Some examples::
from charmhelpers.cli import CommandLine
from charmhelpers.payload import execd
from charmhelpers.foo import bar
cli = CommandLine()
cli.subcommand(execd.execd_run)
@cli.subcommand_builder("bar", help="Bar baz qux")
def barcmd_builder(subparser):
subparser.add_argument('argument1', help="yackety")
return bar
# Copyright 2014-2015 Canonical Limited.
#
# 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 argparse
import sys
from six.moves import zip
import charmhelpers.core.unitdata
class OutputFormatter(object):
def __init__(self, outfile=sys.stdout):
self.formats = (
"raw",
"json",
"py",
"yaml",
"csv",
"tab",
)
self.outfile = outfile
def add_arguments(self, argument_parser):
formatgroup = argument_parser.add_mutually_exclusive_group()
choices = self.supported_formats
formatgroup.add_argument("--format", metavar='FMT',
help="Select output format for returned data, "
"where FMT is one of: {}".format(choices),
choices=choices, default='raw')
for fmt in self.formats:
fmtfunc = getattr(self, fmt)
formatgroup.add_argument("-{}".format(fmt[0]),
"--{}".format(fmt), action='store_const',
const=fmt, dest='format',
help=fmtfunc.__doc__)
@property
def supported_formats(self):
return self.formats
def raw(self, output):
"""Output data as raw string (default)"""
if isinstance(output, (list, tuple)):
output = '\n'.join(map(str, output))
self.outfile.write(str(output))
def py(self, output):
"""Output data as a nicely-formatted python data structure"""
import pprint
pprint.pprint(output, stream=self.outfile)
def json(self, output):
"""Output data in JSON format"""
import json
json.dump(output, self.outfile)
def yaml(self, output):
"""Output data in YAML format"""
import yaml
yaml.safe_dump(output, self.outfile)
def csv(self, output):
"""Output data as excel-compatible CSV"""
import csv
csvwriter = csv.writer(self.outfile)
csvwriter.writerows(output)
def tab(self, output):
"""Output data in excel-compatible tab-delimited format"""
import csv
csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
csvwriter.writerows(output)
def format_output(self, output, fmt='raw'):
fmtfunc = getattr(self, fmt)
fmtfunc(output)
class CommandLine(object):
argument_parser = None
subparsers = None
formatter = None
exit_code = 0
def __init__(self):
if not self.argument_parser:
self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
if not self.formatter:
self.formatter = OutputFormatter()
self.formatter.add_arguments(self.argument_parser)
if not self.subparsers:
self.subparsers = self.argument_parser.add_subparsers(help='Commands')
def subcommand(self, command_name=None):
"""
Decorate a function as a subcommand. Use its arguments as the
command-line arguments"""
def wrapper(decorated):
cmd_name = command_name or decorated.__name__
subparser = self.subparsers.add_parser(cmd_name,
description=decorated.__doc__)
for args, kwargs in describe_arguments(decorated):
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=decorated)
return decorated
return wrapper
def test_command(self, decorated):
"""
Subcommand is a boolean test function, so bool return values should be
converted to a 0/1 exit code.
"""
decorated._cli_test_command = True
return decorated
def no_output(self, decorated):
"""
Subcommand is not expected to return a value, so don't print a spurious None.
"""
decorated._cli_no_output = True
return decorated
def subcommand_builder(self, command_name, description=None):
"""
Decorate a function that builds a subcommand. Builders should accept a
single argument (the subparser instance) and return the function to be
run as the command."""
def wrapper(decorated):
subparser = self.subparsers.add_parser(command_name)
func = decorated(subparser)
subparser.set_defaults(func=func)
subparser.description = description or func.__doc__
return wrapper
def run(self):
"Run cli, processing arguments and executing subcommands."
arguments = self.argument_parser.parse_args()
argspec = inspect.getargspec(arguments.func)
vargs = []
for arg in argspec.args:
vargs.append(getattr(arguments, arg))
if argspec.varargs:
vargs.extend(getattr(arguments, argspec.varargs))
output = arguments.func(*vargs)
if getattr(arguments.func, '_cli_test_command', False):
self.exit_code = 0 if output else 1
output = ''
if getattr(arguments.func, '_cli_no_output', False):
output = ''
self.formatter.format_output(output, arguments.format)
if charmhelpers.core.unitdata._KV:
charmhelpers.core.unitdata._KV.flush()
cmdline = CommandLine()
def describe_arguments(func):
"""
Analyze a function's signature and return a data structure suitable for
passing in as arguments to an argparse parser's add_argument() method."""
argspec = inspect.getargspec(func)
# we should probably raise an exception somewhere if func includes **kwargs
if argspec.defaults:
positional_args = argspec.args[:-len(argspec.defaults)]
keyword_names = argspec.args[-len(argspec.defaults):]
for arg, default in zip(keyword_names, argspec.defaults):
yield ('--{}'.format(arg),), {'default': default}
else:
positional_args = argspec.args
for arg in positional_args:
yield (arg,), {}
if argspec.varargs:
yield (argspec.varargs,), {'nargs': '*'}
# Copyright 2014-2015 Canonical Limited.
#
# 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 . import cmdline
from charmhelpers.contrib.benchmark import Benchmark
@cmdline.subcommand(command_name='benchmark-start')
def start():
Benchmark.start()
@cmdline.subcommand(command_name='benchmark-finish')
def finish():
Benchmark.finish()
@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
def service(subparser):
subparser.add_argument("value", help="The composite score.")
subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
return Benchmark.set_composite_score
# Copyright 2014-2015 Canonical Limited.
#
# 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.
"""
This module loads sub-modules into the python runtime so they can be
discovered via the inspect module. In order to prevent flake8 from (rightfully)
telling us these are unused modules, throw a ' # noqa' at the end of each import
so that the warning is suppressed.
"""
from . import CommandLine # noqa
"""
Import the sub-modules which have decorated subcommands to register with chlp.
"""
from . import host # noqa
from . import benchmark # noqa
from . import unitdata # noqa
from . import hookenv # noqa
# Copyright 2014-2015 Canonical Limited.
#
# 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 . import cmdline
from charmhelpers.core import hookenv
cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
cmdline.subcommand('service-name')(hookenv.service_name)
cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
# Copyright 2014-2015 Canonical Limited.
#
# 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 . import cmdline
from charmhelpers.core import host
@cmdline.subcommand()
def mounts():
"List mounts"
return host.mounts()
@cmdline.subcommand_builder('service', description="Control system services")
def service(subparser):
subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
subparser.add_argument("service_name", help="Name of the service to control")
return host.service
# Copyright 2014-2015 Canonical Limited.
#
# 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 . import cmdline
from charmhelpers.core import unitdata
@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
def unitdata_cmd(subparser):
nested = subparser.add_subparsers()
get_cmd = nested.add_parser('get', help='Retrieve data')
get_cmd.add_argument('key', help='Key to retrieve the value of')
get_cmd.set_defaults(action='get', value=None)
getrange_cmd = nested.add_parser('getrange', help='Retrieve multiple data')
getrange_cmd.add_argument('key', metavar='prefix',
help='Prefix of the keys to retrieve')
getrange_cmd.set_defaults(action='getrange', value=None)
set_cmd = nested.add_parser('set', help='Store data')
set_cmd.add_argument('key', help='Key to set')
set_cmd.add_argument('value', help='Value to store')
set_cmd.set_defaults(action='set')
def _unitdata_cmd(action, key, value):
if action == 'get':
return unitdata.kv().get(key)
elif action == 'getrange':
return unitdata.kv().getrange(key)
elif action == 'set':
unitdata.kv().set(key, value)
unitdata.kv().flush()
return ''
return _unitdata_cmd
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment