blob: 28ba91b35ef3f71260e9befd202cfd0f4e942957 [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
import os
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]"%("\n".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())
if s[0] == "address": s[0] = "router.config.address"
if s[0] == "linkRoute": s[0] = "router.config.linkRoute"
if s[0] == "autoLink": s[0] = "router.config.autoLink"
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"%("\n".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: ((sectionName:{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])
if k == u'sslProfile':
inc[u'sslProfileName'] = inc[u'name']
del inc['name']
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 get_config_types(self):
return self.config_types
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.get_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)
class PolicyConfig(Config):
def __init__(self, filename=None, schema=QdSchema(), raw_json=False):
super(PolicyConfig, self).__init__(filename, schema, raw_json)
def get_config_types(self):
return [s for s in self.config_types if 'policy' in s.name]
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 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)
qd.qd_router_setup_late(dispatch) # Actions requiring active management agent.
agent.activate("$_management_internal")
from qpid_dispatch_internal.display_name.display_name import DisplayNameService
displayname_service = DisplayNameService("$displayname")
policyDir = config.by_type('policy')[0]['policyDir']
policyDefaultVhost = config.by_type('policy')[0]['defaultVhost']
# Remaining configuration
for t in "fixedAddress", "listener", "connector", "waypoint", "linkRoutePattern", \
"router.config.address", "router.config.linkRoute", "router.config.autoLink", \
"policy", "vhost":
for a in config.by_type(t):
configure(a)
if t == "listener":
display_file_name = a.get('displayNameFile')
if display_file_name:
ssl_profile_name = a.get('sslProfileName')
displayname_service.add(ssl_profile_name, display_file_name)
for e in config.entities:
configure(e)
# Load the vhosts from the .json files in policyDir
# Only vhosts are loaded. Other entities are silently discarded.
if not policyDir == '':
apath = os.path.abspath(policyDir)
for i in os.listdir(policyDir):
if i.endswith(".json"):
pconfig = PolicyConfig(os.path.join(apath, i))
for a in pconfig.by_type("vhost"):
agent.configure(a)
# Set policy default application after all rulesets loaded
agent.policy.set_default_vhost(policyDefaultVhost)