def action(self) -> ActionResult: varname = html.request.var("_varname") if not varname: return None action = html.request.var("_action") config_variable = config_variable_registry[varname]() def_value = self._default_values[varname] if not html.check_transaction(): return None if varname in self._current_settings: self._current_settings[ varname] = not self._current_settings[varname] else: self._current_settings[varname] = not def_value msg = _("Changed Configuration variable %s to %s.") % ( varname, "on" if self._current_settings[varname] else "off") watolib.save_global_settings(self._current_settings) watolib.add_change("edit-configvar", msg, domains=[config_variable.domain()], need_restart=config_variable.need_restart()) if action == "_reset": flash(msg) return redirect(mode_url("globalvars"))
def action(self): delname = html.request.var("_delete") if delname and html.transaction_valid(): if delname in watolib.timeperiods.builtin_timeperiods(): raise MKUserError("_delete", _("Builtin timeperiods can not be modified")) usages = self._find_usages_of_timeperiod(delname) if usages: message = "<b>%s</b><br>%s:<ul>" % \ (_("You cannot delete this timeperiod."), _("It is still in use by")) for title, link in usages: message += '<li><a href="%s">%s</a></li>\n' % (link, title) message += "</ul>" raise MKUserError(None, message) c = wato_confirm( _("Confirm deletion of time period %s") % delname, _("Do you really want to delete the time period '%s'? I've checked it: " "it is not being used by any rule or user profile right now." ) % delname) if c: del self._timeperiods[delname] watolib.timeperiods.save_timeperiods(self._timeperiods) watolib.add_change("edit-timeperiods", _("Deleted timeperiod %s") % delname) elif c is False: return ""
def action(self) -> ActionResult: if not html.check_transaction(): return redirect(self.mode_url()) if html.request.var("_delete"): delid = html.request.get_ascii_input_mandatory("_delete") if delid not in self._roles: raise MKUserError(None, _("This role does not exist.")) if html.transaction_valid() and self._roles[delid].get('builtin'): raise MKUserError(None, _("You cannot delete the builtin roles!")) users = userdb.load_users() for user in users.values(): if delid in user["roles"]: raise MKUserError( None, _("You cannot delete roles, that are still in use (%s)!" % delid)) self._rename_user_role(delid, None) # Remove from existing users del self._roles[delid] self._save_roles() watolib.add_change("edit-roles", _("Deleted role '%s'") % delid, sites=config.get_login_sites()) elif html.request.var("_clone"): cloneid = html.request.get_ascii_input_mandatory("_clone") try: cloned_role = self._roles[cloneid] except KeyError: raise MKUserError(None, _("This role does not exist.")) newid = cloneid while newid in self._roles: newid += "x" new_role = {} new_role.update(cloned_role) new_alias = new_role["alias"] while not watolib.is_alias_used("roles", newid, new_alias)[0]: new_alias += _(" (copy)") new_role["alias"] = new_alias if cloned_role.get("builtin"): new_role["builtin"] = False new_role["basedon"] = cloneid self._roles[newid] = new_role self._save_roles() watolib.add_change("edit-roles", _("Created new role '%s'") % newid, sites=config.get_login_sites()) return redirect(self.mode_url())
def action(self) -> ActionResult: if html.request.var("_reset"): if not html.check_transaction(): return None try: del self._current_settings[self._varname] except KeyError: pass msg: Union[ HTML, str] = _("Resetted configuration variable %s to its default." ) % self._varname else: new_value = self._valuespec.from_html_vars("ve") self._valuespec.validate_value(new_value, "ve") self._current_settings[self._varname] = new_value msg = _("Changed global configuration variable %s to %s.") \ % (self._varname, self._valuespec.value_to_text(new_value)) # FIXME: THIS HTML(...) is needed because we do not know what we get from value_to_text!! msg = HTML(msg) self._save() watolib.add_change("edit-configvar", msg, sites=self._affected_sites(), domains=[self._config_variable.domain()], need_restart=self._config_variable.need_restart()) return redirect(self._back_url())
def action(self) -> ActionResult: delname = request.var("_delete") if not delname: return redirect(mode_url("timeperiods")) if not transactions.check_transaction(): return redirect(mode_url("timeperiods")) if delname in watolib.timeperiods.builtin_timeperiods(): raise MKUserError("_delete", _("Builtin timeperiods can not be modified")) usages = self._find_usages_of_timeperiod(delname) if usages: message = "<b>%s</b><br>%s:<ul>" % \ (_("You cannot delete this timeperiod."), _("It is still in use by")) for title, link in usages: message += '<li><a href="%s">%s</a></li>\n' % (link, title) message += "</ul>" raise MKUserError(None, message) del self._timeperiods[delname] watolib.timeperiods.save_timeperiods(self._timeperiods) watolib.add_change("edit-timeperiods", _("Deleted timeperiod %s") % delname) return redirect(mode_url("timeperiods"))
def action(self) -> ActionResult: if request.var("_reset"): if not transactions.check_transaction(): return None try: del self._current_settings[self._varname] except KeyError: pass msg = escape_html_permissive( _("Resetted configuration variable %s to its default.") % self._varname) else: new_value = self._valuespec.from_html_vars("ve") self._valuespec.validate_value(new_value, "ve") self._current_settings[self._varname] = new_value msg = HTML( _("Changed global configuration variable %s to %s.") % ( escaping.escape_attribute(self._varname), self._valuespec.value_to_html(new_value), )) self._save() watolib.add_change( "edit-configvar", msg, sites=self._affected_sites(), domains=[self._config_variable.domain()], need_restart=self._config_variable.need_restart(), ) return redirect(self._back_url())
def _add_change(self, action, entry, text): # type: (str, Dict, Text) -> None """Add a WATO change entry for this object type modifications""" watolib.add_change("%s-%s" % (action, self._mode_type.type_name()), text, domains=self._mode_type.affected_config_domains(), sites=self._mode_type.affected_sites(entry))
def create_rule(param): """Create rule""" body = param["body"] folder = body["folder"] value = body["value_raw"] rulesets = watolib.FolderRulesets(folder) rulesets.load() try: ruleset = rulesets.get(body["ruleset"]) except KeyError: return problem( status=400, detail=f"Ruleset {body['ruleset']!r} could not be found.", ) try: ruleset.valuespec().validate_value(value, "") except exceptions.MKUserError as exc: if exc.varname is None: title = "A field has a problem" else: field_name = exc.varname.replace("_p_", "") title = f"Problem in (sub-)field {field_name!r}" return problem( status=400, detail=strip_tags(exc.message), title=title, ) rule = watolib.Rule( gen_id(), folder, ruleset, RuleConditions( host_folder=folder, host_tags=body["conditions"].get("host_tag"), host_labels=body["conditions"].get("host_label"), host_name=body["conditions"].get("host_name"), service_description=body["conditions"].get("service_description"), service_labels=body["conditions"].get("service_label"), ), RuleOptions.from_config(body["properties"]), value, ) index = ruleset.append_rule(folder, rule) rulesets.save() # TODO Duplicated code is in pages/rulesets.py:2670- # TODO Move to watolib add_change( "new-rule", _l('Created new rule #%d in ruleset "%s" in folder "%s"') % (index, ruleset.title(), folder.alias_path()), sites=folder.all_site_ids(), diff_text=make_diff_text({}, rule.to_log()), object_ref=rule.object_ref(), ) return serve_json(_serialize_rule(folder, index, rule))
def action(self) -> ActionResult: if html.form_submitted("search"): return None alias = html.request.get_unicode_input("alias") unique, info = watolib.is_alias_used("roles", self._role_id, alias) if not unique: raise MKUserError("alias", info) new_id = html.request.get_ascii_input_mandatory("id") if not new_id: raise MKUserError("id", "You have to provide a ID.") if not re.match("^[-a-z0-9A-Z_]*$", new_id): raise MKUserError( "id", _("Invalid role ID. Only the characters a-z, A-Z, 0-9, _ and - are allowed.")) if new_id != self._role_id: if new_id in self._roles: raise MKUserError("id", _("The ID is already used by another role")) self._role["alias"] = alias # based on if not self._role.get("builtin"): basedon = html.request.get_ascii_input_mandatory("basedon") if basedon not in config.builtin_role_ids: raise MKUserError("basedon", _("Invalid valid for based on. Must be id of builtin rule.")) self._role["basedon"] = basedon # Permissions permissions = self._role["permissions"] for var_name, value in html.request.itervars(prefix="perm_"): try: perm = permission_registry[var_name[5:]] except KeyError: continue if value == "yes": permissions[perm.name] = True elif value == "no": permissions[perm.name] = False elif value == "default": try: del permissions[perm.name] except KeyError: pass # Already at defaults if self._role_id != new_id: self._roles[new_id] = self._role del self._roles[self._role_id] self._rename_user_role(self._role_id, new_id) self._save_roles() watolib.add_change("edit-roles", _("Modified user role '%s'") % new_id, sites=config.get_login_sites()) return redirect(mode_url("roles"))
def _move_tag_group(self): move_nr = html.get_integer_input("_move") move_to = html.get_integer_input("_index") moved = self._tag_config.tag_groups.pop(move_nr) self._tag_config.tag_groups.insert(move_to, moved) self._tag_config.validate_config() self._tag_config_file.save(self._tag_config.get_dict_format()) watolib.add_change("edit-tags", _("Changed order of tag groups"))
def action(self): if html.request.var("_delete"): delid = html.request.var("_delete") if delid not in self._roles: raise MKUserError(None, _("This role does not exist.")) if html.transaction_valid() and self._roles[delid].get('builtin'): raise MKUserError(None, _("You cannot delete the builtin roles!")) c = wato_confirm( _("Confirm deletion of role %s") % delid, _("Do you really want to delete the role %s?") % delid) if c: self._rename_user_role(delid, None) # Remove from existing users del self._roles[delid] self._save_roles() watolib.add_change("edit-roles", _("Deleted role '%s'") % delid, sites=config.get_login_sites()) elif c is False: return "" elif html.request.var("_clone"): if html.check_transaction(): cloneid = html.request.var("_clone") try: cloned_role = self._roles[cloneid] except KeyError: raise MKUserError(None, _("This role does not exist.")) newid = cloneid while newid in self._roles: newid += "x" new_role = {} new_role.update(cloned_role) new_alias = new_role["alias"] while not watolib.is_alias_used("roles", newid, new_alias)[0]: new_alias += _(" (copy)") new_role["alias"] = new_alias if cloned_role.get("builtin"): new_role["builtin"] = False new_role["basedon"] = cloneid self._roles[newid] = new_role self._save_roles() watolib.add_change("edit-roles", _("Created new role '%s'") % newid, sites=config.get_login_sites())
def action(self) -> ActionResult: if request.var("_action") != "discard": return None if not transactions.check_transaction(): return None if not self._may_discard_changes(): return None if not self.has_changes(): return None # Now remove all currently pending changes by simply restoring the last automatically # taken snapshot. Then activate the configuration. This should revert all pending changes. file_to_restore = self._get_last_wato_snapshot_file() if not file_to_restore: raise MKUserError(None, _("There is no WATO snapshot to be restored.")) msg = _("Discarded pending changes (Restored %s)") % file_to_restore # All sites and domains can be affected by a restore: Better restart everything. watolib.add_change( "changes-discarded", msg, domains=watolib.ABCConfigDomain.enabled_domains(), need_restart=True, ) self._extract_snapshot(file_to_restore) activate_changes.execute_activate_changes([ d.get_domain_request([]) for d in watolib.ABCConfigDomain.enabled_domains() ]) for site_id in activation_sites(): self.confirm_site_changes(site_id) build_index_background() html.header( self.title(), breadcrumb=self.breadcrumb(), show_body_start=display_options.enabled(display_options.H), show_top_heading=display_options.enabled(display_options.T), ) html.open_div(class_="wato") html.show_message(_("Successfully discarded all pending changes.")) html.javascript("hide_changes_buttons();") html.footer() return FinalizeRequest(code=200)
def _add_change( self, *, action: str, text: str, affected_sites: Optional[List[SiteId]], ) -> None: """Add a WATO change entry for this object type modifications""" watolib.add_change( "%s-%s" % (action, self._mode_type.type_name()), text, domains=self._mode_type.affected_config_domains(), sites=affected_sites, )
def _move_tag_group(self): move_nr = html.request.get_integer_input_mandatory("_move") move_to = html.request.get_integer_input_mandatory("_index") moved = self._tag_config.tag_groups.pop(move_nr) self._tag_config.tag_groups.insert(move_to, moved) try: self._tag_config.validate_config() except MKGeneralException as e: raise MKUserError(None, "%s" % e) self._tag_config_file.save(self._tag_config.get_dict_format()) self._load_effective_config() watolib.add_change("edit-tags", _("Changed order of tag groups"))
def action(self): if html.request.var("_action") != "discard": return if not html.check_transaction(): return if not self._may_discard_changes(): return # TODO: Remove once new changes mechanism has been implemented # Now remove all currently pending changes by simply restoring the last automatically # taken snapshot. Then activate the configuration. This should revert all pending changes. file_to_restore = self._get_last_wato_snapshot_file() if not file_to_restore: raise MKUserError(None, _('There is no WATO snapshot to be restored.')) msg = _("Discarded pending changes (Restored %s)") % file_to_restore # All sites and domains can be affected by a restore: Better restart everything. watolib.add_change("changes-discarded", msg, domains=watolib.ABCConfigDomain.enabled_domains(), need_restart=True) self._extract_snapshot(file_to_restore) cmk.gui.watolib.activate_changes.execute_activate_changes( [d.ident for d in watolib.ABCConfigDomain.enabled_domains()]) for site_id in cmk.gui.watolib.changes.activation_sites(): self.confirm_site_changes(site_id) html.header(self.title(), show_body_start=display_options.enabled(display_options.H), show_top_heading=display_options.enabled( display_options.T)) html.open_div(class_="wato") html.begin_context_buttons() home_button() html.end_context_buttons() html.show_message(_("Successfully discarded all pending changes.")) html.javascript("hide_changes_buttons();") html.footer() return False
def action(self): if not html.check_transaction(): return vs = self._valuespec() vs_spec = vs.from_html_vars("timeperiod") vs.validate_value(vs_spec, "timeperiod") self._timeperiod = self._from_valuespec(vs_spec) if self._name is None: self._name = vs_spec["name"] watolib.add_change("edit-timeperiods", _("Created new time period %s") % self._name) else: watolib.add_change("edit-timeperiods", _("Modified time period %s") % self._name) self._timeperiods[self._name] = self._timeperiod watolib.timeperiods.save_timeperiods(self._timeperiods) return "timeperiods"
def _set(self, request): tag_config_file = TagConfigFile() hosttags_config = cmk.utils.tags.TagConfig() hosttags_config.parse_config(tag_config_file.load_for_modification()) hosttags_dict = hosttags_config.get_dict_format() if "configuration_hash" in request: validate_config_hash(request["configuration_hash"], hosttags_dict) del request["configuration_hash"] changed_hosttags_config = cmk.utils.tags.TagConfig() changed_hosttags_config.parse_config(request) changed_hosttags_config.validate_config() self._verify_no_used_tags_missing(changed_hosttags_config) tag_config_file.save(changed_hosttags_config.get_dict_format()) watolib.add_change("edit-hosttags", _("Updated host tags through Web-API"))
def action(self) -> ActionResult: if not html.check_transaction(): return None vs = self._valuespec() # returns a Dictionary object vs_spec = vs.from_html_vars("timeperiod") vs.validate_value(vs_spec, "timeperiod") self._timeperiod = self._from_valuespec(vs_spec) if self._new: self._name = vs_spec["name"] watolib.add_change("edit-timeperiods", _("Created new time period %s") % self._name) else: watolib.add_change("edit-timeperiods", _("Modified time period %s") % self._name) assert self._name is not None self._timeperiods[self._name] = self._timeperiod watolib.timeperiods.save_timeperiods(self._timeperiods) return redirect(mode_url("timeperiods"))
def action(self): varname = html.request.var("_varname") if not varname: return action = html.request.var("_action") config_variable = config_variable_registry[varname]() def_value = self._default_values[varname] if action == "reset" and not is_a_checkbox( config_variable.valuespec()): c = wato_confirm( _("Resetting configuration variable"), _("Do you really want to reset the configuration variable <b>%s</b> " "back to the default value of <b><tt>%s</tt></b>?") % (varname, config_variable.valuespec().value_to_text(def_value))) else: if not html.check_transaction(): return c = True # no confirmation for direct toggle if c: if varname in self._current_settings: self._current_settings[ varname] = not self._current_settings[varname] else: self._current_settings[varname] = not def_value msg = _("Changed Configuration variable %s to %s.") % ( varname, "on" if self._current_settings[varname] else "off") watolib.save_global_settings(self._current_settings) watolib.add_change("edit-configvar", msg, domains=[config_variable.domain()], need_restart=config_variable.need_restart()) if action == "_reset": return "globalvars", msg return "globalvars" elif c is False: return ""
def action(self): if html.request.var("_reset"): if not is_a_checkbox(self._valuespec): c = wato_confirm( _("Resetting configuration variable"), _("Do you really want to reset this configuration variable " "back to its default value?")) if c is False: return "" if c is None: return None elif not html.check_transaction(): return try: del self._current_settings[self._varname] except KeyError: pass msg: Union[ HTML, str] = _("Resetted configuration variable %s to its default." ) % self._varname else: new_value = self._valuespec.from_html_vars("ve") self._valuespec.validate_value(new_value, "ve") self._current_settings[self._varname] = new_value msg = _("Changed global configuration variable %s to %s.") \ % (self._varname, self._valuespec.value_to_text(new_value)) # FIXME: THIS HTML(...) is needed because we do not know what we get from value_to_text!! msg = HTML(msg) self._save() watolib.add_change("edit-configvar", msg, sites=self._affected_sites(), domains=[self._config_variable.domain()], need_restart=self._config_variable.need_restart()) page_menu = self.parent_mode() assert page_menu is not None return page_menu.name()
def _set(self, request): tag_config_file = TagConfigFile() hosttags_config = cmk.utils.tags.TagConfig() hosttags_config.parse_config(tag_config_file.load_for_modification()) hosttags_dict = hosttags_config.get_dict_format() if "configuration_hash" in request: validate_config_hash(request["configuration_hash"], hosttags_dict) del request["configuration_hash"] # Check for conflicts with existing configuration # Tags may be either specified grouped in a host/folder configuration, e.g agent/cmk-agent, # or specified as the plain id in rules. We need to check both variants.. used_tags = self._get_used_grouped_tags() used_tags.update(self._get_used_rule_tags()) changed_hosttags_config = cmk.utils.tags.TagConfig() changed_hosttags_config.parse_config(request) changed_hosttags_config.validate_config() new_tags = changed_hosttags_config.get_tag_ids() new_tags.update( changed_hosttags_config.get_tag_ids_with_group_prefix()) # Remove the builtin hoststags from the list of used_tags builtin_config = cmk.utils.tags.BuiltinTagConfig() used_tags.discard(builtin_config.get_tag_ids_with_group_prefix()) missing_tags = used_tags - new_tags if missing_tags: raise MKUserError( None, _("Unable to apply new hosttag configuration. The following tags " "are still in use, but not mentioned in the updated " "configuration: %s") % ", ".join(missing_tags)) tag_config_file.save(changed_hosttags_config.get_dict_format()) watolib.add_change("edit-hosttags", _("Updated host tags through Web-API"))
def _set(self, request): # Py2: This encoding here should be kept Otherwise and unicode encoded text will be written # into the configuration file with unknown side effects ruleset_name = ensure_str(request["ruleset_name"]) # Future validation, currently the rule API actions are admin only, so the check is pointless # may_edit_ruleset(ruleset_name) # Check if configuration hash has changed in the meantime ruleset_dict = self._get_ruleset_configuration(ruleset_name) if "configuration_hash" in request: validate_config_hash(request["configuration_hash"], ruleset_dict) # Check permissions of new rules and rules we are going to delete new_ruleset = request["ruleset"] folders_set_ruleset = set(new_ruleset.keys()) folders_obsolete_ruleset = set(ruleset_dict.keys()) - folders_set_ruleset for check_folders in [folders_set_ruleset, folders_obsolete_ruleset]: for folder_path in check_folders: if not watolib.Folder.folder_exists(folder_path): raise MKUserError(None, _("Folder %s does not exist") % folder_path) rule_folder = watolib.Folder.folder(folder_path) rule_folder.need_permission("write") tag_to_group_map = ruleset_matcher.get_tag_to_group_map(config.tags) # Verify all rules rule_vs = watolib.Ruleset(ruleset_name, tag_to_group_map).rulespec.valuespec for folder_path, rules in new_ruleset.items(): for rule in rules: value = rule["value"] try: rule_vs.validate_datatype(value, "test_value") rule_vs.validate_value(value, "test_value") except MKException as e: raise MKGeneralException("ERROR: %s. Affected Rule %r" % (str(e), rule)) # Add new rulesets for folder_path, rules in new_ruleset.items(): folder = watolib.Folder.folder(folder_path) new_ruleset = watolib.Ruleset(ruleset_name, tag_to_group_map) new_ruleset.from_config(folder, rules) folder_rulesets = watolib.FolderRulesets(folder) folder_rulesets.load() # TODO: This add_change() call should be made by the data classes watolib.add_change("edit-ruleset", _("Set ruleset '%s' for '%s' with %d rules") % ( new_ruleset.title(), folder.title(), len(rules), ), sites=folder.all_site_ids(), object_ref=new_ruleset.object_ref()) folder_rulesets.set(ruleset_name, new_ruleset) folder_rulesets.save() # Remove obsolete rulesets for folder_path in folders_obsolete_ruleset: folder = watolib.Folder.folder(folder_path) folder_rulesets = watolib.FolderRulesets(folder) folder_rulesets.load() new_ruleset = watolib.Ruleset(ruleset_name, tag_to_group_map) new_ruleset.from_config(folder, []) # TODO: This add_change() call should be made by the data classes watolib.add_change("edit-ruleset", _("Deleted ruleset '%s' for '%s'") % ( new_ruleset.title(), folder.title(), ), sites=folder.all_site_ids(), object_ref=new_ruleset.object_ref()) folder_rulesets.set(ruleset_name, new_ruleset) folder_rulesets.save()
def _set(self, request): # NOTE: This encoding here should be kept # Otherwise and unicode encoded text will be written into the # configuration file with unknown side effects ruleset_name = request["ruleset_name"].encode("utf-8") # Future validation, currently the rule API actions are admin only, so the check is pointless # may_edit_ruleset(ruleset_name) # Check if configuration hash has changed in the meantime ruleset_dict = self._get_ruleset_configuration(ruleset_name) if "configuration_hash" in request: validate_config_hash(request["configuration_hash"], ruleset_dict) # Check permissions of new rules and rules we are going to delete new_ruleset = request["ruleset"] folders_set_ruleset = set(new_ruleset.keys()) folders_obsolete_ruleset = set( ruleset_dict.keys()) - folders_set_ruleset for check_folders in [folders_set_ruleset, folders_obsolete_ruleset]: for folder_path in check_folders: if not watolib.Folder.folder_exists(folder_path): raise MKUserError( None, _("Folder %s does not exist") % folder_path) rule_folder = watolib.Folder.folder(folder_path) rule_folder.need_permission("write") # Verify all rules rule_vs = watolib.Ruleset(ruleset_name).rulespec.valuespec for folder_path, rules in new_ruleset.items(): for rule in rules: if "negate" in rule: continue # ugly, rules with a boolean value have a different representation value = rule["value"] try: rule_vs.validate_datatype(value, "test_value") rule_vs.validate_value(value, "test_value") except MKException as e: # TODO: The abstract MKException should never be instanciated directly # Change this call site and make MKException an abstract base class raise MKException("ERROR: %s. Affected Rule %r" % (str(e), rule)) # Add new rulesets for folder_path, rules in new_ruleset.items(): folder = watolib.Folder.folder(folder_path) new_ruleset = watolib.Ruleset(ruleset_name) new_ruleset.from_config(folder, rules) folder_rulesets = watolib.FolderRulesets(folder) folder_rulesets.load() # TODO: This add_change() call should be made by the data classes watolib.add_change("edit-ruleset", _("Set ruleset '%s' for '%s' with %d rules") % ( new_ruleset.title(), folder.title(), len(rules), ), sites=folder.all_site_ids()) folder_rulesets.set(ruleset_name, new_ruleset) folder_rulesets.save() # Remove obsolete rulesets for folder_path in folders_obsolete_ruleset: folder = watolib.Folder.folder(folder_path) folder_rulesets = watolib.FolderRulesets(folder) folder_rulesets.load() # TODO: This add_change() call should be made by the data classes watolib.add_change("edit-ruleset", _("Deleted ruleset '%s' for '%s'") % ( watolib.Ruleset(ruleset_name).title(), folder.title(), ), sites=folder.all_site_ids()) new_ruleset = watolib.Ruleset(ruleset_name) new_ruleset.from_config(folder, []) folder_rulesets.set(ruleset_name, new_ruleset) folder_rulesets.save()