Skip to content

Commit

Permalink
feat: Service Watchdog endpoint (#273)
Browse files Browse the repository at this point in the history
* feat: added APIServicesServiceWatchdogRead model

* feat: added APIServicesServiceWatchdogUpdate model

* feat: added /api/v1/services/service_watchdog endpoint

* test: added tests for /api/v1/services/service_watchdog

* docs: added documentation for /api/v1/service/service_watchdog
  • Loading branch information
jaredhendrickson13 authored Sep 5, 2022
1 parent 5578989 commit 535d442
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
// 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.

require_once("api/framework/APIEndpoint.inc");

class APIServicesServiceWatchdog extends APIEndpoint {
public function __construct() {
$this->url = "/api/v1/services/service_watchdog";
}

protected function get() {
return (new APIServicesServiceWatchdogRead())->call();
}

protected function put() {
return (new APIServicesServiceWatchdogUpdate())->call();
}
}
18 changes: 18 additions & 0 deletions pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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 => [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
// 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.

require_once("api/framework/APIModel.inc");
require_once("api/framework/APIResponse.inc");


class APIServicesServiceWatchdogRead extends APIModel {

public function __construct() {
parent::__construct();
$this->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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php
// 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.

require_once("api/framework/APIModel.inc");
require_once("api/framework/APIResponse.inc");


class APIServicesServiceWatchdogUpdate extends APIModel {
public function __construct() {
parent::__construct();
$this->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;
}
}
50 changes: 50 additions & 0 deletions pfSense-pkg-API/files/usr/local/www/api/documentation/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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. <br><br>

_Requires at least one of the following privileges:_ [`page-all`, `page-system-advanced-admin`]<br>
_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.<br><br>

_Requires at least one of the following privileges:_ [`page-all`, `page-system-advanced-admin`]<br>
_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
Expand Down Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions tests/test_api_v1_services_service_watchdog.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 535d442

Please sign in to comment.