Extracted system commands into their own proper API & ViewModel

This commit is contained in:
Gina Häußge 2015-10-01 14:05:23 +02:00
parent 31bc7c1f3e
commit 6ab44849cd
6 changed files with 278 additions and 69 deletions

View file

@ -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

View file

@ -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/<action>".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/<string:source>", 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/<string:source>/<string:command>", 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]

View file

@ -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',

View file

@ -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 = "<p>" + _.sprintf(gettext("The command \"%(command)s\" could not be executed."), {command: action.name}) + "</p>";
error += pnotifyAdditionalInfo("<pre>" + jqXHR.responseText + "</pre>");
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"
]);
});

View file

@ -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 = "<p>" + _.sprintf(gettext("The command \"%(command)s\" could not be executed."), {command: commandSpec.name}) + "</p>";
error += pnotifyAdditionalInfo("<pre>" + jqXHR.responseText + "</pre>");
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"],
[]
]);
});

View file

@ -2,11 +2,11 @@
<i class="icon-off"></i> {{ _('System') }}
<b class="caret"></b>
</a>
<ul class="dropdown-menu" data-bind="foreach: systemActions">
<ul class="dropdown-menu" data-bind="foreach: system.systemActions">
<!-- ko if: action == "divider" -->
<li class="divider" />
<!-- /ko -->
<!-- ko if: action != "divider" -->
<li><a href="#" data-bind="click: $root.triggerAction, text: name"></a></li>
<li><a href="#" data-bind="click: $root.system.triggerCommand, text: name"></a></li>
<!-- /ko -->
</ul>