~billy-olsen/maas/lp1484696-proxypass-websockets-1.9

« back to all changes in this revision

Viewing changes to src/provisioningserver/config.py

[r=allenap][bug=1524007][author=allenap] Backport of r4551 from lp:maas: By default, open configuration files read-only.

This eliminates the need for locking when using the YAML-based back-end.

Show diffs side-by-side

added added

removed removed

Lines of Context:
143
143
    ForEach,
144
144
    Schema,
145
145
)
146
 
from formencode.api import NoDefault
 
146
from formencode.api import (
 
147
    is_validator,
 
148
    NoDefault,
 
149
)
147
150
from formencode.declarative import DeclarativeMeta
148
151
from formencode.validators import (
149
152
    Invalid,
150
 
    is_validator,
151
153
    Number,
152
154
    Set,
153
155
    String,
400
402
    os.close(os.open(path, os.O_CREAT | os.O_APPEND, mode))
401
403
 
402
404
 
 
405
class ConfigurationImmutable(Exception):
 
406
    """The configuration is read-only; it cannot be mutated."""
 
407
 
 
408
 
403
409
class ConfigurationDatabase:
404
410
    """Store configuration in an sqlite3 database."""
405
411
 
406
 
    def __init__(self, database):
 
412
    def __init__(self, database, mutable=False):
407
413
        self.database = database
 
414
        self.mutable = mutable
408
415
        with self.cursor() as cursor:
409
416
            cursor.execute(
410
417
                "CREATE TABLE IF NOT EXISTS configuration "
432
439
            return json.loads(data[0])
433
440
 
434
441
    def __setitem__(self, name, data):
435
 
        with self.cursor() as cursor:
436
 
            cursor.execute(
437
 
                "INSERT OR REPLACE INTO configuration (name, data) "
438
 
                "VALUES (?, ?)", (name, json.dumps(data)))
 
442
        if self.mutable:
 
443
            with self.cursor() as cursor:
 
444
                cursor.execute(
 
445
                    "INSERT OR REPLACE INTO configuration (name, data) "
 
446
                    "VALUES (?, ?)", (name, json.dumps(data)))
 
447
        else:
 
448
            raise ConfigurationImmutable(
 
449
                "%s: Cannot set `%s'." % (self, name))
439
450
 
440
451
    def __delitem__(self, name):
 
452
        if self.mutable:
 
453
            with self.cursor() as cursor:
 
454
                cursor.execute(
 
455
                    "DELETE FROM configuration"
 
456
                    " WHERE name = ?", (name,))
 
457
        else:
 
458
            raise ConfigurationImmutable(
 
459
                "%s: Cannot set `%s'." % (self, name))
 
460
 
 
461
    def __unicode__(self):
441
462
        with self.cursor() as cursor:
442
 
            cursor.execute(
443
 
                "DELETE FROM configuration"
444
 
                " WHERE name = ?", (name,))
 
463
            # https://www.sqlite.org/pragma.html#pragma_database_list
 
464
            databases = "; ".join(
 
465
                "%s=%s" % (name, ":memory:" if path == "" else path)
 
466
                for (_, name, path) in cursor.execute("PRAGMA database_list"))
 
467
        return "%s(%s)" % (self.__class__.__name__, databases)
445
468
 
446
469
    @classmethod
447
470
    @contextmanager
448
471
    def open(cls, dbpath):
449
472
        """Open a configuration database.
450
473
 
 
474
        **Note** that this returns a context manager which will open the
 
475
        database READ-ONLY.
 
476
        """
 
477
        # Ensure `dbpath` exists...
 
478
        touch(dbpath)
 
479
        # before opening it with sqlite.
 
480
        database = sqlite3.connect(dbpath)
 
481
        try:
 
482
            yield cls(database, mutable=False)
 
483
        except:
 
484
            raise
 
485
        else:
 
486
            database.rollback()
 
487
        finally:
 
488
            database.close()
 
489
 
 
490
    @classmethod
 
491
    @contextmanager
 
492
    def open_for_update(cls, dbpath):
 
493
        """Open a configuration database.
 
494
 
451
495
        **Note** that this returns a context manager which will close the
452
 
        database on exit, saving if the exit is clean.
 
496
        database on exit, COMMITTING changes if the exit is clean.
453
497
        """
454
498
        # Ensure `dbpath` exists...
455
499
        touch(dbpath)
456
500
        # before opening it with sqlite.
457
501
        database = sqlite3.connect(dbpath)
458
502
        try:
459
 
            yield cls(database)
 
503
            yield cls(database, mutable=True)
460
504
        except:
461
505
            raise
462
506
        else:
480
524
    got to use this.
481
525
    """
482
526
 
483
 
    def __init__(self, path):
 
527
    def __init__(self, path, mutable=False):
484
528
        super(ConfigurationFile, self).__init__()
485
529
        self.config = {}
486
530
        self.dirty = False
487
531
        self.path = path
 
532
        self.mutable = mutable
488
533
 
489
534
    def __iter__(self):
490
535
        return iter(self.config)
493
538
        return self.config[name]
494
539
 
495
540
    def __setitem__(self, name, data):
496
 
        self.config[name] = data
497
 
        self.dirty = True
498
 
 
499
 
    def __delitem__(self, name):
500
 
        if name in self.config:
501
 
            del self.config[name]
 
541
        if self.mutable:
 
542
            self.config[name] = data
502
543
            self.dirty = True
 
544
        else:
 
545
            raise ConfigurationImmutable(
 
546
                "%s: Cannot set `%s'." % (self, name))
 
547
 
 
548
    def __delitem__(self, name):
 
549
        if self.mutable:
 
550
            if name in self.config:
 
551
                del self.config[name]
 
552
                self.dirty = True
 
553
        else:
 
554
            raise ConfigurationImmutable(
 
555
                "%s: Cannot set `%s'." % (self, name))
503
556
 
504
557
    def load(self):
505
558
        """Load the configuration."""
530
583
            self.path, mode=mode)
531
584
        self.dirty = False
532
585
 
 
586
    def __unicode__(self):
 
587
        return "%s(%r)" % (self.__class__.__name__, self.path)
 
588
 
533
589
    @classmethod
534
590
    @contextmanager
535
591
    def open(cls, path):
 
592
        """Open a configuration file read-only.
 
593
 
 
594
        This avoids all the locking that happens in `open_for_update`. However,
 
595
        it will create the configuration file if it does not yet exist.
 
596
 
 
597
        **Note** that this returns a context manager which will DISCARD
 
598
        changes to the configuration on exit.
 
599
        """
 
600
        # Ensure `path` exists...
 
601
        touch(path)
 
602
        # before loading it in.
 
603
        configfile = cls(path, mutable=False)
 
604
        configfile.load()
 
605
        yield configfile
 
606
 
 
607
    @classmethod
 
608
    @contextmanager
 
609
    def open_for_update(cls, path):
536
610
        """Open a configuration file.
537
611
 
538
612
        Locks are taken so that there can only be *one* reader or writer for a
540
614
        multiple concurrent processes it follows that each process should hold
541
615
        the file open for the shortest time possible.
542
616
 
543
 
        **Note** that this returns a context manager which will save changes
 
617
        **Note** that this returns a context manager which will SAVE changes
544
618
        to the configuration on a clean exit.
545
619
        """
546
620
        time_opened = None
551
625
                # Ensure `path` exists...
552
626
                touch(path)
553
627
                # before loading it in.
554
 
                configfile = cls(path)
 
628
                configfile = cls(path, mutable=True)
555
629
                configfile.load()
556
630
                try:
557
631
                    yield configfile
674
748
        with cls.backend.open(filepath) as store:
675
749
            yield cls(store)
676
750
 
 
751
    @classmethod
 
752
    @contextmanager
 
753
    def open_for_update(cls, filepath=None):
 
754
        if filepath is None:
 
755
            filepath = cls.DEFAULT_FILENAME
 
756
        ensure_dir(os.path.dirname(filepath))
 
757
        with cls.backend.open_for_update(filepath) as store:
 
758
            yield cls(store)
 
759
 
677
760
 
678
761
class ConfigurationOption:
679
762
    """Define a configuration option.