summaryrefslogtreecommitdiff
path: root/src/conf_mode/vpp.py
blob: 7f9adc68bcb1c443c01c5e0c1bb730e89219f789 (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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
#!/usr/bin/env python3
#
# Copyright (C) 2023-2025 VyOS Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

from pathlib import Path

from psutil import virtual_memory
from pyroute2 import IPRoute

from vyos import ConfigError
from vyos import airbag
from vyos.base import Warning
from vyos.config import Config, config_dict_merge
from vyos.configdep import set_dependents, call_dependents
from vyos.configdict import node_changed, leaf_node_changed
from vyos.utils.cpu import get_core_count, get_available_cpus
from vyos.ifconfig import Section
from vyos.template import render
from vyos.utils.boot import boot_configuration_complete
from vyos.utils.process import call
from vyos.utils.system import sysctl_read, sysctl_apply

from vyos.vpp import VPPControl
from vyos.vpp import control_host
from vyos.vpp.config_deps import deps_xconnect_dict
from vyos.vpp.config_verify import verify_dev_driver
from vyos.vpp.config_filter import iface_filter_eth
from vyos.vpp.utils import (
    EthtoolGDrvinfo,
    human_page_memory_to_bytes,
    human_memory_to_bytes,
    bytes_to_human_memory,
)
from vyos.vpp.configdb import JSONStorage

airbag.enable()

service_name = 'vpp'
service_conf = Path(f'/run/vpp/{service_name}.conf')
systemd_override = '/run/systemd/system/vpp.service.d/10-override.conf'

dependency_interface_type_map = {
    'vpp_interfaces_bonding': 'bonding',
    'vpp_interfaces_bridge': 'bridge',
    'vpp_interfaces_ethernet': 'ethernet',
    'vpp_interfaces_geneve': 'geneve',
    'vpp_interfaces_gre': 'gre',
    'vpp_interfaces_ipip': 'ipip',
    'vpp_interfaces_loopback': 'loopback',
    'vpp_interfaces_vxlan': 'vxlan',
    'vpp_interfaces_xconnect': 'xconnect',
}

# dict of drivers that needs to be overrided
override_drivers: dict[str, str] = {
    'hv_netvsc': 'uio_hv_generic',
    'ena': 'vfio-pci',
}

# drivers that does not use PCIe addresses
not_pci_drv: list[str] = ['hv_netvsc']

# drivers that support interrupt RX mode for DPDK and XDP
drivers_support_interrupt: dict[str, list] = {
    'atlantic': ['dpdk', 'xdp'],
    'bnx2x': ['dpdk'],
    'e1000': ['dpdk'],
    'ena': ['dpdk', 'xdp'],
    'i40e': ['dpdk', 'xdp'],
    'ice': ['dpdk', 'xdp'],
    'igb': ['xdp'],
    'igc': ['dpdk', 'xdp'],
    'ixgbe': ['dpdk', 'xdp'],
    'qede': ['dpdk', 'xdp'],
    'vmxnet3': ['xdp'],
    'virtio_net': ['xdp'],
}


def get_config(config=None):
    # use persistent config to store interfaces data between executions
    # this is required because some interfaces after they are connected
    # to VPP is really hard or impossible to restore without knowing
    # their original parameters (like IDs)
    persist_config = JSONStorage('vpp_conf')
    eth_ifaces_persist: dict[str, dict[str, str]] = persist_config.read(
        'eth_ifaces', {}
    )

    if config:
        conf = config
    else:
        conf = Config()

    base = ['vpp']
    base_settings = ['vpp', 'settings']

    # find interfaces removed from VPP
    effective_config = conf.get_config_dict(
        base,
        key_mangling=('-', '_'),
        effective=True,
        get_first_key=True,
        no_tag_node_value_mangle=True,
    )

    xconn_members = deps_xconnect_dict(conf)

    removed_ifaces = []
    tmp = node_changed(conf, base_settings + ['interface'])
    if tmp:
        for removed_iface in tmp:
            to_append = {
                'iface_name': removed_iface,
                'driver': effective_config['settings']['interface'][removed_iface][
                    'driver'
                ],
            }
            removed_ifaces.append(to_append)
            # add an interface to a list of interfaces that need
            # to be reinitialized after the commit
            set_dependents('ethernet', conf, removed_iface)

    # NAT dependency
    if conf.exists(['vpp', 'nat44']):
        set_dependents('vpp_nat', conf)

    if not conf.exists(base):
        return {
            'removed_ifaces': removed_ifaces,
            'xconn_members': xconn_members,
            'persist_config': eth_ifaces_persist,
        }

    config = conf.get_config_dict(
        base,
        get_first_key=True,
        key_mangling=('-', '_'),
        no_tag_node_value_mangle=True,
    )

    # Get default values which we need to conditionally update into the
    # dictionary retrieved.
    default_values = conf.get_config_defaults(**config.kwargs, recursive=True)

    # delete "xdp-options" from defaults if driver is DPDK
    for iface, iface_config in config.get('settings', {}).get('interface', {}).items():
        if iface_config.get('driver') == 'dpdk':
            del default_values['settings']['interface'][iface]['xdp_options']

    config = config_dict_merge(default_values, config)

    # Ignore default XML values if config doesn't exists
    if not conf.exists(base_settings + ['ipsec']):
        del config['settings']['ipsec']

    # add running config
    if effective_config:
        config['effective'] = effective_config

    if 'settings' in config:
        if 'interface' in config['settings']:
            for iface, iface_config in config['settings']['interface'].items():
                # Driver must be configured to continue
                if 'driver' not in iface_config:
                    raise ConfigError(
                        f'"driver" must be configured for {iface} interface!'
                    )

                old_driver = leaf_node_changed(
                    conf, base_settings + ['interface', iface, 'driver']
                )

                # Get current kernel module, required for extra verification and
                # logic for VMBus interfaces
                config['settings']['interface'][iface]['kernel_module'] = (
                    EthtoolGDrvinfo(iface).driver
                )

                # filter unsupported config nodes
                iface_filter_eth(conf, iface)
                set_dependents('ethernet', conf, iface)
                # Interfaces with changed driver should be removed/readded
                if old_driver and old_driver[0] == 'dpdk':
                    removed_ifaces.append(
                        {
                            'iface_name': iface,
                            'driver': 'dpdk',
                        }
                    )

                # Get PCI address or device ID
                if iface_config['driver'] == 'dpdk':
                    if 'dpdk_options' not in iface_config:
                        iface_config['dpdk_options'] = {}
                    # Check in a persistent config first
                    id_from_persisten_conf = eth_ifaces_persist.get(iface, {}).get(
                        'dev_id'
                    )
                    if id_from_persisten_conf:
                        iface_config['dpdk_options']['dev_id'] = id_from_persisten_conf
                    else:
                        try:
                            iface_to_search = iface
                            if old_driver and old_driver[0] == 'xdp':
                                iface_to_search = f'defunct_{iface}'
                            iface_config['dpdk_options']['dev_id'] = (
                                control_host.get_dev_id(iface_to_search)
                            )
                        except Exception:
                            # Return empty address if all attempts failed
                            # We will catch this in verify()
                            iface_config['dpdk_options']['dev_id'] = ''
                # prepare XDP interface parameters
                if iface_config['driver'] == 'xdp':
                    xdp_api_params = {
                        'rxq_size': int(iface_config['xdp_options']['rx_queue_size']),
                        'txq_size': int(iface_config['xdp_options']['tx_queue_size']),
                    }
                    if iface_config['xdp_options']['num_rx_queues'] == 'all':
                        xdp_api_params['rxq_num'] = 0
                    else:
                        xdp_api_params['rxq_num'] = int(
                            iface_config['xdp_options']['num_rx_queues']
                        )
                    if 'zero-copy' in iface_config['xdp_options']:
                        xdp_api_params['mode'] = 'zero-copy'
                    if 'zero-copy' in iface_config['xdp_options']:
                        xdp_api_params['flags'] = 'no_syscall_lock'
                    iface_config['xdp_api_params'] = xdp_api_params

    if removed_ifaces:
        config['removed_ifaces'] = removed_ifaces
        config['xconn_members'] = xconn_members

    # Dependencies
    for dependency, interface_type in dependency_interface_type_map.items():
        # if conf.exists(base + ['interfaces', interface_type]):
        if effective_config.get('interfaces', {}).get(interface_type):
            for iface, iface_config in (
                config.get('interfaces', {}).get(interface_type, {}).items()
            ):
                # filter unsupported config nodes
                if interface_type == 'ethernet':
                    iface_filter_eth(conf, iface)
                set_dependents(dependency, conf, iface)

    # Save important info about all interfaces that cannot be retrieved later
    # Add new interfaces (only if they are first time seen in a config)
    for iface, iface_config in config.get('settings', {}).get('interface', {}).items():
        if iface not in effective_config.get('settings', {}).get('interface', {}):
            eth_ifaces_persist[iface] = {
                'original_driver': config['settings']['interface'][iface][
                    'kernel_module'
                ],
            }
            eth_ifaces_persist[iface]['bus_id'] = control_host.get_bus_name(iface)
            eth_ifaces_persist[iface]['dev_id'] = control_host.get_dev_id(iface)

    # Return to config dictionary
    config['persist_config'] = eth_ifaces_persist

    return config


def verify_memory(settings):
    memory_available: int = virtual_memory().available
    cpus: int = get_core_count()

    nr_hugepages = int(settings['host_resources']['nr_hugepages'])
    hugepages_memory = nr_hugepages * 2 * 1024**2
    memory_required = hugepages_memory

    buffers_per_numa = int(settings.get('buffers', {}).get('buffers_per_numa', 16384))
    data_size = int(settings.get('buffers', {}).get('data_size', 2048))
    buffers_memory = buffers_per_numa * data_size * cpus

    memory_required += buffers_memory

    netlink_buffer_size = int(
        settings.get('lcp', {}).get('netlink', {}).get('rx_buffer_size', 212992)
    )
    memory_required += netlink_buffer_size

    memory_main_heap = human_memory_to_bytes(
        settings.get('memory', {}).get('main_heap_size', '1G')
    )

    if memory_main_heap < 51 << 20:
        # vpp is aborted when we try to use a smaller heap size
        raise ConfigError('The main heap size must be greater than or equal to 51M')

    memory_main_heap_page_size = human_page_memory_to_bytes(
        settings.get('memory', {}).get('main_heap_page_size', 'default')
    )

    if memory_main_heap_page_size > memory_main_heap:
        raise ConfigError(
            f'The main heap size must be greater than or equal to page-size({bytes_to_human_memory(memory_main_heap_page_size, "K")})'
        )

    memory_required += (memory_main_heap + memory_main_heap_page_size - 1) & ~(
        memory_main_heap_page_size - 1
    )

    statseg_size = human_memory_to_bytes(settings.get('statseg', {}).get('size', '96M'))
    memory_required += statseg_size

    if memory_available < memory_required:
        raise ConfigError(
            'Not enough free memory to start VPP:\n'
            f'available: {round(memory_available / 1024 ** 3, 1)}GB\n'
            f'required: {round(memory_required / 1024 ** 3, 1)}GB'
        )


def verify(config):
    # bail out early - looks like removal from running config
    if not config or ('removed_ifaces' in config and 'settings' not in config):
        return None

    if 'settings' not in config:
        raise ConfigError('"settings interface" is required but not set!')

    if 'interface' not in config['settings']:
        raise ConfigError('"settings interface" is required but not set!')

    # check if Ethernet interfaces exist
    ethernet_ifaces = Section.interfaces('ethernet')
    for iface in config['settings']['interface'].keys():
        if iface not in ethernet_ifaces:
            raise ConfigError(f'Interface {iface} does not exist or is not Ethernet!')

    # ensure DPDK/XDP settings are properly configured
    for iface, iface_config in config['settings']['interface'].items():
        # check if selected driver is supported, but only for new interfaces
        if iface not in config.get('effective', {}).get('settings', {}).get(
            'interface', {}
        ):
            if not verify_dev_driver(iface, iface_config['driver']):
                raise ConfigError(
                    f'Driver {iface_config["driver"]} is not compatible with interface {iface}!'
                )
        if iface_config['driver'] == 'xdp' and 'xdp_options' in iface_config:
            if iface_config['xdp_options']['num_rx_queues'] != 'all':
                Warning(f'Not all RX queues will be connected to VPP for {iface}!')

        if iface_config['driver'] == 'xdp' and 'dpdk_options' in iface_config:
            raise ConfigError('DPDK options are not applicable for XDP driver!')

        if iface_config['driver'] == 'dpdk' and 'xdp_options' in iface_config:
            raise ConfigError('XDP options are not applicable for DPDK driver!')

        # RX-mode verification
        rx_mode = iface_config.get('rx_mode')
        if rx_mode and rx_mode != 'polling':
            # By default drivers operate in polling mode. Not all NIC drivers support
            # RX mode interrupt and adaptive
            driver = config.get('persist_config').get(iface).get('original_driver')
            if (
                driver not in drivers_support_interrupt
                or iface_config['driver'] not in drivers_support_interrupt[driver]
            ):
                raise ConfigError(
                    f'RX mode {rx_mode} is not supported for interface {iface}'
                )

    # check GRE tunnels as part of the bridge, only tunnel-type teb is allowed
    #   set vpp interfaces bridge br1 member interface gre1
    #   set vpp interfaces gre gre1 tunnel-type teb
    if 'interfaces' in config:
        if 'bridge' in config['interfaces']:
            for iface, iface_config in config['interfaces']['bridge'].items():
                if 'member' in iface_config:
                    for member in iface_config['member'].get('interface', []):
                        if member.startswith('gre'):
                            if (
                                'gre' in config['interfaces']
                                and config['interfaces']['gre']
                                .get(member, {})
                                .get('tunnel_type')
                                != 'teb'
                            ):
                                raise ConfigError(
                                    f'Only tunnel-type teb is allowed for GRE interfaces in bridge {iface}'
                                )

        # Only one multipoint GRE tunnel is allowed from the same source address
        #   set vpp interfaces gre gre0 mode 'point-to-multipoint'
        #   set vpp interfaces gre gre0 remote '0.0.0.0'
        #   set vpp interfaces gre gre0 source-address '192.0.2.1'
        #   set vpp interfaces gre gre1 mode 'point-to-multipoint'
        #   set vpp interfaces gre gre1 remote '0.0.0.0'
        #   set vpp interfaces gre gre1 source-address '192.0.2.1'
        if 'gre' in config['interfaces']:
            for iface, iface_config in config['interfaces']['gre'].items():
                if iface_config['mode'] == 'point-to-multipoint':
                    for other_iface, other_iface_config in config['interfaces'][
                        'gre'
                    ].items():
                        if (
                            other_iface_config['mode'] == 'point-to-multipoint'
                            and other_iface_config['source_address']
                            == iface_config['source_address']
                            and iface != other_iface
                        ):
                            raise ConfigError(
                                'Only one multipoint GRE tunnel is allowed from the same source address'
                            )

    workers = 0
    if 'cpu' in config['settings']:
        if (
            'corelist_workers' in config['settings']['cpu']
            or 'workers' in config['settings']['cpu']
        ) and 'main_core' not in config['settings']['cpu']:
            raise ConfigError('"cpu main-core" is required but not set!')

        if (
            'corelist_workers' in config['settings']['cpu']
            and 'workers' in config['settings']['cpu']
        ):
            raise ConfigError(
                '"cpu corelist-workers" and "cpu workers" cannot be used at the same time!'
            )

        cpus = int(get_core_count())
        skip_cores = 0

        if 'skip_cores' in config['settings']['cpu']:
            skip_cores = int(config['settings']['cpu']['skip_cores'])
            # the number of skipped cores should not be more than all CPUs - 1 (for main core)
            if skip_cores > cpus - 1:
                raise ConfigError(
                    f'The system does not have enough available CPUs to skip '
                    f'(reduce "cpu skip-cores" to {cpus - 1} or less)'
                )

        if 'workers' in config['settings']['cpu']:
            # number of worker threads must be not more than
            # available CPUs in the system - 1 for main thread - number of skipped cores
            # or - 1 (at least) for system processes
            workers = int(config['settings']['cpu']['workers'])
            available_workers = cpus - 1 - (skip_cores or 1)
            if workers > available_workers:
                raise ConfigError(
                    f'The system does not have enough CPUs for {workers} VPP workers '
                    f'(reduce to {available_workers} or less)'
                )

        cpus_available = list(map(lambda el: el['cpu'], get_available_cpus()))
        # available CPUs are all CPUs without first N skipped cores that will not be used
        cpus_available = cpus_available[skip_cores:]

        if 'main_core' in config['settings']['cpu']:
            main_core = int(config['settings']['cpu']['main_core'])

            if main_core not in cpus_available:
                raise ConfigError(f'"cpu main-core {main_core}" is not available!')

            # CPU main-core must be not included to corelist-workers
            if config.get('settings').get('cpu', {}).get('corelist_workers'):
                corelist_workers = config['settings']['cpu']['corelist_workers']

                all_core_numbers = []
                for worker_range in corelist_workers:
                    core_numbers = worker_range.split('-')
                    if int(core_numbers[0]) > int(core_numbers[-1]):
                        raise ConfigError(
                            f'Range for "cpu corelist-workers {worker_range}" is not correct'
                        )
                    all_core_numbers.extend(
                        range(int(core_numbers[0]), int(core_numbers[-1]) + 1)
                    )

                if main_core in all_core_numbers:
                    raise ConfigError(
                        f'"cpu main-core {main_core}" must not be included in the corelist-workers!'
                    )

                if not all(el in cpus_available for el in all_core_numbers):
                    raise ConfigError('"cpu corelist-workers" is not correct')

                workers = len(all_core_numbers)

    if 'workers' in config['settings']['nat44']:
        nat_workers = []
        for worker_range in config['settings']['nat44']['workers']:
            worker_numbers = worker_range.split('-')
            if int(worker_numbers[0]) > int(worker_numbers[-1]):
                raise ConfigError(
                    f'Range for "nat44 workers {worker_range}" is not correct'
                )
            nat_workers.extend(
                range(int(worker_numbers[0]), int(worker_numbers[-1]) + 1)
            )
        if not all(el in list(range(workers)) for el in nat_workers):
            raise ConfigError('"nat44 workers" is not correct')

    verify_memory(config['settings'])
    if 'host_resources' in config['settings']:
        if (
            'nr_hugepages' in config['settings']['host_resources']
            and 'max_map_count' in config['settings']['host_resources']
        ):
            if int(config['settings']['host_resources']['max_map_count']) < 2 * int(
                config['settings']['host_resources']['nr_hugepages']
            ):
                raise ConfigError(
                    'The max_map_count must be greater than or equal to (2 * nr_hugepages)'
                )

    if 'statseg' in config['settings']:
        _size = human_memory_to_bytes(config['settings']['statseg'].get('size', '96M'))

        if 'size' in config['settings']['statseg']:
            if _size < 1 << 20:
                raise ConfigError(
                    'The statseg size must be greater than or equal to 1M'
                )

        if 'page_size' in config['settings']['statseg']:
            _page_size = human_page_memory_to_bytes(
                config['settings']['statseg']['page_size']
            )
            if _page_size > _size:
                raise ConfigError(
                    f'The statseg size must be greater than or equal to page-size({bytes_to_human_memory(_page_size, "K")})'
                )

    # Check if deleted interfaces are not xconnect memebrs
    for iface_config in config.get('removed_ifaces', []):
        if iface_config['iface_name'] in config.get('xconn_members', {}):
            raise ConfigError(
                f'Interface {iface_config["iface_name"]} is an xconnect member and cannot be removed'
            )


def generate(config):
    if not config or ('removed_ifaces' in config and 'settings' not in config):
        # Remove old config and return
        service_conf.unlink(missing_ok=True)
        return None

    render(service_conf, 'vpp/startup.conf.j2', config['settings'])
    render(systemd_override, 'vpp/override.conf.j2', config)

    # apply sysctl values
    # default: https://github.com/FDio/vpp/blob/v23.10/src/vpp/conf/80-vpp.conf
    sysctl_config: dict[str, str] = {
        'vm.nr_hugepages': config['settings']['host_resources']['nr_hugepages'],
        'vm.max_map_count': config['settings']['host_resources']['max_map_count'],
        'vm.hugetlb_shm_group': '0',
        'kernel.shmmax': config['settings']['host_resources']['shmmax'],
    }
    # we do not want to lower current values
    for sysctl_key, sysctl_value in sysctl_config.items():
        # perform check only for quantitative params
        if sysctl_key == 'vm.hugetlb_shm_group':
            pass
        current_value = sysctl_read(sysctl_key)
        if int(current_value) > int(sysctl_value):
            sysctl_config[sysctl_key] = current_value

    if not sysctl_apply(sysctl_config):
        raise ConfigError('Cannot configure sysctl parameters for VPP')

    return None


def apply(config):
    # Open persistent config
    # It is required for operations with interfaces
    persist_config = JSONStorage('vpp_conf')
    if not config or ('removed_ifaces' in config and 'settings' not in config):
        # Cleanup persistent config
        persist_config.delete()
        # And stop the service
        call(f'systemctl stop {service_name}.service')
    else:
        # Some interfaces required extra preparation before VPP can be started
        if 'settings' in config and 'interface' in config.get('settings'):
            for iface, iface_config in config['settings']['interface'].items():
                if iface_config['driver'] == 'dpdk':
                    # ena interfaces require noiommu mode
                    if iface_config['kernel_module'] == 'ena':
                        control_host.unsafe_noiommu_mode(True)

                    if iface_config['kernel_module'] in override_drivers:
                        control_host.override_driver(
                            config['persist_config'][iface]['bus_id'],
                            config['persist_config'][iface]['dev_id'],
                            override_drivers[iface_config['kernel_module']],
                        )

        call('systemctl daemon-reload')
        call(f'systemctl restart {service_name}.service')

    # Initialize interfaces removed from VPP
    for iface in config.get('removed_ifaces', []):
        # DPDK - rescan PCI to use a proper driver
        if (
            iface['driver'] == 'dpdk'
            and config['persist_config'][iface['iface_name']]['original_driver']
            not in not_pci_drv
        ):
            control_host.pci_rescan(
                config['persist_config'][iface['iface_name']]['dev_id']
            )
            # rename to the proper name
            iface_new_name: str = control_host.get_eth_name(
                config['persist_config'][iface['iface_name']]['dev_id']
            )
            control_host.rename_iface(iface_new_name, iface['iface_name'])
        # XDP - rename an interface , disable promisc and XDP
        if iface['driver'] == 'xdp':
            control_host.set_promisc(f'defunct_{iface["iface_name"]}', 'off')
            control_host.rename_iface(
                f'defunct_{iface["iface_name"]}', iface['iface_name']
            )
            control_host.xdp_remove(iface['iface_name'])
        # Rename Mellanox NIC to a normal name
        try:
            if (
                control_host.get_eth_driver(f'defunct_{iface["iface_name"]}')
                == 'mlx5_core'
            ):
                control_host.rename_iface(
                    f'defunct_{iface["iface_name"]}', iface['iface_name']
                )
        except FileNotFoundError:
            pass
        # Replace a driver with original for VMBus interfaces and rename it
        if (
            iface['driver'] == 'dpdk'
            and config['persist_config'][iface['iface_name']]['original_driver']
            in override_drivers
        ):
            control_host.override_driver(
                config['persist_config'][iface['iface_name']]['bus_id'],
                config['persist_config'][iface['iface_name']]['dev_id'],
            )
            iface_new_name: str = control_host.get_eth_name(
                config['persist_config'][iface['iface_name']]['dev_id']
            )
            control_host.rename_iface(iface_new_name, iface['iface_name'])

        # Remove what is not in the config anymore
        if iface['iface_name'] not in config.get('settings', {}).get('interface', {}):
            del config['persist_config'][iface['iface_name']]

    if 'settings' in config and 'interface' in config.get('settings'):
        # connect to VPP
        # must be performed multiple attempts because API is not available
        # immediately after the service restart
        vpp_control = VPPControl(attempts=20, interval=500)
        # preconfigure LCP plugin
        if 'ignore_kernel_routes' in config.get('settings', {}).get('lcp', {}):
            vpp_control.cli_cmd('lcp param route-no-paths off')
        else:
            vpp_control.cli_cmd('lcp param route-no-paths on')
        # add interfaces
        iproute = IPRoute()
        for iface, iface_config in config['settings']['interface'].items():
            # promisc option for DPDK interfaces
            if iface_config['driver'] == 'dpdk':
                if 'promisc' in iface_config['dpdk_options']:
                    if_index = vpp_control.get_sw_if_index(iface)
                    vpp_control.api.sw_interface_set_promisc(
                        sw_if_index=if_index, promisc_on=True
                    )
            # add XDP interfaces
            if iface_config['driver'] == 'xdp':
                control_host.rename_iface(iface, f'defunct_{iface}')
                vpp_control.xdp_iface_create(
                    host_if=f'defunct_{iface}',
                    name=iface,
                    **iface_config['xdp_api_params'],
                )
                # replicate MAC address of a real interface
                real_mac = control_host.get_eth_mac(f'defunct_{iface}')
                vpp_control.set_iface_mac(iface, real_mac)
                if 'promisc' in iface_config['xdp_options']:
                    control_host.set_promisc(f'defunct_{iface}', 'on')
                control_host.set_status(f'defunct_{iface}', 'up')
            # Rename Mellanox interfaces to hide them and create LCP properly
            if (
                iface in Section.interfaces()
                and control_host.get_eth_driver(iface) == 'mlx5_core'
            ):
                control_host.rename_iface(iface, f'defunct_{iface}')
                control_host.set_status(f'defunct_{iface}', 'up')
            # Create lcp
            if iface not in Section.interfaces():
                vpp_control.lcp_pair_add(iface, iface)

            # For unknown reasons, if multiple interfaces later try to be
            # initialized by configuration scripts, some of them may stuck
            # in an endless UP/DOWN loop
            # We found two workarounds - pause initialization (requires
            # main code modifications).
            # And this one
            dev_index = iproute.link_lookup(ifname=iface)[0]
            iproute.link('set', index=dev_index, state='up')

            # Set rx-mode. Should be configured after interface state set to UP
            rx_mode = iface_config.get('rx_mode')
            if rx_mode:
                # to hardware side
                vpp_control.iface_rxmode(iface, rx_mode)
                # to kernel side
                lcp_name = vpp_control.lcp_pair_find(vpp_name_hw=iface).get(
                    'vpp_name_kernel'
                )
                vpp_control.iface_rxmode(lcp_name, rx_mode)

        # Syncronize routes via LCP
        vpp_control.lcp_resync()

        # NAT44 settings
        nat44_settings = config['settings'].get('nat44', {})

        enable_forwarding = True
        if 'no_forwarding' in nat44_settings:
            enable_forwarding = False
        vpp_control.enable_disable_nat44_forwarding(enable_forwarding)

        vpp_control.set_nat_timeouts(
            icmp=int(nat44_settings.get('timeout').get('icmp')),
            udp=int(nat44_settings.get('timeout').get('udp')),
            tcp_established=int(nat44_settings.get('timeout').get('tcp_established')),
            tcp_transitory=int(nat44_settings.get('timeout').get('tcp_transitory')),
        )

        vpp_control.set_nat44_session_limit(int(nat44_settings.get('session_limit')))

        if nat44_settings.get('workers'):
            bitmask = 0
            for worker_range in nat44_settings['workers']:
                worker_numbers = worker_range.split('-')
                for wid in range(int(worker_numbers[0]), int(worker_numbers[-1]) + 1):
                    bitmask |= 1 << wid
            vpp_control.set_nat_workers(bitmask)

    # Save persistent config
    if 'persist_config' in config and config['persist_config']:
        persist_config.write('eth_ifaces', config['persist_config'])

    # reinitialize interfaces, but not during the first boot
    if boot_configuration_complete():
        call_dependents()


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