Extracted system commands into their own proper API & ViewModel
This commit is contained in:
parent
31bc7c1f3e
commit
6ab44849cd
6 changed files with 278 additions and 69 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
176
src/octoprint/server/api/system.py
Normal file
176
src/octoprint/server/api/system.py
Normal 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]
|
||||
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
96
src/octoprint/static/js/app/viewmodels/system.js
Normal file
96
src/octoprint/static/js/app/viewmodels/system.js
Normal 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"],
|
||||
[]
|
||||
]);
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue