Skip to content

Commit

Permalink
Expand metadata support
Browse files Browse the repository at this point in the history
- Cleanup MetaData interface a bit and add iterative access.  (Not 100% sure I
  like the design, but going with it for now.)  Will likely refactor when a
  proper CONF parser is introduced.
- Added write support with a new full-circle unit test.
  • Loading branch information
lowell80 committed Feb 26, 2019
1 parent 438b81b commit 1f28c43
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 21 deletions.
73 changes: 67 additions & 6 deletions ksconf/conf/meta.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
"""
Incomplete documentation available here:
https://docs.splunk.com/Documentation/Splunk/latest/Admin/Defaultmetaconf
Specifically, attribute-level ACls aren't discussed nor is the magic "import" directive.
LEVELS:
0 - global (or 1 stanza="default")
Expand All @@ -8,6 +15,8 @@
3 - attribute
"""

from __future__ import absolute_import, unicode_literals


"""
Expand All @@ -20,12 +29,14 @@
"""


import re

from urllib import unquote
import six

from six.moves.urllib.parse import quote, unquote

from ksconf.conf.parser import parse_conf
from ksconf.conf.merge import merge_conf_dicts



Expand All @@ -50,6 +61,26 @@ def update(self, *args, **kwargs):
def data(self):
return self._data

def walk(self, _prefix=()):
if self._data:
yield _prefix
if self._children:
for child_name, child in six.iteritems(self._children):
# PY3: yield from
for r in child.walk(_prefix=_prefix+(child_name,)):
yield r

def items(self, prefix=None):
""" Helpful when rebuilding the input file. """
if prefix is None:
prefix = ()
if self._data:
yield prefix, self._data
if self._children:
for child_name, child in six.iteritems(self._children):
# yield from (PY3)
for r in child.items(prefix=prefix+(child_name,)):
yield r



Expand Down Expand Up @@ -99,7 +130,7 @@ def get_layer(self, *names):
node = node.resolve(name)
return node

def get_combined(self, *names):
def get(self, *names):
node = self._meta
layers = [ node.data ]
for name in names:
Expand All @@ -110,14 +141,44 @@ def get_combined(self, *names):
#d["acesss"]
return self.parse_meta(d)

def feed(self, stream):
def feed_file(self, stream):
conf = parse_conf(stream)
self.feed_conf(conf)

def feed_conf(self, conf):
for stanza_name, stanza_data in conf.items():
parts = [ unquote(p) for p in stanza_name.split("/") ]
if len(parts) == 1 and parts[0] in ("", "default", "global"):
parts = []
meta_layer = self.get_layer(*parts)
meta_layer.update(stanza_data)

# def iter_all(self):
# for
def iter_raw(self):
""" RAW """
return self._meta.items()

def walk(self):
for path in self._meta.walk():
yield (path, self.get(*path))

def write_stream(self, stream, sort=True):
if sort:
# Prefix level # to list for sorting purposes
data = [ (len(parts), parts, payload) for parts, payload in self.iter_raw() ]
data.sort()
raw = [ (i[1], i[2]) for i in data ]
del data
else:
raw = self.iter_raw()

for parts, payload in raw:
stanza = "/".join(quote(p, "") for p in parts)
stream.write("[{}]\n".format(stanza))
for attr in sorted(payload):
value = payload[attr]
if attr.startswith("#"):
stream.write("{0}\n".format(value))
else:
stream.write("{} = {}\n".format(attr, value))
# Trailing EOL, oh well... fix that later
stream.write("\n")
57 changes: 42 additions & 15 deletions tests/test_meta.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
#!/usr/bin/env python

from __future__ import absolute_import, unicode_literals

import os
import sys
import unittest

# Allow interactive execution from CLI, cd tests; ./test_cli.py
from io import open

# Allow interactive execution from CLI, cd tests; ./test_meta.py
if __package__ is None:
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from ksconf.conf.meta import MetaData

from ksconf.conf.parser import parse_conf
from ksconf.conf.delta import compare_cfgs, DIFF_OP_EQUAL

from tests.cli_helper import TestWorkDir



class MetaDataTestCase(unittest.TestCase):

def test_simple_all_levels(self):
twd = TestWorkDir()
f = twd.write_file("metadata/default.meta", """\
def setUp(self):
self.twd = TestWorkDir()


@property
def sample01(self):
return self.twd.write_file("metadata/default.meta", """\
[]
access = read : [ * ], write : [ admin, power ]
export = system
Expand Down Expand Up @@ -48,30 +60,31 @@ def test_simple_all_levels(self):
version = 7.0.1
""")

def test_simple_all_levels(self):
md = MetaData()
md.feed(f)
md.feed_file(self.sample01)

d = md.get_combined("macros")
d = md.get("macros")
self.assertEqual(d["export"], "system")
self.assertEqual(d["owner"], "kintyrela")

glob = md.get_combined()
props = md.get_combined("props")
glob = md.get()
props = md.get("props")
self.assertEqual(glob, props, "Expect [props] (undefined) to be identical to []")

d = md.get_combined("props", "Vendor:Engine:Errors")
d = md.get("props", "Vendor:Engine:Errors")
self.assertEqual(d["export"], "system")
self.assertEqual(d["owner"], "joeadmin")

d = md.get_combined("props", "Vendor:Engine:Errors", "EXTRACT-VendorId")
d = md.get("props", "Vendor:Engine:Errors", "EXTRACT-VendorId")
self.assertEqual(d["export"], "system")
self.assertEqual(d["owner"], "admin")
self.assertEqual(d["modtime"], "1518721483.877502000")


def test_encoded_slash(self):
""" Ensure that encoded slashes (%2f) are parsed correctly. """
twd = TestWorkDir()
f = twd.write_file("metadata/default.meta", """\
f = self.twd.write_file("metadata/default.meta", """\
[]
export = system
owner = nobody
Expand All @@ -80,13 +93,27 @@ def test_encoded_slash(self):
modtime = 1518784292
""")
md = MetaData()
md.feed(f)
md.feed_file(f)

self.assertEqual(md.get_combined()["owner"], "nobody")
self.assertEqual(md.get()["owner"], "nobody")
self.assertEqual(
md.get_combined("props", "silly/name", "EXTRACT-other")["modtime"],
md.get("props", "silly/name", "EXTRACT-other")["modtime"],
"1518784292")

def test_write_with_compare(self):
orig = self.sample01
new = self.twd.get_path("metadata/local.meta")
md = MetaData()
md.feed_file(orig)
with open(new, "w", encoding="utf-8") as stream:
md.write_stream(stream)

a = parse_conf(orig)
b = parse_conf(new)
diffs = compare_cfgs(a, b)

self.assertEqual(len(diffs), 1)
self.assertEqual(diffs[0].tag, DIFF_OP_EQUAL)


if __name__ == '__main__': # pragma: no cover
Expand Down

0 comments on commit 1f28c43

Please sign in to comment.