diff --git a/pfSense-pkg-API/files/etc/inc/api/endpoints/APIServicesServiceWatchdog.inc b/pfSense-pkg-API/files/etc/inc/api/endpoints/APIServicesServiceWatchdog.inc
new file mode 100644
index 000000000..8df4dfe0f
--- /dev/null
+++ b/pfSense-pkg-API/files/etc/inc/api/endpoints/APIServicesServiceWatchdog.inc
@@ -0,0 +1,30 @@
+url = "/api/v1/services/service_watchdog";
+ }
+
+ protected function get() {
+ return (new APIServicesServiceWatchdogRead())->call();
+ }
+
+ protected function put() {
+ return (new APIServicesServiceWatchdogUpdate())->call();
+ }
+}
diff --git a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
index 6f02c9309..91b349ffe 100644
--- a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
@@ -2159,6 +2159,24 @@ function get($id, $data=[], $all=false) {
"return" => $id,
"message" => "IPsec phase 2 unique ID does not exist"
],
+ 2259 => [
+ "status" => "bad request",
+ "code" => 400,
+ "return" => $id,
+ "message" => "Service Watchdog service 'name' is required"
+ ],
+ 2260 => [
+ "status" => "bad request",
+ "code" => 400,
+ "return" => $id,
+ "message" => "Service Watchdog service 'name' is unknown"
+ ],
+ 2261 => [
+ "status" => "bad request",
+ "code" => 400,
+ "return" => $id,
+ "message" => "Duplicate Service Watchdog services are not allowed"
+ ],
// 3000-3999 reserved for /api/v1/interfaces API calls
3000 => [
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIServicesServiceWatchdogRead.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIServicesServiceWatchdogRead.inc
new file mode 100644
index 000000000..8e0193ad5
--- /dev/null
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIServicesServiceWatchdogRead.inc
@@ -0,0 +1,46 @@
+privileges = ["page-all", "page-services-servicewatchdog"];
+ $this->packages = ["pfSense-pkg-Service_Watchdog"];
+ $this->package_includes = ["servicewatchdog.inc"];
+ }
+
+ public function action() {
+ return APIResponse\get(0, $this->get_watched_services());
+ }
+
+ public static function get_watched_services() {
+ global $config;
+ $services = ($config["installedpackages"]["servicewatchdog"]["item"]) ?: [];
+
+ # Loop through each configured service and ensure data is formatted correctly
+ foreach ($services as &$service) {
+ $service["enabled"] = isset($service["enabled"]);
+ $service["status"] = isset($service["status"]);
+ $service["notify"] = isset($service["notify"]);
+ }
+
+ return $services;
+ }
+}
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIServicesServiceWatchdogUpdate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIServicesServiceWatchdogUpdate.inc
new file mode 100644
index 000000000..13d90713c
--- /dev/null
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIServicesServiceWatchdogUpdate.inc
@@ -0,0 +1,99 @@
+privileges = ["page-all", "page-services-servicewatchdog"];
+ $this->packages = ["pfSense-pkg-Service_Watchdog"];
+ $this->package_includes = ["servicewatchdog.inc"];
+ $this->change_note = "Modified Service Watchdog configuration via API";
+ }
+
+ public function action() {
+ # Write the updated configuration and reload service watchdog if changes were made
+ if ($this->validated_data) {
+ $this->config["installedpackages"]["servicewatchdog"]["item"] = $this->validated_data;
+ $this->write_config();
+ servicewatchdog_cron_job();
+ }
+
+ return APIResponse\get(0, APIServicesServiceWatchdogRead::get_watched_services());
+ }
+
+ public function validate_payload() {
+ $this->__validate_services();
+ }
+
+ private function __validate_services() {
+ # Validate the optional 'services' field
+ if (isset($this->initial_data["services"])) {
+ # Variables
+ $watched_services = [];
+
+ # Revert validated data so it can be repopulated with the updated values
+ $this->validated_data = [];
+
+ # Loop through each requested service and configure it accordingly
+ foreach ($this->initial_data["services"] as $service) {
+ # Ensure this item is an array
+ $service = (is_array($service)) ? $service : [];
+
+ # Require the 'name' subfield
+ if (!isset($service["name"])) {
+ $this->errors[] = APIResponse\get(2259);
+ }
+ # Require this service to be watchable
+ elseif (!$this->is_service_watchable($service["name"])) {
+ $this->errors[] = APIResponse\get(2260);
+ }
+ # Do not allow duplicates of the same service
+ elseif (in_array($service["name"], $watched_services)) {
+ $this->errors[] = APIResponse\get(2261);
+ }
+ # Otherwise, this service is valid.
+ else {
+ # Mark this service as watched so duplicates will not be allowed
+ $watched_services[] = $service["name"];
+
+ # Get the configuration for this service
+ $serv_conf = $this->is_service_watchable($service["name"]);
+
+ # Allow clients to set the notify field
+ if ($service["notify"] === true) {
+ $serv_conf["notify"] = true;
+ }
+
+ # Add this service to our validated configuration
+ $this->validated_data[] = $serv_conf;
+ }
+ }
+ }
+ }
+
+ public function is_service_watchable($name) {
+ # Loop through compatible services and check if this service is found
+ foreach (get_services() as $service) {
+ if ($service["name"] === $name) {
+ return $service;
+ }
+ }
+ return false;
+ }
+}
diff --git a/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml b/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml
index da5cdf13d..c42940646 100644
--- a/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml
+++ b/pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml
@@ -10816,6 +10816,55 @@ paths:
summary: Restart services
tags:
- Services
+ /api/v1/services/service_watchdog:
+ get:
+ operationId: APIServicesServiceWatchdogRead
+ description: 'Read the current Service Watchdog configuration.
+
+ _Requires at least one of the following privileges:_ [`page-all`, `page-system-advanced-admin`]
+ _Requires all of the following add-on packages:_ [`pfSense-pkg-Service_Watchdog`]'
+ responses:
+ "200":
+ $ref: '#/components/responses/Success'
+ "401":
+ $ref: '#/components/responses/AuthenticationFailed'
+ summary: Read Service Watchdog configuration
+ tags:
+ - Services > Service Watchdog
+ put:
+ operationId: APIServicesServiceWatchdogUpdate
+ description: 'Update the Service Watchdog configuration This will replace any existing Service Watchdog
+ configuration.
+
+ _Requires at least one of the following privileges:_ [`page-all`, `page-system-advanced-admin`]
+ _Requires all of the following add-on packages:_ [`pfSense-pkg-Service_Watchdog`]'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ services:
+ description: Services that Service Watchdog will monitor and automatically restart upon crash.
+ type: array
+ items:
+ type: object
+ properties:
+ name:
+ description: Name of the service that Service Watchdog will monitor.
+ type: string
+ notify:
+ description: Send a notification when Service Watchdog restarts this service.
+ type: boolean
+ required:
+ - name
+ responses:
+ "200":
+ $ref: '#/components/responses/Success'
+ "401":
+ $ref: '#/components/responses/AuthenticationFailed'
+ summary: Update Service Watchdog configuration
+ tags:
+ - Services > Service Watchdog
/api/v1/services/sshd:
get:
operationId: APIServicesSSHdRead
@@ -13897,6 +13946,7 @@ tags:
- name: Services > OpenVPN > CSC
- name: Services > OpenVPN > Client
- name: Services > OpenVPN > Server
+ - name: Services > Service Watchdog
- name: Services > SSHD
- name: Services > SYSLOGD
- name: Services > Unbound
diff --git a/tests/test_api_v1_services_service_watchdog.py b/tests/test_api_v1_services_service_watchdog.py
new file mode 100644
index 000000000..7e469af4c
--- /dev/null
+++ b/tests/test_api_v1_services_service_watchdog.py
@@ -0,0 +1,74 @@
+# Copyright 2022 Jared Hendrickson
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Class used to test the /api/v1/services/service_watchdog endpoint."""
+import e2e_test_framework
+
+
+class APIE2ETestServicesServiceWatchdog(e2e_test_framework.APIE2ETest):
+ """Class used to test the /api/v1/services/service_watchdog endpoint."""
+ uri = "/api/v1/services/service_watchdog"
+ put_tests = [
+ {
+ "name": "Check pfSense-pkg-Service_Watchdog installed constraint",
+ "status": 500,
+ "return": 12
+ },
+ {
+ "name": "Install pfSense-pkg-Service_Watchdog so we can test further",
+ "method": "POST",
+ "uri": "/api/v1/system/package",
+ "resp_time": 30,
+ "payload": {
+ "name": "pfSense-pkg-Service_Watchdog"
+ }
+ },
+ {
+ "name": "Check service 'name' required constraint",
+ "status": 400,
+ "return": 2259,
+ "payload": {"services": [{}]}
+ },
+ {
+ "name": "Check service 'name' options constraint",
+ "status": 400,
+ "return": 2260,
+ "payload": {"services": [{"name": "INVALID"}]}
+ },
+ {
+ "name": "Check service 'name' no duplicates constraint",
+ "status": 400,
+ "return": 2261,
+ "payload": {"services": [{"name": "unbound"}, {"name": "unbound"}]}
+ },
+ {
+ "name": "Update watched services",
+ "payload": {"services": [{"name": "unbound", "notify": True}]}
+ },
+ {
+ "name": "Read the Service Watchdog configuration",
+ "method": "GET"
+ },
+ {
+ "name": "Uninstall pfSense-pkg-Service_Watchdog",
+ "method": "DELETE",
+ "uri": "/api/v1/system/package",
+ "resp_time": 30,
+ "payload": {
+ "name": "pfSense-pkg-Service_Watchdog"
+ }
+ },
+ ]
+
+
+APIE2ETestServicesServiceWatchdog()