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()