xref: /OpenGrok/tools/src/main/python/opengrok_tools/utils/mirror.py (revision cff95066cb05b52120fe4f3cd315b963de174813)
1#!/usr/bin/env python3
2#
3# CDDL HEADER START
4#
5# The contents of this file are subject to the terms of the
6# Common Development and Distribution License (the "License").
7# You may not use this file except in compliance with the License.
8#
9# See LICENSE.txt included in this distribution for the specific
10# language governing permissions and limitations under the License.
11#
12# When distributing Covered Code, include this CDDL HEADER in each
13# file and include the License file at LICENSE.txt.
14# If applicable, add the following below this CDDL HEADER, with the
15# fields enclosed by brackets "[]" replaced with your own identifying
16# information: Portions Copyright [yyyy] [name of copyright owner]
17#
18# CDDL HEADER END
19#
20
21#
22# Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
23#
24
25import re
26import os
27import fnmatch
28import logging
29import urllib
30from tempfile import gettempdir
31
32from requests.exceptions import RequestException
33
34from .exitvals import (
35    FAILURE_EXITVAL,
36    CONTINUE_EXITVAL,
37    SUCCESS_EXITVAL
38)
39from .patterns import PROJECT_SUBST, COMMAND_PROPERTY, CALL_PROPERTY
40from .utils import is_exe, check_create_dir, get_int, get_bool
41from .opengrok import get_repos, get_repo_type, get_uri, delete_project_data
42from .hook import run_hook
43from .command import Command
44from .commandsequence import ApiCall
45from .restful import call_rest_api, do_api_call
46
47from ..scm.repofactory import get_repository
48from ..scm.repository import RepositoryException
49
50
51# "constants"
52HOOK_TIMEOUT_PROPERTY = 'hook_timeout'
53CMD_TIMEOUT_PROPERTY = 'command_timeout'
54IGNORED_REPOS_PROPERTY = 'ignored_repos'
55PROXY_PROPERTY = 'proxy'
56INCOMING_PROPERTY = 'incoming_check'
57IGNORE_ERR_PROPERTY = 'ignore_errors'
58COMMANDS_PROPERTY = 'commands'
59DISABLED_PROPERTY = 'disabled'
60DISABLED_REASON_PROPERTY = 'disabled-reason'
61HOOKDIR_PROPERTY = 'hookdir'
62HOOKS_PROPERTY = 'hooks'
63LOGDIR_PROPERTY = 'logdir'
64PROJECTS_PROPERTY = 'projects'
65DISABLED_CMD_PROPERTY = 'disabled_command'
66HOOK_PRE_PROPERTY = "pre"
67HOOK_POST_PROPERTY = "post"
68STRIP_OUTGOING_PROPERTY = "strip_outgoing"
69
70
71def get_repos_for_project(project_name, uri, source_root,
72                          ignored_repos=None,
73                          commands=None, proxy=None, command_timeout=None,
74                          headers=None, timeout=None):
75    """
76    :param project_name: project name
77    :param uri: web application URI
78    :param source_root source root
79    :param ignored_repos: list of ignored repositories
80    :param commands: dictionary of commands - paths to SCM programs
81    :param proxy: dictionary of proxy servers - to be used as environment
82                  variables
83    :param command_timeout: command timeout value in seconds
84    :param headers: optional HTTP headers dictionary
85    :param timeout: connect timeout for API calls
86    :return: list of Repository objects, the repository matching the project path will be first
87    """
88
89    logger = logging.getLogger(__name__)
90
91    repos = []
92    for repo_path in get_repos(logger, project_name, uri,
93                               headers=headers, timeout=timeout):
94        logger.debug("Repository path = {}".format(repo_path))
95
96        if ignored_repos:
97            r_path = os.path.relpath(repo_path, '/' + project_name)
98            if any(map(lambda repo: fnmatch.fnmatch(r_path, repo),
99                       ignored_repos)):
100                logger.info("repository {} ignored".format(repo_path))
101                continue
102
103        repo_type = get_repo_type(logger, repo_path, uri,
104                                  headers=headers, timeout=timeout)
105        if not repo_type:
106            raise RepositoryException("cannot determine type of repository {}".
107                                      format(repo_path))
108
109        logger.debug("Repository type = {}".format(repo_type))
110
111        repo = None
112        try:
113            # The OpenGrok convention is that the form of repo_path is absolute
114            # so joining the paths would actually spoil things. Hence, be
115            # careful.
116            if repo_path.startswith(os.path.sep):
117                path = source_root + repo_path
118            else:
119                path = os.path.join(source_root, repo_path)
120
121            repo = get_repository(path,
122                                  repo_type,
123                                  project_name,
124                                  env=proxy,
125                                  timeout=command_timeout,
126                                  commands=commands)
127        except (RepositoryException, OSError) as e:
128            logger.error("Cannot get repository for {}: {}".
129                         format(repo_path, e))
130
131        if repo:
132            if repo_path == os.path.sep + project_name:
133                repos.insert(0, repo)
134            else:
135                repos.append(repo)
136
137    return repos
138
139
140def get_project_config(config, project_name):
141    """
142    Return per project configuration, if any.
143    :param config: global configuration
144    :param project_name name of the project
145    :return: project configuration dictionary or None
146    """
147
148    logger = logging.getLogger(__name__)
149
150    project_config = None
151    projects = config.get(PROJECTS_PROPERTY)
152    if projects:
153        project_config = projects.get(project_name)
154        if not project_config:
155            for project_pattern in projects.keys():
156                if re.match(project_pattern, project_name):
157                    logger.debug("Project '{}' matched pattern '{}'".
158                                 format(project_name, project_pattern))
159                    project_config = projects.get(project_pattern)
160                    break
161
162    return project_config
163
164
165def get_project_properties(project_config, project_name, hookdir):
166    """
167    Get properties of project needed to perform mirroring.
168    :param project_config: project configuration dictionary
169    :param project_name: name of the project
170    :param hookdir: directory with hooks
171    :return: list of properties: prehook, posthook, hook_timeout,
172    command_timeout, use_proxy, ignored_repos, check_changes, strip_outgoing, ignore_errors
173    """
174
175    prehook = None
176    posthook = None
177    hook_timeout = None
178    command_timeout = None
179    use_proxy = False
180    ignored_repos = None
181    check_changes = None
182    strip_outgoing = None
183    ignore_errors = None
184
185    logger = logging.getLogger(__name__)
186
187    if project_config:
188        logger.debug("Project '{}' has specific (non-default) config".
189                     format(project_name))
190
191        project_command_timeout = get_int(logger, "command timeout for "
192                                                  "project {}".
193                                          format(project_name),
194                                          project_config.
195                                          get(CMD_TIMEOUT_PROPERTY))
196        if project_command_timeout:
197            command_timeout = project_command_timeout
198            logger.debug("Project command timeout = {}".
199                         format(command_timeout))
200
201        project_hook_timeout = get_int(logger, "hook timeout for "
202                                               "project {}".
203                                       format(project_name),
204                                       project_config.
205                                       get(HOOK_TIMEOUT_PROPERTY))
206        if project_hook_timeout:
207            hook_timeout = project_hook_timeout
208            logger.debug("Project hook timeout = {}".
209                         format(hook_timeout))
210
211        ignored_repos = project_config.get(IGNORED_REPOS_PROPERTY)
212        if ignored_repos:
213            logger.debug("has ignored repositories: {}".
214                         format(ignored_repos))
215
216        hooks = project_config.get(HOOKS_PROPERTY)
217        if hooks:
218            for hookname in hooks:
219                if hookname == HOOK_PRE_PROPERTY:
220                    prehook = os.path.join(hookdir, hooks['pre'])
221                    logger.debug("pre-hook = {}".format(prehook))
222                elif hookname == HOOK_POST_PROPERTY:
223                    posthook = os.path.join(hookdir, hooks['post'])
224                    logger.debug("post-hook = {}".format(posthook))
225
226        if project_config.get(PROXY_PROPERTY):
227            logger.debug("will use proxy")
228            use_proxy = True
229
230        if project_config.get(INCOMING_PROPERTY) is not None:
231            check_changes = get_bool(logger, ("incoming check for project {}".
232                                              format(project_name)),
233                                     project_config.get(INCOMING_PROPERTY))
234            logger.debug("incoming check = {}".format(check_changes))
235
236        if project_config.get(STRIP_OUTGOING_PROPERTY) is not None:
237            strip_outgoing = get_bool(logger, ("outgoing check for project {}".
238                                               format(project_name)),
239                                      project_config.get(STRIP_OUTGOING_PROPERTY))
240            logger.debug("outgoing check = {}".format(check_changes))
241
242        if project_config.get(IGNORE_ERR_PROPERTY) is not None:
243            ignore_errors = get_bool(logger, ("ignore errors for project {}".
244                                              format(project_name)),
245                                     project_config.get(IGNORE_ERR_PROPERTY))
246            logger.debug("ignore errors = {}".format(check_changes))
247
248    if not ignored_repos:
249        ignored_repos = []
250
251    return prehook, posthook, hook_timeout, command_timeout, \
252        use_proxy, ignored_repos, check_changes, strip_outgoing, ignore_errors
253
254
255def process_hook(hook_ident, hook, source_root, project_name, proxy,
256                 hook_timeout):
257    """
258    :param hook_ident: ident of the hook to be used in log entries
259    :param hook: hook
260    :param source_root: source root path
261    :param project_name: project name
262    :param proxy: proxy or None
263    :param hook_timeout: hook run timeout
264    :return: False if hook failed, else True
265    """
266    if hook:
267        logger = logging.getLogger(__name__)
268
269        logger.info("Running {} hook".format(hook_ident))
270        if run_hook(logger, hook,
271                    os.path.join(source_root, project_name), proxy,
272                    hook_timeout) != SUCCESS_EXITVAL:
273            logger.error("{} hook failed for project {}".
274                         format(hook_ident, project_name))
275            return False
276
277    return True
278
279
280def process_changes(repos, project_name, uri, headers=None):
281    """
282    :param repos: repository list
283    :param project_name: project name
284    :param uri: web application URI
285    :param headers: optional dictionary of HTTP headers
286    :return: exit code
287    """
288    logger = logging.getLogger(__name__)
289
290    changes_detected = False
291
292    logger.debug("Checking incoming changes for project {}".format(project_name))
293
294    # check if the project is a new project - full index is necessary
295    try:
296        r = do_api_call('GET', get_uri(uri, 'api', 'v1', 'projects',
297                                       urllib.parse.quote_plus(project_name),
298                                       'property', 'indexed'),
299                        headers=headers)
300        if not bool(r.json()):
301            changes_detected = True
302            logger.info('Project {} has not been indexed yet, '
303                        'overriding incoming check'
304                        .format(project_name))
305    except ValueError as e:
306        logger.error('Unable to parse project \'{}\' indexed flag: {}'
307                     .format(project_name, e))
308        return FAILURE_EXITVAL
309    except RequestException as e:
310        logger.error('Unable to determine project \'{}\' indexed flag: {}'
311                     .format(project_name, e))
312        return FAILURE_EXITVAL
313
314    # check if the project has any new changes in the SCM
315    if not changes_detected:
316        for repo in repos:
317            try:
318                if repo.incoming():
319                    logger.debug('Repository {} has incoming changes'.
320                                 format(repo))
321                    changes_detected = True
322                    break
323
324                if repo.top_level():
325                    logger.debug('Repository {} is top level, finishing incoming check'.
326                                 format(repo))
327                    break
328            except RepositoryException:
329                logger.error('Cannot determine incoming changes for '
330                             'repository {}'.format(repo))
331                return FAILURE_EXITVAL
332
333    if not changes_detected:
334        logger.info('No incoming changes for repositories in '
335                    'project {}'.
336                    format(project_name))
337        return CONTINUE_EXITVAL
338
339    return SUCCESS_EXITVAL
340
341
342def run_command(cmd, project_name):
343    cmd.execute()
344    if cmd.getretcode() != 0:
345        logger = logging.getLogger(__name__)
346
347        logger.error("Command for disabled project '{}' failed "
348                     "with error code {}: {}".
349                     format(project_name, cmd.getretcode(),
350                            cmd.getoutputstr()))
351
352
353def handle_disabled_project(config, project_name, disabled_msg, headers=None,
354                            timeout=None, api_timeout=None):
355
356    disabled_command = config.get(DISABLED_CMD_PROPERTY)
357    if disabled_command:
358        logger = logging.getLogger(__name__)
359
360        if disabled_command.get(CALL_PROPERTY):
361            call = disabled_command.get(CALL_PROPERTY)
362            api_call = ApiCall(call)
363
364            text = None
365            data = api_call.data
366            if type(data) is dict:
367                text = data.get("text")
368
369            # Is this perhaps OpenGrok API call to supply a Message for the UI ?
370            # If so and there was a string supplied, append it to the message text.
371            if text and api_call.uri.find("/api/v1/") > 0 and type(disabled_msg) is str:
372                logger.debug("Appending text to message: {}".
373                             format(disabled_msg))
374                api_call.data["text"] = text + ": " + disabled_msg
375
376            try:
377                call_rest_api(api_call, {PROJECT_SUBST: project_name},
378                              http_headers=headers, timeout=timeout, api_timeout=api_timeout)
379            except RequestException as e:
380                logger.error("API call failed for disabled command of "
381                             "project '{}': {}".
382                             format(project_name, e))
383        elif disabled_command.get(COMMAND_PROPERTY):
384            command_args = disabled_command.get(COMMAND_PROPERTY)
385            args = [project_name]
386            if disabled_msg and type(disabled_msg) is str:
387                args.append(disabled_command)
388
389            cmd = Command(command_args,
390                          env_vars=disabled_command.get("env"),
391                          resource_limits=disabled_command.get("limits"),
392                          args_subst={PROJECT_SUBST: project_name},
393                          args_append=args, excl_subst=True)
394            run_command(cmd, project_name)
395        else:
396            raise Exception(f"unknown disabled action: {disabled_command}")
397
398
399def get_mirror_retcode(ignore_errors, value):
400    if ignore_errors and value != SUCCESS_EXITVAL:
401        logger = logging.getLogger(__name__)
402        logger.info("error code is {} however '{}' property is true, "
403                    "so returning success".format(value, IGNORE_ERR_PROPERTY))
404        return SUCCESS_EXITVAL
405
406    return value
407
408
409def process_outgoing(repos, project_name):
410    """
411    Detect and strip any outgoing changes for the repositories.
412    :param repos: list of repository objects
413    :param project_name: name of the project
414    :return: if any of the repositories had to be reset
415    """
416
417    logger = logging.getLogger(__name__)
418    logger.info("Checking outgoing changes for project {}".format(project_name))
419
420    ret = False
421    for repo in repos:
422        logger.debug("Checking outgoing changes for repository {}".format(repo))
423        if repo.strip_outgoing():
424            logger.debug('Repository {} in project {} had outgoing changes stripped'.
425                         format(repo, project_name))
426            ret = True
427
428        if repo.top_level():
429            logger.debug('Repository {} is top level, finishing outgoing changeset handling'.
430                         format(repo))
431            break
432
433    return ret
434
435
436def mirror_project(config, project_name, check_changes, strip_outgoing, uri,
437                   source_root, headers=None, timeout=None, api_timeout=None):
438    """
439    Mirror the repositories of single project.
440    :param config global configuration dictionary
441    :param project_name: name of the project
442    :param check_changes: check for changes in the project or its repositories
443     and terminate if no change is found
444    :param strip_outgoing: check for outgoing changes in the repositories of the project,
445     strip the changes and wipe project data if such changes were found
446    :param uri web application URI
447    :param source_root source root
448    :param headers: optional dictionary of HTTP headers
449    :param timeout: connect timeout
450    :param api_timeout: optional timeout in seconds for API call response
451    :return exit code
452    """
453
454    ret = SUCCESS_EXITVAL
455
456    logger = logging.getLogger(__name__)
457
458    project_config = get_project_config(config, project_name)
459    prehook, posthook, hook_timeout, command_timeout, use_proxy, \
460        ignored_repos, \
461        check_changes_proj, \
462        strip_outgoing_proj, \
463        ignore_errors_proj = get_project_properties(project_config,
464                                                    project_name,
465                                                    config.
466                                                    get(HOOKDIR_PROPERTY))
467
468    if not command_timeout:
469        command_timeout = config.get(CMD_TIMEOUT_PROPERTY)
470    if not hook_timeout:
471        hook_timeout = config.get(HOOK_TIMEOUT_PROPERTY)
472
473    if check_changes_proj is None:
474        check_changes_config = config.get(INCOMING_PROPERTY)
475    else:
476        check_changes_config = check_changes_proj
477
478    if strip_outgoing_proj is None:
479        strip_outgoing_config = config.get(STRIP_OUTGOING_PROPERTY)
480    else:
481        strip_outgoing_config = strip_outgoing_proj
482
483    if ignore_errors_proj is None:
484        ignore_errors = config.get(IGNORE_ERR_PROPERTY)
485    else:
486        ignore_errors = ignore_errors_proj
487
488    proxy = None
489    if use_proxy:
490        proxy = config.get(PROXY_PROPERTY)
491
492    # We want this to be logged to the log file (if any).
493    if project_config and project_config.get(DISABLED_PROPERTY):
494        handle_disabled_project(config, project_name,
495                                project_config.
496                                get(DISABLED_REASON_PROPERTY),
497                                headers=headers,
498                                timeout=timeout,
499                                api_timeout=api_timeout)
500        logger.info("Project '{}' disabled, exiting".
501                    format(project_name))
502        return CONTINUE_EXITVAL
503
504    #
505    # Cache the repositories first. This way it will be known that
506    # something is not right, avoiding any needless pre-hook run.
507    #
508    repos = get_repos_for_project(project_name,
509                                  uri,
510                                  source_root,
511                                  ignored_repos=ignored_repos,
512                                  commands=config.
513                                  get(COMMANDS_PROPERTY),
514                                  proxy=proxy,
515                                  command_timeout=command_timeout,
516                                  headers=headers,
517                                  timeout=timeout)
518    if not repos:
519        logger.info("No repositories for project {}".
520                    format(project_name))
521        return CONTINUE_EXITVAL
522
523    if check_changes_config is not None:
524        check_changes = check_changes_config
525
526    if strip_outgoing_config is not None:
527        strip_outgoing = strip_outgoing_config
528
529    # Check outgoing changes first. If there are any, such changes will be stripped
530    # and the subsequent incoming check will do the right thing.
531    if strip_outgoing:
532        try:
533            r = process_outgoing(repos, project_name)
534        except RepositoryException as exc:
535            logger.error('Failed to handle outgoing changes for '
536                         'a repository in project {}: {}'.format(project_name, exc))
537            return get_mirror_retcode(ignore_errors, FAILURE_EXITVAL)
538        if r:
539            logger.info("Found outgoing changesets, removing data for project {}".
540                        format(project_name))
541            r = delete_project_data(logger, project_name, uri,
542                                    headers=headers, timeout=timeout, api_timeout=api_timeout)
543            if not r:
544                return get_mirror_retcode(ignore_errors, FAILURE_EXITVAL)
545
546    # Check if the project or any of its repositories have changed.
547    if check_changes:
548        r = process_changes(repos, project_name, uri, headers=headers)
549        if r != SUCCESS_EXITVAL:
550            return get_mirror_retcode(ignore_errors, r)
551
552    if not process_hook(HOOK_PRE_PROPERTY, prehook, source_root, project_name,
553                        proxy, hook_timeout):
554        return get_mirror_retcode(ignore_errors, FAILURE_EXITVAL)
555
556    #
557    # If one of the repositories fails to sync, the whole project sync
558    # is treated as failed, i.e. the program will return FAILURE_EXITVAL.
559    #
560    for repo in repos:
561        logger.info("Synchronizing repository {}".format(repo.path))
562        if repo.sync() != SUCCESS_EXITVAL:
563            logger.error("failed to synchronize repository {}".
564                         format(repo.path))
565            ret = FAILURE_EXITVAL
566
567        if repo.top_level():
568            logger.debug("Repository {} is top level, breaking".format(repo))
569            break
570
571    if not process_hook(HOOK_POST_PROPERTY, posthook, source_root, project_name,
572                        proxy, hook_timeout):
573        return get_mirror_retcode(ignore_errors, FAILURE_EXITVAL)
574
575    return get_mirror_retcode(ignore_errors, ret)
576
577
578def check_project_configuration(multiple_project_config, hookdir=False,
579                                proxy=False):
580    """
581    Check configuration of given project
582    :param multiple_project_config: project configuration dictionary
583    :param hookdir: hook directory
584    :param proxy: proxy setting
585    :return: True if the configuration checks out, False otherwise
586    """
587
588    logger = logging.getLogger(__name__)
589
590    # Quick sanity check.
591    known_project_tunables = [DISABLED_PROPERTY, CMD_TIMEOUT_PROPERTY,
592                              HOOK_TIMEOUT_PROPERTY, PROXY_PROPERTY,
593                              IGNORED_REPOS_PROPERTY, HOOKS_PROPERTY,
594                              DISABLED_REASON_PROPERTY, INCOMING_PROPERTY,
595                              IGNORE_ERR_PROPERTY, STRIP_OUTGOING_PROPERTY]
596
597    if not multiple_project_config:
598        return True
599
600    logger.debug("Checking project configuration")
601
602    for project_name, project_config in multiple_project_config.items():
603        logger.debug("Checking configuration of project {}".
604                     format(project_name))
605
606        if project_config is None:
607            logger.warning("Project {} has empty configuration".
608                           format(project_name))
609            continue
610
611        diff = set(project_config.keys()).difference(known_project_tunables)
612        if diff:
613            logger.error("unknown project configuration option(s) '{}' "
614                         "for project {}".format(diff, project_name))
615            return False
616
617        if project_config.get(PROXY_PROPERTY) and not proxy:
618            logger.error("global proxy setting is needed in order to"
619                         "have per-project proxy")
620            return False
621
622        hooks = project_config.get(HOOKS_PROPERTY)
623        if hooks:
624            if not hookdir:
625                logger.error("Need to have '{}' in the configuration "
626                             "to run hooks".format(HOOKDIR_PROPERTY))
627                return False
628
629            if not os.path.isdir(hookdir):
630                logger.error("Not a directory: {}".format(hookdir))
631                return False
632
633            for hookname in hooks.keys():
634                if hookname not in ["pre", "post"]:
635                    logger.error("Unknown hook name '{}' for project '{}'".
636                                 format(hookname, project_name))
637                    return False
638
639                hookpath = os.path.join(hookdir, hooks.get(hookname))
640                if not is_exe(hookpath):
641                    logger.error("hook file {} for project '{}' does not exist"
642                                 " or not executable".
643                                 format(hookpath, project_name))
644                    return False
645
646        ignored_repos = project_config.get(IGNORED_REPOS_PROPERTY)
647        if ignored_repos:
648            if not isinstance(ignored_repos, list):
649                logger.error("{} for project {} is not a list".
650                             format(IGNORED_REPOS_PROPERTY, project_name))
651                return False
652
653        try:
654            re.compile(project_name)
655        except re.error:
656            logger.error("Not a valid regular expression: {}".
657                         format(project_name))
658            return False
659
660    return True
661
662
663def check_commands(commands):
664    """
665    Validate the commands section of the configuration that allows
666    to override files used for SCM synchronization and incoming check.
667    This should be simple dictionary of string values.
668    :return: True if valid, False otherwise
669    """
670
671    logger = logging.getLogger(__name__)
672
673    if commands is None:
674        return True
675
676    if type(commands) is not dict:
677        logger.error("commands sections is not a dictionary")
678        return False
679
680    for name, value in commands.items():
681        try:
682            repo = get_repository(gettempdir(), name, "dummy_project",
683                                  commands=commands)
684        except RepositoryException as e:
685            logger.error("failed to check repository for '{}': '{}'".
686                         format(name, e))
687            return False
688
689        if repo is None:
690            logger.error("unknown repository type '{}' under '{}': '{}'".
691                         format(name, COMMANDS_PROPERTY, value))
692            return False
693
694        if not repo.check_command():
695            return False
696
697    return True
698
699
700def check_configuration(config):
701    """
702    Validate configuration
703    :param config: global configuration dictionary
704    :return: True if valid, False otherwise
705    """
706
707    logger = logging.getLogger(__name__)
708
709    global_tunables = [HOOKDIR_PROPERTY, PROXY_PROPERTY, LOGDIR_PROPERTY,
710                       COMMANDS_PROPERTY, PROJECTS_PROPERTY,
711                       HOOK_TIMEOUT_PROPERTY, CMD_TIMEOUT_PROPERTY,
712                       DISABLED_CMD_PROPERTY, INCOMING_PROPERTY,
713                       IGNORE_ERR_PROPERTY, STRIP_OUTGOING_PROPERTY]
714
715    diff = set(config.keys()).difference(global_tunables)
716    if diff:
717        logger.error("unknown global configuration option(s): '{}'"
718                     .format(diff))
719        return False
720
721    # Make sure the log directory exists.
722    logdir = config.get(LOGDIR_PROPERTY)
723    if logdir:
724        check_create_dir(logger, logdir)
725
726    disabled_command = config.get(DISABLED_CMD_PROPERTY)
727    if disabled_command:
728        logger.debug("Disabled command: {}".format(disabled_command))
729
730    if not check_commands(config.get(COMMANDS_PROPERTY)):
731        return False
732
733    if not check_project_configuration(config.get(PROJECTS_PROPERTY),
734                                       config.get(HOOKDIR_PROPERTY),
735                                       config.get(PROXY_PROPERTY)):
736        return False
737
738    return True
739