Merge branch 'devel' into dev/clientlib

Conflicts:
	src/octoprint/server/api/__init__.py
	src/octoprint/static/js/app/viewmodels/navigation.js
This commit is contained in:
Gina Häußge 2015-10-01 14:22:15 +02:00
commit 8875f257ea
11 changed files with 430 additions and 107 deletions

View file

@ -933,7 +933,7 @@ class PluginManager(object):
additional_pre_inits=additional_pre_inits,
additional_post_inits=additional_post_inits)
self.logger.info("Initialized {count} plugin(s)".format(count=len(self.plugin_implementations)))
self.logger.info("Initialized {count} plugin implementation(s)".format(count=len(self.plugin_implementations)))
def initialize_implementation_of_plugin(self, name, plugin, additional_injects=None, additional_inject_factories=None, additional_pre_inits=None, additional_post_inits=None):
if plugin.implementation is None:
@ -1027,15 +1027,13 @@ class PluginManager(object):
self.logger.info("No plugins available")
else:
self.logger.info("{count} plugin(s) registered with the system:\n{plugins}".format(count=len(all_plugins), plugins="\n".join(
sorted(
map(lambda x: "| " + x.long_str(show_bundled=show_bundled,
bundled_strs=bundled_str,
show_location=show_location,
location_str=location_str,
show_enabled=show_enabled,
enabled_strs=enabled_str),
self.enabled_plugins.values())
)
map(lambda x: "| " + x.long_str(show_bundled=show_bundled,
bundled_strs=bundled_str,
show_location=show_location,
location_str=location_str,
show_enabled=show_enabled,
enabled_strs=enabled_str),
sorted(self.plugins.values(), key=lambda x: str(x).lower()))
)))
def get_plugin(self, identifier, require_enabled=True):

View file

@ -72,6 +72,7 @@ $(function() {
self.loginState = parameters[0];
self.settingsViewModel = parameters[1];
self.printerState = parameters[2];
self.systemViewModel = parameters[3];
self.config_repositoryUrl = ko.observable();
self.config_repositoryTtl = ko.observable();
@ -171,6 +172,20 @@ $(function() {
self.workingDialog = undefined;
self.workingOutput = undefined;
self.restartCommandSpec = undefined;
self.systemViewModel.systemActions.subscribe(function() {
var lastResponse = self.systemViewModel.lastCommandResponse;
if (!lastResponse || !lastResponse.core) {
self.restartCommandSpec = undefined;
return;
}
var restartSpec = _.filter(lastResponse.core, function(spec) { return spec.action == "restart" });
self.restartCommandSpec = restartSpec != undefined && restartSpec.length > 0 ? restartSpec[0] : undefined;
});
self.notifications = [];
self.enableManagement = ko.computed(function() {
return !self.printerState.isPrinting();
});
@ -535,15 +550,65 @@ $(function() {
};
self._displayNotification = function(response, titleSuccess, textSuccess, textRestart, textReload, titleError, textError) {
var notification;
var beforeClose = function(notification) {
self.notifications = _.without(self.notifications, notification);
};
if (response.result) {
if (response.needs_restart) {
new PNotify({
var options = {
title: titleSuccess,
text: textRestart,
buttons: {
closer: false,
sticker: false
},
callbacks: {
before_close: beforeClose
},
hide: false
});
};
if (self.restartCommandSpec) {
options.confirm = {
confirm: true,
buttons: [{
text: gettext("Restart now"),
click: function () {
showConfirmationDialog({
message: gettext("This will restart your OctoPrint server."),
onproceed: function() {
$.ajax({
url: self.restartCommandSpec.resource,
type: "POST",
dataType: "json",
data: "{}",
contentType: "application/json; charset=UTF-8",
success: function() {
new PNotify({
title: gettext("Restart in progress"),
text: gettext("The server is now being restarted in the background")
})
},
error: function() {
new PNotify({
title: gettext("Something went wrong"),
text: gettext("Trying to restart the server produced an error, please check octoprint.log for details. You'll have to restart manually.")
})
}
});
}
});
}
}]
}
}
notification = PNotify.singleButtonNotify(options);
} else if (response.needs_refresh) {
new PNotify({
notification = PNotify.singleButtonNotify({
title: titleSuccess,
text: textReload,
confirm: {
@ -559,24 +624,35 @@ $(function() {
closer: false,
sticker: false
},
callbacks: {
before_close: beforeClose
},
hide: false
})
} else {
new PNotify({
notification = new PNotify({
title: titleSuccess,
text: textSuccess,
type: "success",
callbacks: {
before_close: beforeClose
},
hide: false
})
}
} else {
new PNotify({
notification = new PNotify({
title: titleError,
text: textError,
type: "error",
callbacks: {
before_close: beforeClose
},
hide: false
});
}
self.notifications.push(notification);
};
self._markWorking = function(title, line) {
@ -621,6 +697,16 @@ $(function() {
self.onUserLoggedIn = function(user) {
if (user.admin) {
self.requestData();
} else {
self.onUserLoggedOut();
}
};
self.onUserLoggedOut = function() {
if (self.notifications) {
_.each(self.notifications, function(notification) {
notification.remove();
});
}
};
@ -774,5 +860,9 @@ $(function() {
}
// view model class, parameters for constructor, container to bind to
ADDITIONAL_VIEWMODELS.push([PluginManagerViewModel, ["loginStateViewModel", "settingsViewModel", "printerStateViewModel"], "#settings_plugin_pluginmanager"]);
ADDITIONAL_VIEWMODELS.push([
PluginManagerViewModel,
["loginStateViewModel", "settingsViewModel", "printerStateViewModel", "systemViewModel"],
"#settings_plugin_pluginmanager"
]);
});

View file

@ -6,8 +6,9 @@
{% macro pluginmanager_nopip() %}
<div class="alert" data-bind="visible: !pipAvailable()">{% trans %}
The <code>pip</code> command could not be found.
Please configure it manually. No installation and uninstallation of plugin
The <code>pip</code> command could not be found or does not work correctly
for this installation of OctoPrint - please consult the log file for details
and if necessary configure it manually. No installation and uninstallation of plugin
packages is possible while <code>pip</code> is unavailable.
{% endtrans %}</div>
{% endmacro %}

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,45 +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__)
data = request.values
if hasattr(request, "json") and request.json:
data = request.json
if "action" in data:
action = data["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

@ -80,6 +80,26 @@ $(function() {
PNotify.prototype.options.styling = "bootstrap2";
PNotify.prototype.options.mouse_reset = false;
PNotify.singleButtonNotify = function(options) {
if (!options.confirm || !options.confirm.buttons || !options.confirm.buttons.length) {
return new PNotify(options);
}
var autoDisplay = options.auto_display != false;
var params = $.extend(true, {}, options);
params.auto_display = false;
var notify = new PNotify(params);
notify.options.confirm.buttons = [notify.options.confirm.buttons[0]];
notify.modules.confirm.makeDialog(notify, notify.options.confirm);
if (autoDisplay) {
notify.open();
}
return notify;
};
//~~ Initialize view models
// the view model map is our basic look up table for dependencies that may be injected into other view models

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,36 +16,11 @@ $(function() {
return classes;
});
self.triggerAction = function(action) {
var callback = function() {
OctoPrint.control.executeSystemCommand(action.action)
.done(function() {
new PNotify({title: gettext("Success"), text: _.sprintf(gettext("The command \"%(command)s\" executed successfully"), {command: action.name}), type: "success"});
})
.fail(function() {
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,98 @@
$(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 = [];
if (response.core && response.core.length) {
_.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>

View file

@ -198,7 +198,7 @@ class PipCaller(CommandlineCaller):
ok, pip_user, pip_virtual_env, pip_install_dir = self._check_pip_setup(pip_command)
if not ok:
self._logger.error("Pip install directory {} is not writable and is part of a virtual environment, can't use this constellation".format(pip_install_dir))
self._logger.error("Cannot use pip at {}".format(pip_command))
return
self._logger.info("pip at {} installs to {}, --user flag needed => {}, virtual env => {}".format(pip_command, pip_install_dir, "yes" if pip_user else "no", "yes" if pip_virtual_env else "no"))
@ -299,26 +299,29 @@ class PipCaller(CommandlineCaller):
cwd=testballoon)
output = p.stdout.text
self._logger.debug("Got output from {}: {}".format(" ".join(sarge_command), output))
install_dir_match = self.__class__.pip_install_dir_regex.search(output)
virtual_env_match = self.__class__.pip_virtual_env_regex.search(output)
writable_match = self.__class__.pip_writable_regex.search(output)
if install_dir_match and virtual_env_match and writable_match:
install_dir = install_dir_match.group(1)
virtual_env = virtual_env_match.group(1) == "True"
writable = writable_match.group(1) == "True"
# ok, enable user flag, virtual env yes/no, installation dir
result = writable or not virtual_env, \
not writable and not virtual_env and site.ENABLE_USER_SITE, \
virtual_env, \
install_dir
_cache["setup"][pip_command] = result
return result
except:
self._logger.exception("Error while trying to install testballoon to figure out pip setup")
return False, False, False, None
finally:
sarge_command = [pip_command, "uninstall", "-y", "OctoPrint-PipTestBalloon"]
sarge.run(sarge_command, stdout=sarge.Capture(), stderr=sarge.Capture())
install_dir_match = self.__class__.pip_install_dir_regex.search(output)
virtual_env_match = self.__class__.pip_virtual_env_regex.search(output)
writable_match = self.__class__.pip_writable_regex.search(output)
if install_dir_match and virtual_env_match and writable_match:
install_dir = install_dir_match.group(1)
virtual_env = virtual_env_match.group(1) == "True"
writable = writable_match.group(1) == "True"
# ok, enable user flag, virtual env yes/no, installation dir
result = writable or not virtual_env, \
not writable and not virtual_env and site.ENABLE_USER_SITE, \
virtual_env, \
install_dir
_cache["setup"][pip_command] = result
return result
else:
self._logger.debug("Could not detect desired output from testballoon install, got this instead: {}".format(" ".join(sarge_command), output))
return False, False, False, None