summaryrefslogtreecommitdiff
path: root/python/vyos/debug.py
blob: 6ce42b1738ae861afd0f068b60ba9a38d91f0006 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.  If not, see <http://www.gnu.org/licenses/>.

import os
import sys
from datetime import datetime

def message(message, flag='', destination=sys.stdout):
    """
    print a debug message line on stdout if debugging is enabled for the flag
    also log it to a file if the flag 'log' is enabled

    message: the message to print
    flag: which flag must be set for it to print
    destination: which file like object to write to (default: sys.stdout)

    returns if any message was logged or not
    """
    enable = enabled(flag)
    if enable:
        destination.write(_format(flag,message))

    # the log flag is special as it logs all the commands
    # executed to a log
    logfile = _logfile('log', '/tmp/developer-log')
    if not logfile:
        return enable

    try:
        # at boot the file is created as root:vyattacfg
        # at runtime the file is created as user:vyattacfg
        # but the helper scripts are not run as this so it
        # need the default permission to be 666 (an not 660)
        mask = os.umask(0o111)

        with open(logfile, 'a') as f:
            f.write(_timed(_format('log', message)))
    finally:
        os.umask(mask)

    return enable


def enabled(flag):
    """
    a flag can be set by touching the file in /tmp or /config

    The current flags are:
     - developer: the code will drop into PBD on un-handled exception
     - log: the code will log all command to a file
     - ifconfig: when modifying an interface,
       prints command with result and sysfs access on stdout for interface
     - command: print command run with result

    Having the flag setup on the filesystem is required to have
    debuging at boot time, however, setting the flag via environment
    does not require a seek to the filesystem and is more efficient
    it can be done on the shell on via .bashrc for the user

    The function returns an empty string if the flag was not set otherwise
    the function returns either the file or environment name used to set it up
    """

    # this is to force all new flags to be registered here to be
    # documented both here and a reminder to update readthedocs :-)
    if flag not in ['developer', 'log', 'ifconfig', 'command']:
        return ''

    return _fromenv(flag) or _fromfile(flag)


def _timed(message):
    now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    return f'{now} {message}'


def _remove_invisible(string):
    for char in ('\0', '\a', '\b', '\f', '\v'):
        string = string.replace(char, '')
    return string


def _format(flag, message):
    """
    format a log message
    """
    message = _remove_invisible(message)
    return f'DEBUG/{flag.upper():<7} {message}\n'


def _fromenv(flag):
    """
    check if debugging is set for this flag via environment

    For a given debug flag named "test"
    The presence of the environment VYOS_TEST_DEBUG (uppercase) enables it

    return empty string if not
    return content of env value it is
    """

    flagname = f'VYOS_{flag.upper()}_DEBUG'
    flagenv = os.environ.get(flagname, None)

    if flagenv is None:
        return ''
    return flagenv


def _fromfile(flag):
    """
    Check if debug exist for a given debug flag name

    Check is a debug flag was set by the user. the flag can be set either:
     - in /tmp for a non-persistent presence between reboot
     - in /config for always on (an existence at boot time)

    For a given debug flag named "test"
    The presence of the file vyos.test.debug (all lowercase) enables it

    The function returns an empty string if the flag was not set otherwise
    the function returns the full flagname
    """

    for folder in ('/tmp', '/config'):
        flagfile = f'{folder}/vyos.{flag}.debug'
        if os.path.isfile(flagfile):
            return flagfile

    return ''


def _contentenv(flag):
    return os.environ.get(f'VYOS_{flag.upper()}_DEBUG', '').strip()


def _contentfile(flag, default=''):
    """
    Check if debug exist for a given debug flag name

    Check is a debug flag was set by the user. the flag can be set either:
     - in /tmp for a non-persistent presence between reboot
     - in /config for always on (an existence at boot time)

    For a given debug flag named "test"
    The presence of the file vyos.test.debug (all lowercase) enables it

    The function returns an empty string if the flag was not set otherwise
    the function returns the full flagname
    """

    for folder in ('/tmp', '/config'):
        flagfile = f'{folder}/vyos.{flag}.debug'
        if not os.path.isfile(flagfile):
            continue
        with open(flagfile) as f:
            content = f.readline().strip()
        return content or default

    return ''


def _logfile(flag, default):
    """
    return the name of the file to use for logging when the flag 'log' is set
    if it could not be established or the location is invalid it returns
    an empty string
    """

    # For log we return the location of the log file
    log_location = _contentenv(flag) or _contentfile(flag, default)

    # it was not set
    if not log_location:
        return ''

    # Make sure that the logs can only be in /tmp, /var/log, or /tmp
    if not log_location.startswith('/tmp/') and \
       not log_location.startswith('/config/') and \
       not log_location.startswith('/var/log/'):
        return default
    # Do not allow to escape the folders
    if '..' in log_location:
        return default

    if not os.path.exists(log_location):
        return log_location

    # this permission is unique the the config and var folder
    stat = os.stat(log_location).st_mode
    if stat != 0o100666:
        return default
    return log_location