blob: e0833724662f9201efe88c1113ae92acfae67680 [file] [log] [blame]
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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
#
"""
Configuration file parsing
"""
import json, re, sys
from copy import copy
from qpid_dispatch.management.entity import camelcase
from ..dispatch import QdDll
from .qdrouter import QdSchema
class Config(object):
"""Load config entities from qdrouterd.conf and validated against L{QdSchema}."""
def __init__(self, filename=None, schema=QdSchema(), raw_json=False):
self.schema = schema
self.config_types = [et for et in schema.entity_types.itervalues()
if schema.is_configuration(et)]
if filename:
try:
self.load(filename, raw_json)
except Exception, e:
raise Exception, "Cannot load configuration file %s: %s" % (filename, e), sys.exc_info()[2]
else:
self.entities = []
@staticmethod
def _parse(lines):
"""Parse config file format into a section list"""
begin = re.compile(r'([\w-]+)[ \t]*{') # WORD {
end = re.compile(r'}') # }
attr = re.compile(r'([\w-]+)[ \t]*:[ \t]*(.+)') # WORD1: VALUE
def sub(line):
"""Do substitutions to make line json-friendly"""
line = line.split('#')[0].strip() # Strip comments
line = re.sub(begin, r'["\1", {', line)
line = re.sub(end, r'}],', line)
line = re.sub(attr, r'"\1": "\2",', line)
return line
js_text = "[%s]"%("".join([sub(l) for l in lines]))
spare_comma = re.compile(r',\s*([]}])') # Strip spare commas
js_text = re.sub(spare_comma, r'\1', js_text)
# Convert dictionary keys to camelCase
sections = json.loads(js_text)
for s in sections:
s[0] = camelcase(s[0])
s[1] = dict((camelcase(k), v) for k, v in s[1].iteritems())
return sections
@staticmethod
def _parserawjson(lines):
"""Parse raw json config file format into a section list"""
def sub(line):
"""Do substitutions to make line json-friendly"""
line = line.split('#')[0].strip() # Strip comments
return line
js_text = "%s"%("".join([sub(l) for l in lines]))
sections = json.loads(js_text)
return sections
def _expand(self, content):
"""
Find annotation sections (defined by schema) in the content,
expand references and remove the annotation sections.
@param content: ((section-name:{name:value...}))
"""
def _expand_section(section, annotations):
"""Expand one section"""
attrs = section[1]
for k in attrs.keys(): # Iterate over keys() because we will modify attr
inc = [i[1] for i in annotations if i[0] == k and i[1]['name'] == attrs[k]]
if inc:
assert len(inc) == 1
inc = copy(inc[0])
del inc['name'] # Not a real attribute, just an annotation id.
attrs.update(inc)
del attrs[k] # Delete the annotation attribute.
return section
annotations = [s for s in content if self.schema.annotation(s[0], error=False)]
return [_expand_section(s, annotations) for s in content
if self.schema.is_configuration(self.schema.entity_type(s[0], False))]
def load(self, source, raw_json=False):
"""
Load a configuration file.
@param source: A file name, open file object or iterable list of lines
@param raw_json: Source is pure json not needing conf-style substitutions
"""
if isinstance(source, basestring):
raw_json |= source.endswith(".json")
with open(source) as f:
self.load(f, raw_json)
else:
sections = self._parserawjson(source) if raw_json else self._parse(source)
# Add missing singleton sections
for et in self.config_types:
if et.singleton and not [s for s in sections if s[0] == et.short_name]:
sections.append((et.short_name, {}))
sections = self._expand(sections)
entities = [dict(type=self.schema.long_name(s[0]), **s[1]) for s in sections]
self.schema.validate_all(entities)
self.entities = entities
def by_type(self, entity_type):
"""Return entities of given type"""
entity_type = self.schema.long_name(entity_type)
return [e for e in self.entities if e['type'] == entity_type]
def remove(self, entity):
self.entities.remove(entity)
def configure_dispatch(dispatch, lib_handle, filename):
"""Called by C router code to load configuration file and do configuration"""
qd = QdDll(lib_handle)
dispatch = qd.qd_dispatch_p(dispatch)
config = Config(filename)
# NOTE: Can't import agent till till dispatch C extension module is initialized.
from .agent import Agent
agent = Agent(dispatch, qd)
qd.qd_dispatch_set_agent(dispatch, agent)
def configure(attributes):
"""Configure an entity and remove it from config"""
agent.configure(attributes)
config.remove(attributes)
modules = set(agent.schema.entity_type("log").attributes["module"].atype.tags)
for l in config.by_type('log'):
configure(l)
modules.remove(l["module"])
# Add default entities for any log modules not configured.
for m in modules: agent.configure(attributes=dict(type="log", module=m))
# Configure and prepare container and router before we can activate the agent.
configure(config.by_type('container')[0])
configure(config.by_type('router')[0])
qd.qd_dispatch_prepare(dispatch)
agent.activate("$management")
qd.qd_router_setup_late(dispatch) # Actions requiring active management agent.
# Remaining configuration
for t in "fixedAddress", "listener", "connector", "waypoint", "linkRoutePattern":
for a in config.by_type(t): configure(a)
for e in config.entities:
configure(e)