fuss-server 25.6 KB
Newer Older
Elena Grandi's avatar
Elena Grandi committed
1
#!/usr/bin/env python3
2
# -*- python -*-
3
#
4
#  File: fuss-server-config
5
#
Elena Grandi's avatar
Elena Grandi committed
6
7
8
#  Copyright (C) 2007-2016 Christopher R. Gabriel <cgabriel@truelite.it>,
#                          Elena Grandi <elena@truelite.it>,
#                          Progetto Fuss <info@fuss.bz.it>
9
#
10
11
12
13
14
15
16
17
18
19
20
#  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.

from __future__ import division
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals

import argparse
21
import getpass
22
23
import logging
import os
24
import re
25
import shutil
26
import subprocess
27
28
29
30
import sys

from gettext import gettext as _

31
import apt
32
import netaddr
33
import netifaces
34
import ruamel.yaml
35

36
37
38
39
40
41
42
43
44
45
46
47
48
xwin = False
try:
    import gtk
    r = gtk.gdk.display_get_default()
    if r:
        xwin = True
    else:
        xwin = False
    import gnome
    import gnome.ui
except ImportError:
    xwin = False

Elena Grandi's avatar
Elena Grandi committed
49
50
51
52
53
try:
    input = raw_input
except NameError:
    pass

54
ansible_data_path = "/usr/share/fuss-server/"
55
conf_file = '/etc/fuss-server/fuss-server.yaml'
56
clean_config_file = '/usr/share/doc/fuss-server/examples/fuss-server.yaml.example'
57

58
59
60
61
try:
    VERSION = apt.cache.Cache().get('fuss-server').installed.version
except AttributeError:
    VERSION = 'dev'
62
63
64
65
66
67
68
69
70
71
72

class Configuration(object):
    known = {
        "localnet": [
            _("Local network address"),
            _("The format is netaddr/cidr, ex. 192.168.1.0/24")
            ],
        "domain": [
            _("Domain name"),
            _("The domain for this network, ex. 'institute.lan'")
            ],
73
        "pass": [
74
75
76
77
78
            _("Master password"),
            _("The master password for this server")
            ],
        "geoplace": [
            _("Locality"),
79
            _("Locality e/o address name, ex. 'Bolzano'")
80
81
82
83
84
85
86
87
88
89
90
            ],
        "workgroup": [
            _("Windows Workgroup"),
            _("The Windows WorkGroup for this network, ex. 'institute'")
            ],
        "dhcp_range": [
            _("DHCP Server Range"),
            _("The IP range of address given by the DHCP Server, ex. '192.168.1.10 192.168.1.100'")
            ],
        "external_ifaces": [
            _("WAN Interface"),
91
            _("The WAN interface(s) of the server, ex. 'eth0'")
92
93
94
            ],
        "internal_ifaces": [
            _("LAN Interfaces"),
95
            _("The LAN interface(s) of the server, ex. 'eth1 eth2'")
96
            ],
97
98
99
100
101
102
103
104
        "hotspot_iface": [
            _("Hot Spot Interface"),
            _("The Hotspot interface of the server, ex. 'eth3'")
            ],
        "hotspot_network": [
            _("Hot Spot Network (CIDR)"),
            _("The Hotspot network of the server, ex. '10.1.0.0/24'")
            ],
105
106
        }

107
    def __init__(self, c_file=conf_file, reconf_all=False, cp_mandatory=False):
108
109
        self.c_file = c_file
        self.reconf_all = reconf_all
110
        self.cp_mandatory = cp_mandatory
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

    def check(self):
        """
        Check the current configuration.

        Return a list of entries that are missing or problematic
        """
        if not os.path.isfile(self.c_file):
            logging.error("Can't find configuration file - exiting")
            # TODO: this should probably raise an exception
            sys.exit(3)
        found_keys = []
        missing_keys = []
        for key, value in self.data.items():
            found_keys.append(key)
            # If we don't have a method to check for the validity of
            # data, it means that every non-empty value is valid.
            method = getattr(self, '_check_{}'.format(key), lambda x: bool(x))
            if self.reconf_all or not method(value):
                missing_keys.append(key)
        for key in self.known:
            if key not in found_keys:
                missing_keys.append(key)
134
135
136
137
        if 'localnet' not in missing_keys:
            for key in self._crosscheck_network():
                if key not in missing_keys:
                    missing_keys.append(key)
138
139
140
        for key in self._crosscheck_hotspot():
            if key not in missing_keys:
                missing_keys.append(key)
141
142
        return missing_keys

143
144
145
146
147
148
149
150
151
152
    def _crosscheck_network(self):
        wrong = []
        all_networks = {
            netaddr.IPNetwork(x['addr']+'/'+x['netmask']): iface
            for iface in netifaces.interfaces()
            for x in netifaces.ifaddresses(iface).get(
                netifaces.AF_INET,
                [])
            }
        localnet = netaddr.IPNetwork(self.data['localnet'])
153
154
155
        if not self.data['internal_ifaces']:
            logging.warning(_("No internal interfaces are configured"))
            return ['internal_ifaces']
156
157
        try:
            if all_networks[localnet] not in self.data['internal_ifaces']:
158
                logging.warning(_(
159
160
161
                    "The value for local network {localnet} is not " +
                    "configured on any local interface ({ifaces})"
                    ).format(
162
                        localnet=str(localnet),
163
164
165
166
167
                        ifaces=str(self.data['internal_ifaces'])
                        )
                    )
                wrong = ['localnet', 'internal_ifaces']
        except KeyError:
168
            logging.warning("No interface found for localnet {}".format(
169
170
171
                self.data['localnet']
                ))
            wrong = ['localnet']
172
        for ip in self.data['dhcp_range'].split():
173
174
175
176
177
            try:
                range_addrs = netaddr.IPAddress(ip)
            except ValueError:
                wrong.append('dhcp_range')
                break
178
179
180
            if range_addrs not in localnet:
                wrong.append('dhcp_range')
                break
181
182
        return wrong

183
184
185
186
187
188
189
    def _crosscheck_hotspot(self):
        # Either both hotspot values should be filled or none (and if
        # none, we're done with the crosscheck.
        fields = ('hotspot_iface', 'hotspot_network')
        filled = [bool(self.data[f]) for f in fields]
        if not all(filled):
            if any(filled):
Elena Grandi's avatar
cleanup    
Elena Grandi committed
190
                return [f for i, f in enumerate(fields) if not filled[i]]
191
192
193
194
195
196
197
198
199
200
201
202
203
            else:
                return []

        if self.data['hotspot_iface'] in self.data['internal_ifaces']:
            logging.warning(_("Hot spot interface cannot be the same as a LAN interface"))
            return ['hotspot_iface']
        if self.data['hotspot_iface'] in self.data['external_ifaces']:
            logging.warning(_("Hot spot interface cannot be the same as a WAN interface"))
            return ['hotspot_iface']
        if 'tun' in self.data['hotspot_iface']:
            logging.warning(_("Hot spot interface cannot be a tunnel interface"))
            return ['hotspot_iface']

204
205
        ip_route = subprocess.check_output(['ip', 'route'])
        hotspot_net = netaddr.IPNetwork(self.data['hotspot_network'])
206
207
        hs_net_s = str(hotspot_net.network).encode('utf-8')

Elena Grandi's avatar
Elena Grandi committed
208
        for line in ip_route.split(b'\n'):
209
            if line.strip().startswith(hs_net_s) and b'tun' not in line:
210
211
212
213
214
                logging.warning((
                    "Network {} already used\n" +
                    "Please choose another one"
                    ).format(str(hotspot_net.network)))
                return ['hotspot_network']
215
216

        return []
217

218
219
220
221
222
223
224
225
226
227
228
229
    def _check_external_ifaces(self, value):
        if not isinstance(value, list):
            return False
        for iface in value:
            if iface not in netifaces.interfaces():
                logging.warning("Interface {} is not available".format(
                    iface))
                return False
        return True

    _check_internal_ifaces = _check_external_ifaces

230
231
232
233
234
235
236
237
238
239
240
241
242
243
    def _check_localnet(self, value):
        """
        Localnet should be a valid address in CIDR format
        """
        if not value:
            return False
        if not len(value.split('/')) == 2:
            return False
        try:
            netaddr.IPNetwork(value)
        except netaddr.AddrFormatError:
            return False
        return True

244
245
246
247
248
249
250
251
252
253
254
255
    def _check_dhcp_range(self, value):
        """
        dhcp_range should be made of valid ips
        """
        if not value:
            return False
        ips = value.split(' ')
        if len(ips) != 2:
            return False
        for ip in ips:
            try:
                netaddr.IPAddress(value)
256
            except (netaddr.AddrFormatError, ValueError):
257
258
259
                return False
        return True

260
261
    def _check_pass(self, value):
        """
262
263
        pass should no contain any of &, \, /, $ chars, nor be composed
        of just numbers
264
265
266
        """
        if not value:
            return False
267
268
269
270
271
272
273
274
275
        try:
            int(value)
        except ValueError:
            # if we can't get a number out of the password everything is
            # fine
            pass
        else:
            logging.warning("password must not be composed by just numbers")
            return False
276
277
278
279
280
281
282
283
        # add more forbidden char if neeeded
        forbiddenchars = set('$\/&')
        if any((c in forbiddenchars) for c in value):
            logging.warning("password must not contain &, \\, /, or $")
            return False
        else:
            return True

284
285
286
287
    def _check_domain(self, value):
        """
        domain should be made up of two alphanumeric names separated by
        one dot.
Elena Grandi's avatar
Elena Grandi committed
288
        The TLD .local isn't allowed, because it's reserved to mDNS
289
290
291
292
293
294
        """
        if not value:
            return False
        allowed = re.compile("^[\w]+\.[\w]+$")
        if not allowed.match(value):
            return False
Elena Grandi's avatar
Elena Grandi committed
295
296
297
        if value.endswith('.local'):
            logging.warning(".local domains are not allowed")
            return False
298
299
300
301
302
303
304
305
306
307
        return True

    def _check_workgroup(self, value):
        """
        workgroup should be made of alphanumeric
        """
        if not value:
            return False
        allowed = re.compile("^[\w]+$")
        if not allowed.match(value):
308
            logging.warning("Domain must contains only alphanumeric")
309
310
311
            return False
        return True

312
313
    def _check_hotspot_iface(self, value):
        if not value:
314
315
316
317
318
            if self.cp_mandatory:
                return False
            else:
                # empty values are allowed, in case no hotspot is present
                return True
319
320
        if value not in netifaces.interfaces():
            logging.warning("Interface {} is not available".format(
321
                value))
322
323
324
325
326
327
328
329
            return False
        return True

    def _check_hotspot_network(self, value):
        """
        Hotspot network should be a valid address in CIDR format
        """
        if not value:
330
331
332
333
334
            if self.cp_mandatory:
                return False
            else:
                # empty values are allowed, in case no hotspot is present
                return True
335
336
337
338
339
340
341
342
        if not len(value.split('/')) == 2:
            return False
        try:
            netaddr.IPNetwork(value)
        except netaddr.AddrFormatError:
            return False
        return True

343
    def load(self, bootstrap=False):
344
345
346
        """
        Load configuration data from a file.
        """
347
        if bootstrap or not os.path.exists(self.c_file):
348
            logging.info("Creating a new configuration file with empty values")
349
350
351
            confdir = os.path.dirname(os.path.realpath(self.c_file))
            if not os.path.isdir(confdir):
                os.makedirs(confdir)
352
            shutil.copyfile(clean_config_file, os.path.realpath(self.c_file))
353
354
        with open(self.c_file) as fp:
            self.data = ruamel.yaml.load(fp, ruamel.yaml.RoundTripLoader)
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
        if not self.data:
            logging.error(
                "The configuration file seems to be empty.\n" +
                "Please delete it to restart from a new valid one."
                )
            # TODO: this should probably raise an exception
            sys.exit(3)
        invalid = False
        for k in self.known:
            if k not in self.data:
                logging.error(
                    "Missing value in the configuration file: {}".format(k)
                    )
                invalid = True
        if invalid:
            logging.error(
                "Please add the missing values to the configuration file\n" +
                "or delete it to start from a clean one."
                )
            # TODO: this should probably raise an exception
            sys.exit(3)
376
377
378
379
380
381
382
383
384
385
386
387
388

    def save(self):
        """
        Save configuration data to file, setting safe permissions.
        """
        os.chmod(self.c_file, 0o640)
        os.umask(0o27)
        with open(self.c_file, "w") as fp:
            ruamel.yaml.dump(
                self.data,
                stream=fp,
                Dumper=ruamel.yaml.RoundTripDumper
                )
389
        os.umask(0o22)
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
    def ask(self, missing_conf):
        logging.info("Asking for configuration")
        if xwin:
            entries = {}

            def build_druid_page(key, question, help, default=""):
                page = gnome.ui.DruidPageStandard()
                page.set_title(question)
                v = gtk.VBox()
                page.append_item(help, v, '')

                h = gtk.HBox()
                h.pack_start(gtk.Label(_("Please enter you choice")))
                entry = gtk.Entry()
                if 'assword' in question:
                    entry.set_visibility(False)
                entry.set_text(default)
                entries[key] = entry
                h.pack_start(entry)
                v.pack_start(h)
                page.show_all()
                return page

            def completed(widget, pars):
                for i in entries.keys():
416
                    if "ifaces" in i:
417
418
419
420
421
                        self.data[i] = entries[i].get_text().split()
                    else:
                        self.data[i] = ruamel.yaml.safe_load(
                            entries[i].get_text()
                            )
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
                self.save()
                gtk.main_quit()

            if len(missing_conf) > 0:
                w = gtk.Window()
                w.set_default_size(500, 500)
                w.set_title(_("Fuss Server Configuration"))
                w.connect("delete_event", gtk.main_quit)

                druid = gnome.ui.Druid()
                druid.connect("cancel", gtk.main_quit)
                w.add(druid)
                start_page = gnome.ui.DruidPageEdge(0)
                start_page.set_title(_("Fuss Server Configuration"))
                start_page.set_text(_("Welcome to the Fuss Server configuration"))
                druid.add(start_page)
                for i in missing_conf:
439
                    if 'ifaces' in i and self.data[i]:
440
441
442
443
                        current = " ".join((str(x) for x in self.data[i]))
                    else:
                        current = str(self.data[i]) or ''

444
445
446
447
                    druid.add(build_druid_page(
                        i,
                        self.known[i][0],
                        self.known[i][1],
448
449
                        current
                        ))
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

                end_page = gnome.ui.DruidPageEdge(1)
                end_page.set_title(_("Fuss Server Configuration"))
                end_page.set_text(_("All done! Thank you!"))
                end_page.connect("finish", completed)
                druid.add(end_page)

                w.show_all()
            else:
                d = gtk.MessageDialog(
                    parent=None,
                    flags=gtk.DIALOG_MODAL,
                    type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK
                    )
                d.set_markup(_("Looks like you've already configured this Fuss Server.\n\nUse the '-r' option to reconfigure it all"))
                d.show_all()
                d.run()
                d.destroy()
                sys.exit(8)
            gtk.main()
        else:
            if len(missing_conf) > 0:
                for i in missing_conf:
                    print("#"*80)
                    print(_("Please insert"), self.known[i][0])
                    print("")
                    print(self.known[i][1])
477
                    if 'ifaces' in i and self.data[i]:
478
479
480
481
482
                        current = " ".join((str(x) for x in self.data[i]))
                    else:
                        current = str(self.data[i]) or ''
                    if current:
                        print(_("Current value")+": ", current)
483
484
485
                        print("")
                    if "assword" in self.known[i][0]:
                        self.data[i] = ruamel.yaml.safe_load(getpass.getpass())
486
                    elif "ifaces" in i:
487
                        self.data[i] = input(_("Your choice? ")).split()
488
489
                    else:
                        self.data[i] = ruamel.yaml.safe_load(
Elena Grandi's avatar
Elena Grandi committed
490
                            input(_("Your choice? "))
491
492
493
494
495
496
497
498
499
500
501
502
503
                            )
                self.save()
            else:
                print(_("Looks like you've already configured this Fuss Server."))
                print("")
                print(_("Use the '-r' option to reconfigure it all"))
                sys.exit(8)


def fail_if_not_root():
    if os.getuid() > 0:
        logging.error("Can't execute fuss-server - Are you root?")
        sys.exit(5)
504
505


506
507
def _config(c, bootstrap=False):
    c.load(bootstrap)
508
    res = c.check()
Elena Grandi's avatar
Elena Grandi committed
509
510
511
    # in any case, only reconfigure everything once, then ask just the
    # missing bits
    c.reconf_all = False
512
513
514
515
516
517
518
    while len(res) > 0:
        c.ask(res)
        res = c.check()


def configure(args):
    logging.info("Asking for missing configuration")
Elena Grandi's avatar
typo    
Elena Grandi committed
519
    if args.configuration_file == conf_file:
Elena Grandi's avatar
Elena Grandi committed
520
521
522
523
524
525
526
527
        # Usually we can't work except as root, but when working on a
        # different configuration file it is convenient to allow to
        # check and set the configuration as a normal user.
        fail_if_not_root()
    c = Configuration(
        reconf_all=args.reconfigure_all,
        c_file=args.configuration_file
        )
528
    _config(c, bootstrap=args.bootstrap)
529

530

531
def create(args):
532
    logging.info("Applying configuration")
533
    fail_if_not_root()
534
535
    c = Configuration()
    _config(c)
536
    os.chdir(ansible_data_path)
Elena Grandi's avatar
Elena Grandi committed
537
    os.execvp(os.path.join(ansible_data_path, 'create.yml'), [
538
539
540
        'fuss-server',
        '-i', 'localhost,',
        '-c', 'local',
541
        '--force-handlers',
542
        '-e', 'fuss_server_version={}'.format(VERSION),
543
        ])
544

545

546
547
548
549
550
551
552
553
554
555
def upgrade(args):
    logging.info("Upgrading configuration")
    fail_if_not_root()
    c = Configuration()
    _config(c)
    os.chdir(ansible_data_path)
    os.execvp(os.path.join(ansible_data_path, 'upgrade.yml'), [
        'fuss-server',
        '-i', 'localhost,',
        '-c', 'local',
556
        '--force-handlers',
557
        '-e', 'fuss_server_version={}'.format(VERSION),
558
559
560
        ])


561
562
def purge(args):
    logging.info("Purging")
563
564
565
566
567
568
569
570
    fail_if_not_root()
    c = Configuration()
    _config(c)
    os.chdir(ansible_data_path)
    os.execvp(os.path.join(ansible_data_path, 'purge.yml'), [
        'fuss-server',
        '-i', 'localhost,',
        '-c', 'local',
571
        '--force-handlers',
572
        ])
573

574

575
576
577
def captive_portal(args):
    logging.info("Applying configuration for a captive portal")
    fail_if_not_root()
578
    c = Configuration(cp_mandatory=True)
579
580
581
582
583
584
    _config(c)
    os.chdir(ansible_data_path)
    os.execvp(os.path.join(ansible_data_path, 'captive_portal.yml'), [
        'fuss-server',
        '-i', 'localhost,',
        '-c', 'local',
585
        '--force-handlers',
586
        '-e', 'fuss_server_version={}'.format(VERSION),
587
588
589
        ])


590
def test(args):
591
592
593
594
595
596
597
598
599
    logging.info("Testing the server")
    fail_if_not_root()
    os.chdir(ansible_data_path)
    os.execvp(os.path.join(ansible_data_path, 'test.sh'), [
        'fuss-server',
        ])


def self_test(args):
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
    import unittest

    class TestCheck(unittest.TestCase):
        def setUp(self):
            self.c = Configuration()

        def test_localnet(self):
            self.assertTrue(self.c._check_localnet('192.168.5.23/24'))
            self.assertFalse(self.c._check_localnet(''))
            self.assertFalse(
                self.c._check_localnet('192.168.5.23 255.255.255.0')
                )

        def test_dhcp_range(self):
            self.assertTrue(
                self.c._check_dhcp_range('192.168.5.23 192.168.5.42')
                )
            self.assertFalse(
                self.c._check_dhcp_range('192.168.5.23')
                )
620
621
622
            self.assertFalse(
                self.c._check_dhcp_range('192.168.5.0/24')
                )
623
624
625
626
627
628
629
630

        def test_check_domain(self):
            self.assertTrue(
                self.c._check_domain('scuola.lan')
                )
            self.assertFalse(
                self.c._check_domain('this.is.not.valid')
                )
Elena Grandi's avatar
Elena Grandi committed
631
632
633
634
635
636
            self.assertFalse(
                self.c._check_domain('scuola.local')
                )
            self.assertTrue(
                self.c._check_domain('local.lan')
                )
637
638
639
640
641
642
643
644
645

        def test_check_workgroup(self):
            self.assertTrue(
                self.c._check_workgroup('workgroup')
                )
            self.assertFalse(
                self.c._check_workgroup('scuola.lan')
                )

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
        def test_crosscheck_hotspot(self):
            self.c.data = {
                'external_ifaces': ['eth0'],
                'internal_ifaces': ['eth1', 'eth2'],
                'hotspot_iface': 'eth3',
                'hotspot_network': '192.168.5.0/24',
                }

            # All valid values
            self.assertEqual(self.c._crosscheck_hotspot(), [])

            # both hotspot variables empty: valid
            self.c.data['hotspot_iface'] = ''
            self.c.data['hotspot_network'] = ''
            self.assertEqual(self.c._crosscheck_hotspot(), [])

            # only one hotspot variabile empty: invalid
            self.c.data['hotspot_iface'] = 'eth3'
            self.c.data['hotspot_network'] = ''
            self.assertEqual(self.c._crosscheck_hotspot(), ['hotspot_network'])

            self.c.data['hotspot_iface'] = ''
            self.c.data['hotspot_network'] = '192.168.5.0/24'
            self.assertEqual(self.c._crosscheck_hotspot(), ['hotspot_iface'])

            # hotspot interface can't be the same as an internal or
            # external one
            self.c.data['hotspot_iface'] = 'eth0'
            self.assertEqual(self.c._crosscheck_hotspot(), ['hotspot_iface'])
            self.c.data['hotspot_iface'] = 'eth1'
            self.assertEqual(self.c._crosscheck_hotspot(), ['hotspot_iface'])

            # hotspot interface can't be a tun one
            self.c.data['hotspot_iface'] = 'eth1'
            self.assertEqual(self.c._crosscheck_hotspot(), ['hotspot_iface'])

682
683
684
685
686
687
688
689
690
        def test_password(self):
            self.assertTrue(self.c._check_pass('abcdefg'))
            self.assertFalse(self.c._check_pass('abcd&'))
            self.assertFalse(self.c._check_pass('abcd\\'))
            self.assertFalse(self.c._check_pass('abcd/'))
            self.assertFalse(self.c._check_pass('abcd$'))
            self.assertFalse(self.c._check_pass('1234'))
            self.assertFalse(self.c._check_pass(1234))

691
692
693
694
    suite = unittest.TestLoader().loadTestsFromTestCase(TestCheck)
    unittest.TextTestRunner(verbosity=1).run(suite)


695
696
697
698
699
700
701
def main():
    parser = argparse.ArgumentParser(
        description='Configure a FUSS server.'
        )
    parser.set_defaults(func=create)
    subparser = parser.add_subparsers(
        title='subcommands',
Elena Grandi's avatar
Elena Grandi committed
702
        description='Run fuss-server <command> -h for help on the subcommands.',
703
        dest='create'  # this is ignored by python 2.7, but works with 3.4+
704
        )
705

706
707
708
709
    create_parser = subparser.add_parser(
        'create',
        help='install dependencies and configuration'
        )
710
711
712
713
    create_parser.add_argument(
        '--limit',
        help="Ignored for compatibility"
        )
714
    create_parser.set_defaults(func=create)
715

716
717
    upgrade_parser = subparser.add_parser(
        'upgrade',
Elena Grandi's avatar
Elena Grandi committed
718
        help='apply a new configuration to an existing fuss-server'
719
720
721
722
723
724
        )
    upgrade_parser.add_argument(
        '--limit',
        help="Ignored for compatibility"
        )
    upgrade_parser.set_defaults(func=upgrade)
725

726
727
728
729
    purge_parser = subparser.add_parser(
        'purge',
        help='clean configuration'
        )
730
731
732
733
    purge_parser.add_argument(
        '--limit',
        help="Ignored for compatibility"
        )
734
    purge_parser.set_defaults(func=purge)
735

736
737
738
739
740
741
742
743
744
    configure_parser = subparser.add_parser(
        'configure',
        help='configure configuration'
        )
    configure_parser.add_argument(
        '-r', '--reconfigure-all',
        action="store_true",
        help="Reconfigure all options"
        )
745
746
747
748
749
    configure_parser.add_argument(
        '-b', '--bootstrap',
        action="store_true",
        help="Delete all current configuration and start with a new empty file"
        )
Elena Grandi's avatar
Elena Grandi committed
750
751
752
753
754
    configure_parser.add_argument(
        '-f', '--configuration-file',
        help="Use a different configuration file (for testing)",
        default=conf_file
        )
755
    configure_parser.set_defaults(func=configure)
756

757
758
    test_parser = subparser.add_parser(
        'test',
759
        help='test the server configuration'
760
761
        )
    test_parser.set_defaults(func=test)
762
763
764
765
766
767
768
769
770
771
772

    captive_portal_parser = subparser.add_parser(
        'cp',
        help='install a captive portal'
        )
    captive_portal_parser.add_argument(
        '--limit',
        help="Ignored for compatibility"
        )
    captive_portal_parser.set_defaults(func=captive_portal)

773
774
775
776
777
    self_test_parser = subparser.add_parser(
        'selftest',
        help='run tests on this script'
        )
    self_test_parser.set_defaults(func=self_test)
778

779
780
781
    args = parser.parse_args()
    args.func(args)

782
783
784

if __name__ == '__main__':
    main()