Statistics
| Branch: | Tag: | Revision:

root / snf-common / synnefo / settings / setup.py @ 513309ba

History | View | Annotate | Download (34.1 kB)

1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from collections import defaultdict
35
from pprint import pformat
36
from textwrap import wrap
37
from itertools import chain
38
from os import environ
39

    
40

    
41
class Setting(object):
42
    """Setting is the parent class of all setting annotations.
43

44
    Setting.initialize_settings() will register a dictionary
45
    of setting names to Setting instances, process all
46
    annotations.
47

48
    Setting.load_configuration() will load user-specific
49
    configurations to the defaults.
50
    A setting set in this stage will be flagged 'configured'.
51
    Those that are not set in this stage will be flagged 'default'.
52

53
    Setting.configure_settings() will post-process all settings,
54
    maintaining the various registries and calling configuration
55
    callbacks for auto-generation or validation.
56

57
    A setting has the following attributes:
58

59
    ** CONFIGURATION ATTRIBUTES **
60

61
    default_value:
62
        The default value to be assigned if not given any by the
63
        administrator in the config files. To omit a default value
64
        assign Setting.NoValue to it.
65

66
    example_value:
67
        A value to serve as an informative guide for those who
68
        want to configure the setting. The default_value might not
69
        be that informative.
70

71
    description:
72
        Setting description for administrators and developers.
73

74
    category:
75
        An all-lowercase category name for grouping settings together.
76

77
    dependencies:
78
        A list of setting names and categories whose values will be
79
        given as input to configure_callback.
80

81
    configure_callback:
82
        A function that accepts three arguments, a Setting instance,
83
        the corresponding setting value given by the configuration
84
        files, and a dictionary with all setting dependencies declared.
85
        If the setting value has not be set by a config file,
86
        then its value (second argument) will be Setting.NoValue.
87

88
        If no value was provided by a config file, the callback must
89
        return the final setting value which will be assigned to it.
90
        If a value was provided, the callback must return
91
        Setting.NoValue to acknowledge it.
92

93
    export:
94
        If export is false then the setting will be marked not to be
95
        advertised in user-friendly lists or files.
96

97
    ** STATE ATTRIBUTES **
98

99
    setting_name:
100
        This is the name this setting annotation was given to.
101
        Initialized as None. Setting.initialize_settings will set it.
102

103
    configured_value:
104
        This is the value given by a configuration file.
105
        If it does not exist, then the setting was not set by
106
        the administrator.
107

108
        Initialized as Setting.NoValue.
109
        Setting.load_configuration will set it.
110

111
    configured_source:
112
        This is the source (e.g. configuration file path) where the
113
        setting was configured. It is None if the setting has not
114
        been configured.
115

116
    configured_depth:
117
        The depth of the setting in the dependency tree (forest).
118

119
    runtime_value:
120
        This is the final setting value after all configuration
121
        processing. Its existence indicates that all processing
122
        for this setting has been completed.
123

124
        Initialized as Setting.NoValue.
125
        Setting.configure_one_setting will set it upon completion.
126

127
    serial:
128
        The setting's serial index in the 'registry' catalog.
129
        Represents the chronological order of execution of its annotation.
130

131
    dependents:
132
        A list of the names of the settings that depend on this one.
133

134
    fail_exception:
135
        If the configuration of a setting has failed, this holds
136
        the exception raised, marking it as failed to prevent
137
        further attempts, and to be able to re-raise the error.
138
        Initialized as None.
139

140
    Subclasses may expose any subset of the above, as well as additional
141
    attributes of their own.
142

143
    Setting will construct certain setting catalogs during runtime
144
    initialization, later accessible via a 'Catalogs' class attribute
145
    dictionary:
146

147
        catalog = Setting.Catalogs['catalog_name']
148

149
    The catalogs are:
150

151
    1. Catalog 'settings'
152

153
       A catalog of all setting annotations collected at initialization.
154
       This is useful to access setting annotations at runtime,
155
       for example a setting's default value:
156

157
       settings = Setting.Catalogs['settings']
158
       settings['SOCKET_CONNECT_RETRIES'].default_value
159

160
    2. Catalog 'types'
161

162
       A catalog of the different types of settings.
163
       At each Setting instantiation, the (sub)class attribute
164
       'setting_type' will be used as the setting type name in the
165
       catalog.  Each entry in the catalog is a dictionary of setting
166
       names and setting annotation instances. For example:
167

168
       setting_types = Setting.Catalogs['types']
169
       setting_types['mandatory']['SETTING_NAME'] == setting_instance
170

171
    3. Catalog 'categories'
172

173
       A catalog of the different setting categories.
174
       At each Setting instantiation, the instance attribute 'category'
175
       will be used to group settings in categories. For example:
176

177
       categories = Setting.Catalogs['categories']
178
       categories['django']['SETTING_NAME'] == setting_instance
179

180
    4. Catalog 'defaults'
181

182
       A catalog of all settings that have been initialized and their
183
       default, pre-configuration values. This catalog is useful as
184
       input to the configuration from files.
185

186
    5. Catalog 'configured'
187

188
       A catalog of all settings that were configured by the
189
       administrator in the configuration files. Very relevant
190
       to the administrator as it effectively represents the
191
       deployment-specific configuration, without noise from
192
       all settings left default.
193
       Each setting is registered in the catalog just before
194
       the configure_callback is called.
195
       The catalog values are final setting values, not instances.
196

197
       configured = Setting.Catalogs['configured']
198
       print '\n'.join(("%s = %s" % it) for it in configured.items())
199

200
    5. Catalog 'runtime'
201

202
        A catalog of all finalized settings and their runtime values.
203
        This is output of the setting configuration process and
204
        where setting values must be read from at runtime.
205

206
    """
207

    
208
    class SettingsError(Exception):
209
        pass
210

    
211
    NoValue = type('NoValue', (), {})
212

    
213
    setting_type = 'setting'
214
    _serial = 0
215

    
216
    Catalogs = {
217
        'registry': {},
218
        'settings': {},
219
        'types': defaultdict(dict),
220
        'categories': defaultdict(dict),
221
        'defaults': {},
222
        'configured': {},
223
        'runtime': {},
224
        'init_dependents': {},
225
        'init_pending': {},
226
    }
227

    
228
    default_value = None
229
    example_value = None
230
    description = 'This setting is missing documentation'
231
    category = 'misc'
232
    dependencies = ()
233
    dependents = ()
234
    init_dependencies = ()
235
    init_dependents = ()
236
    export = True
237

    
238
    serial = None
239
    configured_value = NoValue
240
    configured_source = None
241
    configured_depth = 0
242
    runtime_value = NoValue
243
    fail_exception = None
244
    required_args = ()
245
    disallowed_args = ()
246
    _checked_arguments = False
247

    
248
    def __repr__(self):
249
        flags = []
250
        if self.configured_value is not Setting.NoValue:
251
            flags.append("configured")
252
            value = self.configured_value
253
        else:
254
            flags.append("default")
255
            value = self.default_value
256

    
257
        if self.runtime_value is not Setting.NoValue:
258
            flags.append("finalized")
259

    
260
        if self.fail_exception is not None:
261
            flags.append("failed({0})".format(self.fail_exception))
262
        r = "<{setting_type}[{flags}]: {value}>" 
263
        r = r.format(setting_type=self.setting_type,
264
                     value=repr(value),
265
                     flags=','.join(flags))
266
        return r
267

    
268
    __str__ = __repr__
269

    
270
    def present_as_comment(self, runtime=False):
271
        header = "# {name}: type {type}, category '{categ}'"
272
        header = header.format(name=self.setting_name,
273
                               type=self.setting_type.upper(),
274
                               categ=self.category)
275
        header = [header]
276

    
277
        if self.dependencies:
278
            header += ["# Depends on: "]
279
            header += ["#     " + d for d in sorted(self.dependencies)]
280

    
281
        description = wrap(self.description, 70)
282
        description = [("# " + s) for s in description]
283

    
284
        example_value = self.example_value
285
        default_value = self.default_value
286
        runtime_value = self.runtime_value
287
        if example_value != default_value:
288
            example = "Example value: {0}"
289
            example = example.format(pformat(example_value)).split('\n')
290
            description += ["# "]
291
            description += [("# " + s) for s in example]
292

    
293
        assignment = "{name} = {value}"
294
        assignment = assignment.format(name=self.setting_name,
295
                                       value=pformat(default_value))
296
        assignment = [("#" + s) for s in assignment.split('\n')]
297
        if runtime and self.configured_value is not Setting.NoValue \
298
                and runtime_value != self.default_value:
299
            runtime_assignment = "{name} = {value}"
300
            runtime_assignment = runtime_assignment.format(
301
                name=self.setting_name,
302
                value=pformat(self.runtime_value))
303
            assignment += [runtime_assignment]
304

    
305
        return '\n'.join(chain(header, ['#'],
306
                               description, ['#'],
307
                               assignment))
308

    
309
    @staticmethod
310
    def init_callback(setting):
311
        return
312

    
313
    @staticmethod
314
    def configure_callback(setting, value, dependencies):
315
        if value is Setting.NoValue:
316
            return setting.default_value
317
        else:
318
            # by default, acknowledge the configured value
319
            # and allow it to be used.
320
            return Setting.NoValue
321

    
322
    def validate(self):
323
        """Example setting validate method"""
324

    
325
        NoValue = Setting.NoValue
326
        setting_name = self.setting_name
327
        if self is not Setting.Catalogs['settings'][setting_name]:
328
            raise AssertionError()
329

    
330
        runtime_value = self.runtime_value
331
        if runtime_value is NoValue:
332
            raise AssertionError()
333

    
334
        configured_value = self.configured_value
335
        if configured_value not in (NoValue, runtime_value):
336
            raise AssertionError()
337

    
338
    def check_arguments(self, kwargs):
339
        if self._checked_arguments:
340
            return
341

    
342
        arg_set = set(kwargs.keys())
343
        required_set = set(self.required_args)
344
        intersection = arg_set.intersection(required_set)
345
        if intersection != required_set:
346
            missing = ', '.join(required_set - intersection)
347
            m = "{self}: Required arguments [{missing}] not found."
348
            m = m.format(self=self, missing=missing)
349
            raise Setting.SettingsError(m)
350

    
351
        disallowed_set = set(self.disallowed_args)
352
        intersection = arg_set.intersection(disallowed_set)
353
        if intersection:
354
            m = "{self}: Given arguments [{disallowed}] are disallowed."
355
            m = m.format(self=self, disallowed=', '.join(intersection))
356
            raise Setting.SettingsError(m)
357

    
358
        self._checked_arguments = True
359

    
360
    def __init__(self, **kwargs):
361

    
362
        attr_names = ['default_value', 'example_value', 'description',
363
                      'category', 'export',
364
                      'init_dependencies', 'init_callback',
365
                      'dependencies', 'configure_callback']
366

    
367
        self.check_arguments(kwargs)
368

    
369
        for name in attr_names:
370
            if name in kwargs:
371
                setattr(self, name, kwargs[name])
372

    
373
        serial = Setting._serial
374
        Setting._serial = serial + 1
375
        registry = Setting.Catalogs['registry']
376
        self.serial = serial
377
        registry[serial] = self
378

    
379
    @staticmethod
380
    def is_valid_setting_name(name):
381
        return name.isupper() and not name.startswith('_')
382

    
383
    @staticmethod
384
    def get_settings_from_object(settings_object):
385
        var_list = []
386
        is_valid_setting_name = Setting.is_valid_setting_name
387
        for name in dir(settings_object):
388
            if not is_valid_setting_name(name):
389
                continue
390
            var_list.append((name, getattr(settings_object, name)))
391
        return var_list
392

    
393
    @staticmethod
394
    def initialize_settings(settings_dict, strict=False):
395
        Catalogs = Setting.Catalogs
396
        settings = Catalogs['settings']
397
        categories = Catalogs['categories']
398
        defaults = Catalogs['defaults']
399
        init_dependents = Catalogs['init_dependents']
400
        pending = Catalogs['init_pending']
401
        types = Catalogs['types']
402

    
403
        for name, setting in settings_dict.iteritems():
404
            if not isinstance(setting, Setting):
405
                if strict:
406
                    m = "Setting name '{name}' has non-annotated value '{value}'!"
407
                    m = m.format(name=name, value=setting)
408
                    raise Setting.SettingsError(m)
409
                else:
410
                    setting = Setting(default_value=setting)
411

    
412
            # FIXME: duplicate annotations?
413
            #if name in settings:
414
            #    m = ("Duplicate annotation for setting '{name}': '{value}'. "
415
            #         "Original annotation: '{original}'")
416
            #    m = m.format(name=name, value=setting, original=settings[name])
417
            #    raise Setting.SettingsError(m)
418
            for init_dep in setting.init_dependencies:
419
                if init_dep in settings:
420
                    if init_dep in init_dependents:
421
                        m = ("Init dependency '{depname}' of setting '{name}' "
422
                             "found both in settings and in forward "
423
                             "dependencies!")
424
                        m = m.format(depname=init_dep, name=name)
425
                        raise AssertionError(m)
426
                    dep_setting = settings[init_dep]
427
                    if not dep_setting.init_dependents:
428
                        dep_setting.init_dependents = []
429
                    dep_setting.init_dependents.append(name)
430
                else:
431
                    if init_dep not in init_dependents:
432
                        init_dependents[init_dep] = []
433
                    init_dependents[init_dep].append(name)
434

    
435
            setting.setting_name = name
436
            if name in init_dependents:
437
                setting.init_dependents = init_dependents.pop(name)
438
            settings[name] = setting
439
            pending[name] = setting
440
            categories[setting.category][name] = setting
441
            types[setting.setting_type][name] = setting
442
            default_value = setting.default_value
443
            defaults[name] = default_value
444
            setting.initialize()
445

    
446
        defaults['_SETTING_CATALOGS'] = Catalogs
447

    
448
    @staticmethod
449
    def load_settings_from_file(path, settings_dict=None):
450
        if settings_dict is None:
451
            settings_dict = {}
452
        new_settings = {}
453
        execfile(path, settings_dict, new_settings)
454
        return new_settings
455

    
456
    @staticmethod
457
    def load_configuration(new_settings,
458
                           source='unknonwn',
459
                           allow_override=False,
460
                           allow_unknown=False,
461
                           allow_known=True):
462

    
463
        settings = Setting.Catalogs['settings']
464
        defaults = Setting.Catalogs['defaults']
465
        configured = Setting.Catalogs['configured']
466
        is_valid_setting_name = Setting.is_valid_setting_name
467

    
468
        for name, value in new_settings.iteritems():
469
            if not is_valid_setting_name(name):
470
                # silently ignore it?
471
                continue
472

    
473
            if name in settings:
474
                if not allow_known:
475
                    m = ("{source}: setting '{name} = {value}' not allowed to "
476
                         "be set here")
477
                    m = m.format(source=source, name=name, value=value)
478
                    raise Setting.SettingsError(m)
479
            else:
480
                if allow_unknown:
481
                    # pretend it was declared in a default settings module
482
                    desc = "Unknown setting from {source}"
483
                    desc = desc.format(source=source)
484
                    setting = Setting(default_value=value,
485
                                      category='unknown',
486
                                      description=desc)
487
                    Setting.initialize_settings({name: setting}, strict=True)
488
                else:
489
                    m = ("{source}: unknown setting '{name} = {value}' not "
490
                         "allowed to be set here")
491
                    m = m.format(source=source, name=name, value=value)
492
                    raise Setting.SettingsError(m)
493

    
494
            if not allow_override and name in configured:
495
                m = ("{source}: new setting '{name} = {value}' "
496
                     "overrides setting '{name} = {oldval}'")
497
                m = m.format(source=source, name=name, value=value,
498
                             oldval=defaults[name])
499
                raise Setting.SettingsError(m)
500

    
501
            # setting has been accepted for configuration
502
            setting = settings[name]
503
            setting.configured_value = value
504
            setting.configured_source = source
505
            configured[name] = value
506
            defaults[name] = value
507

    
508
        return new_settings
509

    
510
    @staticmethod
511
    def configure_settings(setting_names=()):
512
        settings = Setting.Catalogs['settings']
513
        if not setting_names:
514
            setting_names = settings.keys()
515

    
516
        pending = Setting.Catalogs['init_pending']
517
        dependents= Setting.Catalogs['init_dependents']
518
        m = ""
519
        if pending:
520
            m += ("There are settings that failed to initialize: "
521
                  "[{failed}]\n")
522
            failed = ', '.join(sorted(pending.iterkeys()))
523
            m = m.format(failed=failed)
524
        if dependents:
525
            m += ("There are settings that are referenced but not found: "
526
                  "{notfound}\n")
527
            notfound = ', '.join(
528
                ("%s referenced by %s" % (name, ', '.join(dependents)))
529
                for name, dependents in dependents.iteritems())
530
            m = m.format(notfound=notfound)
531
        if m:
532
            raise Setting.SettingsError(m)
533

    
534
        bottom = set(settings.keys())
535
        for name, setting in settings.iteritems():
536
            dependencies = setting.dependencies
537
            if not dependencies:
538
                continue
539
            bottom.discard(name)
540
            for dep_name in setting.dependencies:
541
                dep_setting = settings[dep_name]
542
                if not dep_setting.dependents:
543
                    dep_setting.dependents = []
544
                dep_setting.dependents.append(name)
545

    
546
        depth = 1
547
        while True:
548
            dependents = []
549
            for name in bottom:
550
                setting = settings[name]
551
                setting.configured_depth = depth
552
                dependents.extend(setting.dependents)
553
            if not dependents:
554
                break
555
            bottom = dependents
556
            depth += 1
557

    
558
        bottom = set(settings.keys())
559
        for name, setting in settings.iteritems():
560
            dependencies = setting.dependencies
561
            if not dependencies:
562
                continue
563
            bottom.discard(name)
564
            for dep_name in setting.dependencies:
565
                dep_setting = settings[dep_name]
566
                if not dep_setting.dependents:
567
                    dep_setting.dependents = []
568
                dep_setting.dependents.append(name)
569

    
570
        failed = []
571
        for name, setting in Setting.Catalogs['settings'].items():
572
            try:
573
                setting.configure()
574
            except Setting.SettingsError as e:
575
                failed.append(e)
576

    
577
        if failed:
578
            import sys
579
            sys.stderr.write('\n')
580
            sys.stderr.write('\n'.join(map(str, failed)))
581
            sys.stderr.write('\n\n')
582
            raise Setting.SettingsError("Failed to configure settings.")
583

    
584
    def initialize(setting, dep_stack=()):
585
        Catalogs = Setting.Catalogs
586
        settings = Catalogs['settings']
587
        pending = Catalogs['init_pending']
588
        setting_name = setting.setting_name
589
        dep_stack += (setting_name,)
590

    
591
        if setting_name not in pending:
592
            m = "Attempt to initialize already initialized setting '{name}'!"
593
            m = m.format(name=setting_name)
594
            raise AssertionError(m)
595

    
596
        for init_dep in setting.init_dependencies:
597
            if init_dep in dep_stack:
598
                m = "Settings init dependency cycle detected: {stack}"
599
                m = m.format(stack=dep_stack + (init_dep,))
600
                raise Setting.SettingsError(m)
601

    
602
            if init_dep not in settings:
603
                pending[setting_name] = setting
604
                return False
605

    
606
            dep_setting = settings[init_dep]
607
            if init_dep in pending:
608
                if not dep_setting.initialize(dep_stack):
609
                    pending[setting_name] = setting
610
                    return False
611

    
612
        init_callback = setting.init_callback
613
        init_callback(setting)
614
        del pending[setting_name]
615
        for dependent in setting.init_dependents:
616
            if dependent in pending:
617
                settings[dependent].initialize(dep_stack[:-1])
618
        return True
619

    
620
    def configure(setting, dep_stack=()):
621
        Catalogs = Setting.Catalogs
622
        settings = Catalogs['settings']
623
        runtime = Catalogs['runtime']
624
        NoValue = Setting.NoValue
625
        setting_name = setting.setting_name
626
        dep_stack += (setting_name,)
627

    
628
        if setting.runtime_value is not NoValue:
629
            # already configured, nothing to do.
630
            return
631

    
632
        if setting.fail_exception is not None:
633
            # it has previously failed, re-raise the error
634
            exc = setting.fail_exception
635
            if not isinstance(exc, Exception):
636
                exc = Setting.SettingsError(str(exc))
637
            raise exc
638

    
639
        setting_value = setting.configured_value
640
        if isinstance(setting_value, Setting):
641
            m = ("Unprocessed setting annotation '{name} = {value}' "
642
                 "in setting configuration stage!")
643
            m = m.format(name=setting_name, value=setting_value)
644
            raise AssertionError(m)
645

    
646
        configure_callback = setting.configure_callback
647
        if not configure_callback:
648
            setting.runtime_value = setting_value
649
            return
650

    
651
        if not callable(configure_callback):
652
            m = ("attribute 'configure_callback' of "
653
                 "'{setting}' is not callable!")
654
            m = m.format(setting=setting)
655
            exc = Setting.SettingsError(m)
656
            setting.fail_exception = exc
657
            raise exc
658

    
659
        deps = {}
660
        for dep_name in setting.dependencies:
661
            if dep_name not in settings:
662
                m = ("Unknown dependecy setting '{dep_name}' "
663
                     "for setting '{name}'!")
664
                m = m.format(dep_name=dep_name, name=setting_name)
665
                raise Setting.SettingsError(m)
666

    
667
            if dep_name in dep_stack:
668
                m = "Settings dependency cycle detected: {stack}"
669
                m = m.format(stack=dep_stack + (dep_name,))
670
                exc = Setting.SettingsError(m)
671
                setting.fail_exception = exc
672
                raise exc
673

    
674
            dep_setting = settings[dep_name]
675
            if dep_setting.fail_exception is not None:
676
                m = ("Cannot configure setting {name} because it depends "
677
                     "on '{dep}' which has failed to configure.")
678
                m = m.format(name=setting_name, dep=dep_name)
679
                exc = Setting.SettingsError(m)
680
                setting.fail_exception = exc
681
                raise exc
682

    
683
            if dep_setting.runtime_value is NoValue:
684
                dep_setting.configure(dep_stack)
685

    
686
            dep_value = dep_setting.runtime_value
687
            deps[dep_name] = dep_value
688

    
689
        try:
690
            new_value = configure_callback(setting, setting_value, deps)
691
        except Setting.SettingsError as e:
692
            setting.fail_exception = e
693
            raise
694

    
695
        if new_value is not NoValue:
696
            if setting_value is not NoValue:
697
                m = ("Configure callback of setting '{name}' does not "
698
                     "acknowledge the fact that a value '{value}' was "
699
                     "provided by '{source}' and wants to assign "
700
                     "a value '{newval}' anyway!")
701
                m = m.format(name=setting_name, value=setting_value,
702
                             source=setting.configured_source,
703
                             newval=new_value)
704
                exc = Setting.SettingsError(m)
705
                setting.fail_exception = exc
706
                raise exc
707
            else:
708
                setting_value = new_value
709

    
710
        setting.runtime_value = setting_value
711
        runtime[setting_name] = setting_value
712

    
713
    def enforce_not_configurable(setting, value, deps=None):
714
        if value is not Setting.NoValue:
715
            m = "Setting '{name}' is not configurable."
716
            m = m.format(name=setting.setting_name)
717
            raise Setting.SettingsError(m)
718
        return setting.default_value
719

    
720

    
721
class Mandatory(Setting):
722
    """Mandatory settings have to be to be configured by the
723
    administrator in the configuration files. There are no defaults,
724
    and not giving a value will raise an exception.
725

726
    """
727
    setting_type = 'mandatory'
728
    disallowed_args = ('export', 'default_value')
729
    required_args = ('example_value', 'description', 'category')
730

    
731
    def __init__(self, **kwargs):
732
        kwargs.pop('export', None)
733
        self.check_arguments(kwargs)
734
        kwargs['export'] = True
735
        Setting.__init__(self, **kwargs)
736

    
737
    @staticmethod
738
    def configure_callback(setting, value, deps):
739
        if value is Setting.NoValue:
740
            if environ.get('SYNNEFO_RELAX_MANDATORY_SETTINGS'):
741
                return setting.example_value
742

    
743
            m = ("Setting '{name}' is mandatory. "
744
                 "Please provide a real value. "
745
                 "Example value: '{example}'")
746
            m = m.format(name=setting.setting_name,
747
                         example=setting.example_value)
748
            raise Setting.SettingsError(m)
749

    
750
        return Setting.NoValue
751

    
752

    
753
class SubMandatory(Setting):
754
    """SubMandatory settings are made mandatory only if a condition
755
    on their dependencies is true. The default condition is that the
756
    settings it depends on have a true value via bool().
757

758
    If the dependency list is not empty then 'export' and 'category' attributes
759
    are inherited from the settings in that list, and they must all agree on
760
    the same value.
761

762
    Example:
763
    ENABLE_FAST_BACKEND = Default(default_value=False, ...)
764
    FAST_BACKEND_SPEED = ConditionalMandatory(
765
        example_value=9001,
766
        dependencies=['ENABLE_FAST_BACKEND'])
767

768
    """
769
    setting_type = 'submandatory'
770
    disallowed_args = ('init_dependencies', 'init_callback',
771
                       'configure_callback')
772
    required_args = ('example_value', 'description')
773

    
774
    def __init__(self, depends=None, condition_callback=None, **kwargs):
775

    
776
        if depends:
777
            if 'dependencies' in kwargs:
778
                m = "Cannot specify both 'depends' and 'dependencies'"
779
                raise TypeError(m)
780
            kwargs['dependencies'] = (depends,)
781

    
782
        dependencies = kwargs.get('dependencies', ())
783
        if dependencies:
784
            # if we depend on other settings,
785
            # we inherit their export and category.
786
            self.disallowed_args += ('export', 'category')
787

    
788
        self.check_arguments(kwargs)
789

    
790
        kwargs['init_dependencies'] = dependencies
791
        Setting.__init__(self, **kwargs)
792
        if condition_callback is not None:
793
            self.condition_callback = condition_callback
794

    
795

    
796
    @staticmethod
797
    def condition_callback(setting, value, deps):
798
        if not deps:
799
            m = "Setting '{name}' requires at least one dependency."
800
            m = m.format(name=setting.setting_name)
801
            raise Setting.SettingsError(m)
802
        return any(bool(setting_value) for setting_value in deps.itervalues())
803

    
804
    @staticmethod
805
    def configure_callback(setting, value, deps):
806
        condition_callback = setting.condition_callback
807
        if condition_callback(setting, value, deps):
808
            # We are mandatory
809
            if value is Setting.NoValue:
810
                if environ.get('SYNNEFO_RELAX_MANDATORY_SETTINGS'):
811
                    return setting.example_value
812

    
813
                m = ("Setting '{name}' is mandatory due to configuration of "
814
                     "{due}. Please provide a real value. "
815
                     "Example value: '{example}'")
816
                if deps:
817
                    due = "[" + ', '.join(sorted(deps.keys())) + "]"
818
                else:
819
                    due = "[runtime-determined parameters]"
820
                m = m.format(name=setting.setting_name,
821
                             example=setting.example_value,
822
                             due=due)
823
                raise Setting.SettingsError(m)
824

    
825
        elif value is Setting.NoValue:
826
            return setting.default_value
827

    
828
        # acknowledge configured value
829
        return Setting.NoValue
830

    
831

    
832
    @staticmethod
833
    def init_callback(setting):
834
        # if we depend on other settings,
835
        # they must all agree on export and category
836

    
837
        dependencies = setting.init_dependencies
838
        if not dependencies:
839
            return
840

    
841
        settings = Setting.Catalogs['settings']
842
        firstdep = dependencies[0]
843
        firstdep_setting = settings[firstdep]
844
        export = firstdep_setting.export
845
        category = firstdep_setting.category
846
        for dep in dependencies[1:]:
847
            dep_setting = settings[dep]
848
            if dep_setting.category != category:
849
                m = ("SubMandatory settings require that all "
850
                     "their dependencies have the same 'category' value. "
851
                     "However '{firstdep}' has '{firstval}' while "
852
                     "'{dep}' has '{val}'")
853
                m = m.format(firstdep=firstdep, firstval=category,
854
                             dep=dep, val=dep_setting.category)
855
                raise Setting.SettingsError(m)
856
            if dep_setting.export != export:
857
                m = ("SubMandatory settings require that all "
858
                     "their dependencies have the same 'export' value. "
859
                     "However '{firstdep}' has '{firstval}' while "
860
                     "'{dep}' has '{val}'")
861
                m = m.format(firstdep=firstdep, firstval=export,
862
                             dep=dep, val=dep_setting.export)
863
                raise Setting.SettingsError(m)
864
        setting.export = export
865
        setting.category = category
866

    
867

    
868
class Default(Setting):
869
    """Default settings are not mandatory.
870
    There are default values that are meant to work well, and also serve as an
871
    example if no explicit example is given.
872

873
    """
874
    setting_type = 'default'
875
    required_args = ('default_value', 'description')
876

    
877
    def __init__(self, **kwargs):
878
        self.check_arguments(kwargs)
879
        if 'example_value' not in kwargs:
880
            kwargs['example_value'] = kwargs['default_value']
881
        Setting.__init__(self, **kwargs)
882

    
883

    
884
class Constant(Setting):
885
    """Constant settings are a like defaults, only they are not intended to be
886
    visible or configurable by the administrator.
887

888
    """
889
    setting_type = 'constant'
890
    disallowed_args = ('export',)
891
    required_args = ('default_value', 'description')
892

    
893
    def __init__(self, **kwargs):
894
        kwargs.pop('export', None)
895
        self.check_arguments(kwargs)
896
        kwargs['export'] = False
897
        Setting.__init__(self, **kwargs)
898

    
899

    
900
class Auto(Setting):
901
    """Auto settings can be computed automatically.
902
    Administrators may attempt to override them and the setting
903
    may or may not accept being overriden. If override is not accepted
904
    it will result in an error, not in a silent discarding of user input.
905

906
    """
907
    setting_type = 'auto'
908
    required_args = ('configure_callback', 'description')
909

    
910
    @staticmethod
911
    def configure_callback(setting, value, deps):
912
        raise NotImplementedError()
913

    
914

    
915
class Deprecated(object):
916
    """Deprecated settings must be removed, renamed, or otherwise fixed."""
917

    
918
    setting_type = 'deprecated'
919
    disallowed_args = ('export', 'default_value', 'example_value')
920
    required_args = ('description',)
921

    
922
    def __init__(self, rename_to=None, **kwargs):
923
        self.rename_to = rename_to
924
        self.check_arguments(kwargs)
925
        kwargs['export'] = False
926
        Setting.__init__(self, **kwargs)
927

    
928
    @staticmethod
929
    def configure_callback(setting, value, deps):
930
        m = ("Setting {name} has been deprecated. "
931
             "Please consult upgrade notes and ")
932

    
933
        if setting.rename_to:
934
            m += "rename to {rename_to}."
935
        else:
936
            m += "remove it."
937

    
938
        m = m.format(name=setting.setting_name, rename_to=setting.rename_to)
939
        raise Setting.SettingsError(m)
940