diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 7bb87bc4..6b924236 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -38,6 +38,7 @@ from . import log as api_logs from . import slicing as api_slicing from . import printer_profiles as api_printer_profiles from . import languages as api_languages +from . import system as api_system VERSION = "0.1" @@ -177,40 +178,6 @@ def apiVersion(): "api": VERSION }) -#~~ system control - - -@api.route("/system", methods=["POST"]) -@restricted_access -@admin_permission.require(403) -def performSystemAction(): - logger = logging.getLogger(__name__) - if "action" in request.values.keys(): - action = request.values["action"] - available_actions = s().get(["system", "actions"]) - for availableAction in available_actions: - if availableAction["action"] == action: - async = availableAction["async"] if "async" in availableAction else False - ignore = availableAction["ignore"] if "ignore" in availableAction else False - logger.info("Performing command: %s" % availableAction["command"]) - try: - # we run this with shell=True since we have to trust whatever - # our admin configured as command and since we want to allow - # shell-alike handling here... - p = sarge.run(availableAction["command"], stderr=sarge.Capture(), shell=True, async=async) - if not async: - if not ignore and p.returncode != 0: - returncode = p.returncode - stderr_text = p.stderr.text - logger.warn("Command failed with return code %i: %s" % (returncode, stderr_text)) - return make_response(("Command failed with return code %i: %s" % (returncode, stderr_text), 500, [])) - except Exception, e: - if not ignore: - logger.warn("Command failed: %s" % e) - return make_response(("Command failed: %s" % e, 500, [])) - break - return NO_CONTENT - #~~ Login/user handling diff --git a/src/octoprint/server/api/system.py b/src/octoprint/server/api/system.py new file mode 100644 index 00000000..fba71f43 --- /dev/null +++ b/src/octoprint/server/api/system.py @@ -0,0 +1,176 @@ +# coding=utf-8 +from __future__ import absolute_import + +__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' +__copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License" + +import collections +import logging +import sarge + +from flask import request, make_response, jsonify, url_for +from flask.ext.babel import gettext + +from octoprint.settings import settings as s + +from octoprint.server import admin_permission, NO_CONTENT +from octoprint.server.api import api +from octoprint.server.util.flask import restricted_access, get_remote_address + + +@api.route("/system", methods=["POST"]) +@restricted_access +@admin_permission.require(403) +def performSystemAction(): + logging.getLogger(__name__).warn("Deprecated API call to /api/system made by {}, should be migrated to use /system/commands/custom/".format(get_remote_address(request))) + + data = request.values + if hasattr(request, "json") and request.json: + data = request.json + + if not "action" in data: + return make_response("action for perform is not defined", 400) + + return executeSystemCommand("custom", data["action"]) + + +@api.route("/system/commands", methods=["GET"]) +@restricted_access +@admin_permission.require(403) +def retrieveSystemCommands(): + return jsonify(core=_to_client_specs(_get_core_command_specs()), + custom=_to_client_specs(_get_custom_command_specs())) + + +@api.route("/system/commands/", methods=["GET"]) +@restricted_access +@admin_permission.require(403) +def retrieveSystemCommandsForSource(source): + if source == "core": + specs = _get_core_command_specs() + elif source == "custom": + specs = _get_custom_command_specs() + else: + return make_response("Unknown system command source: {}".format(source), 400) + + return jsonify(_to_client_specs(specs)) + + +@api.route("/system/commands//", methods=["POST"]) +@restricted_access +@admin_permission.require(403) +def executeSystemCommand(source, command): + logger = logging.getLogger(__name__) + + command_spec = _get_command_spec(source, command) + if not command_spec: + return make_response("Command {}:{} not found".format(source, command), 404) + + if not "command" in command_spec: + return make_response("Command {}:{} does not define a command to execute, can't proceed".format(source, command), 500) + + async = command_spec["async"] if "async" in command_spec else False + ignore = command_spec["ignore"] if "ignore" in command_spec else False + logger.info("Performing command for {}:{}: {}".format(source, command, command_spec["command"])) + try: + # we run this with shell=True since we have to trust whatever + # our admin configured as command and since we want to allow + # shell-alike handling here... + p = sarge.run(command_spec["command"], + stdout=sarge.Capture(), + stderr=sarge.Capture(), + shell=True, + async=async) + if not async: + if not ignore and p.returncode != 0: + returncode = p.returncode + stdout_text = p.stdout.text + stderr_text = p.stderr.text + + error = "Command failed with return code {}:\nSTDOUT: {}\nSTDERR: {}".format(returncode, stdout_text, stderr_text) + logger.warn(error) + return make_response(error, 500) + except Exception, e: + if not ignore: + error = "Command failed: {}".format(str(e)) + logger.warn(error) + return make_response(error, 500) + + return NO_CONTENT + + +def _to_client_specs(specs): + result = list() + for spec in specs.values(): + if not "action" in spec or not "source" in spec: + continue + copied = dict((k, v) for k, v in spec.items() if k in ("source", "action", "name", "confirm")) + copied["resource"] = url_for(".executeSystemCommand", + source=spec["source"], + command=spec["action"], + _external=True) + result.append(copied) + return result + + +def _get_command_spec(source, action): + if source == "core": + return _get_core_command_spec(action) + elif source == "custom": + return _get_custom_command_spec(action) + else: + return None + + +def _get_core_command_specs(): + commands = collections.OrderedDict( + shutdown=dict( + command=s().get(["server", "commands", "systemShutdownCommand"]), + name=gettext("Shutdown"), + confirm=gettext("You are about to shutdown the system.")), + reboot=dict( + command=s().get(["server", "commands", "systemRestartCommand"]), + name=gettext("Reboot"), + confirm=gettext("You are about to reboot the system.")), + restart=dict( + command=s().get(["server", "commands", "serverRestartCommand"]), + name="Restart OctoPrint", + confirm="You are about to restart the OctoPrint server.") + ) + + available_commands = dict() + for action, spec in commands.items(): + if not spec["command"]: + continue + spec.update(dict(action=action, source="core", async=True, ignore=True)) + available_commands[action] = spec + return available_commands + + +def _get_core_command_spec(action): + available_actions = _get_core_command_specs() + if not action in available_actions: + logging.getLogger(__name__).warn("Command for core action {} is not configured, you need to configure the command before it can be used".format(action)) + return None + + return available_actions[action] + + +def _get_custom_command_specs(): + specs = collections.OrderedDict() + for spec in s().get(["system", "actions"]): + if not "action" in spec: + continue + copied = dict(spec) + copied["source"] = "custom" + specs[spec["action"]] = copied + return specs + + +def _get_custom_command_spec(action): + available_actions = _get_custom_command_specs() + if not action in available_actions: + return None + + return available_actions[action] + diff --git a/src/octoprint/server/util/flask.py b/src/octoprint/server/util/flask.py index 4fb4d6cb..b1c386af 100644 --- a/src/octoprint/server/util/flask.py +++ b/src/octoprint/server/util/flask.py @@ -631,6 +631,7 @@ def collect_plugin_assets(enable_gcodeviewer=True, preferred_stylesheet="css"): 'js/app/viewmodels/printerprofiles.js', 'js/app/viewmodels/settings.js', 'js/app/viewmodels/slicing.js', + 'js/app/viewmodels/system.js', 'js/app/viewmodels/temperature.js', 'js/app/viewmodels/terminal.js', 'js/app/viewmodels/timelapse.js', diff --git a/src/octoprint/static/js/app/viewmodels/navigation.js b/src/octoprint/static/js/app/viewmodels/navigation.js index 736fc949..37bebbe8 100644 --- a/src/octoprint/static/js/app/viewmodels/navigation.js +++ b/src/octoprint/static/js/app/viewmodels/navigation.js @@ -6,8 +6,7 @@ $(function() { self.appearance = parameters[1]; self.settings = parameters[2]; self.usersettings = parameters[3]; - - self.systemActions = self.settings.system_actions; + self.system = parameters[4]; self.appearanceClasses = ko.computed(function() { var classes = self.appearance.color(); @@ -17,41 +16,11 @@ $(function() { return classes; }); - self.triggerAction = function(action) { - var callback = function() { - $.ajax({ - url: API_BASEURL + "system", - type: "POST", - dataType: "json", - data: "action=" + action.action, - success: function() { - new PNotify({title: "Success", text: _.sprintf(gettext("The command \"%(command)s\" executed successfully"), {command: action.name}), type: "success"}); - }, - error: function(jqXHR, textStatus, errorThrown) { - if (!action.hasOwnProperty("ignore") || !action.ignore) { - var error = "

" + _.sprintf(gettext("The command \"%(command)s\" could not be executed."), {command: action.name}) + "

"; - error += pnotifyAdditionalInfo("
" + jqXHR.responseText + "
"); - new PNotify({title: gettext("Error"), text: error, type: "error", hide: false}); - } - } - }) - }; - if (action.confirm) { - showConfirmationDialog({ - message: action.confirm, - onproceed: function(e) { - callback(); - } - }); - } else { - callback(); - } - } } OCTOPRINT_VIEWMODELS.push([ NavigationViewModel, - ["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "userSettingsViewModel"], + ["loginStateViewModel", "appearanceViewModel", "settingsViewModel", "userSettingsViewModel", "systemViewModel"], "#navbar" ]); }); diff --git a/src/octoprint/static/js/app/viewmodels/system.js b/src/octoprint/static/js/app/viewmodels/system.js new file mode 100644 index 00000000..fd2307e0 --- /dev/null +++ b/src/octoprint/static/js/app/viewmodels/system.js @@ -0,0 +1,96 @@ +$(function() { + function SystemViewModel(parameters) { + var self = this; + + self.loginState = parameters[0]; + + self.lastCommandResponse = undefined; + self.systemActions = ko.observableArray([]); + + self.requestData = function() { + self.requestCommandData(); + }; + + self.requestCommandData = function() { + if (!self.loginState.isAdmin()) { + return; + } + + $.ajax({ + url: API_BASEURL + "system/commands", + type: "GET", + dataType: "json", + success: self.fromCommandResponse + }); + }; + + self.fromCommandResponse = function(response) { + var actions = []; + _.each(response.core, function(data) { + var action = _.extend({}, data); + action.actionSource = "core"; + actions.push(action); + }); + actions.push({action: "divider"}); + _.each(response.custom, function(data) { + var action = _.extend({}, data); + action.actionSource = "custom"; + actions.push(action); + }); + self.lastCommandResponse = response; + self.systemActions(actions); + }; + + self.triggerCommand = function(commandSpec) { + var callback = function() { + $.ajax({ + url: commandSpec.resource, + type: "POST", + dataType: "json", + data: "{}", + contentType: "application/json; charset=UTF-8", + success: function() { + new PNotify({title: "Success", text: _.sprintf(gettext("The command \"%(command)s\" executed successfully"), {command: commandSpec.name}), type: "success"}); + }, + error: function(jqXHR, textStatus, errorThrown) { + if (!commandSpec.hasOwnProperty("ignore") || !commandSpec.ignore) { + var error = "

" + _.sprintf(gettext("The command \"%(command)s\" could not be executed."), {command: commandSpec.name}) + "

"; + error += pnotifyAdditionalInfo("
" + jqXHR.responseText + "
"); + new PNotify({title: gettext("Error"), text: error, type: "error", hide: false}); + } + } + }) + }; + if (commandSpec.confirm) { + showConfirmationDialog({ + message: commandSpec.confirm, + onproceed: function(e) { + callback(); + } + }); + } else { + callback(); + } + }; + + self.onUserLoggedIn = function(user) { + if (user.admin) { + self.requestData(); + } else { + self.onUserLoggedOut(); + } + }; + + self.onUserLoggedOut = function() { + self.lastCommandResponse = undefined; + self.systemActions([]); + } + } + + // view model class, parameters for constructor, container to bind to + ADDITIONAL_VIEWMODELS.push([ + SystemViewModel, + ["loginStateViewModel"], + [] + ]); +}); diff --git a/src/octoprint/templates/navbar/systemmenu.jinja2 b/src/octoprint/templates/navbar/systemmenu.jinja2 index a2ee3f3b..603d2a09 100644 --- a/src/octoprint/templates/navbar/systemmenu.jinja2 +++ b/src/octoprint/templates/navbar/systemmenu.jinja2 @@ -2,11 +2,11 @@ {{ _('System') }} -