<filename>src/servicelib/config/server.py
# (C) Copyright 2020- ECMWF.
#
# This software is licensed under the terms of the Apache Licence Version 2.0
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
#
# In applying this licence, ECMWF does not waive the privileges and immunities
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.

from __future__ import absolute_import, unicode_literals

import copy
import json
import os
import threading

import falcon
import yaml

from servicelib import logutils
from servicelib.config import types


__all__ = ["ConfigServer"]


class ConfigServer(object):

    log = logutils.get_logger(__name__)

    def __init__(self, fname):
        self.fname = fname
        self.config = types.ConfigDict()

        with open(fname, "rb") as f:
            self.config.reset(yaml.safe_load(f))

        self.read_only = os.environ.get("SERVICELIB_CONFIG_SERVER_READ_ONLY") == "true"
        self.lock = threading.Lock()

    def on_get(self, req, resp, key):
        """Returns the whole configuration encoded in JSON.

        """
        resp.data = json.dumps(self.config.as_dict()).encode("utf-8")
        resp.status = falcon.HTTP_200
        return

    def on_post(self, req, resp, key):
        """Updates the value for the setting specified in the HTTP request
        path.

        """
        if self.read_only:
            resp.status = falcon.HTTP_403
            resp.data = json.dumps("Config server in read-only mode").encode("utf-8")
            return

        resp.status = falcon.HTTP_200

        self.log.debug("on_post(req.path=%s, key=%s): Entering", req.path, key)
        if req.content_length:
            value = json.load(req.stream)
            self.log.debug("Setting '%s' to: %s", key, value)
            old_config = copy.deepcopy(self.config)
            self.config.set(key, value)
            try:
                self.save_config()
            except Exception as exc:
                msg = "Cannot save config: {}".format(exc)
                self.log.error(msg, exc_info=True, stack_info=True)
                self.config = old_config
                resp.status = falcon.HTTP_500
                resp.data = json.dumps(msg).encode("utf-8")

    def on_delete(self, req, resp, key=None):
        """Removes the setting specified in the HTTP request path.

        Returns an HTTP 200 response on success, or an HTTP 404 response if
        the setting was not found.

        """
        if self.read_only:
            resp.status = falcon.HTTP_403
            resp.data = json.dumps("Config server in read-only mode").encode("utf-8")
            return

        old_config = copy.deepcopy(self.config)
        try:
            self.config.delete(key)
        except KeyError:
            resp.status = falcon.HTTP_404
            return

        resp.status = falcon.HTTP_200
        try:
            self.save_config()
        except Exception as exc:
            msg = "Cannot save config: {}".format(exc)
            self.log.error(msg, exc_info=True, stack_info=True)
            self.config = old_config
            resp.status = falcon.HTTP_500
            resp.data = json.dumps(msg).encode("utf-8")

    def save_config(self):
        fname_new = "{}.new".format(self.fname)

        config = json.dumps(self.config.as_dict())
        with self.lock:
            with open(fname_new, "wb") as f:
                yaml.safe_dump(config, f, encoding="utf-8", allow_unicode=True)
                f.flush()
                os.fsync(f.fileno())

            os.rename(fname_new, self.fname)

<filename>src/servicelib/config/server.py
# (C) Copyright 2020- ECMWF.
#
# This software is licensed under the terms of the Apache Licence Version 2.0
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
#
# In applying this licence, ECMWF does not waive the privileges and immunities
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.

from __future__ import absolute_import, unicode_literals

import copy
import json
import os
import threading

import falcon
import yaml

from servicelib import logutils
from servicelib.config import types


__all__ = ["ConfigServer"]


class ConfigServer(object):

    log = logutils.get_logger(__name__)

    def __init__(self, fname):
        self.fname = fname
        self.config = types.ConfigDict()

        with open(fname, "rb") as f:
            self.config.reset(yaml.safe_load(f))

        self.read_only = os.environ.get("SERVICELIB_CONFIG_SERVER_READ_ONLY") == "true"
        self.lock = threading.Lock()

    def on_get(self, req, resp, key):
        """Returns the whole configuration encoded in JSON.

        """
        resp.data = json.dumps(self.config.as_dict()).encode("utf-8")
        resp.status = falcon.HTTP_200
        return

    def on_post(self, req, resp, key):
        """Updates the value for the setting specified in the HTTP request
        path.

        """
        if self.read_only:
            resp.status = falcon.HTTP_403
            resp.data = json.dumps("Config server in read-only mode").encode("utf-8")
            return

        resp.status = falcon.HTTP_200

        self.log.debug("on_post(req.path=%s, key=%s): Entering", req.path, key)
        if req.content_length:
            value = json.load(req.stream)
            self.log.debug("Setting '%s' to: %s", key, value)
            old_config = copy.deepcopy(self.config)
            self.config.set(key, value)
            try:
                self.save_config()
            except Exception as exc:
                msg = "Cannot save config: {}".format(exc)
                self.log.error(msg, exc_info=True, stack_info=True)
                self.config = old_config
                resp.status = falcon.HTTP_500
                resp.data = json.dumps(msg).encode("utf-8")

    def on_delete(self, req, resp, key=None):
        """Removes the setting specified in the HTTP request path.

        Returns an HTTP 200 response on success, or an HTTP 404 response if
        the setting was not found.

        """
        if self.read_only:
            resp.status = falcon.HTTP_403
            resp.data = json.dumps("Config server in read-only mode").encode("utf-8")
            return

        old_config = copy.deepcopy(self.config)
        try:
            self.config.delete(key)
        except KeyError:
            resp.status = falcon.HTTP_404
            return

        resp.status = falcon.HTTP_200
        try:
            self.save_config()
        except Exception as exc:
            msg = "Cannot save config: {}".format(exc)
            self.log.error(msg, exc_info=True, stack_info=True)
            self.config = old_config
            resp.status = falcon.HTTP_500
            resp.data = json.dumps(msg).encode("utf-8")

    def save_config(self):
        fname_new = "{}.new".format(self.fname)

        config = json.dumps(self.config.as_dict())
        with self.lock:
            with open(fname_new, "wb") as f:
                yaml.safe_dump(config, f, encoding="utf-8", allow_unicode=True)
                f.flush()
                os.fsync(f.fileno())

            os.rename(fname_new, self.fname)

# (C) Copyright 2020- ECMWF. # # This software is licensed under the terms of the Apache Licence Version 2.0 # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. # # In applying this licence, ECMWF does not waive the privileges and immunities # granted to it by virtue of its status as an intergovernmental organisation # nor does it submit to any jurisdiction. Returns the whole configuration encoded in JSON. Updates the value for the setting specified in the HTTP request         path. Removes the setting specified in the HTTP request path.          Returns an HTTP 200 response on success, or an HTTP 404 response if         the setting was not found.