1"""Sanity test for symlinks in the bin directory."""
2from __future__ import (absolute_import, division, print_function)
3__metaclass__ = type
4
5import os
6
7from .. import types as t
8
9from ..sanity import (
10    SanityVersionNeutral,
11    SanityMessage,
12    SanityFailure,
13    SanitySuccess,
14)
15
16from ..config import (
17    SanityConfig,
18)
19
20from ..data import (
21    data_context,
22)
23
24from ..payload import (
25    ANSIBLE_BIN_SYMLINK_MAP,
26    __file__ as symlink_map_full_path,
27)
28
29from ..util import (
30    ANSIBLE_BIN_PATH,
31    ANSIBLE_TEST_DATA_ROOT,
32)
33
34
35class BinSymlinksTest(SanityVersionNeutral):
36    """Sanity test for symlinks in the bin directory."""
37    ansible_only = True
38
39    @property
40    def can_ignore(self):  # type: () -> bool
41        """True if the test supports ignore entries."""
42        return False
43
44    @property
45    def no_targets(self):  # type: () -> bool
46        """True if the test does not use test targets. Mutually exclusive with all_targets."""
47        return True
48
49    # noinspection PyUnusedLocal
50    def test(self, args, targets):  # pylint: disable=locally-disabled, unused-argument
51        """
52        :type args: SanityConfig
53        :type targets: SanityTargets
54        :rtype: TestResult
55        """
56        bin_root = ANSIBLE_BIN_PATH
57        bin_names = os.listdir(bin_root)
58        bin_paths = sorted(os.path.join(bin_root, path) for path in bin_names)
59
60        injector_root = os.path.join(ANSIBLE_TEST_DATA_ROOT, 'injector')
61        injector_names = os.listdir(injector_root)
62
63        errors = []  # type: t.List[t.Tuple[str, str]]
64
65        symlink_map_path = os.path.relpath(symlink_map_full_path, data_context().content.root)
66
67        for bin_path in bin_paths:
68            if not os.path.islink(bin_path):
69                errors.append((bin_path, 'not a symbolic link'))
70                continue
71
72            dest = os.readlink(bin_path)
73
74            if not os.path.exists(bin_path):
75                errors.append((bin_path, 'points to non-existent path "%s"' % dest))
76                continue
77
78            if not os.path.isfile(bin_path):
79                errors.append((bin_path, 'points to non-file "%s"' % dest))
80                continue
81
82            map_dest = ANSIBLE_BIN_SYMLINK_MAP.get(os.path.basename(bin_path))
83
84            if not map_dest:
85                errors.append((bin_path, 'missing from ANSIBLE_BIN_SYMLINK_MAP in file "%s"' % symlink_map_path))
86                continue
87
88            if dest != map_dest:
89                errors.append((bin_path, 'points to "%s" instead of "%s" from ANSIBLE_BIN_SYMLINK_MAP in file "%s"' % (dest, map_dest, symlink_map_path)))
90                continue
91
92            if not os.access(bin_path, os.X_OK):
93                errors.append((bin_path, 'points to non-executable file "%s"' % dest))
94                continue
95
96        for bin_name, dest in ANSIBLE_BIN_SYMLINK_MAP.items():
97            if bin_name not in bin_names:
98                bin_path = os.path.join(bin_root, bin_name)
99                errors.append((bin_path, 'missing symlink to "%s" defined in ANSIBLE_BIN_SYMLINK_MAP in file "%s"' % (dest, symlink_map_path)))
100
101            if bin_name not in injector_names:
102                injector_path = os.path.join(injector_root, bin_name)
103                errors.append((injector_path, 'missing symlink to "python.py"'))
104
105        messages = [SanityMessage(message=message, path=os.path.relpath(path, data_context().content.root), confidence=100) for path, message in errors]
106
107        if errors:
108            return SanityFailure(self.name, messages=messages)
109
110        return SanitySuccess(self.name)
111