summaryrefslogtreecommitdiffstats
path: root/formats/simpleconfig.py
blob: af32ad780ec28561b4fdd8a5578dcd86e8f4ed62 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# -*- coding: UTF-8 -*-
# Copyright 2015 Red Hat, Inc.
# Part of clufter project
# Licensed under GPLv2+ (a copy included | http://gnu.org/licenses/gpl-2.0.txt)
"""Structured configuration formats such as corosync.conf"""
__author__ = "Jan Pokorný <jpokorny @at@ Red Hat .dot. com>"

from ..format import SimpleFormat
from ..protocol import Protocol
from ..utils_func import apply_aggregation_preserving_passing_depth
from ..utils_prog import getenv_namespaced


class simpleconfig(SimpleFormat):
    """"Structured configuration formats such as corosync.conf

    Internally ('struct'), it is structured liked this:

    SECTION  ::= tuple(TAG, OPTIONS, SECTIONS)  # [] tolerated, but fixed width
    SECTIONS ::= list-or-tuple(SECTION)
    OPTIONS  ::= list-or-tuple(OPTION)
    OPTION   ::= tuple(TAG, VALUE)  # must be tuple due to inner arragement
    TAG      ::= .*   # XXX and comment handling [^#@].*
    VALUE    ::= .*

    where SECTIONS form logical subsections (arbitrarily nested)

    Example:

    ('corosync-ONLY-INTERNAL-TAG-NOT-EXTERNALIZED-ANYWAY',
     [],
     [('totem', [('version', '2'), ('cluster_name', 'aus-cluster')], {}),
      ('nodelist',
       [],
       [('node', [('nodeid', '1'), ('ring0_addr', 'lolek.example.com')], []),
        ('node', [('nodeid', '2'), ('ring0_addr', 'bolek.example.com')], [])]),
      ('quorum',
       [('provider', 'corosync_votequorum'),
        ('expected_votes', '1'),
        ('two_node', '1')],
       [])])
    """
    # NOTE yacc-based parser in fence-virt
    native_protocol = STRUCT = Protocol('struct')
    BYTESTRING = SimpleFormat.BYTESTRING

    # notable lexical units for input
    lbrace_i, rbrace_i, optsep_i = '{',            '}',      ':'
    # notable lexical units for output (~ pretty-printing, hence with spaces)
    lbrace_o, rbrace_o, optsep_o = ' ' + lbrace_i, rbrace_i, optsep_i + ' '
    # same for input/output
    csep = '#'

    @SimpleFormat.producing(BYTESTRING)
    def get_bytestring(self, *protodecl):
        """Externalize 'struct', that is basically, pretty print it

        For example above, the result is something like:

        totem {
            version: 2
            cluster_name: aus-cluster
        }
        nodelist {
            node {
                id: 1
                ring0_addr: lolek.example.com
            }
            node {
                id: 2
                ring0_addr: bolek.example.com
            }
        }
        quorum {
            provider: corosync_votequorum
            expected_votes: 1
            two_node: 1
        }
        """
        # try to look (indirectly) if we have a file at hand first
        ret = super(simpleconfig, self).get_bytestring(self.BYTESTRING)
        if ret is not None:
            return ret

        # fallback
        struct = self.STRUCT(protect_safe=True)
        indent, optindent = (getenv_namespaced('COROINDENT', '\t'), ) * 2
        lbrace, rbrace, optsep = self.lbrace_o, self.rbrace_o, self.optsep_o
        ret = '\n'.join(
            apply_aggregation_preserving_passing_depth(
                lambda x, d:
                    # avoid IndexError
                    x if not x or not isinstance(x[0], basestring)
                    else
                    # OPTION
                    ((d - 3) / 2 * indent + x[0] + optsep + x[1], )
                        if isinstance(x, tuple) and len(x) == 2  # dif. SECTION
                    else
                    # rest ([''] at the end is for a final newline)
                    ([(d - 3) / 2 * indent + x[0] + lbrace] if d > 1 else [])
                        + list(xxxx for xx in filter(bool, x[1:])
                               for xxx in xx for xxxx in xxx)
                        + ([(d - 3) / 2 * indent + rbrace] if d > 1 else [''])
            )(struct)
        )
        return ret

    @SimpleFormat.producing(STRUCT, protect=True)
    def get_struct(self, *protodecl):
        # similar to pcs.corosync_conf._parse_section (and Corosync itself)
        lbrace, rbrace, optsep = self.lbrace_i, self.rbrace_i, self.optsep_i
        csep = self.csep
        attrs, children = [], []
        work, t = [('', attrs, children)], self.BYTESTRING()
        while t:
            h, t = t.split('\n', 1)
            if h.startswith(csep):
                continue
            elif lbrace in h:
                work.append((h.split(lbrace, 1)[0].strip(), [], []))
                attrs, children = work[-1][1:]
            elif rbrace in h:
                try:
                    cur = work.pop()
                    attrs, children = work[-1][1:]
                except IndexError:
                    raise RuntimeError("Unexpected closing brace")
                children.append(cur)
            elif optsep in h:
                attrs.append(tuple(x.strip() for x in h.split(optsep, 1)))
        ret = work.pop()
        if work:
            raise RuntimeError("Missing {0} closing brace(s)".format(len(work)))
        return ret


class simpleconfig_normalized(simpleconfig):
    """Structured configuration formats such as corosync.conf, normalized form

    Here, "normalized" is a synonym to "bijectively convertible to XML".
    Trivially, any elements-attributes XML is convertible to simpleconfig,
    but because simpleconfig can carry name-/key-duplicated options, something
    not suitable for reverse options-to-attributes mapping has to be normalized
    and because this is to serve in simpleconfig2needlexml filter (hence
    affecting only? uidgid entries), we define the normalization as follows:

    1. analyze current section whether it contains repeated options
      1a. no  -> goto 2. right away
      1b. yes -> for each duplicated option but without its value duplication
                 (in that case, such issue a warning), create a new
                 "following sibling" section and move the option here on its
                 own; if the current section contains subsection,
                 issue a warning about that
    2. continue with the next section in a defined traversal

    Fair enough, we've just described the simpleconfig-normalize filter :)
    """
    pass