moonraker: Add configparser support

Rather than receive its configuration from Klippy, moonraker will receive its configuration from a config file.  By default this file is located at ~/moonraker.conf.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Arksine 2020-08-05 20:44:21 -04:00
parent e2850ee77e
commit 7a94fb3a6b
2 changed files with 139 additions and 74 deletions

89
moonraker/confighelper.py Normal file
View File

@ -0,0 +1,89 @@
# Configuration Helper
#
# Copyright (C) 2020 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license
import configparser
import os
import logging
class ConfigError(Exception):
pass
class Sentinel:
pass
class ConfigHelper:
error = ConfigError
def __init__(self, server, config, section):
self.server = server
self.config = config
self.section = section
self.sections = config.sections
self.has_section = config.has_section
def get_server(self):
return self.server
def __getitem__(self, key):
return self.getsection(key)
def __contains__(self, key):
return key in self.config
def getsection(self, section):
if section not in self.config:
raise ConfigError(f"No section [{section}] in config")
return ConfigHelper(self.server, self.config, section)
def _get_option(self, func, option, default):
try:
val = func(option, default)
except Exception:
raise ConfigError(
f"Error parsing option ({option}) from "
f"section [{self.section}]")
if val == Sentinel:
raise ConfigError(
f"No option found ({option}) in section [{self.section}]")
return val
def get(self, option, default=Sentinel):
return self._get_option(
self.config[self.section].get, option, default)
def getint(self, option, default=Sentinel):
return self._get_option(
self.config[self.section].getint, option, default)
def getboolean(self, option, default=Sentinel):
return self._get_option(
self.config[self.section].getboolean, option, default)
def getfloat(self, option, default=Sentinel):
return self._get_item(
self.config[self.section].getfloat, option, default)
def get_configuration(server, cmd_line_args):
cfg_file_path = os.path.normpath(os.path.expanduser(
cmd_line_args.configfile))
if not os.path.isfile(cfg_file_path):
raise ConfigError(f"Configuration File Not Found: '{cfg_file_path}''")
config = configparser.ConfigParser(interpolation=None)
try:
config.read(cfg_file_path)
except Exception:
raise ConfigError(f"Error Reading Config: '{cfg_file_path}'") from None
try:
server_cfg = config['server']
except KeyError:
raise ConfigError("No section [server] in config")
if server_cfg.get('enable_debug_logging', True):
logging.getLogger().setLevel(logging.DEBUG)
config['cmd_args'] = {
'logfile': cmd_line_args.logfile,
'socketfile': cmd_line_args.socketfile}
return ConfigHelper(server, config, 'server')

View File

@ -14,6 +14,7 @@ import json
import errno import errno
import tornado import tornado
import tornado.netutil import tornado.netutil
import confighelper
from tornado import gen from tornado import gen
from tornado.ioloop import IOLoop, PeriodicCallback from tornado.ioloop import IOLoop, PeriodicCallback
from tornado.util import TimeoutError from tornado.util import TimeoutError
@ -33,14 +34,16 @@ class Sentinel:
class Server: class Server:
error = ServerError error = ServerError
def __init__(self, args): def __init__(self, args):
self.host = args.address config = confighelper.get_configuration(self, args)
self.port = args.port self.host = config.get('host', "0.0.0.0")
self.port = config.getint('port', 7125)
# Event initialization # Event initialization
self.events = {} self.events = {}
# Klippy Connection Handling # Klippy Connection Handling
socketfile = os.path.normpath(os.path.expanduser(args.socketfile)) socketfile = config['cmd_args'].get('socketfile', "/tmp/moonraker")
socketfile = os.path.normpath(os.path.expanduser(socketfile))
self.klippy_server_sock = tornado.netutil.bind_unix_socket( self.klippy_server_sock = tornado.netutil.bind_unix_socket(
socketfile, backlog=1) socketfile, backlog=1)
self.remove_server_sock = tornado.netutil.add_accept_handler( self.remove_server_sock = tornado.netutil.add_accept_handler(
@ -48,23 +51,17 @@ class Server:
self.klippy_sock = None self.klippy_sock = None
self.is_klippy_connected = False self.is_klippy_connected = False
self.is_klippy_ready = False self.is_klippy_ready = False
self.server_configured = False self.moonraker_available = False
self.partial_data = b"" self.partial_data = b""
# Server/IOLoop # Server/IOLoop
self.server_running = False self.server_running = False
self.moonraker_app = app = MoonrakerApp(self, args) self.moonraker_app = app = MoonrakerApp(config)
self.io_loop = IOLoop.current()
self.init_cb = PeriodicCallback(self._initialize, INIT_MS)
# Plugin initialization
self.plugins = {}
self.register_endpoint = app.register_local_handler self.register_endpoint = app.register_local_handler
self.register_static_file_handler = app.register_static_file_handler self.register_static_file_handler = app.register_static_file_handler
self.register_upload_handler = app.register_upload_handler self.register_upload_handler = app.register_upload_handler
self.io_loop = IOLoop.current()
for plugin in CORE_PLUGINS: self.init_cb = PeriodicCallback(self._initialize, INIT_MS)
self.load_plugin(plugin)
# Setup remote methods accessable to Klippy. Note that all # Setup remote methods accessable to Klippy. Note that all
# registered remote methods should be of the notification type, # registered remote methods should be of the notification type,
@ -80,6 +77,10 @@ class Server:
self.register_remote_method( self.register_remote_method(
'process_status_update', self._process_status_update) 'process_status_update', self._process_status_update)
# Plugin initialization
self.plugins = {}
self._load_plugins(config)
def start(self): def start(self):
logging.info( logging.info(
"Starting Moonraker on (%s, %d)" % "Starting Moonraker on (%s, %d)" %
@ -88,7 +89,18 @@ class Server:
self.server_running = True self.server_running = True
# ***** Plugin Management ***** # ***** Plugin Management *****
def load_plugin(self, plugin_name, default=Sentinel): def _load_plugins(self, config):
# load core plugins
for plugin in CORE_PLUGINS:
self.load_plugin(config, plugin)
# check for optional plugins
opt_sections = set(config.sections()) - \
set(['server', 'authorization', 'cmd_args'])
for section in opt_sections:
self.load_plugin(config[section], section, None)
def load_plugin(self, config, plugin_name, default=Sentinel):
if plugin_name in self.plugins: if plugin_name in self.plugins:
return self.plugins[plugin_name] return self.plugins[plugin_name]
# Make sure plugin exists # Make sure plugin exists
@ -103,7 +115,7 @@ class Server:
module = importlib.import_module("plugins." + plugin_name) module = importlib.import_module("plugins." + plugin_name)
try: try:
load_func = getattr(module, "load_plugin") load_func = getattr(module, "load_plugin")
plugin = load_func(self) plugin = load_func(config)
except Exception: except Exception:
msg = "Unable to load plugin (%s)" % (plugin_name) msg = "Unable to load plugin (%s)" % (plugin_name)
logging.info(msg) logging.info(msg)
@ -215,13 +227,14 @@ class Server:
async def _initialize(self): async def _initialize(self):
await self._request_endpoints() await self._request_endpoints()
if not self.server_configured: if not self.moonraker_available:
await self._request_config() await self._check_available()
if not self.is_klippy_ready: elif not self.is_klippy_ready:
await self._request_ready() await self._check_ready()
if self.is_klippy_ready and self.server_configured: else:
# Make sure we have all registered endpoints # Moonraker is enabled in the Klippy module
await self._request_endpoints() # and Klippy is ready. We can stop the init
# procedure.
self.init_cb.stop() self.init_cb.stop()
async def _request_endpoints(self): async def _request_endpoints(self):
@ -236,20 +249,21 @@ class Server:
self.moonraker_app.register_static_file_handler( self.moonraker_app.register_static_file_handler(
sp['resource_id'], sp['file_path']) sp['resource_id'], sp['file_path'])
async def _request_config(self): async def _check_available(self):
request = self.make_request( request = self.make_request(
"moonraker/get_configuration", "GET", {}) "moonraker/check_available", "GET", {})
result = await request.wait() result = await request.wait()
if not isinstance(result, ServerError): if not isinstance(result, ServerError):
self._load_config(result) self.send_event("server:moonraker_available", result)
self.server_configured = True self.moonraker_available = True
else: else:
logging.info( logging.info(
"Error receiving configuration. This indicates a " "\nCheck for moonraker availability has failed. This "
"potential configuration issue in printer.cfg. Please check " "indicates that the [moonraker] section has not been added to "
"klippy.log for more information") "printer.cfg, or that Klippy has experienced an error "
"parsing its configuraton. Check klippy.log for more info.")
async def _request_ready(self): async def _check_ready(self):
request = self.make_request("info", "GET", {}) request = self.make_request("info", "GET", {})
result = await request.wait() result = await request.wait()
if not isinstance(result, ServerError): if not isinstance(result, ServerError):
@ -265,33 +279,6 @@ class Server:
"may have experienced an error during startup. Please check " "may have experienced an error during startup. Please check "
"klippy.log for more information") "klippy.log for more information")
def _load_config(self, config):
self.moonraker_app.load_config(config)
# load config for core plugins
for plugin_name in CORE_PLUGINS:
plugin = self.plugins[plugin_name]
if hasattr(plugin, "load_config"):
plugin.load_config(config)
# Load and apply optional plugin Configuration
plugin_cfgs = {name[7:]: cfg for name, cfg in config.items()
if name.startswith("plugin_")}
for name, cfg in plugin_cfgs.items():
plugin = self.plugins.get(name)
if plugin is None:
plugin = self.load_plugin(name, None)
if hasattr(plugin, "load_config"):
plugin.load_config(cfg)
# Remove plugins that are loaded but no longer configured
valid_plugins = CORE_PLUGINS + list(plugin_cfgs.keys())
self.io_loop.spawn_callback(self._prune_plugins, valid_plugins)
async def _prune_plugins(self, valid_plugins):
for name, plugin in self.plugins.items():
if name not in valid_plugins:
if hasattr(plugin, "close"):
await plugin.close()
self.plugins.pop(name, None)
def _handle_klippy_response(self, request_id, response): def _handle_klippy_response(self, request_id, response):
req = self.pending_requests.pop(request_id, None) req = self.pending_requests.pop(request_id, None)
if req is not None: if req is not None:
@ -345,7 +332,7 @@ class Server:
def close_client_sock(self): def close_client_sock(self):
self.is_klippy_ready = False self.is_klippy_ready = False
self.server_configured = False self.moonraker_available = False
self.init_cb.stop() self.init_cb.stop()
for request in self.pending_requests.values(): for request in self.pending_requests.values():
request.notify(ServerError("Klippy Disconnected", 503)) request.notify(ServerError("Klippy Disconnected", 503))
@ -407,23 +394,15 @@ def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Moonraker - Klipper API Server") description="Moonraker - Klipper API Server")
parser.add_argument( parser.add_argument(
"-a", "--address", default='0.0.0.0', metavar='<address>', "-c", "--configfile", default="~/moonraker.conf",
help="host name or ip to bind to the Web Server") metavar='<configfile>',
parser.add_argument( help="Location of moonraker configuration file")
"-p", "--port", type=int, default=7125, metavar='<port>',
help="port the Web Server will listen on")
parser.add_argument( parser.add_argument(
"-s", "--socketfile", default="/tmp/moonraker", metavar='<socketfile>', "-s", "--socketfile", default="/tmp/moonraker", metavar='<socketfile>',
help="file name and location for the Unix Domain Socket") help="file name and location for the Unix Domain Socket")
parser.add_argument( parser.add_argument(
"-l", "--logfile", default="/tmp/moonraker.log", metavar='<logfile>', "-l", "--logfile", default="/tmp/moonraker.log", metavar='<logfile>',
help="log file name and location") help="log file name and location")
parser.add_argument(
"-k", "--apikey", default="~/.moonraker_api_key",
metavar='<apikeyfile>', help="API Key file location")
parser.add_argument(
"-d", "--debug", action='store_true',
help="Enable Debug Logging")
cmd_line_args = parser.parse_args() cmd_line_args = parser.parse_args()
# Setup Logging # Setup Logging
@ -433,10 +412,7 @@ def main():
file_hdlr = MoonrakerLoggingHandler( file_hdlr = MoonrakerLoggingHandler(
log_file, when='midnight', backupCount=2) log_file, when='midnight', backupCount=2)
root_logger.addHandler(file_hdlr) root_logger.addHandler(file_hdlr)
if cmd_line_args.debug: root_logger.setLevel(logging.INFO)
root_logger.setLevel(logging.DEBUG)
else:
root_logger.setLevel(logging.INFO)
formatter = logging.Formatter( formatter = logging.Formatter(
'%(asctime)s [%(filename)s:%(funcName)s()] - %(message)s') '%(asctime)s [%(filename)s:%(funcName)s()] - %(message)s')
file_hdlr.setFormatter(formatter) file_hdlr.setFormatter(formatter)
@ -454,7 +430,7 @@ def main():
server = Server(cmd_line_args) server = Server(cmd_line_args)
except Exception: except Exception:
logging.exception("Moonraker Error") logging.exception("Moonraker Error")
return exit(1)
try: try:
server.start() server.start()
io_loop.start() io_loop.start()