Statistics
| Branch: | Tag: | Revision:

root / snf-common / synnefo / settings / setup.py @ 32401481

History | View | Annotate | Download (25.2 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. Settings.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
        Settings.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
        Settings.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
    }
225

    
226
    default_value = None
227
    example_value = None
228
    description = 'This setting is missing documentation'
229
    category = 'misc'
230
    dependencies = ()
231
    dependents = ()
232
    export = True
233

    
234
    serial = None
235
    configured_value = NoValue
236
    configured_source = None
237
    configured_depth = 0
238
    runtime_value = NoValue
239
    fail_exception = None
240

    
241
    def __repr__(self):
242
        flags = []
243
        if self.configured_value is not Setting.NoValue:
244
            flags.append("configured")
245
            value = self.configured_value
246
        else:
247
            flags.append("default")
248
            value = self.default_value
249

    
250
        if self.runtime_value is not Setting.NoValue:
251
            flags.append("finalized")
252

    
253
        if self.fail_exception is not None:
254
            flags.append("failed({0})".format(self.fail_exception))
255
        r = "<{setting_type}[{flags}]: {value}>" 
256
        r = r.format(setting_type=self.setting_type,
257
                     value=repr(value),
258
                     flags=','.join(flags))
259
        return r
260

    
261
    __str__ = __repr__
262

    
263
    def present_as_comment(self):
264
        header = "# {name}: type {type}, category '{categ}'"
265
        header = header.format(name=self.setting_name,
266
                               type=self.setting_type.upper(),
267
                               categ=self.category)
268
        header = [header]
269

    
270
        if self.dependencies:
271
            header += ["# Depends on: "]
272
            header += ["#     " + d for d in sorted(self.dependencies)]
273

    
274
        description = wrap(self.description, 70)
275
        description = [("# " + s) for s in description]
276

    
277
        example_value = self.example_value
278
        default_value = self.default_value
279
        if example_value != default_value:
280
            example = "Example value: {0}"
281
            example = example.format(pformat(example_value)).split('\n')
282
            description += ["# "]
283
            description += [("# " + s) for s in example]
284

    
285
        assignment = "{name} = {value}"
286
        assignment = assignment.format(name=self.setting_name,
287
                                       value=pformat(default_value))
288
        assignment = [("#" + s) for s in assignment.split('\n')]
289

    
290
        return '\n'.join(chain(header, ['#'],
291
                               description, ['#'],
292
                               assignment))
293

    
294
    @staticmethod
295
    def configure_callback(setting, value, dependencies):
296
        if value is Setting.NoValue:
297
            return setting.default_value
298
        else:
299
            # by default, acknowledge the configured value
300
            # and allow it to be used.
301
            return Setting.NoValue
302

    
303
    def validate(self):
304
        """Example setting validate method"""
305

    
306
        NoValue = Setting.NoValue
307
        setting_name = self.setting_name
308
        if self is not Setting.Catalogs['settings'][setting_name]:
309
            raise AssertionError()
310

    
311
        runtime_value = self.runtime_value
312
        if runtime_value is NoValue:
313
            raise AssertionError()
314

    
315
        configured_value = self.configured_value
316
        if configured_value not in (NoValue, runtime_value):
317
            raise AssertionError()
318

    
319
    def __init__(self, **kwargs):
320

    
321
        attr_names = ['default_value', 'example_value', 'description',
322
                      'category', 'dependencies', 'configure_callback',
323
                      'export']
324

    
325
        for name in attr_names:
326
            if name in kwargs:
327
                setattr(self, name, kwargs[name])
328

    
329
        serial = Setting._serial
330
        Setting._serial = serial + 1
331
        registry = Setting.Catalogs['registry']
332
        self.serial = serial
333
        registry[serial] = self
334

    
335
    @staticmethod
336
    def is_valid_setting_name(name):
337
        return name.isupper() and not name.startswith('_')
338

    
339
    @staticmethod
340
    def get_settings_from_object(settings_object):
341
        var_list = []
342
        is_valid_setting_name = Setting.is_valid_setting_name
343
        for name in dir(settings_object):
344
            if not is_valid_setting_name(name):
345
                continue
346
            var_list.append((name, getattr(settings_object, name)))
347
        return var_list
348

    
349
    @staticmethod
350
    def initialize_settings(settings_dict, strict=False):
351
        Catalogs = Setting.Catalogs
352
        settings = Catalogs['settings']
353
        categories = Catalogs['categories']
354
        defaults = Catalogs['defaults']
355
        types = Catalogs['types']
356

    
357
        for name, value in settings_dict.iteritems():
358
            if not isinstance(value, Setting):
359
                if strict:
360
                    m = "Setting name '{name}' has non-annotated value '{value}'!"
361
                    m = m.format(name=name, value=value)
362
                    raise Setting.SettingsError(m)
363
                else:
364
                    value = Setting(default_value=value)
365

    
366
            # FIXME: duplicate annotations?
367
            #if name in settings:
368
            #    m = ("Duplicate annotation for setting '{name}': '{value}'. "
369
            #         "Original annotation: '{original}'")
370
            #    m = m.format(name=name, value=value, original=settings[name])
371
            #    raise Setting.SettingsError(m)
372
            value.setting_name = name
373
            settings[name] = value
374
            categories[value.category][name] = value
375
            types[value.setting_type][name] = value
376
            default_value = value.default_value
377
            defaults[name] = default_value
378

    
379
        defaults['_SETTING_CATALOGS'] = Catalogs
380

    
381
    @staticmethod
382
    def load_settings_from_file(path, settings_dict=None):
383
        if settings_dict is None:
384
            settings_dict = {}
385
        new_settings = {}
386
        execfile(path, settings_dict, new_settings)
387
        return new_settings
388

    
389
    @staticmethod
390
    def load_configuration(new_settings,
391
                           source='unknonwn',
392
                           allow_override=False,
393
                           allow_unknown=False,
394
                           allow_known=True):
395

    
396
        settings = Setting.Catalogs['settings']
397
        defaults = Setting.Catalogs['defaults']
398
        configured = Setting.Catalogs['configured']
399
        is_valid_setting_name = Setting.is_valid_setting_name
400

    
401
        for name, value in new_settings.iteritems():
402
            if not is_valid_setting_name(name):
403
                # silently ignore it?
404
                continue
405

    
406
            if name in settings:
407
                if not allow_known:
408
                    m = ("{source}: setting '{name} = {value}' not allowed to "
409
                         "be set here")
410
                    m = m.format(source=source, name=name, value=value)
411
                    raise Setting.SettingsError(m)
412
            else:
413
                if allow_unknown:
414
                    # pretend this was declared in a default settings module
415
                    desc = "Unknown setting from {source}".format(source=source)
416

    
417
                    setting = Setting(default_value=value,
418
                                      category='unknown',
419
                                      description=desc)
420
                    Setting.initialize_settings({name: setting}, strict=True)
421
                else:
422
                    m = ("{source}: unknown setting '{name} = {value}' not "
423
                         "allowed to be set here")
424
                    m = m.format(source=source, name=name, value=value)
425
                    raise Setting.SettingsError(m)
426

    
427
            if not allow_override and name in configured:
428
                m = ("{source}: new setting '{name} = {value}' "
429
                     "overrides setting '{name} = {oldval}'")
430
                m = m.format(source=source, name=name, value=value,
431
                             oldval=defaults[name])
432
                raise Setting.SettingsError(m)
433

    
434
            # setting has been accepted for configuration
435
            setting = settings[name]
436
            setting.configured_value = value
437
            setting.configured_source = source
438
            configured[name] = value
439
            defaults[name] = value
440

    
441
        return new_settings
442

    
443
    @staticmethod
444
    def configure_one_setting(setting_name, dep_stack=()):
445
        dep_stack += (setting_name,)
446
        Catalogs = Setting.Catalogs
447
        settings = Catalogs['settings']
448
        runtime = Catalogs['runtime']
449
        NoValue = Setting.NoValue
450

    
451
        if setting_name not in settings:
452
            m = "Unknown setting '{name}'"
453
            m = m.format(name=setting_name)
454
            raise Setting.SettingsError(m)
455

    
456
        setting = settings[setting_name]
457
        if setting.runtime_value is not NoValue:
458
            # already configured, nothing to do.
459
            return
460

    
461
        if setting.fail_exception is not None:
462
            # it has previously failed, re-raise the error
463
            exc = setting.fail_exception
464
            if not isinstance(exc, Exception):
465
                exc = Setting.SettingsError(str(exc))
466
            raise exc
467

    
468
        setting_value = setting.configured_value
469
        if isinstance(setting_value, Setting):
470
            m = ("Unprocessed setting annotation '{name} = {value}' "
471
                 "in setting configuration stage!")
472
            m = m.format(name=setting_name, value=setting_value)
473
            raise AssertionError(m)
474

    
475
        configure_callback = setting.configure_callback
476
        if not configure_callback:
477
            setting.runtime_value = setting_value
478
            return
479

    
480
        if not callable(configure_callback):
481
            m = ("attribute 'configure_callback' of "
482
                 "'{setting}' is not callable!")
483
            m = m.format(setting=setting)
484
            exc = Setting.SettingsError(m)
485
            setting.fail_exception = exc
486
            raise exc
487

    
488
        deps = {}
489
        for dep_name in setting.dependencies:
490
            if dep_name not in settings:
491
                m = ("Unknown dependecy setting '{dep_name}' "
492
                     "for setting '{name}'!")
493
                m = m.format(dep_name=dep_name, name=setting_name)
494
                raise Setting.SettingsError(m)
495

    
496
            if dep_name in dep_stack:
497
                m = "Settings dependency cycle detected: {stack}"
498
                m = m.format(stack=dep_stack)
499
                exc = Setting.SettingsError(m)
500
                setting.fail_exception = exc
501
                raise exc
502

    
503
            dep_setting = settings[dep_name]
504
            if dep_setting.fail_exception is not None:
505
                m = ("Cannot configure setting {name} because it depends "
506
                     "on '{dep}' which has failed to configure.")
507
                m = m.format(name=setting_name, dep=dep_name)
508
                exc = Setting.SettingsError(m)
509
                setting.fail_exception = exc
510
                raise exc
511

    
512
            if dep_setting.runtime_value is NoValue:
513
                Setting.configure_one_setting(dep_name, dep_stack)
514

    
515
            dep_value = dep_setting.runtime_value
516
            deps[dep_name] = dep_value
517

    
518
        try:
519
            new_value = configure_callback(setting, setting_value, deps)
520
        except Setting.SettingsError as e:
521
            setting.fail_exception = e
522
            raise
523

    
524
        if new_value is not NoValue:
525
            if setting_value is not NoValue:
526
                m = ("Configure callback of setting '{name}' does not "
527
                     "acknowledge the fact that a value '{value}' was "
528
                     "provided by '{source}' and wants to assign "
529
                     "a value '{newval}' anyway!")
530
                m = m.format(name=setting_name, value=setting_value,
531
                             source=setting.configured_source,
532
                             newval=new_value)
533
                exc = Setting.SettingsError(m)
534
                setting.fail_exception = exc
535
                raise exc
536
            else:
537
                setting_value = new_value
538

    
539
        setting.runtime_value = setting_value
540
        runtime[setting_name] = setting_value
541

    
542
    @staticmethod
543
    def configure_settings(setting_names=()):
544
        settings = Setting.Catalogs['settings']
545
        if not setting_names:
546
            setting_names = settings.keys()
547

    
548
        bottom = set(settings.keys())
549
        for name, setting in settings.iteritems():
550
            dependencies = setting.dependencies
551
            if not dependencies:
552
                continue
553
            bottom.discard(name)
554
            for dep_name in setting.dependencies:
555
                dep_setting = settings[dep_name]
556
                if not dep_setting.dependents:
557
                    dep_setting.dependents = []
558
                dep_setting.dependents.append(name)
559

    
560
        depth = 1
561
        while True:
562
            dependents = []
563
            for name in bottom:
564
                setting = settings[name]
565
                setting.configured_depth = depth
566
                dependents.extend(setting.dependents)
567
            if not dependents:
568
                break
569
            bottom = dependents
570
            depth += 1
571

    
572
        failed = []
573
        for name in Setting.Catalogs['settings']:
574
            try:
575
                Setting.configure_one_setting(name)
576
            except Setting.SettingsError as e:
577
                failed.append(e)
578

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

    
586
    @staticmethod
587
    def enforce_not_configurable(setting, value, deps=None):
588
        if value is not Setting.NoValue:
589
            m = "Setting '{name}' is not configurable."
590
            m = m.format(name=setting.setting_name)
591
            raise Setting.SettingsError(m)
592
        return setting.default_value
593

    
594

    
595
class Mandatory(Setting):
596
    """Mandatory settings have to be to be configured by the
597
    administrator in the configuration files. There are no defaults,
598
    and not giving a value will raise an exception.
599

600
    """
601
    setting_type = 'mandatory'
602

    
603
    def __init__(self, example_value=Setting.NoValue, **kwargs):
604
        if example_value is Setting.NoValue:
605
            m = "Mandatory settings require an example_value"
606
            raise Setting.SettingsError(m)
607
        kwargs['example_value'] = example_value
608
        kwargs['export'] = True
609
        Setting.__init__(self, **kwargs)
610

    
611
    @staticmethod
612
    def configure_callback(setting, value, deps):
613
        if value is Setting.NoValue:
614
            if environ.get('SYNNEFO_RELAX_MANDATORY_SETTINGS'):
615
                return setting.example_value
616

    
617
            m = ("Setting '{name}' is mandatory. "
618
                 "Please provide a real value. "
619
                 "Example value: '{example}'")
620
            m = m.format(name=setting.setting_name,
621
                         example=setting.example_value)
622
            raise Setting.SettingsError(m)
623

    
624
        return Setting.NoValue
625

    
626

    
627
class Default(Setting):
628
    """Default settings are not mandatory.
629
    There are default values that are meant to work well, and also serve as an
630
    example if no explicit example is given.
631

632
    """
633
    setting_type = 'default'
634

    
635
    def __init__(self, default_value=Setting.NoValue,
636
                 description="No description", **kwargs):
637
        if default_value is Setting.NoValue:
638
            m = "Default settings require a default_value"
639
            raise Setting.SettingsError(m)
640

    
641
        kwargs['default_value'] = default_value
642
        if 'example_value' not in kwargs:
643
            kwargs['example_value'] = default_value
644
        kwargs['description'] = description
645
        Setting.__init__(self, **kwargs)
646

    
647

    
648
class Constant(Setting):
649
    """Constant settings are a like defaults, only they are not intended to be
650
    visible or configurable by the administrator.
651

652
    """
653
    setting_type = 'constant'
654

    
655
    def __init__(self, default_value=Setting.NoValue,
656
                 description="No description", **kwargs):
657
        if default_value is Setting.NoValue:
658
            m = "Constant settings require a default_value"
659
            raise Setting.SettingsError(m)
660

    
661
        kwargs['default_value'] = default_value
662
        if 'example_value' not in kwargs:
663
            kwargs['example_value'] = default_value
664
        kwargs['export'] = False
665
        kwargs['description'] = description
666
        Setting.__init__(self, **kwargs)
667

    
668

    
669
class Auto(Setting):
670
    """Auto settings can be computed automatically.
671
    Administrators may attempt to override them and the setting
672
    may or may not accept being overriden. If override is not accepted
673
    it will result in an error, not in a silent discarding of user input.
674

675
    """
676
    setting_type = 'auto'
677

    
678
    def __init__(self, configure_callback=None, **kwargs):
679
        if not configure_callback:
680
            m = "Auto settings must provide a configure_callback"
681
            raise Setting.SettingsError(m)
682

    
683
        kwargs['configure_callback'] = configure_callback
684
        Setting.__init__(self, **kwargs)
685

    
686
    @staticmethod
687
    def configure_callback(setting, value, deps):
688
        raise NotImplementedError()
689

    
690

    
691
class Deprecated(object):
692
    """Deprecated settings must be removed, renamed, or otherwise fixed."""
693

    
694
    setting_type = 'deprecated'
695

    
696
    def __init__(self, rename_to=None, **kwargs):
697
        self.rename_to = rename_to
698
        kwargs['export'] = False
699
        Setting.__init__(self, **kwargs)
700

    
701
    @staticmethod
702
    def configure_callback(setting, value, deps):
703
        m = ("Setting {name} has been deprecated. "
704
             "Please consult upgrade notes and ")
705

    
706
        if setting.rename_to:
707
            m += "rename to {rename_to}."
708
        else:
709
            m += "remove it."
710

    
711
        m = m.format(name=setting.setting_name, rename_to=setting.rename_to)
712
        raise Setting.SettingsError(m)
713