class SmartService(SystemServiceService): class Config: service = "smartd" service_model = "smart" datastore_extend = "smart.smart_extend" datastore_prefix = "smart_" @private async def smart_extend(self, smart): smart["powermode"] = smart["powermode"].upper() smart["email"] = smart["email"].split(",") return smart @accepts( Dict('smart_update', Int('interval'), Str('powermode', enum=['NEVER', 'SLEEP', 'STANDBY', 'IDLE']), Int('difference'), Int('informational'), Int('critical'), List('email', validators=[Unique()], items=[Str('email', validators=[Email()])]), update=True)) async def do_update(self, data): """ Update SMART Service Configuration. `interval` is an integer value in minutes which defines how often smartd activates to check if any tests are configured to run. `critical`, `informational` and `difference` are integer values on which alerts for SMART are configured if the disks temperature crosses the assigned threshold for each respective attribute. They default to 0 which indicates they are disabled. Email of log level LOG_CRIT is issued when disk temperature crosses `critical`. Email of log level LOG_INFO is issued when disk temperature crosses `informational`. If temperature of a disk changes by `difference` degree Celsius since the last report, SMART reports this. `email` is a list of valid emails to receive SMART alerts. """ old = await self.config() new = old.copy() new.update(data) new["powermode"] = new["powermode"].lower() new["email"] = ",".join([email.strip() for email in new["email"]]) await self._update_service(old, new) if new["powermode"] != old["powermode"]: await self.middleware.call("service.restart", "collectd", {"onetime": False}) await self.smart_extend(new) return new
class SmartService(SystemServiceService): class Config: service = "smartd" service_model = "smart" datastore_extend = "smart.smart_extend" datastore_prefix = "smart_" @private async def smart_extend(self, smart): smart["powermode"] = smart["powermode"].upper() smart["email"] = smart["email"].split(",") return smart @accepts( Dict('smart_update', Int('interval'), Str('powermode', enum=['NEVER', 'SLEEP', 'STANDBY', 'IDLE']), Int('difference'), Int('informational'), Int('critical'), List('email', validators=[Unique()], items=[Str('email', validators=[Email()])]), update=True)) async def do_update(self, data): old = await self.config() new = old.copy() new.update(data) new["powermode"] = new["powermode"].lower() new["email"] = ",".join([email.strip() for email in new["email"]]) await self._update_service(old, new) if new["powermode"] != old["powermode"]: await self.middleware.call("service.restart", "collectd", {"onetime": False}) await self.smart_extend(new) return new
class ReplicationService(CRUDService): class Config: datastore = "storage.replication" datastore_prefix = "repl_" datastore_extend = "replication.extend" datastore_extend_context = "replication.extend_context" cli_namespace = "task.replication" @private async def extend_context(self, rows, extra): return { "state": await self.middleware.call("zettarepl.get_state"), } @private async def extend(self, data, context): data["periodic_snapshot_tasks"] = [{ k.replace("task_", ""): v for k, v in task.items() } for task in data["periodic_snapshot_tasks"]] for task in data["periodic_snapshot_tasks"]: Cron.convert_db_format_to_schedule(task, begin_end=True) if data["direction"] == "PUSH": data["also_include_naming_schema"] = data["naming_schema"] data["naming_schema"] = [] if data["direction"] == "PULL": data["also_include_naming_schema"] = [] Cron.convert_db_format_to_schedule(data, "schedule", key_prefix="schedule_", begin_end=True) Cron.convert_db_format_to_schedule(data, "restrict_schedule", key_prefix="restrict_schedule_", begin_end=True) if "error" in context["state"]: data["state"] = context["state"]["error"] else: data["state"] = context["state"]["tasks"].get( f"replication_task_{data['id']}", { "state": "PENDING", }) data["job"] = data["state"].pop("job", None) return data @private async def compress(self, data): if data["direction"] == "PUSH": data["naming_schema"] = data["also_include_naming_schema"] del data["also_include_naming_schema"] Cron.convert_schedule_to_db_format(data, "schedule", key_prefix="schedule_", begin_end=True) Cron.convert_schedule_to_db_format(data, "restrict_schedule", key_prefix="restrict_schedule_", begin_end=True) del data["periodic_snapshot_tasks"] return data @accepts( Dict( "replication_create", Str("name", required=True), Str("direction", enum=["PUSH", "PULL"], required=True), Str("transport", enum=["SSH", "SSH+NETCAT", "LOCAL"], required=True), Int("ssh_credentials", null=True, default=None), Str("netcat_active_side", enum=["LOCAL", "REMOTE"], null=True, default=None), Str("netcat_active_side_listen_address", null=True, default=None), Int("netcat_active_side_port_min", null=True, default=None, validators=[Port()]), Int("netcat_active_side_port_max", null=True, default=None, validators=[Port()]), Str("netcat_passive_side_connect_address", null=True, default=None), List("source_datasets", items=[Dataset("dataset")], empty=False), Dataset("target_dataset", required=True), Bool("recursive", required=True), List("exclude", items=[Dataset("dataset")]), Bool("properties", default=True), List("properties_exclude", items=[Str("property", empty=False)]), Dict("properties_override", additional_attrs=True), Bool("replicate", default=False), Bool("encryption", default=False), Str("encryption_key", null=True, default=None), Str("encryption_key_format", enum=["HEX", "PASSPHRASE"], null=True, default=None), Str("encryption_key_location", null=True, default=None), List("periodic_snapshot_tasks", items=[Int("periodic_snapshot_task")], validators=[Unique()]), List("naming_schema", items=[ Str("naming_schema", validators=[ReplicationSnapshotNamingSchema()]) ]), List("also_include_naming_schema", items=[ Str("naming_schema", validators=[ReplicationSnapshotNamingSchema()]) ]), Bool("auto", required=True), Cron("schedule", defaults={"minute": "00"}, begin_end=True, null=True, default=None), Cron("restrict_schedule", defaults={"minute": "00"}, begin_end=True, null=True, default=None), Bool("only_matching_schedule", default=False), Bool("allow_from_scratch", default=False), Str("readonly", enum=["SET", "REQUIRE", "IGNORE"], default="SET"), Bool("hold_pending_snapshots", default=False), Str("retention_policy", enum=["SOURCE", "CUSTOM", "NONE"], required=True), Int("lifetime_value", null=True, default=None, validators=[Range(min=1)]), Str("lifetime_unit", null=True, default=None, enum=["HOUR", "DAY", "WEEK", "MONTH", "YEAR"]), Str("compression", enum=["LZ4", "PIGZ", "PLZIP"], null=True, default=None), Int("speed_limit", null=True, default=None, validators=[Range(min=1)]), Bool("large_block", default=True), Bool("embed", default=False), Bool("compressed", default=True), Int("retries", default=5, validators=[Range(min=1)]), Str("logging_level", enum=["DEBUG", "INFO", "WARNING", "ERROR"], null=True, default=None), Bool("enabled", default=True), register=True, strict=True, )) async def do_create(self, data): """ Create a Replication Task Create a Replication Task that will push or pull ZFS snapshots to or from remote host.. * `name` specifies a name for replication task * `direction` specifies whether task will `PUSH` or `PULL` snapshots * `transport` is a method of snapshots transfer: * `SSH` transfers snapshots via SSH connection. This method is supported everywhere but does not achieve great performance `ssh_credentials` is a required field for this transport (Keychain Credential ID of type `SSH_CREDENTIALS`) * `SSH+NETCAT` uses unencrypted connection for data transfer. This can only be used in trusted networks and requires a port (specified by range from `netcat_active_side_port_min` to `netcat_active_side_port_max`) to be open on `netcat_active_side` `ssh_credentials` is also required for control connection * `LOCAL` replicates to or from localhost * `source_datasets` is a non-empty list of datasets to replicate snapshots from * `target_dataset` is a dataset to put snapshots into. It must exist on target side * `recursive` and `exclude` have the same meaning as for Periodic Snapshot Task * `properties` control whether we should send dataset properties along with snapshots * `periodic_snapshot_tasks` is a list of periodic snapshot task IDs that are sources of snapshots for this replication task. Only push replication tasks can be bound to periodic snapshot tasks. * `naming_schema` is a list of naming schemas for pull replication * `also_include_naming_schema` is a list of naming schemas for push replication * `auto` allows replication to run automatically on schedule or after bound periodic snapshot task * `schedule` is a schedule to run replication task. Only `auto` replication tasks without bound periodic snapshot tasks can have a schedule * `restrict_schedule` restricts when replication task with bound periodic snapshot tasks runs. For example, you can have periodic snapshot tasks that run every 15 minutes, but only run replication task every hour. * Enabling `only_matching_schedule` will only replicate snapshots that match `schedule` or `restrict_schedule` * `allow_from_scratch` will destroy all snapshots on target side and replicate everything from scratch if none of the snapshots on target side matches source snapshots * `readonly` controls destination datasets readonly property: * `SET` will set all destination datasets to readonly=on after finishing the replication * `REQUIRE` will require all existing destination datasets to have readonly=on property * `IGNORE` will avoid this kind of behavior * `hold_pending_snapshots` will prevent source snapshots from being deleted by retention of replication fails for some reason * `retention_policy` specifies how to delete old snapshots on target side: * `SOURCE` deletes snapshots that are absent on source side * `CUSTOM` deletes snapshots that are older than `lifetime_value` and `lifetime_unit` * `NONE` does not delete any snapshots * `compression` compresses SSH stream. Available only for SSH transport * `speed_limit` limits speed of SSH stream. Available only for SSH transport * `large_block`, `embed` and `compressed` are various ZFS stream flag documented in `man zfs send` * `retries` specifies number of retries before considering replication failed .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.create", "params": [{ "name": "Work Backup", "direction": "PUSH", "transport": "SSH", "ssh_credentials": [12], "source_datasets", ["data/work"], "target_dataset": "repl/work", "recursive": true, "periodic_snapshot_tasks": [5], "auto": true, "restrict_schedule": { "minute": "0", "hour": "*/2", "dom": "*", "month": "*", "dow": "1,2,3,4,5", "begin": "09:00", "end": "18:00" }, "only_matching_schedule": true, "retention_policy": "CUSTOM", "lifetime_value": 1, "lifetime_unit": "WEEK", }] } """ verrors = ValidationErrors() verrors.add_child("replication_create", await self._validate(data)) if verrors: raise verrors periodic_snapshot_tasks = data["periodic_snapshot_tasks"] await self.compress(data) id = await self.middleware.call( "datastore.insert", self._config.datastore, data, {"prefix": self._config.datastore_prefix}) await self._set_periodic_snapshot_tasks(id, periodic_snapshot_tasks) await self.middleware.call("zettarepl.update_tasks") return await self._get_instance(id) @accepts(Int("id"), Patch( "replication_create", "replication_update", ("attr", { "update": True }), )) async def do_update(self, id, data): """ Update a Replication Task with specific `id` See the documentation for `create` method for information on payload contents .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.update", "params": [ 7, { "name": "Work Backup", "direction": "PUSH", "transport": "SSH", "ssh_credentials": [12], "source_datasets", ["data/work"], "target_dataset": "repl/work", "recursive": true, "periodic_snapshot_tasks": [5], "auto": true, "restrict_schedule": { "minute": "0", "hour": "*/2", "dom": "*", "month": "*", "dow": "1,2,3,4,5", "begin": "09:00", "end": "18:00" }, "only_matching_schedule": true, "retention_policy": "CUSTOM", "lifetime_value": 1, "lifetime_unit": "WEEK", } ] } """ old = await self._get_instance(id) new = old.copy() if new["ssh_credentials"]: new["ssh_credentials"] = new["ssh_credentials"]["id"] new["periodic_snapshot_tasks"] = [ task["id"] for task in new["periodic_snapshot_tasks"] ] new.update(data) verrors = ValidationErrors() verrors.add_child("replication_update", await self._validate(new, id)) if verrors: raise verrors periodic_snapshot_tasks = new["periodic_snapshot_tasks"] await self.compress(new) new.pop("state", None) new.pop("job", None) await self.middleware.call("datastore.update", self._config.datastore, id, new, {'prefix': self._config.datastore_prefix}) await self._set_periodic_snapshot_tasks(id, periodic_snapshot_tasks) await self.middleware.call("zettarepl.update_tasks") return await self._get_instance(id) @accepts(Int("id")) async def do_delete(self, id): """ Delete a Replication Task with specific `id` .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.delete", "params": [ 1 ] } """ response = await self.middleware.call("datastore.delete", self._config.datastore, id) await self.middleware.call("zettarepl.update_tasks") return response @item_method @accepts(Int("id"), Bool("really_run", default=True, hidden=True)) @job(logs=True) async def run(self, job, id, really_run): """ Run Replication Task of `id`. """ if really_run: task = await self._get_instance(id) if not task["enabled"]: raise CallError("Task is not enabled") if task["state"]["state"] == "RUNNING": raise CallError("Task is already running") if task["state"]["state"] == "HOLD": raise CallError("Task is on hold") await self.middleware.call("zettarepl.run_replication_task", id, really_run, job) @accepts( Patch( "replication_create", "replication_run_onetime", ("rm", { "name": "name" }), ("rm", { "name": "auto" }), ("rm", { "name": "schedule" }), ("rm", { "name": "only_matching_schedule" }), ("rm", { "name": "enabled" }), ), ) @job(logs=True) async def run_onetime(self, job, data): """ Run replication task without creating it. """ data["name"] = f"Temporary replication task for job {job.id}" data["schedule"] = None data["only_matching_schedule"] = False data["auto"] = False data["enabled"] = True verrors = ValidationErrors() verrors.add_child("replication_run_onetime", await self._validate(data)) if verrors: raise verrors await self.middleware.call("zettarepl.run_onetime_replication_task", job, data) async def _validate(self, data, id=None): verrors = ValidationErrors() await self._ensure_unique(verrors, "", "name", data["name"], id) # Direction snapshot_tasks = [] if data["direction"] == "PUSH": e, snapshot_tasks = await self._query_periodic_snapshot_tasks( data["periodic_snapshot_tasks"]) verrors.add_child("periodic_snapshot_tasks", e) if data["naming_schema"]: verrors.add("naming_schema", "This field has no sense for push replication") if not snapshot_tasks and not data["also_include_naming_schema"]: verrors.add( "periodic_snapshot_tasks", "You must at least either bind a periodic snapshot task or provide " "\"Also Include Naming Schema\" for push replication task") if data["schedule"] is None and data[ "auto"] and not data["periodic_snapshot_tasks"]: verrors.add( "auto", "Push replication that runs automatically must be either " "bound to a periodic snapshot task or have a schedule") if data["direction"] == "PULL": if data["schedule"] is None and data["auto"]: verrors.add( "auto", "Pull replication that runs automatically must have a schedule" ) if data["periodic_snapshot_tasks"]: verrors.add( "periodic_snapshot_tasks", "Pull replication can't be bound to a periodic snapshot task" ) if not data["naming_schema"]: verrors.add("naming_schema", "Naming schema is required for pull replication") if data["also_include_naming_schema"]: verrors.add("also_include_naming_schema", "This field has no sense for pull replication") if data["hold_pending_snapshots"]: verrors.add( "hold_pending_snapshots", "Pull replication tasks can't hold pending snapshots because " "they don't do source retention") # Transport if data["transport"] == "SSH+NETCAT": if data["netcat_active_side"] is None: verrors.add( "netcat_active_side", "You must choose active side for SSH+netcat replication") if data["netcat_active_side_port_min"] is not None and data[ "netcat_active_side_port_max"] is not None: if data["netcat_active_side_port_min"] > data[ "netcat_active_side_port_max"]: verrors.add( "netcat_active_side_port_max", "Please specify value greater or equal than netcat_active_side_port_min" ) if data["compression"] is not None: verrors.add( "compression", "Compression is not supported for SSH+netcat replication") if data["speed_limit"] is not None: verrors.add( "speed_limit", "Speed limit is not supported for SSH+netcat replication") else: if data["netcat_active_side"] is not None: verrors.add( "netcat_active_side", "This field only has sense for SSH+netcat replication") for k in [ "netcat_active_side_listen_address", "netcat_active_side_port_min", "netcat_active_side_port_max", "netcat_passive_side_connect_address" ]: if data[k] is not None: verrors.add( k, "This field only has sense for SSH+netcat replication") if data["transport"] == "LOCAL": if data["ssh_credentials"] is not None: verrors.add( "ssh_credentials", "Remote credentials have no sense for local replication") if data["compression"] is not None: verrors.add("compression", "Compression has no sense for local replication") if data["speed_limit"] is not None: verrors.add("speed_limit", "Speed limit has no sense for local replication") else: if data["ssh_credentials"] is None: verrors.add( "ssh_credentials", "SSH Credentials are required for non-local replication") else: try: await self.middleware.call( "keychaincredential.get_of_type", data["ssh_credentials"], "SSH_CREDENTIALS") except CallError as e: verrors.add("ssh_credentials", str(e)) # Common for all directions and transports for i, source_dataset in enumerate(data["source_datasets"]): for snapshot_task in snapshot_tasks: if is_child(source_dataset, snapshot_task["dataset"]): if data["recursive"]: for exclude in snapshot_task["exclude"]: if is_child(exclude, source_dataset ) and exclude not in data["exclude"]: verrors.add( "exclude", f"You should exclude {exclude!r} as bound periodic snapshot " f"task dataset {snapshot_task['dataset']!r} does" ) else: if source_dataset in snapshot_task["exclude"]: verrors.add( f"source_datasets.{i}", f"Dataset {source_dataset!r} is excluded by bound " f"periodic snapshot task for dataset " f"{snapshot_task['dataset']!r}") if not data["recursive"] and data["exclude"]: verrors.add( "exclude", "Excluding child datasets is only supported for recursive replication" ) for i, v in enumerate(data["exclude"]): if not any( v.startswith(ds + "/") for ds in data["source_datasets"]): verrors.add( f"exclude.{i}", "This dataset is not a child of any of source datasets") if data["replicate"]: if not data["recursive"]: verrors.add( "recursive", "This option is required for full filesystem replication") if data["exclude"]: verrors.add( "exclude", "This option is not supported for full filesystem replication" ) if not data["properties"]: verrors.add( "properties", "This option is required for full filesystem replication") if data["encryption"]: for k in [ "encryption_key", "encryption_key_format", "encryption_key_location" ]: if data[k] is None: verrors.add( k, "This property is required when remote dataset encryption is enabled" ) if data["schedule"]: if not data["auto"]: verrors.add( "schedule", "You can't have schedule for replication that does not run automatically" ) else: if data["only_matching_schedule"]: verrors.add( "only_matching_schedule", "You can't have only-matching-schedule without schedule") if data["retention_policy"] == "CUSTOM": if data["lifetime_value"] is None: verrors.add( "lifetime_value", "This field is required for custom retention policy") if data["lifetime_unit"] is None: verrors.add( "lifetime_value", "This field is required for custom retention policy") else: if data["lifetime_value"] is not None: verrors.add( "lifetime_value", "This field has no sense for specified retention policy") if data["lifetime_unit"] is not None: verrors.add( "lifetime_unit", "This field has no sense for specified retention policy") if data["enabled"]: for i, snapshot_task in enumerate(snapshot_tasks): if not snapshot_task["enabled"]: verrors.add( f"periodic_snapshot_tasks.{i}", "You can't bind disabled periodic snapshot task to enabled replication task" ) return verrors async def _set_periodic_snapshot_tasks(self, replication_task_id, periodic_snapshot_tasks_ids): await self.middleware.call( "datastore.delete", "storage.replication_repl_periodic_snapshot_tasks", [["replication_id", "=", replication_task_id]]) for periodic_snapshot_task_id in periodic_snapshot_tasks_ids: await self.middleware.call( "datastore.insert", "storage.replication_repl_periodic_snapshot_tasks", { "replication_id": replication_task_id, "task_id": periodic_snapshot_task_id, }, ) async def _query_periodic_snapshot_tasks(self, ids): verrors = ValidationErrors() query_result = await self.middleware.call("pool.snapshottask.query", [["id", "in", ids]]) snapshot_tasks = [] for i, task_id in enumerate(ids): for task in query_result: if task["id"] == task_id: snapshot_tasks.append(task) break else: verrors.add(str(i), "This snapshot task does not exist") return verrors, snapshot_tasks @accepts( Str("transport", enum=["SSH", "SSH+NETCAT", "LOCAL"], required=True), Int("ssh_credentials", null=True, default=None)) async def list_datasets(self, transport, ssh_credentials): """ List datasets on remote side Accepts `transport` and SSH credentials ID (for non-local transport) .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.list_datasets", "params": [ "SSH", 7 ] } """ return await self.middleware.call("zettarepl.list_datasets", transport, ssh_credentials) @accepts(Str("dataset", required=True), Str("transport", enum=["SSH", "SSH+NETCAT", "LOCAL"], required=True), Int("ssh_credentials", null=True, default=None)) async def create_dataset(self, dataset, transport, ssh_credentials): """ Creates dataset on remote side Accepts `dataset` name, `transport` and SSH credentials ID (for non-local transport) .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.create_dataset", "params": [ "repl/work", "SSH", 7 ] } """ return await self.middleware.call("zettarepl.create_dataset", dataset, transport, ssh_credentials) @accepts() async def list_naming_schemas(self): """ List all naming schemas used in periodic snapshot and replication tasks. """ naming_schemas = [] for snapshottask in await self.middleware.call( "pool.snapshottask.query"): naming_schemas.append(snapshottask["naming_schema"]) for replication in await self.middleware.call("replication.query"): naming_schemas.extend(replication["naming_schema"]) naming_schemas.extend(replication["also_include_naming_schema"]) return sorted(set(naming_schemas)) @accepts( List("datasets", empty=False, items=[Dataset("dataset")]), List("naming_schema", empty=False, items=[ Str("naming_schema", validators=[ReplicationSnapshotNamingSchema()]) ]), Str("transport", enum=["SSH", "SSH+NETCAT", "LOCAL"], required=True), Int("ssh_credentials", null=True, default=None), ) async def count_eligible_manual_snapshots(self, datasets, naming_schema, transport, ssh_credentials): """ Count how many existing snapshots of `dataset` match `naming_schema`. .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.count_eligible_manual_snapshots", "params": [ "repl/work", ["auto-%Y-%m-%d_%H-%M"], "SSH", 4 ] } """ return await self.middleware.call( "zettarepl.count_eligible_manual_snapshots", datasets, naming_schema, transport, ssh_credentials) @accepts( Str("direction", enum=["PUSH", "PULL"], required=True), List("source_datasets", items=[Dataset("dataset")], required=True, empty=False), Dataset("target_dataset", required=True), Str("transport", enum=["SSH", "SSH+NETCAT", "LOCAL", "LEGACY"], required=True), Int("ssh_credentials", null=True, default=None), ) async def target_unmatched_snapshots(self, direction, source_datasets, target_dataset, transport, ssh_credentials): """ Check if target has any snapshots that do not exist on source. .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.target_unmatched_snapshots", "params": [ "PUSH", ["repl/work", "repl/games"], "backup", "SSH", 4 ] } Returns { "backup/work": ["auto-2019-10-15_13-00", "auto-2019-10-15_09-00"], "backup/games": ["auto-2019-10-15_13-00"], } """ return await self.middleware.call( "zettarepl.target_unmatched_snapshots", direction, source_datasets, target_dataset, transport, ssh_credentials) @private def new_snapshot_name(self, naming_schema): return datetime.now().strftime(naming_schema) # Legacy pair support @private @accepts( Dict( "replication-pair-data", Str("hostname", required=True), Str("public-key", required=True), Str("user", null=True), )) async def pair(self, data): result = await self.middleware.call( "keychaincredential.ssh_pair", { "remote_hostname": data["hostname"], "username": data["user"] or "root", "public_key": data["public-key"], }) return { "ssh_port": result["port"], "ssh_hostkey": result["host_key"], }
class ReplicationService(CRUDService): class Config: datastore = "storage.replication" datastore_prefix = "repl_" datastore_extend = "replication.extend" datastore_extend_context = "replication.extend_context" @private async def extend_context(self): legacy_result, legacy_result_datetime = await self.middleware.run_in_thread( self._legacy_extend_context) return { "state": await self.middleware.call("zettarepl.get_state"), "legacy_result": legacy_result, "legacy_result_datetime": legacy_result_datetime, } def _legacy_extend_context(self): try: with open("/tmp/.repl-result", "rb") as f: data = f.read() legacy_result = pickle.loads(data) legacy_result_datetime = datetime.fromtimestamp( os.stat("/tmp/.repl-result").st_mtime) except Exception: legacy_result = defaultdict(dict) legacy_result_datetime = None return legacy_result, legacy_result_datetime @private async def extend(self, data, context): data["periodic_snapshot_tasks"] = [{ k.replace("task_", ""): v for k, v in task.items() } for task in data["periodic_snapshot_tasks"]] for task in data["periodic_snapshot_tasks"]: Cron.convert_db_format_to_schedule(task, begin_end=True) if data["direction"] == "PUSH": data["also_include_naming_schema"] = data["naming_schema"] data["naming_schema"] = [] if data["direction"] == "PULL": data["also_include_naming_schema"] = [] Cron.convert_db_format_to_schedule(data, "schedule", key_prefix="schedule_", begin_end=True) Cron.convert_db_format_to_schedule(data, "restrict_schedule", key_prefix="restrict_schedule_", begin_end=True) if data["transport"] == 'LEGACY': if data["id"] in context["legacy_result"]: legacy_result = context["legacy_result"][data["id"]] msg = legacy_result.get("msg") if msg == "Running": state = "RUNNING" elif msg in ["Succeeded", "Up to date"]: state = "FINISHED" else: state = "ERROR" data["state"] = { "datetime": context["legacy_result_datetime"], "state": state, "last_snapshot": legacy_result.get("last_snapshot"), } if state == "ERROR": data["state"]["error"] = msg else: data["state"] = { "state": "PENDING", } else: data["state"] = context["state"].get( f"replication_task_{data['id']}", { "state": "PENDING", }) return data @private async def compress(self, data): if data["direction"] == "PUSH": data["naming_schema"] = data["also_include_naming_schema"] del data["also_include_naming_schema"] Cron.convert_schedule_to_db_format(data, "schedule", key_prefix="schedule_", begin_end=True) Cron.convert_schedule_to_db_format(data, "restrict_schedule", key_prefix="restrict_schedule_", begin_end=True) del data["periodic_snapshot_tasks"] return data @accepts( Dict( "replication_create", Str("name", required=True), Str("direction", enum=["PUSH", "PULL"], required=True), Str("transport", enum=["SSH", "SSH+NETCAT", "LOCAL", "LEGACY"], required=True), Int("ssh_credentials", null=True, default=None), Str("netcat_active_side", enum=["LOCAL", "REMOTE"], null=True, default=None), Str("netcat_active_side_listen_address", null=True, default=None), Int("netcat_active_side_port_min", null=True, default=None, validators=[Port()]), Int("netcat_active_side_port_max", null=True, default=None, validators=[Port()]), Str("netcat_passive_side_connect_address", null=True, default=None), List("source_datasets", items=[Path("dataset", empty=False)], required=True, empty=False), Path("target_dataset", required=True, empty=False), Bool("recursive", required=True), List("exclude", items=[Path("dataset", empty=False)], default=[]), List("periodic_snapshot_tasks", items=[Int("periodic_snapshot_task")], default=[], validators=[Unique()]), List("naming_schema", items=[ Str("naming_schema", validators=[ReplicationSnapshotNamingSchema()]) ], default=[]), List("also_include_naming_schema", items=[ Str("naming_schema", validators=[ReplicationSnapshotNamingSchema()]) ], default=[]), Bool("auto", required=True), Cron("schedule", defaults={"minute": "00"}, begin_end=True, null=True, default=None), Cron("restrict_schedule", defaults={"minute": "00"}, begin_end=True, null=True, default=None), Bool("only_matching_schedule", default=False), Bool("allow_from_scratch", default=False), Bool("hold_pending_snapshots", default=False), Str("retention_policy", enum=["SOURCE", "CUSTOM", "NONE"], required=True), Int("lifetime_value", null=True, default=None, validators=[Range(min=1)]), Str("lifetime_unit", null=True, default=None, enum=["HOUR", "DAY", "WEEK", "MONTH", "YEAR"]), Str("compression", enum=["LZ4", "PIGZ", "PLZIP"], null=True, default=None), Int("speed_limit", null=True, default=None, validators=[Range(min=1)]), Bool("dedup", default=True), Bool("large_block", default=True), Bool("embed", default=True), Bool("compressed", default=True), Int("retries", default=5, validators=[Range(min=1)]), Str("logging_level", enum=["DEBUG", "INFO", "WARNING", "ERROR"], null=True, default=None), Bool("enabled", default=True), register=True, strict=True, )) async def do_create(self, data): """ Create a Replication Task Create a Replication Task that will push or pull ZFS snapshots to or from remote host.. * `name` specifies a name for replication task * `direction` specifies whether task will `PUSH` or `PULL` snapshots * `transport` is a method of snapshots transfer: * `SSH` transfers snapshots via SSH connection. This method is supported everywhere but does not achieve great performance `ssh_credentials` is a required field for this transport (Keychain Credential ID of type `SSH_CREDENTIALS`) * `SSH+NETCAT` uses unencrypted connection for data transfer. This can only be used in trusted networks and requires a port (specified by range from `netcat_active_side_port_min` to `netcat_active_side_port_max`) to be open on `netcat_active_side` `ssh_credentials` is also required for control connection * `LOCAL` replicates to or from localhost * `LEGACY` uses legacy replication engine prior to FreeNAS 11.3 * `source_datasets` is a non-empty list of datasets to replicate snapshots from * `target_dataset` is a dataset to put snapshots into. It must exist on target side * `recursive` and `exclude` have the same meaning as for Periodic Snapshot Task * `periodic_snapshot_tasks` is a list of periodic snapshot task IDs that are sources of snapshots for this replication task. Only push replication tasks can be bound to periodic snapshot tasks. * `naming_schema` is a list of naming schemas for pull replication * `also_include_naming_schema` is a list of naming schemas for push replication * `auto` allows replication to run automatically on schedule or after bound periodic snapshot task * `schedule` is a schedule to run replication task. Only `auto` replication tasks without bound periodic snapshot tasks can have a schedule * `restrict_schedule` restricts when replication task with bound periodic snapshot tasks runs. For example, you can have periodic snapshot tasks that run every 15 minutes, but only run replication task every hour. * Enabling `only_matching_schedule` will only replicate snapshots that match `schedule` or `restrict_schedule` * `allow_from_scratch` will destroy all snapshots on target side and replicate everything from scratch if none of the snapshots on target side matches source snapshots * `hold_pending_snapshots` will prevent source snapshots from being deleted by retention of replication fails for some reason * `retention_policy` specifies how to delete old snapshots on target side: * `SOURCE` deletes snapshots that are absent on source side * `CUSTOM` deletes snapshots that are older than `lifetime_value` and `lifetime_unit` * `NONE` does not delete any snapshots * `compression` compresses SSH stream. Available only for SSH transport * `speed_limit` limits speed of SSH stream. Available only for SSH transport * `dedup`, `large_block`, `embed` and `compressed` are various ZFS stream flag documented in `man zfs send` * `retries` specifies number of retries before considering replication failed .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.create", "params": [{ "name": "Work Backup", "direction": "PUSH", "transport": "SSH", "ssh_credentials": [12], "source_datasets", ["data/work"], "target_dataset": "repl/work", "recursive": true, "periodic_snapshot_tasks": [5], "auto": true, "restrict_schedule": { "minute": "0", "hour": "*/2", "dom": "*", "month": "*", "dow": "1,2,3,4,5", "begin": "09:00", "end": "18:00" }, "only_matching_schedule": true, "retention_policy": "CUSTOM", "lifetime_value": 1, "lifetime_unit": "WEEK", }] } """ verrors = ValidationErrors() verrors.add_child("replication_create", await self._validate(data)) if verrors: raise verrors periodic_snapshot_tasks = data["periodic_snapshot_tasks"] await self.compress(data) id = await self.middleware.call( "datastore.insert", self._config.datastore, data, {"prefix": self._config.datastore_prefix}) await self._set_periodic_snapshot_tasks(id, periodic_snapshot_tasks) await self.middleware.call("service.restart", "cron") await self.middleware.call("zettarepl.update_tasks") return await self._get_instance(id) @accepts(Int("id"), Patch( "replication_create", "replication_update", ("attr", { "update": True }), )) async def do_update(self, id, data): """ Update a Replication Task with specific `id` See the documentation for `create` method for information on payload contents .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.update", "params": [ 7, { "name": "Work Backup", "direction": "PUSH", "transport": "SSH", "ssh_credentials": [12], "source_datasets", ["data/work"], "target_dataset": "repl/work", "recursive": true, "periodic_snapshot_tasks": [5], "auto": true, "restrict_schedule": { "minute": "0", "hour": "*/2", "dom": "*", "month": "*", "dow": "1,2,3,4,5", "begin": "09:00", "end": "18:00" }, "only_matching_schedule": true, "retention_policy": "CUSTOM", "lifetime_value": 1, "lifetime_unit": "WEEK", } ] } """ old = await self._get_instance(id) new = old.copy() if new["ssh_credentials"]: new["ssh_credentials"] = new["ssh_credentials"]["id"] new["periodic_snapshot_tasks"] = [ task["id"] for task in new["periodic_snapshot_tasks"] ] new.update(data) verrors = ValidationErrors() verrors.add_child("replication_update", await self._validate(new)) if verrors: raise verrors periodic_snapshot_tasks = new["periodic_snapshot_tasks"] await self.compress(new) new.pop('state', None) await self.middleware.call("datastore.update", self._config.datastore, id, new, {'prefix': self._config.datastore_prefix}) await self._set_periodic_snapshot_tasks(id, periodic_snapshot_tasks) await self.middleware.call("service.restart", "cron") await self.middleware.call("zettarepl.update_tasks") return await self._get_instance(id) @accepts(Int("id")) async def do_delete(self, id): """ Delete a Replication Task with specific `id` .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.delete", "params": [ 1 ] } """ response = await self.middleware.call("datastore.delete", self._config.datastore, id) await self.middleware.call("service.restart", "cron") await self.middleware.call("zettarepl.update_tasks") return response @item_method @accepts(Int("id")) async def run(self, id): """ Run Replication Task of `id`. """ task = await self._get_instance(id) if not task["enabled"]: raise CallError("Task is not enabled") await self.middleware.call("zettarepl.run_replication_task", task["id"]) async def _validate(self, data): verrors = ValidationErrors() # Direction snapshot_tasks = [] if data["direction"] == "PUSH": e, snapshot_tasks = await self._query_periodic_snapshot_tasks( data["periodic_snapshot_tasks"]) verrors.add_child("periodic_snapshot_tasks", e) if data["naming_schema"]: verrors.add("naming_schema", "This field has no sense for push replication") if data["schedule"]: if data["periodic_snapshot_tasks"]: verrors.add( "schedule", "Push replication can't be bound to periodic snapshot task and have " "schedule at the same time") else: if data["auto"] and not data[ "periodic_snapshot_tasks"] and data[ "transport"] != "LEGACY": verrors.add( "auto", "Push replication that runs automatically must be either " "bound to periodic snapshot task or have schedule") if data["direction"] == "PULL": if data["schedule"]: pass else: if data["auto"]: verrors.add( "auto", "Pull replication that runs automatically must have schedule" ) if data["periodic_snapshot_tasks"]: verrors.add( "periodic_snapshot_tasks", "Pull replication can't be bound to periodic snapshot task" ) if not data["naming_schema"]: verrors.add("naming_schema", "Naming schema is required for pull replication") if data["also_include_naming_schema"]: verrors.add("also_include_naming_schema", "This field has no sense for pull replication") if data["hold_pending_snapshots"]: verrors.add( "hold_pending_snapshots", "Pull replication tasks can't hold pending snapshots because " "they don't do source retention") # Transport if data["transport"] == "SSH+NETCAT": if data["netcat_active_side"] is None: verrors.add( "netcat_active_side", "You must choose active side for SSH+netcat replication") if data["netcat_active_side_port_min"] is not None and data[ "netcat_active_side_port_max"] is not None: if data["netcat_active_side_port_min"] > data[ "netcat_active_side_port_max"]: verrors.add( "netcat_active_side_port_max", "Please specify value greater or equal than netcat_active_side_port_min" ) if data["compression"] is not None: verrors.add( "compression", "Compression is not supported for SSH+netcat replication") if data["speed_limit"] is not None: verrors.add( "speed_limit", "Speed limit is not supported for SSH+netcat replication") else: if data["netcat_active_side"] is not None: verrors.add( "netcat_active_side", "This field only has sense for SSH+netcat replication") for k in [ "netcat_active_side_listen_address", "netcat_active_side_port_min", "netcat_active_side_port_max", "netcat_passive_side_connect_address" ]: if data[k] is not None: verrors.add( k, "This field only has sense for SSH+netcat replication") if data["transport"] == "LOCAL": if data["ssh_credentials"] is not None: verrors.add( "ssh_credentials", "Remote credentials have no sense for local replication") if data["compression"] is not None: verrors.add("compression", "Compression has no sense for local replication") if data["speed_limit"] is not None: verrors.add("speed_limit", "Speed limit has no sense for local replication") else: if data["ssh_credentials"] is None: verrors.add( "ssh_credentials", "SSH Credentials are required for non-local replication") else: try: await self.middleware.call( "keychaincredential.get_of_type", data["ssh_credentials"], "SSH_CREDENTIALS") except CallError as e: verrors.add("ssh_credentials", str(e)) if data["transport"] == "LEGACY": for should_be_true in ["auto", "allow_from_scratch"]: if not data[should_be_true]: verrors.add( should_be_true, "Legacy replication does not support disabling this option" ) for should_be_false in [ "exclude", "periodic_snapshot_tasks", "naming_schema", "also_include_naming_schema", "only_matching_schedule", "dedup", "large_block", "embed", "compressed" ]: if data[should_be_false]: verrors.add( should_be_false, "Legacy replication does not support this option") if data["direction"] != "PUSH": verrors.add( "direction", "Only push application is allowed for Legacy transport") if len(data["source_datasets"]) != 1: verrors.add( "source_datasets", "You can only have one source dataset for legacy replication" ) if data["retries"] != 1: verrors.add("retries", "This value should be 1 for legacy replication") # Common for all directions and transports for i, source_dataset in enumerate(data["source_datasets"]): for snapshot_task in snapshot_tasks: if is_child(source_dataset, snapshot_task["dataset"]): if data["recursive"]: for exclude in snapshot_task["exclude"]: if exclude not in data["exclude"]: verrors.add( "exclude", f"You should exclude {exclude!r} as bound periodic snapshot " f"task dataset {snapshot_task['dataset']!r} does" ) else: if source_dataset in snapshot_task["exclude"]: verrors.add( f"source_datasets.{i}", f"Dataset {source_dataset!r} is excluded by bound " f"periodic snapshot task for dataset " f"{snapshot_task['dataset']!r}") if not data["recursive"] and data["exclude"]: verrors.add( "exclude", "Excluding child datasets is only supported for recursive replication" ) for i, v in enumerate(data["exclude"]): if not any( v.startswith(ds + "/") for ds in data["source_datasets"]): verrors.add( f"exclude.{i}", "This dataset is not a child of any of source datasets") if data["schedule"]: if not data["auto"]: verrors.add( "schedule", "You can't have schedule for replication that does not run automatically" ) else: if data["only_matching_schedule"]: verrors.add( "only_matching_schedule", "You can't have only-matching-schedule without schedule") if data["retention_policy"] == "CUSTOM": if data["lifetime_value"] is None: verrors.add( "lifetime_value", "This field is required for custom retention policy") if data["lifetime_unit"] is None: verrors.add( "lifetime_value", "This field is required for custom retention policy") else: if data["lifetime_value"] is not None: verrors.add( "lifetime_value", "This field has no sense for specified retention policy") if data["lifetime_unit"] is not None: verrors.add( "lifetime_unit", "This field has no sense for specified retention policy") if data["enabled"]: for i, snapshot_task in enumerate(snapshot_tasks): if not snapshot_task["enabled"]: verrors.add( f"periodic_snapshot_tasks.{i}", "You can't bind disabled periodic snapshot task to enabled replication task" ) return verrors async def _set_periodic_snapshot_tasks(self, replication_task_id, periodic_snapshot_tasks_ids): await self.middleware.call( "datastore.delete", "storage.replication_repl_periodic_snapshot_tasks", [["replication_id", "=", replication_task_id]]) for periodic_snapshot_task_id in periodic_snapshot_tasks_ids: await self.middleware.call( "datastore.insert", "storage.replication_repl_periodic_snapshot_tasks", { "replication_id": replication_task_id, "task_id": periodic_snapshot_task_id, }, ) async def _query_periodic_snapshot_tasks(self, ids): verrors = ValidationErrors() query_result = await self.middleware.call("pool.snapshottask.query", [["id", "in", ids]]) snapshot_tasks = [] for i, task_id in enumerate(ids): for task in query_result: if task["id"] == task_id: snapshot_tasks.append(task) break else: verrors.add(str(i), "This snapshot task does not exist") return verrors, snapshot_tasks @accepts( Str("transport", enum=["SSH", "SSH+NETCAT", "LOCAL", "LEGACY"], required=True), Int("ssh_credentials", null=True, default=None)) async def list_datasets(self, transport, ssh_credentials=None): """ List datasets on remote side Accepts `transport` and SSH credentials ID (for non-local transport) .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.list_datasets", "params": [ "SSH", 7 ] } """ return await self.middleware.call("zettarepl.list_datasets", transport, ssh_credentials) @accepts(Str("dataset", required=True), Str("transport", enum=["SSH", "SSH+NETCAT", "LOCAL", "LEGACY"], required=True), Int("ssh_credentials", null=True, default=None)) async def create_dataset(self, dataset, transport, ssh_credentials=None): """ Creates dataset on remote side Accepts `dataset` name, `transport` and SSH credentials ID (for non-local transport) .. examples(websocket):: :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "replication.create_dataset", "params": [ "repl/work", "SSH", 7 ] } """ return await self.middleware.call("zettarepl.create_dataset", dataset, transport, ssh_credentials) # Legacy pair support @private @accepts( Dict( "replication-pair-data", Str("hostname", required=True), Str("public-key", required=True), Str("user"), )) async def pair(self, data): result = await self.middleware.call( "keychaincredential.ssh_pair", { "remote_hostname": data["hostname"], "username": data["user"], "public_key": data["public-key"], }) return { "ssh_port": result["port"], "ssh_hostkey": result["host_key"], }
class NetDataService(SystemServiceService): class Config: service = 'netdata' service_model = 'netdataglobalsettings' service_verb = 'restart' datastore_extend = 'netdata.netdata_extend' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._alarms = {} self._initialize_alarms() @private async def netdata_extend(self, data): # We get data alarms as a dict e.g # {"alarms": {"alarm1": {"enabled": True}, "alarm2": {"enabled": True}}} alarms = copy.deepcopy(self._alarms) alarms.update(data['alarms']) data['alarms'] = alarms for alarm in data['alarms']: # Remove conf file paths data['alarms'][alarm].pop('path', None) return data @private async def list_alarms(self): alarms = copy.deepcopy(self._alarms) config = await self.config() for alarm in config['alarms']: if alarm not in alarms: # An unlikely case when a previously configured alarm does not exist in conf files anymore alarms[alarm] = {} alarms[alarm]['enabled'] = config['alarms'][alarm]['enabled'] return alarms @private def _initialize_alarms(self): path = '/usr/local/etc/netdata/health.d/' pattern = re.compile(r'alarm: +(.*)(?:[\s\S]*?os: +(.*)\n)?') for file in [f for f in os.listdir(path) if 'sample' not in f]: with open(path + file, 'r') as f: for alarm in re.findall(pattern, f.read()): # By default all alarms are enabled in netdata # When we list alarms, alarms which have been configured by user to be disabled # will show up as disabled only if 'freebsd' in alarm[1] or not alarm[1]: self._alarms[alarm[0].strip()] = { 'path': path + file, 'enabled': True } @private async def validate_attrs(self, data): verrors = ValidationErrors() additional_params = data.get('additional_params') if additional_params: # Let's be very generic here and introduce very basic validation # Expected format is as following # [ipv6.icmpneighbor] # history = 86400 # enabled = yes # # While we are here, we will also introduce basic formatting to the file to ensure # that we can make it as compliable as possible param_str = '' for i in additional_params.split('\n'): i = i.strip() if not i: continue if i.startswith('#'): # Let's not validate this if i.replace('#', '').startswith('['): param_str += f'\n\n{i}' else: param_str += f'\n\t{i}' continue if i.startswith('[') and not i.endswith(']'): verrors.add( 'netdata_update.additional_params', f'Please correct format for {i}. i.e [system.intr]') elif not i.startswith('[') and '=' not in i: verrors.add( 'netdata_update.additional_params', f'Please correct format for {i}. i.e enabled = yes') if i.startswith('['): param_str += f'\n\n{i}' else: param_str += f'\n\t{i}' data['additional_params'] = param_str + '\n' bind_to_ips = data.get('bind') if bind_to_ips: valid_ips = [ ip['address'] for ip in await self.middleware.call('interfaces.ip_in_use') ] valid_ips.extend(['127.0.0.1', '::1', '0.0.0.0', '::']) for bind_ip in bind_to_ips: if bind_ip not in valid_ips: verrors.add('netdata_update.bind', f'Invalid {bind_ip} bind IP') else: verrors.add('netdata_update.bind', 'This field is required') update_alarms = data.pop('update_alarms', {}) valid_alarms = self._alarms if update_alarms: for alarm in update_alarms: if alarm not in valid_alarms: verrors.add('netdata_update.alarms', f'{alarm} not a valid alarm') verrors.extend( validate_attributes([ Dict(key, Bool('enabled', required=True)) for key in update_alarms ], {'attributes': update_alarms})) # Validating streaming metrics now stream_mode = data.get('stream_mode') if stream_mode == 'SLAVE': for key in ('api_key', 'destination'): if not data.get(key): verrors.add( f'netdata_update.{key}', f'{key} is required with stream mode as SLAVE') destinations = data.get('destination') if destinations: ip_addr = IpAddress() port = Port() for dest in destinations: ip = dest.split(':')[0] try: ip_addr(ip) except ValueError as e: verrors.add('netdata_update.destination', str(e)) else: if ':' in dest: try: port(dest.split(':')[1]) except ValueError as e: verrors.add('netdata_update.destination', f'Not a valid port: {e}') elif stream_mode == 'MASTER': for key in ('allow_from', 'api_key'): if not data.get(key): verrors.add( f'netdata_update.{key}', f'{key} is required with stream mode as MASTER') verrors.check() data['alarms'].update(update_alarms) return data @accepts( Dict('netdata_update', Str('additional_params'), Dict('alarms', additional_attrs=True), List('allow_from', items=[Str('pattern')]), Str('api_key', validators=[UUID()]), List('bind', validators=[Unique()], items=[Str('bind_ip')]), Int('port', validators=[Port()]), List('destination', validators=[Unique()], items=[Str('destination')]), Int('history'), Int('http_port_listen_backlog'), Str('stream_mode', enum=['NONE', 'MASTER', 'SLAVE']), Int('update_every'), update=True)) async def do_update(self, data): """ Update Netdata Service Configuration `alarms` is a dictionary where user specifies a key,value pair with key being alarm name and value is a dictionary which is of the schema "{'enabled': True}" indicating should the alarm be enabled or not. Middlewared supports interacting (changing) alarms in /usr/local/etc/netdata/health.d/ directory. `allow_from` is used when netdata service is expected to be used as a master. It defaults to "['*']". This field expects a list of Netdata patterns which Netdata will use to set restrictions on incoming connections from slave accordingly. `api_key` is a valid UUID which can be generated in command line by typing uuidgen. `destination` is used when netdata service is expected to be used as a slave. Destination is a list of potential destinations to which netdata should stream metrics. We expect the format to be IP:PORT ( port is optional ). The first working destination is used by Netdata service. `history` is the number of entries the netdata daemon will by default keep in memory for each chart dimension. It defaults to 86400. .. examples(websocket):: Update Netdata Service Configuration :::javascript { "id": "6841f242-840a-11e6-a437-00e04d680384", "msg": "method", "method": "netdata.update", "params": [{ "history": 80000, "alarms": { "used_swap": {"enabled": true}, "ram_in_swap": {"enabled": true} } }] } """ old = await self.config() new = old.copy() # We separate alarms we have in db and the ones user supplies new['update_alarms'] = data.pop('alarms', {}) new.update(data) new = await self.validate_attrs(new) # If port value is updated, we don't restart nginx, that has to be done manually await self._update_service(old, new) return await self.config()