Files
core-build/build/lib/dsl.py
Suraj Ravichandran 80453d8124 Add general purpose mechanism to build system to allow config var
override via environment variables.

Example usage:
`env dsl.config.repos.middleware.branch=update_diffreduc_and_flakefix make update`

Things to note:
	1. `dsl.config.` is a general purpose prefix to identify build system config
	    related env vars amidst the rest of them
	2. This mechanism is not additive and will not allow you to add new config
	   config vars instead just edit existing ones.
	3. It will inform you via a log of a misformed env var that has `dsl.config.` prefix.
	4. It will also log a successful override of a var.

Feel free to review/comment for correctness and/or complexity.

Ticket: #16118
2016-07-08 05:15:46 -07:00

210 lines
6.3 KiB
Python

#+
# Copyright 2015 iXsystems, Inc.
# All rights reserved
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted providing that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
#####################################################################
import os
import ast
import json
DSL_PREFIX = 'dsl.config.'
class ConfigDict(dict):
def __getattr__(self, item):
return self.get(item)
class ConfigArray(list):
def __iadd__(self, other):
if type(other) not in (list, ConfigArray):
self.append(other)
return self
return super(ConfigArray, self).__iadd__(other)
def where(self, **predicates):
for i in self:
found = True
for k, v in predicates.items():
if i[k] != v:
found = False
break
if found:
return i
return None
class GlobalsWrapper(dict):
def __init__(self, env, filename):
super(GlobalsWrapper, self).__init__()
self.dict = ConfigDict()
self.filename = filename
self.env = env
def __getitem__(self, item):
if item.isupper():
if item in self.dict:
return self.dict[item]
return self.env.get(item, None)
if item == 'ConfigDict':
return ConfigDict
if item == 'ConfigArray':
return ConfigArray
if item == 'e':
from utils import e
return e
if item == 'sh':
from utils import sh_str
return sh_str
if item == 'int':
return int
if item == 'str':
return str
if item == 'include':
return self.include
if item == 'exists':
return os.path.exists
if item in self.dict:
return self.dict[item]
arr = ConfigArray()
self.dict[item] = arr
return arr
def __setitem__(self, key, value):
if key.isupper():
self.env[key] = value
else:
self.dict[key] = value
def include(self, filename):
filename = os.path.expandvars(filename)
if filename[0] != '/':
filename = os.path.join(os.path.dirname(self.filename), filename)
d = load_file(filename, self.env)
for k, v in d.items():
if k in self.dict:
if isinstance(self.dict[k], dict):
self.dict[k].update(v)
elif isinstance(self.dict[k], list):
self.dict[k] += v
else:
self.dict[k] = v
class AstTransformer(ast.NodeTransformer):
def visit_Str(self, node):
return ast.Call(
func=ast.Name(id='e', ctx=ast.Load()),
args=[node],
keywords=[],
starargs=None,
kwargs=None
)
def visit_List(self, node):
self.generic_visit(node)
return ast.Call(
func=ast.Name(id='ConfigArray', ctx=ast.Load()),
args=[node],
keywords=[],
starargs=None,
kwargs=None
)
def visit_Dict(self, node):
self.generic_visit(node)
return ast.Call(
func=ast.Name(id='ConfigDict', ctx=ast.Load()),
args=[node],
keywords=[],
starargs=None,
kwargs=None
)
def load_file(filename, env):
from utils import e
filename = e(filename)
g = GlobalsWrapper(env, filename)
with open(filename, 'r') as f:
tree = ast.parse(f.read(), filename)
t2 = ast.fix_missing_locations(AstTransformer().visit(tree))
exec(compile(t2, filename, 'exec'), g)
return g.dict
def override_profile_config(config):
# local import may be needed to avoid circular import issues
from utils import info
override = {k.replace(DSL_PREFIX, ''): v for k, v in os.environ.items() if k.startswith(DSL_PREFIX)}
for k, v in override.items():
dest = None
split_keys = k.split('.')
try:
for subkey in split_keys[:-1]:
dest = dest or config
if isinstance(dest, dict):
dest = dest[subkey]
elif isinstance(dest, list):
for dictitem in dest:
if dictitem['name'] == subkey:
dest = dictitem
break
else:
raise KeyError(subkey)
if dest and isinstance(dest, dict):
dest[split_keys[-1]] = v
else:
# means that the split resulted in a single element
# thus we can just use the original key
config[k] = v
info('Overriding {0}{1} build config var to: {2}'.format(DSL_PREFIX, k, v))
except KeyError as e:
# this key does not exist in the build config
# moving on post logging this
info('{0}{1} is not a proper build config key! KeyError for {2}'.format(DSL_PREFIX, k, e))
return config
def load_profile_config():
return override_profile_config(load_file(
'${BUILD_PROFILES}/${PROFILE}/config.pyd', os.environ
))