summaryrefslogtreecommitdiff
path: root/src/conf_mode/snmp.py
blob: f4611e15e8e5a3ce8c27baf8ec992b295f1c835c (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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
#!/usr/bin/env python3
#
# Copyright (C) 2018-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os

from sys import exit

from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configverify import verify_vrf
from vyos.snmpv3_hashgen import plaintext_to_md5
from vyos.snmpv3_hashgen import plaintext_to_sha1
from vyos.snmpv3_hashgen import random
from vyos.template import render
from vyos.util import call
from vyos.util import chmod_755
from vyos.util import dict_search
from vyos.validate import is_addr_assigned
from vyos.version import get_version_data
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()

config_file_client  = r'/etc/snmp/snmp.conf'
config_file_daemon  = r'/etc/snmp/snmpd.conf'
config_file_access  = r'/usr/share/snmp/snmpd.conf'
config_file_user    = r'/var/lib/snmp/snmpd.conf'
systemd_override    = r'/run/systemd/system/snmpd.service.d/override.conf'
systemd_service     = 'snmpd.service'

def get_config(config=None):
    if config:
        conf = config
    else:
        conf = Config()
    base = ['service', 'snmp']

    snmp = conf.get_config_dict(base, key_mangling=('-', '_'),
                                get_first_key=True, no_tag_node_value_mangle=True)
    if not conf.exists(base):
        snmp.update({'deleted' : ''})

    if conf.exists(['service', 'lldp', 'snmp', 'enable']):
        snmp.update({'lldp_snmp' : ''})

    if 'deleted' in snmp:
        return snmp

    version_data = get_version_data()
    snmp['version'] = version_data['version']

    # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx'
    snmp['vyos_user'] = 'vyos' + random(8)
    snmp['vyos_user_pass'] = random(16)

    # We have gathered the dict representation of the CLI, but there are default
    # options which we need to update into the dictionary retrived.
    default_values = defaults(base)

    # We can not merge defaults for tagNodes - those need to be blended in
    # per tagNode instance
    if 'listen_address' in default_values:
        del default_values['listen_address']
    if 'community' in default_values:
        del default_values['community']
    if 'trap_target' in default_values:
        del default_values['trap_target']
    if 'v3' in default_values:
        del default_values['v3']
    snmp = dict_merge(default_values, snmp)

    if 'listen_address' in snmp:
        default_values = defaults(base + ['listen-address'])
        for address in snmp['listen_address']:
            snmp['listen_address'][address] = dict_merge(
                default_values, snmp['listen_address'][address])

        # Always listen on localhost if an explicit address has been configured
        # This is a safety measure to not end up with invalid listen addresses
        # that are not configured on this system. See https://vyos.dev/T850
        if '127.0.0.1' not in snmp['listen_address']:
            tmp = {'127.0.0.1': {'port': '161'}}
            snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])

        if '::1' not in snmp['listen_address']:
            tmp = {'::1': {'port': '161'}}
            snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])

    if 'community' in snmp:
        default_values = defaults(base + ['community'])
        if 'network' in default_values:
            # convert multiple default networks to list
            default_values['network'] = default_values['network'].split()
        for community in snmp['community']:
            snmp['community'][community] = dict_merge(
                default_values, snmp['community'][community])

    if 'trap_target' in snmp:
        default_values = defaults(base + ['trap-target'])
        for trap in snmp['trap_target']:
            snmp['trap_target'][trap] = dict_merge(
                default_values, snmp['trap_target'][trap])

    if 'v3' in snmp:
        default_values = defaults(base + ['v3'])
        # tagNodes need to be merged in individually later on
        for tmp in ['user', 'group', 'trap_target']:
            del default_values[tmp]
        snmp['v3'] = dict_merge(default_values, snmp['v3'])

        for user_group in ['user', 'group']:
            if user_group in snmp['v3']:
                default_values = defaults(base + ['v3', user_group])
                for tmp in snmp['v3'][user_group]:
                    snmp['v3'][user_group][tmp] = dict_merge(
                        default_values, snmp['v3'][user_group][tmp])

            if 'trap_target' in snmp['v3']:
                default_values = defaults(base + ['v3', 'trap-target'])
                for trap in snmp['v3']['trap_target']:
                    snmp['v3']['trap_target'][trap] = dict_merge(
                        default_values, snmp['v3']['trap_target'][trap])

    return snmp

def verify(snmp):
    if not snmp:
        return None

    if {'deleted', 'lldp_snmp'} <= set(snmp):
        raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!')

    ### check if the configured script actually exist
    if 'script_extensions' in snmp and 'extension_name' in snmp['script_extensions']:
        for extension, extension_opt in snmp['script_extensions']['extension_name'].items():
            if 'script' not in extension_opt:
                raise ConfigError(f'Script extension "{extension}" requires an actual script to be configured!')

            tmp = extension_opt['script']
            if not os.path.isfile(tmp):
                Warning(f'script "{tmp}" does not exist!')
            else:
                chmod_755(extension_opt['script'])

    if 'listen_address' in snmp:
        for address in snmp['listen_address']:
            # We only wan't to configure addresses that exist on the system.
            # Hint the user if they don't exist
            if 'vrf' in snmp:
                vrf_name = snmp['vrf']
                if not is_addr_assigned(address, vrf_name) and address not in ['::1','127.0.0.1']:
                    raise ConfigError(f'SNMP listen address "{address}" not configured in vrf "{vrf_name}"!')
            elif not is_addr_assigned(address):
                raise ConfigError(f'SNMP listen address "{address}" not configured in default vrf!')

    if 'trap_target' in snmp:
        for trap, trap_config in snmp['trap_target'].items():
            if 'community' not in trap_config:
                raise ConfigError(f'Trap target "{trap}" requires a community to be set!')

    if 'oid_enable' in snmp:
        Warning(f'Custom OIDs are enabled and may lead to system instability and high resource consumption')


    verify_vrf(snmp)

    # bail out early if SNMP v3 is not configured
    if 'v3' not in snmp:
        return None

    if 'user' in snmp['v3']:
        for user, user_config in snmp['v3']['user'].items():
            if 'group' not in user_config:
                raise ConfigError(f'Group membership required for user "{user}"!')

            if 'plaintext_password' not in user_config['auth'] and 'encrypted_password' not in user_config['auth']:
                raise ConfigError(f'Must specify authentication encrypted-password or plaintext-password for user "{user}"!')

            if 'plaintext_password' not in user_config['privacy'] and 'encrypted_password' not in user_config['privacy']:
                raise ConfigError(f'Must specify privacy encrypted-password or plaintext-password for user "{user}"!')

    if 'group' in snmp['v3']:
        for group, group_config in snmp['v3']['group'].items():
            if 'seclevel' not in group_config:
                raise ConfigError(f'Must configure "seclevel" for group "{group}"!')
            if 'view' not in group_config:
                raise ConfigError(f'Must configure "view" for group "{group}"!')

            # Check if 'view' exists
            view = group_config['view']
            if 'view' not in snmp['v3'] or view not in snmp['v3']['view']:
                raise ConfigError(f'You must create view "{view}" first!')

    if 'view' in snmp['v3']:
        for view, view_config in snmp['v3']['view'].items():
            if 'oid' not in view_config:
                raise ConfigError(f'Must configure an "oid" for view "{view}"!')

    if 'trap_target' in snmp['v3']:
        for trap, trap_config in snmp['v3']['trap_target'].items():
            if 'plaintext_password' not in trap_config['auth'] and 'encrypted_password' not in trap_config['auth']:
                raise ConfigError(f'Must specify one of authentication encrypted-password or plaintext-password for trap "{trap}"!')

            if {'plaintext_password', 'encrypted_password'} <= set(trap_config['auth']):
                raise ConfigError(f'Can not specify both authentication encrypted-password and plaintext-password for trap "{trap}"!')

            if 'plaintext_password' not in trap_config['privacy'] and 'encrypted_password' not in trap_config['privacy']:
                raise ConfigError(f'Must specify one of privacy encrypted-password or plaintext-password for trap "{trap}"!')

            if {'plaintext_password', 'encrypted_password'} <= set(trap_config['privacy']):
                raise ConfigError(f'Can not specify both privacy encrypted-password and plaintext-password for trap "{trap}"!')

            if 'type' not in trap_config:
                raise ConfigError('SNMP v3 trap "type" must be specified!')

    return None

def generate(snmp):

    #
    # As we are manipulating the snmpd user database we have to stop it first!
    # This is even save if service is going to be removed
    call(f'systemctl stop {systemd_service}')
    # Clean config files
    config_files = [config_file_client, config_file_daemon,
                    config_file_access, config_file_user, systemd_override]
    for file in config_files:
        if os.path.isfile(file):
            os.unlink(file)

    if not snmp:
        return None

    if 'v3' in snmp:
        # net-snmp is now regenerating the configuration file in the background
        # thus we need to re-open and re-read the file as the content changed.
        # After that we can no read the encrypted password from the config and
        # replace the CLI plaintext password with its encrypted version.
        os.environ['vyos_libexec_dir'] = '/usr/libexec/vyos'

        if 'user' in snmp['v3']:
            for user, user_config in snmp['v3']['user'].items():
                if dict_search('auth.type', user_config)  == 'sha':
                    hash = plaintext_to_sha1
                else:
                    hash = plaintext_to_md5

                if dict_search('auth.plaintext_password', user_config) is not None:
                    tmp = hash(dict_search('auth.plaintext_password', user_config),
                        dict_search('v3.engineid', snmp))

                    snmp['v3']['user'][user]['auth']['encrypted_password'] = tmp
                    del snmp['v3']['user'][user]['auth']['plaintext_password']

                    call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" auth encrypted-password "{tmp}" > /dev/null')
                    call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" auth plaintext-password > /dev/null')

                if dict_search('privacy.plaintext_password', user_config) is not None:
                    tmp = hash(dict_search('privacy.plaintext_password', user_config),
                        dict_search('v3.engineid', snmp))

                    snmp['v3']['user'][user]['privacy']['encrypted_password'] = tmp
                    del snmp['v3']['user'][user]['privacy']['plaintext_password']

                    call(f'/opt/vyatta/sbin/my_set service snmp v3 user "{user}" privacy encrypted-password "{tmp}" > /dev/null')
                    call(f'/opt/vyatta/sbin/my_delete service snmp v3 user "{user}" privacy plaintext-password > /dev/null')

    # Write client config file
    render(config_file_client, 'snmp/etc.snmp.conf.j2', snmp)
    # Write server config file
    render(config_file_daemon, 'snmp/etc.snmpd.conf.j2', snmp)
    # Write access rights config file
    render(config_file_access, 'snmp/usr.snmpd.conf.j2', snmp)
    # Write access rights config file
    render(config_file_user, 'snmp/var.snmpd.conf.j2', snmp)
    # Write daemon configuration file
    render(systemd_override, 'snmp/override.conf.j2', snmp)

    return None

def apply(snmp):
    # Always reload systemd manager configuration
    call('systemctl daemon-reload')

    if not snmp:
        return None

    # start SNMP daemon
    call(f'systemctl restart {systemd_service}')

    # Enable AgentX in FRR
    # This should be done for each daemon individually because common command
    # works only if all the daemons started with SNMP support
    frr_daemons_list = [
        'bgpd', 'ospf6d', 'ospfd', 'ripd', 'ripngd', 'isisd', 'ldpd', 'zebra'
    ]
    for frr_daemon in frr_daemons_list:
        call(
            f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null'
        )

    return None

if __name__ == '__main__':
    try:
        c = get_config()
        verify(c)
        generate(c)
        apply(c)
    except ConfigError as e:
        print(e)
        exit(1)