#!/usr/bin/env python3
# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab
#########################################################################
# Copyright 2018- Martin Sinn m.sinn@gmx.de
#########################################################################
# This file is part of SmartHomeNG.
#
# SmartHomeNG is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SmartHomeNG is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SmartHomeNG. If not, see <http://www.gnu.org/licenses/>.
#########################################################################
import os
import shutil
import logging
import json
import cherrypy
import lib.shyaml as shyaml
import lib.config
from lib.module import Modules
from lib.plugin import Plugins
from lib.metadata import Metadata
from lib.model.smartplugin import SmartPlugin
from lib.constants import (KEY_CLASS_PATH, YAML_FILE, DIR_PLUGINS)
from .rest import RESTResource
[Doku]class PluginController(RESTResource):
def __init__(self, module, jwt_secret=False):
self._sh = module._sh
self.base_dir = self._sh.get_basedir()
self.plugins_dir = self._sh.get_config_dir(DIR_PLUGINS)
self.logger = logging.getLogger(__name__.split('.')[0] + '.' + __name__.split('.')[1] + '.' + __name__.split('.')[2][4:])
self.logger.info("PluginController(): __init__")
self.plugins = Plugins.get_instance()
self.plugin_data = {}
self.jwt_secret = jwt_secret
return
[Doku] def get_body(self):
"""
Get content body of received request header
:return:
"""
cl = cherrypy.request.headers.get('Content-Length', 0)
if cl == 0:
# cherrypy.reponse.headers["Status"] = "400"
# return 'Bad request'
raise cherrypy.HTTPError(status=411)
rawbody = cherrypy.request.body.read(int(cl))
self.logger.debug("PluginController(): ___(): rawbody = {}".format(rawbody))
try:
params = json.loads(rawbody.decode('utf-8'))
except Exception as e:
self.logger.warning("PluginController(): ___(): Exception {}".format(e))
return None
return params
[Doku] def test_for_old_config(self, config_filename):
# make it 'readonly', if plugin.conf is used
result = not(os.path.splitext(config_filename)[1].lower() == '.yaml')
_etc_dir = os.path.dirname(config_filename)
if not result:
# for beta-testing: create a backup of ../etc/plugin.yaml
if not os.path.isfile(os.path.join(_etc_dir, 'plugin_before_admin_config.yaml')):
shutil.copy2(config_filename, os.path.join(_etc_dir, 'plugin_before_admin_config.yaml'))
self.logger.warning('Created a backup copy of plugin.yaml ({})'.format(os.path.join(_etc_dir, 'plugin_before_admin_config.yaml')))
return result
[Doku] def get_config_filename(self):
if self.plugins is None:
self.plugins = Plugins.get_instance()
return self.plugins._get_plugin_conf_filename()
# ======================================================================
# GET /api/plugin
#
[Doku] def read(self, id=None):
"""
return an object with type info about all installed plugins
"""
self.logger.info("PluginController(): index('{}')".format(id))
config_filename = self.get_config_filename()
info = {}
info['_readonly'] = self.test_for_old_config(config_filename)
# get path to plugin configuration file, without extension
_conf = lib.config.parse_basename(os.path.splitext(config_filename)[0], configtype='plugin')
plg_found = False
if id is not None:
for confplg in _conf:
if (confplg == id) or (id == None):
self.logger.info(f"PluginController(): index('{id}') - confplg {confplg}")
info['config'] = _conf[confplg]
plg_found = True
if plg_found:
return json.dumps(info)
raise cherrypy.NotFound
read.expose_resource = True
read.authentication_needed = True
[Doku] def add(self, id=None):
self.logger.info("PluginController(): add('{}')".format(id))
params = self.get_body()
if params is None:
self.logger.warning("PluginController(): add(): section '{}': Bad, add request".format(id))
raise cherrypy.HTTPError(status=411)
self.logger.info("PluginController(): add(): section '{}' = {}".format(id, params))
config_filename = self.get_config_filename()
if self.test_for_old_config(config_filename):
# make it 'readonly', if plugin.conf is used
response = {'result': 'error', 'description': 'Updateing .CONF files is not supported'}
else:
response = {}
plugin_conf = shyaml.yaml_load_roundtrip(config_filename)
sect = plugin_conf.get(id)
if sect is not None:
response = {'result': 'error', 'description': "Configuration section '{}' already exists".format(id)}
else:
plugin_conf[id] = params.get('config', {})
shyaml.yaml_save_roundtrip(config_filename, plugin_conf, False)
response = {'result': 'ok'}
self.logger.info("PluginController(): add(): response = {}".format(response))
return json.dumps(response)
add.expose_resource = True
add.authentication_needed = True
[Doku] def handle_plugin_action(self, id, action):
if self.plugins is None:
self.plugins = Plugins.get_instance()
plugin = self.plugins.return_plugin(id)
if plugin is None:
response = {'result': 'error', 'description': "No running plugin instance found for '{}'".format(id)}
return response
response = {}
if action == 'start':
self.logger.info("PluginController.handle_plugin_action(): Starting plugin '{}'".format(id))
plugin.run()
response = {'result': 'ok'}
elif action == 'stop':
self.logger.info("PluginController.handle_plugin_action(): Stopping plugin '{}'".format(id))
plugin.stop()
response = {'result': 'ok'}
return response
[Doku] def update(self, id='', action=''):
self.logger.info("PluginController.update(id='{}', action='{}')".format(id, action))
if action == '':
# Update section for plugin in etc/plugin.yaml
params = self.get_body()
if params is None:
self.logger.warning("PluginController.update(): section '{}': Bad, add request".format(id))
raise cherrypy.HTTPError(status=411)
self.logger.info("PluginController.update(): section '{}' = {}".format(id, params))
config_filename = self.get_config_filename()
if self.test_for_old_config(config_filename):
# make it 'readonly', if plugin.conf is used
response = {'result': 'error', 'description': 'Updateing .CONF files is not supported'}
else:
response = {}
plugin_conf = shyaml.yaml_load_roundtrip(config_filename)
sect = plugin_conf.get(id)
if sect is None:
response = {'result': 'error', 'description': "Configuration section '{}' does not exist".format(id)}
else:
self.logger.debug("update: params = {}".format(params))
if params.get('config', {}).get('plugin_enabled', None) == True:
del params['config']['plugin_enabled']
plugin_conf[id] = params.get('config', {})
shyaml.yaml_save_roundtrip(config_filename, plugin_conf, False)
response = {'result': 'ok'}
elif action in ['start','stop']:
response = self.handle_plugin_action(id, action)
else:
response = {'result': 'error', 'description': "Plugin '{}': unknown action '{}'".format(id, action)}
self.logger.warning("PluginController.update(): " + response['description'])
self.logger.info("PluginController.update(): response = {}".format(response))
return json.dumps(response)
update.expose_resource = True
update.authentication_needed = True
[Doku] @cherrypy.expose
def delete(self, id=None):
self.logger.info("PluginController(): delete('{}')".format(id))
config_filename = self.get_config_filename()
if self.test_for_old_config(config_filename):
# make it 'readonly', if plugin.conf is used
response = {'result': 'error', 'description': 'Updateing .CONF files is not supported'}
else:
response = {}
plugin_conf = shyaml.yaml_load_roundtrip(config_filename)
sect = plugin_conf.pop(id, None)
if sect is None:
response = {'result': 'error', 'description': "Configuration section '{}' does not exist".format(id)}
else:
shyaml.yaml_save_roundtrip(config_filename, plugin_conf, False)
response = {'result': 'ok'}
self.logger.info("PluginController(): delete(): response = {}".format(response))
return json.dumps(response)
delete.expose_resource = True
delete.authentication_needed = True