def test_config(web, site): users = { "hh": { "alias": "Harry Hirsch", "password": "******", "email": u"%s@localhost" % web.site.id, 'contactgroups': ['all'], }, } expected_users = set(["cmkadmin", "automation"] + users.keys()) web.add_htpasswd_users(users) all_users = web.get_all_users() assert not expected_users - set(all_users.keys()) site.live.command("[%d] STOP_EXECUTING_HOST_CHECKS" % time.time()) site.live.command("[%d] STOP_EXECUTING_SVC_CHECKS" % time.time()) web.add_host("notify-test", attributes={ "ipaddress": "127.0.0.1", }) web.activate_changes() yield site.live.command("[%d] START_EXECUTING_HOST_CHECKS" % time.time()) site.live.command("[%d] START_EXECUTING_SVC_CHECKS" % time.time()) web.delete_host("notify-test") web.delete_htpasswd_users(users.keys()) web.activate_changes()
def test_log(request, web, site, fake_sendmail): core, log = request.param site.set_config("CORE", core, with_restart=True) users = { "hh": { "alias": "Harry Hirsch", "password": "******", "email": u"%s@localhost" % web.site.id, 'contactgroups': ['all'], }, } expected_users = set(["cmkadmin", "automation"] + users.keys()) web.add_htpasswd_users(users) all_users = web.get_all_users() assert not expected_users - set(all_users.keys()) site.live.command("[%d] STOP_EXECUTING_HOST_CHECKS" % time.time()) site.live.command("[%d] STOP_EXECUTING_SVC_CHECKS" % time.time()) web.add_host("notify-test", attributes={ "ipaddress": "127.0.0.1", }) web.activate_changes() with WatchLog(site, log, default_timeout=20) as l: yield l site.live.command("[%d] START_EXECUTING_HOST_CHECKS" % time.time()) site.live.command("[%d] START_EXECUTING_SVC_CHECKS" % time.time()) web.delete_host("notify-test") web.delete_htpasswd_users(users.keys()) web.activate_changes()
def graph_test_config(web, site): # No graph yet... with pytest.raises(APIError) as exc_info: web.get_regular_graph("test-host-get-graph", "Check_MK", 0, expect_error=True) assert "Cannot calculate graph recipes" in "%s" % exc_info try: # Now add the host web.add_host("test-host-get-graph", attributes={ "ipaddress": "127.0.0.1", }) site.write_file( "etc/check_mk/conf.d/test-host-get-graph.mk", "datasource_programs.append(('cat ~/var/check_mk/agent_output/<HOST>', [], ['test-host-get-graph']))\n" ) site.makedirs("var/check_mk/agent_output/") site.write_file( "var/check_mk/agent_output/test-host-get-graph", open( "%s/tests/integration/cmk/base/test-files/linux-agent-output" % repo_path()).read()) web.discover_services("test-host-get-graph") web.activate_changes() site.schedule_check("test-host-get-graph", "Check_MK", 0) # Wait for RRD file creation. Isn't this a bug that the graph is not instantly available? rrd_path = site.path( "var/check_mk/rrd/test-host-get-graph/Check_MK.rrd") for attempt in xrange(50): time.sleep(0.1) proc = subprocess.Popen([ site.path("bin/unixcat"), site.path("tmp/run/rrdcached.sock") ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = proc.communicate("FLUSH %s\n" % rrd_path) if os.path.exists(rrd_path): break sys.stdout.write("waiting for %s (attempt %d)%s%s\n" % ( rrd_path, attempt + 1, # ", stdout: %s" % out if out else "", ", stderr: %s" % err if err else "")) else: assert False, "RRD file %s missing" % rrd_path yield finally: web.delete_host("test-host-get-graph") site.delete_file("etc/check_mk/conf.d/test-host-get-graph.mk") web.activate_changes()
def cfg_setup_fixture(request, web, site): # noqa: F811 # pylint: disable=redefined-outer-name hostname = "test-prediction" # Enforce use of the pre-created RRD file from the git. The restart of the core # is needed to make it renew it's internal RRD file cache site.makedirs("var/check_mk/rrd/test-prediction") with open(site.path("var/check_mk/rrd/test-prediction/CPU_load.rrd"), "wb") as f: f.write( open("%s/tests-py3/integration/cmk/base/test-files/CPU_load.rrd" % repo_path(), "rb").read()) site.write_file( "var/check_mk/rrd/test-prediction/CPU_load.info", open("%s/tests-py3/integration/cmk/base/test-files/CPU_load.info" % repo_path()).read()) site.restart_core() create_linux_test_host(request, web, site, "test-prediction") site.write_file( "etc/check_mk/conf.d/linux_test_host_%s_cpu_load.mk" % hostname, """ globals().setdefault('custom_checks', []) custom_checks = [ ( {'service_description': u'CPU load', 'has_perfdata': True}, [], ALL_HOSTS, {} ), ] + custom_checks """) web.activate_changes() yield # Cleanup site.delete_file("etc/check_mk/conf.d/linux_test_host_%s_cpu_load.mk" % hostname) site.delete_dir("var/check_mk/rrd")
def test_cfg(web, site): print("Applying default config") web.add_host("modes-test-host", attributes={ "ipaddress": "127.0.0.1", }) web.add_host("modes-test-host2", attributes={ "ipaddress": "127.0.0.1", "tag_criticality": "test", }) web.add_host("modes-test-host3", attributes={ "ipaddress": "127.0.0.1", "tag_criticality": "test", }) web.add_host("modes-test-host4", attributes={ "ipaddress": "127.0.0.1", "tag_criticality": "offline", }) site.write_file( "etc/check_mk/conf.d/modes-test-host.mk", "datasource_programs.append(('cat ~/var/check_mk/agent_output/<HOST>', [], ALL_HOSTS))\n" ) site.makedirs("var/check_mk/agent_output/") site.write_file( "var/check_mk/agent_output/modes-test-host", file("%s/tests/integration/cmk_base/test-files/linux-agent-output" % repo_path()).read()) site.write_file( "var/check_mk/agent_output/modes-test-host2", file("%s/tests/integration/cmk_base/test-files/linux-agent-output" % repo_path()).read()) site.write_file( "var/check_mk/agent_output/modes-test-host3", file("%s/tests/integration/cmk_base/test-files/linux-agent-output" % repo_path()).read()) web.discover_services("modes-test-host") web.discover_services("modes-test-host2") web.discover_services("modes-test-host3") web.activate_changes() yield None # # Cleanup code # print("Cleaning up test config") site.delete_dir("var/check_mk/agent_output") site.delete_file("etc/check_mk/conf.d/modes-test-host.mk") web.delete_host("modes-test-host") web.delete_host("modes-test-host2") web.delete_host("modes-test-host3") web.delete_host("modes-test-host4")
def test_cfg_fixture(web, site): # noqa: F811 # pylint: disable=redefined-outer-name print("Applying default config") web.add_host("modes-test-host", attributes={ "ipaddress": "127.0.0.1", }) web.add_host("modes-test-host2", attributes={ "ipaddress": "127.0.0.1", "tag_criticality": "test", }) web.add_host("modes-test-host3", attributes={ "ipaddress": "127.0.0.1", "tag_criticality": "test", }) web.add_host("modes-test-host4", attributes={ "ipaddress": "127.0.0.1", "tag_criticality": "offline", }) site.write_file( "etc/check_mk/conf.d/modes-test-host.mk", "datasource_programs.append(('cat ~/var/check_mk/agent_output/<HOST>', [], ALL_HOSTS))\n" ) site.makedirs("var/check_mk/agent_output/") site.write_file("var/check_mk/agent_output/modes-test-host", get_standard_linux_agent_output()) site.write_file("var/check_mk/agent_output/modes-test-host2", get_standard_linux_agent_output()) site.write_file("var/check_mk/agent_output/modes-test-host3", get_standard_linux_agent_output()) web.discover_services("modes-test-host") web.discover_services("modes-test-host2") web.discover_services("modes-test-host3") try: web.activate_changes() yield None finally: # # Cleanup code # print("Cleaning up test config") site.delete_dir("var/check_mk/agent_output") site.delete_file("etc/check_mk/conf.d/modes-test-host.mk") web.delete_host("modes-test-host") web.delete_host("modes-test-host2") web.delete_host("modes-test-host3") web.delete_host("modes-test-host4") web.activate_changes()
def graph_test_config(web, site): # No graph yet... with pytest.raises(APIError) as e: web.get_regular_graph("test-host-get-graph", "Check_MK", 0, expect_error=True) assert "Cannot calculate graph recipes" in "%s" % e try: # Now add the host web.add_host("test-host-get-graph", attributes={ "ipaddress": "127.0.0.1", }) site.write_file( "etc/check_mk/conf.d/test-host-get-graph.mk", "datasource_programs.append(('cat ~/var/check_mk/agent_output/<HOST>', [], ['test-host-get-graph']))\n" ) site.makedirs("var/check_mk/agent_output/") site.write_file( "var/check_mk/agent_output/test-host-get-graph", file( "%s/tests/integration/cmk_base/test-files/linux-agent-output" % repo_path()).read()) web.discover_services("test-host-get-graph") web.activate_changes() site.schedule_check("test-host-get-graph", "Check_MK", 0) # Wait for RRD file creation # Isn't this a bug that the graph is not instantly available? timeout = 10 print "Checking for graph..." while timeout and not site.file_exists( "var/check_mk/rrd/test-host-get-graph/Check_MK.rrd"): try: web.get_regular_graph("test-host-get-graph", "Check_MK", 0, expect_error=True) except Exception: pass timeout -= 1 time.sleep(1) print "Checking for graph..." assert site.file_exists("var/check_mk/rrd/test-host-get-graph/Check_MK.rrd"), \ "RRD %s is still missing" % "var/check_mk/rrd/test-host-get-graph/Check_MK.rrd" yield finally: web.delete_host("test-host-get-graph") site.delete_file("etc/check_mk/conf.d/test-host-get-graph.mk") web.activate_changes()
def default_cfg_fixture(request, site, web): # noqa: F811 # pylint: disable=redefined-outer-name config = DefaultConfig(core=request.param) site.set_config("CORE", config.core, with_restart=True) print("Applying default config (%s)" % config.core) create_linux_test_host(request, web, site, "livestatus-test-host") create_linux_test_host(request, web, site, "livestatus-test-host.domain") web.discover_services("livestatus-test-host") web.activate_changes() return config
def default_cfg(request, site, web): config = DefaultConfig(core=request.param) site.set_config("CORE", config.core, with_restart=True) print "Applying default config (%s)" % config.core create_linux_test_host(request, web, site, "livestatus-test-host") create_linux_test_host(request, web, site, "livestatus-test-host.domain") web.discover_services("livestatus-test-host") web.activate_changes() return config
def test_activate_changes(web, site): try: web.add_host("test-host-activate", attributes={ "ipaddress": "127.0.0.1", }) web.activate_changes() result = site.live.query("GET hosts\nColumns: name\nFilter: name = test-host-activate\n") assert result == [["test-host-activate"]] finally: web.delete_host("test-host-activate") web.activate_changes()
def test_get_graph(web, site): try: # No graph yet... with pytest.raises(APIError) as e: data = web.get_regular_graph("test-host-get-graph", "Check_MK", 0, expect_error=True) assert "Cannot calculate graph recipes" in "%s" % e # Now add the host web.add_host("test-host-get-graph", attributes={ "ipaddress": "127.0.0.1", }) web.discover_services("test-host-get-graph") web.activate_changes() # Issue a reschedule site.live.command( "SCHEDULE_FORCED_SERVICE_CHECK;test-host-get-graph;Check_MK;%d" % int(time.time())) # Wait for RRD file creation # Isn't this a bug that the graph is not instantly available? timeout = 10 print "Checking for graph..." while timeout and not site.file_exists( "var/check_mk/rrd/test-host-get-graph/Check_MK.rrd"): try: data = web.get_regular_graph("test-host-get-graph", "Check_MK", 0, expect_error=True) except Exception: pass timeout -= 1 time.sleep(1) print "Checking for graph..." assert site.file_exists("var/check_mk/rrd/test-host-get-graph/Check_MK.rrd"), \ "RRD %s is still missing" % "var/check_mk/rrd/test-host-get-graph/Check_MK.rrd" _test_get_graph_api(web) _test_get_graph_image(web) _test_get_graph_notification_image(web) finally: web.delete_host("test-host-get-graph") web.activate_changes()
def default_cfg(web): web.add_host("livestatus-test-host", attributes={ "ipaddress": "127.0.0.1", }) web.discover_services("livestatus-test-host") web.activate_changes() yield None # # Cleanup code # web.delete_host("livestatus-test-host")
def test_active_check_execution(test_cfg, site, web): try: # TODO: Remove bytestr marker once the GUI uses Python 3 web.set_ruleset( api_str_type("custom_checks"), { api_str_type("ruleset"): { # Main folder api_str_type(""): [ { api_str_type("value"): { api_str_type('service_description'): u'\xc4ctive-Check', api_str_type('command_line'): api_str_type('echo "123"') }, api_str_type("condition"): {}, api_str_type("options"): {}, }, ], } }) web.activate_changes() site.schedule_check("test-host", u'\xc4ctive-Check', 0) result = site.live.query_row( u"GET services\nColumns: host_name description state plugin_output has_been_checked\nFilter: host_name = test-host\nFilter: description = \xc4ctive-Check" ) print("Result: %r" % result) assert result[4] == 1 assert result[0] == "test-host" assert result[1] == u'\xc4ctive-Check' assert result[2] == 0 assert result[3] == "123" finally: # TODO: Remove bytestr marker once the GUI uses Python 3 web.set_ruleset( api_str_type("custom_checks"), { api_str_type("ruleset"): { api_str_type(""): [], # -> folder } }) web.activate_changes()
def default_cfg(web): print "Applying default config" web.add_host("livestatus-test-host", attributes={ "ipaddress": "127.0.0.1", }) web.discover_services("livestatus-test-host") web.activate_changes() yield None # # Cleanup code # print "Cleaning up default config" web.delete_host("livestatus-test-host")
def test_active_check_execution(test_cfg, site, web): try: web.set_ruleset( "custom_checks", { "ruleset": { # Main folder "": [ { "value": { 'service_description': u'\xc4ctive-Check', 'command_line': 'echo "123"' }, "conditions": { "host_specs": config.ALL_HOSTS, "host_tags": [], }, "options": {}, }, ], } }) web.activate_changes() site.schedule_check("test-host", u'\xc4ctive-Check', 0) result = site.live.query_row( "GET services\nColumns: host_name description state plugin_output has_been_checked\nFilter: host_name = test-host\nFilter: description = Äctive-Check" ) print "Result: %r" % result assert result[4] == 1 assert result[0] == "test-host" assert result[1] == u'\xc4ctive-Check' assert result[2] == 0 assert result[3] == "123" finally: web.set_ruleset( "custom_checks", { "ruleset": { "": [], # -> folder } }) web.activate_changes()
def configure_service_tags_fixture(site, web, default_cfg): # noqa: F811 # pylint: disable=redefined-outer-name web.set_ruleset( "service_tag_rules", { "ruleset": { "": [{ "value": [("criticality", "prod")], "condition": { "host_name": ["livestatus-test-host"], "service_description": [{ "$regex": "CPU load$", }], }, },], } }) web.activate_changes() yield web.set_ruleset("service_tag_rules", {"ruleset": {"": [],}}) web.activate_changes()
def configure_service_tags(site, web, default_cfg): web.set_ruleset( "service_tag_rules", { "ruleset": { "": [{ "value": [("criticality", "prod")], "condition": { "host_name": ["livestatus-test-host"], "service_description": [{ "$regex": "CPU load$", }], }, },], } }) web.activate_changes() yield web.set_ruleset("service_tag_rules", {"ruleset": {"": [],}}) web.activate_changes()
def test_cfg(request, web, site): config = DefaultConfig(core=request.param) site.set_config("CORE", config.core, with_restart=True) print("Applying default config") web.add_host("test-host", attributes={ "ipaddress": "127.0.0.1", "tag_agent": "no-agent", }) web.activate_changes() yield config # # Cleanup code # print("Cleaning up test config") web.delete_host("test-host")
def test_cfg(web, site): print("Applying default config") web.add_host("modes-test-host", attributes={ "ipaddress": "127.0.0.1", }) site.write_file( "etc/check_mk/conf.d/modes-test-host.mk", "datasource_programs.append(('cat ~/var/check_mk/agent_output/<HOST>', [], ['modes-test-host']))\n" ) site.makedirs("var/check_mk/agent_output/") web.activate_changes() yield None # # Cleanup code # print("Cleaning up test config") web.delete_host("modes-test-host")
def configure_service_tags(site, web): web.set_ruleset( "service_tag_rules", { "ruleset": { "": [ { "value": [("criticality", "prod")], "conditions": { "host_tags": [], "host_specs": ["livestatus-test-host"], "service_specs": ["CPU load$"], }, }, ], } }) web.activate_changes() yield web.set_ruleset("service_tag_rules", {"ruleset": { "": [], }}) web.activate_changes()
def test_cfg(web, clear_config_caches, enable_debug): reload_config() yield # # Cleanup code # print("Cleaning up test config") if web.host_exists("mgmt-host"): web.delete_host("mgmt-host") if web.folder_exists("folder1"): web.delete_folder("folder1") web.set_ruleset( "management_board_config", { "ruleset": { "": [], # -> folder } }) web.activate_changes()
def test_cfg(web, site): print("Applying default config") web.add_host("ds-test-host1", attributes={ "ipaddress": "127.0.0.1", }) web.add_host("ds-test-host2", attributes={ "ipaddress": "127.0.0.1", }) web.add_host("ds-test-node1", attributes={ "ipaddress": "127.0.0.1", }) web.add_host("ds-test-node2", attributes={ "ipaddress": "127.0.0.1", }) web.add_host( "ds-test-cluster1", attributes={ "ipaddress": "127.0.0.1", }, cluster_nodes=[ "ds-test-node1", "ds-test-node2", ], ) site.write_file( "etc/check_mk/conf.d/ds-test-host.mk", "datasource_programs.append(('cat ~/var/check_mk/agent_output/<HOST>', [], ALL_HOSTS))\n" ) site.makedirs("var/check_mk/agent_output/") for h in [ "ds-test-host1", "ds-test-host2", "ds-test-node1", "ds-test-node2" ]: site.write_file( "var/check_mk/agent_output/%s" % h, file( "%s/tests/integration/cmk_base/test-files/linux-agent-output" % repo_path()).read()) web.activate_changes() import cmk.utils.debug cmk.utils.debug.enable() # Needs to be done together, even when the checks are not directly needed import cmk_base.check_api as check_api config.load_all_checks(check_api.get_check_api_context) config.load() yield None # # Cleanup code # print("Cleaning up test config") cmk.utils.debug.disable() site.delete_dir("var/check_mk/agent_output") site.delete_file("etc/check_mk/conf.d/ds-test-host.mk") web.delete_host("ds-test-host1") web.delete_host("ds-test-host2") web.delete_host("ds-test-node1") web.delete_host("ds-test-node2") web.delete_host("ds-test-cluster1") web.activate_changes()
def scenario(request, web, site): core = request.param.core unreachable_enabled = request.param.unreachable_enabled site.set_core(core) try: print("Applying test config") web.add_host("notify-test-parent", attributes={ "ipaddress": "127.0.0.1", }) web.add_host("notify-test-child", attributes={ "ipaddress": "127.0.0.1", "parents": ["notify-test-parent"], }) if unreachable_enabled: notification_options = 'd,u,r,f,s' else: notification_options = 'd,r,f,s' rule_result = web.get_ruleset("extra_host_conf:notification_options") rule_result["ruleset"] = { "": [{ 'condition': {}, 'options': {}, 'value': notification_options }] } web.set_ruleset("extra_host_conf:notification_options", rule_result) # Make nagios check more often for incoming commands and add more # details to the log site.write_file( "etc/nagios/nagios.d/zzz_test_unreachable_notifications.cfg", "log_passive_checks=1\n" "command_check_interval=1s\n") web.activate_changes() site.live.command("[%d] DISABLE_HOST_CHECK;notify-test-parent" % time.time()) site.live.command("[%d] DISABLE_HOST_CHECK;notify-test-child" % time.time()) site.live.command("[%d] DISABLE_FLAP_DETECTION" % time.time()) yield request.param finally: # # Cleanup code # print("Cleaning up default config") site.live.command("[%d] ENABLE_FLAP_DETECTION" % time.time()) site.live.command("[%d] ENABLE_HOST_CHECK;notify-test-child" % time.time()) site.live.command("[%d] ENABLE_HOST_CHECK;notify-test-parent" % time.time()) site.delete_file( "etc/nagios/nagios.d/zzz_test_unreachable_notifications.cfg") web.delete_host("notify-test-child") web.delete_host("notify-test-parent") web.activate_changes()
def scenario_fixture(request, web, site): # noqa: F811 # pylint: disable=redefined-outer-name core = request.param.core unreachable_enabled = request.param.unreachable_enabled site.set_core(core) try: print("Applying test config") web.add_host("notify-test-parent", attributes={ "ipaddress": "127.0.0.1", }) web.add_host("notify-test-child", attributes={ "ipaddress": "127.0.0.1", "parents": ["notify-test-parent"], }) if unreachable_enabled: notification_options = 'd,u,r,f,s' else: notification_options = 'd,r,f,s' rule_result = web.get_ruleset("extra_host_conf:notification_options") rule_result["ruleset"] = { "": [{ 'condition': {}, 'options': {}, 'value': notification_options }] } web.set_ruleset("extra_host_conf:notification_options", rule_result) web.activate_changes() site.live.command("[%d] DISABLE_HOST_CHECK;notify-test-parent" % time.time()) site.live.command("[%d] DISABLE_SVC_CHECK;notify-test-parent;PING" % time.time()) site.live.command( "[%d] DISABLE_SVC_CHECK;notify-test-parent;Check_MK Discovery" % time.time()) site.live.command("[%d] DISABLE_HOST_CHECK;notify-test-child" % time.time()) site.live.command("[%d] DISABLE_SVC_CHECK;notify-test-child;PING" % time.time()) site.live.command( "[%d] DISABLE_SVC_CHECK;notify-test-child;Check_MK Discovery" % time.time()) site.live.command("[%d] DISABLE_FLAP_DETECTION" % time.time()) yield request.param finally: # # Cleanup code # print("Cleaning up default config") site.live.command("[%d] ENABLE_FLAP_DETECTION" % time.time()) site.live.command("[%d] ENABLE_HOST_CHECK;notify-test-child" % time.time()) site.live.command("[%d] ENABLE_HOST_CHECK;notify-test-parent" % time.time()) web.delete_host("notify-test-child") web.delete_host("notify-test-parent") web.activate_changes()
def test_active_check_macros(test_cfg, site, web): macros = { "$HOSTADDRESS$": "127.0.0.1", "$HOSTNAME$": "test-host", "$_HOSTTAGS$": " ".join( sorted([ "/wato/", "auto-piggyback", "ip-v4", "ip-v4-only", "lan", "no-agent", "no-snmp", "ping", "prod", "site:%s" % site.id ])), "$_HOSTADDRESS_4$": "127.0.0.1", "$_HOSTADDRESS_6$": "", "$_HOSTADDRESS_FAMILY$": "4", "$USER1$": "/omd/sites/%s/lib/nagios/plugins" % site.id, "$USER2$": "/omd/sites/%s/local/lib/nagios/plugins" % site.id, "$USER3$": site.id, "$USER4$": site.root, } def descr(var): return "Macro %s" % var.strip("$") ruleset = [] for var, value in macros.items(): ruleset.append({ "value": { 'service_description': descr(var), # TODO: Remove this once the GUI uses Python 3 'command_line': api_str_type('echo "Output: %s"' % var), }, "condition": {}, }) try: web.set_ruleset( "custom_checks", { "ruleset": { # Main folder "": ruleset, } }) web.activate_changes() for var, value in macros.items(): description = descr(var) logger.info(description) site.schedule_check("test-host", description, 0) logger.info("Get service row") row = site.live.query_row( "GET services\n" "Columns: host_name description state plugin_output has_been_checked\n" "Filter: host_name = test-host\n" "Filter: description = %s\n" % description) logger.info(row) name, description, state, plugin_output, has_been_checked = row assert name == "test-host" assert has_been_checked == 1 assert state == 0 expected_output = "Output: %s" % value # TODO: Cleanup difference between nagios/cmc if test_cfg.core == "nagios": expected_output = expected_output.strip() if var == "$_HOSTTAGS$": splitted_output = plugin_output.split(" ") plugin_output = splitted_output[0] + " " + " ".join( sorted(splitted_output[1:])) assert expected_output == plugin_output, \ "Macro %s has wrong value (%r instead of %r)" % (var, plugin_output, expected_output) finally: web.set_ruleset( "custom_checks", { "ruleset": { "": [], # -> folder } }) web.activate_changes()