This commit is contained in:
Paulus Schoutsen
2019-07-31 12:25:30 -07:00
parent da05dfe708
commit 4de97abc3a
2676 changed files with 163166 additions and 140084 deletions

View File

@ -17,162 +17,164 @@ from homeassistant.exceptions import HomeAssistantError
# mypy: allow-untyped-calls, allow-untyped-defs
REQUIREMENTS = ('colorlog==4.0.2',)
REQUIREMENTS = ("colorlog==4.0.2",)
_LOGGER = logging.getLogger(__name__)
# pylint: disable=protected-access
MOCKS = {
'load': ("homeassistant.util.yaml.loader.load_yaml",
yaml_loader.load_yaml),
'load*': ("homeassistant.config.load_yaml", yaml_loader.load_yaml),
'secrets': ("homeassistant.util.yaml.loader.secret_yaml",
yaml_loader.secret_yaml),
"load": ("homeassistant.util.yaml.loader.load_yaml", yaml_loader.load_yaml),
"load*": ("homeassistant.config.load_yaml", yaml_loader.load_yaml),
"secrets": ("homeassistant.util.yaml.loader.secret_yaml", yaml_loader.secret_yaml),
} # type: Dict[str, Tuple[str, Callable]]
SILENCE = (
'homeassistant.scripts.check_config.yaml_loader.clear_secret_cache',
)
SILENCE = ("homeassistant.scripts.check_config.yaml_loader.clear_secret_cache",)
PATCHES = {}
C_HEAD = 'bold'
ERROR_STR = 'General Errors'
C_HEAD = "bold"
ERROR_STR = "General Errors"
def color(the_color, *args, reset=None):
"""Color helper."""
from colorlog.escape_codes import escape_codes, parse_colors
try:
if not args:
assert reset is None, "You cannot reset if nothing being printed"
return parse_colors(the_color)
return parse_colors(the_color) + ' '.join(args) + \
escape_codes[reset or 'reset']
return parse_colors(the_color) + " ".join(args) + escape_codes[reset or "reset"]
except KeyError as k:
raise ValueError("Invalid color {} in {}".format(str(k), the_color))
def run(script_args: List) -> int:
"""Handle check config commandline script."""
parser = argparse.ArgumentParser(
description="Check Home Assistant configuration.")
parser = argparse.ArgumentParser(description="Check Home Assistant configuration.")
parser.add_argument("--script", choices=["check_config"])
parser.add_argument(
'--script', choices=['check_config'])
parser.add_argument(
'-c', '--config',
"-c",
"--config",
default=get_default_config_dir(),
help="Directory that contains the Home Assistant configuration")
help="Directory that contains the Home Assistant configuration",
)
parser.add_argument(
'-i', '--info', nargs='?',
default=None, const='all',
help="Show a portion of the config")
"-i",
"--info",
nargs="?",
default=None,
const="all",
help="Show a portion of the config",
)
parser.add_argument(
'-f', '--files',
action='store_true',
help="Show used configuration files")
"-f", "--files", action="store_true", help="Show used configuration files"
)
parser.add_argument(
'-s', '--secrets',
action='store_true',
help="Show secret information")
"-s", "--secrets", action="store_true", help="Show secret information"
)
args, unknown = parser.parse_known_args()
if unknown:
print(color('red', "Unknown arguments:", ', '.join(unknown)))
print(color("red", "Unknown arguments:", ", ".join(unknown)))
config_dir = os.path.join(os.getcwd(), args.config)
print(color('bold', "Testing configuration at", config_dir))
print(color("bold", "Testing configuration at", config_dir))
res = check(config_dir, args.secrets)
domain_info = [] # type: List[str]
if args.info:
domain_info = args.info.split(',')
domain_info = args.info.split(",")
if args.files:
print(color(C_HEAD, 'yaml files'), '(used /',
color('red', 'not used') + ')')
deps = os.path.join(config_dir, 'deps')
yaml_files = [f for f in glob(os.path.join(config_dir, '**/*.yaml'),
recursive=True)
if not f.startswith(deps)]
print(color(C_HEAD, "yaml files"), "(used /", color("red", "not used") + ")")
deps = os.path.join(config_dir, "deps")
yaml_files = [
f
for f in glob(os.path.join(config_dir, "**/*.yaml"), recursive=True)
if not f.startswith(deps)
]
for yfn in sorted(yaml_files):
the_color = '' if yfn in res['yaml_files'] else 'red'
print(color(the_color, '-', yfn))
the_color = "" if yfn in res["yaml_files"] else "red"
print(color(the_color, "-", yfn))
if res['except']:
print(color('bold_white', 'Failed config'))
for domain, config in res['except'].items():
if res["except"]:
print(color("bold_white", "Failed config"))
for domain, config in res["except"].items():
domain_info.append(domain)
print(' ', color('bold_red', domain + ':'),
color('red', '', reset='red'))
dump_dict(config, reset='red')
print(color('reset'))
print(" ", color("bold_red", domain + ":"), color("red", "", reset="red"))
dump_dict(config, reset="red")
print(color("reset"))
if domain_info:
if 'all' in domain_info:
print(color('bold_white', 'Successful config (all)'))
for domain, config in res['components'].items():
print(' ', color(C_HEAD, domain + ':'))
if "all" in domain_info:
print(color("bold_white", "Successful config (all)"))
for domain, config in res["components"].items():
print(" ", color(C_HEAD, domain + ":"))
dump_dict(config)
else:
print(color('bold_white', 'Successful config (partial)'))
print(color("bold_white", "Successful config (partial)"))
for domain in domain_info:
if domain == ERROR_STR:
continue
print(' ', color(C_HEAD, domain + ':'))
dump_dict(res['components'].get(domain, None))
print(" ", color(C_HEAD, domain + ":"))
dump_dict(res["components"].get(domain, None))
if args.secrets:
flatsecret = {} # type: Dict[str, str]
for sfn, sdict in res['secret_cache'].items():
for sfn, sdict in res["secret_cache"].items():
sss = []
for skey in sdict:
if skey in flatsecret:
_LOGGER.error('Duplicated secrets in files %s and %s',
flatsecret[skey], sfn)
_LOGGER.error(
"Duplicated secrets in files %s and %s", flatsecret[skey], sfn
)
flatsecret[skey] = sfn
sss.append(color('green', skey) if skey in res['secrets']
else skey)
print(color(C_HEAD, 'Secrets from', sfn + ':'), ', '.join(sss))
sss.append(color("green", skey) if skey in res["secrets"] else skey)
print(color(C_HEAD, "Secrets from", sfn + ":"), ", ".join(sss))
print(color(C_HEAD, 'Used Secrets:'))
for skey, sval in res['secrets'].items():
print(color(C_HEAD, "Used Secrets:"))
for skey, sval in res["secrets"].items():
if sval is None:
print(' -', skey + ':', color('red', "not found"))
print(" -", skey + ":", color("red", "not found"))
continue
print(' -', skey + ':', sval, color('cyan', '[from:', flatsecret
.get(skey, 'keyring') + ']'))
print(
" -",
skey + ":",
sval,
color("cyan", "[from:", flatsecret.get(skey, "keyring") + "]"),
)
return len(res['except'])
return len(res["except"])
def check(config_dir, secrets=False):
"""Perform a check by mocking hass load functions."""
logging.getLogger('homeassistant.loader').setLevel(logging.CRITICAL)
logging.getLogger("homeassistant.loader").setLevel(logging.CRITICAL)
res = {
'yaml_files': OrderedDict(), # yaml_files loaded
'secrets': OrderedDict(), # secret cache and secrets loaded
'except': OrderedDict(), # exceptions raised (with config)
"yaml_files": OrderedDict(), # yaml_files loaded
"secrets": OrderedDict(), # secret cache and secrets loaded
"except": OrderedDict(), # exceptions raised (with config)
#'components' is a HomeAssistantConfig # noqa: E265
'secret_cache': None,
"secret_cache": None,
} # type: Dict[str, Any]
# pylint: disable=possibly-unused-variable
def mock_load(filename):
"""Mock hass.util.load_yaml to save config file names."""
res['yaml_files'][filename] = True
return MOCKS['load'][1](filename)
res["yaml_files"][filename] = True
return MOCKS["load"][1](filename)
# pylint: disable=possibly-unused-variable
def mock_secrets(ldr, node):
"""Mock _get_secrets."""
try:
val = MOCKS['secrets'][1](ldr, node)
val = MOCKS["secrets"][1](ldr, node)
except HomeAssistantError:
val = None
res['secrets'][node.value] = val
res["secrets"][node.value] = val
return val
# Patches to skip functions
@ -181,11 +183,11 @@ def check(config_dir, secrets=False):
# Patches with local mock functions
for key, val in MOCKS.items():
if not secrets and key == 'secrets':
if not secrets and key == "secrets":
continue
# The * in the key is removed to find the mock_function (side_effect)
# This allows us to use one side_effect to patch multiple locations
mock_function = locals()['mock_' + key.replace('*', '')]
mock_function = locals()["mock_" + key.replace("*", "")]
PATCHES[key] = patch(val[0], side_effect=mock_function)
# Start all patches
@ -194,26 +196,26 @@ def check(config_dir, secrets=False):
if secrets:
# Ensure !secrets point to the patched function
yaml_loader.yaml.SafeLoader.add_constructor('!secret',
yaml_loader.secret_yaml)
yaml_loader.yaml.SafeLoader.add_constructor("!secret", yaml_loader.secret_yaml)
try:
hass = core.HomeAssistant()
hass.config.config_dir = config_dir
res['components'] = hass.loop.run_until_complete(
async_check_ha_config_file(hass))
res['secret_cache'] = OrderedDict(yaml_loader.__SECRET_CACHE)
for err in res['components'].errors:
res["components"] = hass.loop.run_until_complete(
async_check_ha_config_file(hass)
)
res["secret_cache"] = OrderedDict(yaml_loader.__SECRET_CACHE)
for err in res["components"].errors:
domain = err.domain or ERROR_STR
res['except'].setdefault(domain, []).append(err.message)
res["except"].setdefault(domain, []).append(err.message)
if err.config:
res['except'].setdefault(domain, []).append(err.config)
res["except"].setdefault(domain, []).append(err.config)
except Exception as err: # pylint: disable=broad-except
_LOGGER.exception("BURB")
print(color('red', 'Fatal error while loading config:'), str(err))
res['except'].setdefault(ERROR_STR, []).append(str(err))
print(color("red", "Fatal error while loading config:"), str(err))
res["except"].setdefault(ERROR_STR, []).append(str(err))
finally:
# Stop all patches
for pat in PATCHES.values():
@ -221,7 +223,8 @@ def check(config_dir, secrets=False):
if secrets:
# Ensure !secrets point to the original function
yaml_loader.yaml.SafeLoader.add_constructor(
'!secret', yaml_loader.secret_yaml)
"!secret", yaml_loader.secret_yaml
)
bootstrap.clear_secret_cache()
return res
@ -229,11 +232,13 @@ def check(config_dir, secrets=False):
def line_info(obj, **kwargs):
"""Display line config source."""
if hasattr(obj, '__config_file__'):
return color('cyan', "[source {}:{}]"
.format(obj.__config_file__, obj.__line__ or '?'),
**kwargs)
return '?'
if hasattr(obj, "__config_file__"):
return color(
"cyan",
"[source {}:{}]".format(obj.__config_file__, obj.__line__ or "?"),
**kwargs,
)
return "?"
def dump_dict(layer, indent_count=3, listi=False, **kwargs):
@ -241,25 +246,26 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs):
A friendly version of print yaml_loader.yaml.dump(config).
"""
def sort_dict_key(val):
"""Return the dict key for sorting."""
key = str(val[0]).lower()
return '0' if key == 'platform' else key
return "0" if key == "platform" else key
indent_str = indent_count * ' '
indent_str = indent_count * " "
if listi or isinstance(layer, list):
indent_str = indent_str[:-1] + '-'
indent_str = indent_str[:-1] + "-"
if isinstance(layer, Dict):
for key, value in sorted(layer.items(), key=sort_dict_key):
if isinstance(value, (dict, list)):
print(indent_str, str(key) + ':', line_info(value, **kwargs))
print(indent_str, str(key) + ":", line_info(value, **kwargs))
dump_dict(value, indent_count + 2)
else:
print(indent_str, str(key) + ':', value)
indent_str = indent_count * ' '
print(indent_str, str(key) + ":", value)
indent_str = indent_count * " "
if isinstance(layer, Sequence):
for i in layer:
if isinstance(i, dict):
dump_dict(i, indent_count + 2, True)
else:
print(' ', indent_str, i)
print(" ", indent_str, i)