[gnuoy,trivial] Pre-release charmhelper sync
This commit is contained in:
parent
d4b6983c3b
commit
c29499f318
@ -2,6 +2,7 @@ branch: lp:charm-helpers
|
|||||||
destination: hooks/charmhelpers
|
destination: hooks/charmhelpers
|
||||||
include:
|
include:
|
||||||
- core
|
- core
|
||||||
|
- cli
|
||||||
- fetch
|
- fetch
|
||||||
- contrib.openstack|inc=*
|
- contrib.openstack|inc=*
|
||||||
- contrib.hahelpers
|
- contrib.hahelpers
|
||||||
|
195
hooks/charmhelpers/cli/__init__.py
Normal file
195
hooks/charmhelpers/cli/__init__.py
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from six.moves import zip
|
||||||
|
|
||||||
|
from charmhelpers.core import 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 = []
|
||||||
|
kwargs = {}
|
||||||
|
for arg in argspec.args:
|
||||||
|
vargs.append(getattr(arguments, arg))
|
||||||
|
if argspec.varargs:
|
||||||
|
vargs.extend(getattr(arguments, argspec.varargs))
|
||||||
|
if argspec.keywords:
|
||||||
|
for kwarg in argspec.keywords.items():
|
||||||
|
kwargs[kwarg] = getattr(arguments, kwarg)
|
||||||
|
output = arguments.func(*vargs, **kwargs)
|
||||||
|
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 unitdata._KV:
|
||||||
|
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': '*'}
|
36
hooks/charmhelpers/cli/benchmark.py
Normal file
36
hooks/charmhelpers/cli/benchmark.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
32
hooks/charmhelpers/cli/commands.py
Normal file
32
hooks/charmhelpers/cli/commands.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
import host # noqa
|
||||||
|
import benchmark # noqa
|
||||||
|
import unitdata # noqa
|
||||||
|
from charmhelpers.core import hookenv # noqa
|
31
hooks/charmhelpers/cli/host.py
Normal file
31
hooks/charmhelpers/cli/host.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
39
hooks/charmhelpers/cli/unitdata.py
Normal file
39
hooks/charmhelpers/cli/unitdata.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
||||||
|
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 == 'set':
|
||||||
|
unitdata.kv().set(key, value)
|
||||||
|
unitdata.kv().flush()
|
||||||
|
return ''
|
||||||
|
return _unitdata_cmd
|
@ -1051,13 +1051,22 @@ class SubordinateConfigContext(OSContextGenerator):
|
|||||||
:param config_file : Service's config file to query sections
|
:param config_file : Service's config file to query sections
|
||||||
:param interface : Subordinate interface to inspect
|
:param interface : Subordinate interface to inspect
|
||||||
"""
|
"""
|
||||||
self.service = service
|
|
||||||
self.config_file = config_file
|
self.config_file = config_file
|
||||||
self.interface = interface
|
if isinstance(service, list):
|
||||||
|
self.services = service
|
||||||
|
else:
|
||||||
|
self.services = [service]
|
||||||
|
if isinstance(interface, list):
|
||||||
|
self.interfaces = interface
|
||||||
|
else:
|
||||||
|
self.interfaces = [interface]
|
||||||
|
|
||||||
def __call__(self):
|
def __call__(self):
|
||||||
ctxt = {'sections': {}}
|
ctxt = {'sections': {}}
|
||||||
for rid in relation_ids(self.interface):
|
rids = []
|
||||||
|
for interface in self.interfaces:
|
||||||
|
rids.extend(relation_ids(interface))
|
||||||
|
for rid in rids:
|
||||||
for unit in related_units(rid):
|
for unit in related_units(rid):
|
||||||
sub_config = relation_get('subordinate_configuration',
|
sub_config = relation_get('subordinate_configuration',
|
||||||
rid=rid, unit=unit)
|
rid=rid, unit=unit)
|
||||||
@ -1069,29 +1078,32 @@ class SubordinateConfigContext(OSContextGenerator):
|
|||||||
'setting from %s' % rid, level=ERROR)
|
'setting from %s' % rid, level=ERROR)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.service not in sub_config:
|
for service in self.services:
|
||||||
log('Found subordinate_config on %s but it contained'
|
if service not in sub_config:
|
||||||
'nothing for %s service' % (rid, self.service),
|
log('Found subordinate_config on %s but it contained'
|
||||||
level=INFO)
|
'nothing for %s service' % (rid, service),
|
||||||
continue
|
level=INFO)
|
||||||
|
continue
|
||||||
|
|
||||||
sub_config = sub_config[self.service]
|
sub_config = sub_config[service]
|
||||||
if self.config_file not in sub_config:
|
if self.config_file not in sub_config:
|
||||||
log('Found subordinate_config on %s but it contained'
|
log('Found subordinate_config on %s but it contained'
|
||||||
'nothing for %s' % (rid, self.config_file),
|
'nothing for %s' % (rid, self.config_file),
|
||||||
level=INFO)
|
level=INFO)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sub_config = sub_config[self.config_file]
|
|
||||||
for k, v in six.iteritems(sub_config):
|
|
||||||
if k == 'sections':
|
|
||||||
for section, config_dict in six.iteritems(v):
|
|
||||||
log("adding section '%s'" % (section),
|
|
||||||
level=DEBUG)
|
|
||||||
ctxt[k][section] = config_dict
|
|
||||||
else:
|
|
||||||
ctxt[k] = v
|
|
||||||
|
|
||||||
|
sub_config = sub_config[self.config_file]
|
||||||
|
for k, v in six.iteritems(sub_config):
|
||||||
|
if k == 'sections':
|
||||||
|
for section, config_list in six.iteritems(v):
|
||||||
|
log("adding section '%s'" % (section),
|
||||||
|
level=DEBUG)
|
||||||
|
if ctxt[k].get(section):
|
||||||
|
ctxt[k][section].extend(config_list)
|
||||||
|
else:
|
||||||
|
ctxt[k][section] = config_list
|
||||||
|
else:
|
||||||
|
ctxt[k] = v
|
||||||
log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
|
log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
|
||||||
return ctxt
|
return ctxt
|
||||||
|
|
||||||
|
45
hooks/charmhelpers/core/files.py
Normal file
45
hooks/charmhelpers/core/files.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2014-2015 Canonical Limited.
|
||||||
|
#
|
||||||
|
# This file is part of charm-helpers.
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def sed(filename, before, after, flags='g'):
|
||||||
|
"""
|
||||||
|
Search and replaces the given pattern on filename.
|
||||||
|
|
||||||
|
:param filename: relative or absolute file path.
|
||||||
|
:param before: expression to be replaced (see 'man sed')
|
||||||
|
:param after: expression to replace with (see 'man sed')
|
||||||
|
:param flags: sed-compatible regex flags in example, to make
|
||||||
|
the search and replace case insensitive, specify ``flags="i"``.
|
||||||
|
The ``g`` flag is always specified regardless, so you do not
|
||||||
|
need to remember to include it when overriding this parameter.
|
||||||
|
:returns: If the sed command exit code was zero then return,
|
||||||
|
otherwise raise CalledProcessError.
|
||||||
|
"""
|
||||||
|
expression = r's/{0}/{1}/{2}'.format(before,
|
||||||
|
after, flags)
|
||||||
|
|
||||||
|
return subprocess.check_call(["sed", "-i", "-r", "-e",
|
||||||
|
expression,
|
||||||
|
os.path.expanduser(filename)])
|
@ -34,6 +34,23 @@ import errno
|
|||||||
import tempfile
|
import tempfile
|
||||||
from subprocess import CalledProcessError
|
from subprocess import CalledProcessError
|
||||||
|
|
||||||
|
try:
|
||||||
|
from charmhelpers.cli import cmdline
|
||||||
|
except ImportError as e:
|
||||||
|
# due to the anti-pattern of partially synching charmhelpers directly
|
||||||
|
# into charms, it's possible that charmhelpers.cli is not available;
|
||||||
|
# if that's the case, they don't really care about using the cli anyway,
|
||||||
|
# so mock it out
|
||||||
|
if str(e) == 'No module named cli':
|
||||||
|
class cmdline(object):
|
||||||
|
@classmethod
|
||||||
|
def subcommand(cls, *args, **kwargs):
|
||||||
|
def _wrap(func):
|
||||||
|
return func
|
||||||
|
return _wrap
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
import six
|
import six
|
||||||
if not six.PY3:
|
if not six.PY3:
|
||||||
from UserDict import UserDict
|
from UserDict import UserDict
|
||||||
@ -173,9 +190,20 @@ def relation_type():
|
|||||||
return os.environ.get('JUJU_RELATION', None)
|
return os.environ.get('JUJU_RELATION', None)
|
||||||
|
|
||||||
|
|
||||||
def relation_id():
|
@cmdline.subcommand()
|
||||||
"""The relation ID for the current relation hook"""
|
@cached
|
||||||
return os.environ.get('JUJU_RELATION_ID', None)
|
def relation_id(relation_name=None, service_or_unit=None):
|
||||||
|
"""The relation ID for the current or a specified relation"""
|
||||||
|
if not relation_name and not service_or_unit:
|
||||||
|
return os.environ.get('JUJU_RELATION_ID', None)
|
||||||
|
elif relation_name and service_or_unit:
|
||||||
|
service_name = service_or_unit.split('/')[0]
|
||||||
|
for relid in relation_ids(relation_name):
|
||||||
|
remote_service = remote_service_name(relid)
|
||||||
|
if remote_service == service_name:
|
||||||
|
return relid
|
||||||
|
else:
|
||||||
|
raise ValueError('Must specify neither or both of relation_name and service_or_unit')
|
||||||
|
|
||||||
|
|
||||||
def local_unit():
|
def local_unit():
|
||||||
@ -188,14 +216,27 @@ def remote_unit():
|
|||||||
return os.environ.get('JUJU_REMOTE_UNIT', None)
|
return os.environ.get('JUJU_REMOTE_UNIT', None)
|
||||||
|
|
||||||
|
|
||||||
|
@cmdline.subcommand()
|
||||||
def service_name():
|
def service_name():
|
||||||
"""The name service group this unit belongs to"""
|
"""The name service group this unit belongs to"""
|
||||||
return local_unit().split('/')[0]
|
return local_unit().split('/')[0]
|
||||||
|
|
||||||
|
|
||||||
|
@cmdline.subcommand()
|
||||||
|
@cached
|
||||||
|
def remote_service_name(relid=None):
|
||||||
|
"""The remote service name for a given relation-id (or the current relation)"""
|
||||||
|
if relid is None:
|
||||||
|
unit = remote_unit()
|
||||||
|
else:
|
||||||
|
units = related_units(relid)
|
||||||
|
unit = units[0] if units else None
|
||||||
|
return unit.split('/')[0] if unit else None
|
||||||
|
|
||||||
|
|
||||||
def hook_name():
|
def hook_name():
|
||||||
"""The name of the currently executing hook"""
|
"""The name of the currently executing hook"""
|
||||||
return os.path.basename(sys.argv[0])
|
return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
|
||||||
|
|
||||||
|
|
||||||
class Config(dict):
|
class Config(dict):
|
||||||
@ -468,6 +509,63 @@ def relation_types():
|
|||||||
return rel_types
|
return rel_types
|
||||||
|
|
||||||
|
|
||||||
|
@cached
|
||||||
|
def relation_to_interface(relation_name):
|
||||||
|
"""
|
||||||
|
Given the name of a relation, return the interface that relation uses.
|
||||||
|
|
||||||
|
:returns: The interface name, or ``None``.
|
||||||
|
"""
|
||||||
|
return relation_to_role_and_interface(relation_name)[1]
|
||||||
|
|
||||||
|
|
||||||
|
@cached
|
||||||
|
def relation_to_role_and_interface(relation_name):
|
||||||
|
"""
|
||||||
|
Given the name of a relation, return the role and the name of the interface
|
||||||
|
that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
|
||||||
|
|
||||||
|
:returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
|
||||||
|
"""
|
||||||
|
_metadata = metadata()
|
||||||
|
for role in ('provides', 'requires', 'peer'):
|
||||||
|
interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
|
||||||
|
if interface:
|
||||||
|
return role, interface
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
@cached
|
||||||
|
def role_and_interface_to_relations(role, interface_name):
|
||||||
|
"""
|
||||||
|
Given a role and interface name, return a list of relation names for the
|
||||||
|
current charm that use that interface under that role (where role is one
|
||||||
|
of ``provides``, ``requires``, or ``peer``).
|
||||||
|
|
||||||
|
:returns: A list of relation names.
|
||||||
|
"""
|
||||||
|
_metadata = metadata()
|
||||||
|
results = []
|
||||||
|
for relation_name, relation in _metadata.get(role, {}).items():
|
||||||
|
if relation['interface'] == interface_name:
|
||||||
|
results.append(relation_name)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@cached
|
||||||
|
def interface_to_relations(interface_name):
|
||||||
|
"""
|
||||||
|
Given an interface, return a list of relation names for the current
|
||||||
|
charm that use that interface.
|
||||||
|
|
||||||
|
:returns: A list of relation names.
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for role in ('provides', 'requires', 'peer'):
|
||||||
|
results.extend(role_and_interface_to_relations(role, interface_name))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def charm_name():
|
def charm_name():
|
||||||
"""Get the name of the current charm as is specified on metadata.yaml"""
|
"""Get the name of the current charm as is specified on metadata.yaml"""
|
||||||
@ -644,6 +742,21 @@ def action_fail(message):
|
|||||||
subprocess.check_call(['action-fail', message])
|
subprocess.check_call(['action-fail', message])
|
||||||
|
|
||||||
|
|
||||||
|
def action_name():
|
||||||
|
"""Get the name of the currently executing action."""
|
||||||
|
return os.environ.get('JUJU_ACTION_NAME')
|
||||||
|
|
||||||
|
|
||||||
|
def action_uuid():
|
||||||
|
"""Get the UUID of the currently executing action."""
|
||||||
|
return os.environ.get('JUJU_ACTION_UUID')
|
||||||
|
|
||||||
|
|
||||||
|
def action_tag():
|
||||||
|
"""Get the tag for the currently executing action."""
|
||||||
|
return os.environ.get('JUJU_ACTION_TAG')
|
||||||
|
|
||||||
|
|
||||||
def status_set(workload_state, message):
|
def status_set(workload_state, message):
|
||||||
"""Set the workload state with a message
|
"""Set the workload state with a message
|
||||||
|
|
||||||
|
@ -152,6 +152,7 @@ associated to the hookname.
|
|||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import datetime
|
import datetime
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
@ -164,8 +165,7 @@ __author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
|
|||||||
class Storage(object):
|
class Storage(object):
|
||||||
"""Simple key value database for local unit state within charms.
|
"""Simple key value database for local unit state within charms.
|
||||||
|
|
||||||
Modifications are automatically committed at hook exit. That's
|
Modifications are not persisted unless :meth:`flush` is called.
|
||||||
currently regardless of exit code.
|
|
||||||
|
|
||||||
To support dicts, lists, integer, floats, and booleans values
|
To support dicts, lists, integer, floats, and booleans values
|
||||||
are automatically json encoded/decoded.
|
are automatically json encoded/decoded.
|
||||||
@ -173,8 +173,11 @@ class Storage(object):
|
|||||||
def __init__(self, path=None):
|
def __init__(self, path=None):
|
||||||
self.db_path = path
|
self.db_path = path
|
||||||
if path is None:
|
if path is None:
|
||||||
self.db_path = os.path.join(
|
if 'UNIT_STATE_DB' in os.environ:
|
||||||
os.environ.get('CHARM_DIR', ''), '.unit-state.db')
|
self.db_path = os.environ['UNIT_STATE_DB']
|
||||||
|
else:
|
||||||
|
self.db_path = os.path.join(
|
||||||
|
os.environ.get('CHARM_DIR', ''), '.unit-state.db')
|
||||||
self.conn = sqlite3.connect('%s' % self.db_path)
|
self.conn = sqlite3.connect('%s' % self.db_path)
|
||||||
self.cursor = self.conn.cursor()
|
self.cursor = self.conn.cursor()
|
||||||
self.revision = None
|
self.revision = None
|
||||||
@ -189,15 +192,8 @@ class Storage(object):
|
|||||||
self.conn.close()
|
self.conn.close()
|
||||||
self._closed = True
|
self._closed = True
|
||||||
|
|
||||||
def _scoped_query(self, stmt, params=None):
|
|
||||||
if params is None:
|
|
||||||
params = []
|
|
||||||
return stmt, params
|
|
||||||
|
|
||||||
def get(self, key, default=None, record=False):
|
def get(self, key, default=None, record=False):
|
||||||
self.cursor.execute(
|
self.cursor.execute('select data from kv where key=?', [key])
|
||||||
*self._scoped_query(
|
|
||||||
'select data from kv where key=?', [key]))
|
|
||||||
result = self.cursor.fetchone()
|
result = self.cursor.fetchone()
|
||||||
if not result:
|
if not result:
|
||||||
return default
|
return default
|
||||||
@ -206,33 +202,81 @@ class Storage(object):
|
|||||||
return json.loads(result[0])
|
return json.loads(result[0])
|
||||||
|
|
||||||
def getrange(self, key_prefix, strip=False):
|
def getrange(self, key_prefix, strip=False):
|
||||||
stmt = "select key, data from kv where key like '%s%%'" % key_prefix
|
"""
|
||||||
self.cursor.execute(*self._scoped_query(stmt))
|
Get a range of keys starting with a common prefix as a mapping of
|
||||||
|
keys to values.
|
||||||
|
|
||||||
|
:param str key_prefix: Common prefix among all keys
|
||||||
|
:param bool strip: Optionally strip the common prefix from the key
|
||||||
|
names in the returned dict
|
||||||
|
:return dict: A (possibly empty) dict of key-value mappings
|
||||||
|
"""
|
||||||
|
self.cursor.execute("select key, data from kv where key like ?",
|
||||||
|
['%s%%' % key_prefix])
|
||||||
result = self.cursor.fetchall()
|
result = self.cursor.fetchall()
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return None
|
return {}
|
||||||
if not strip:
|
if not strip:
|
||||||
key_prefix = ''
|
key_prefix = ''
|
||||||
return dict([
|
return dict([
|
||||||
(k[len(key_prefix):], json.loads(v)) for k, v in result])
|
(k[len(key_prefix):], json.loads(v)) for k, v in result])
|
||||||
|
|
||||||
def update(self, mapping, prefix=""):
|
def update(self, mapping, prefix=""):
|
||||||
|
"""
|
||||||
|
Set the values of multiple keys at once.
|
||||||
|
|
||||||
|
:param dict mapping: Mapping of keys to values
|
||||||
|
:param str prefix: Optional prefix to apply to all keys in `mapping`
|
||||||
|
before setting
|
||||||
|
"""
|
||||||
for k, v in mapping.items():
|
for k, v in mapping.items():
|
||||||
self.set("%s%s" % (prefix, k), v)
|
self.set("%s%s" % (prefix, k), v)
|
||||||
|
|
||||||
def unset(self, key):
|
def unset(self, key):
|
||||||
|
"""
|
||||||
|
Remove a key from the database entirely.
|
||||||
|
"""
|
||||||
self.cursor.execute('delete from kv where key=?', [key])
|
self.cursor.execute('delete from kv where key=?', [key])
|
||||||
if self.revision and self.cursor.rowcount:
|
if self.revision and self.cursor.rowcount:
|
||||||
self.cursor.execute(
|
self.cursor.execute(
|
||||||
'insert into kv_revisions values (?, ?, ?)',
|
'insert into kv_revisions values (?, ?, ?)',
|
||||||
[key, self.revision, json.dumps('DELETED')])
|
[key, self.revision, json.dumps('DELETED')])
|
||||||
|
|
||||||
|
def unsetrange(self, keys=None, prefix=""):
|
||||||
|
"""
|
||||||
|
Remove a range of keys starting with a common prefix, from the database
|
||||||
|
entirely.
|
||||||
|
|
||||||
|
:param list keys: List of keys to remove.
|
||||||
|
:param str prefix: Optional prefix to apply to all keys in ``keys``
|
||||||
|
before removing.
|
||||||
|
"""
|
||||||
|
if keys is not None:
|
||||||
|
keys = ['%s%s' % (prefix, key) for key in keys]
|
||||||
|
self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
|
||||||
|
if self.revision and self.cursor.rowcount:
|
||||||
|
self.cursor.execute(
|
||||||
|
'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
|
||||||
|
list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
|
||||||
|
else:
|
||||||
|
self.cursor.execute('delete from kv where key like ?',
|
||||||
|
['%s%%' % prefix])
|
||||||
|
if self.revision and self.cursor.rowcount:
|
||||||
|
self.cursor.execute(
|
||||||
|
'insert into kv_revisions values (?, ?, ?)',
|
||||||
|
['%s%%' % prefix, self.revision, json.dumps('DELETED')])
|
||||||
|
|
||||||
def set(self, key, value):
|
def set(self, key, value):
|
||||||
|
"""
|
||||||
|
Set a value in the database.
|
||||||
|
|
||||||
|
:param str key: Key to set the value for
|
||||||
|
:param value: Any JSON-serializable value to be set
|
||||||
|
"""
|
||||||
serialized = json.dumps(value)
|
serialized = json.dumps(value)
|
||||||
|
|
||||||
self.cursor.execute(
|
self.cursor.execute('select data from kv where key=?', [key])
|
||||||
'select data from kv where key=?', [key])
|
|
||||||
exists = self.cursor.fetchone()
|
exists = self.cursor.fetchone()
|
||||||
|
|
||||||
# Skip mutations to the same value
|
# Skip mutations to the same value
|
||||||
|
Loading…
Reference in New Issue
Block a user