Skip to content
9 changes: 5 additions & 4 deletions docs/checks/commands/check_tasksched.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ Naemon Config

| Argument | Description |
| --------- | --------------------------------------------------------------------------------------------------------- |
| folder | The folder where the scheduled task is saved. This is used for exact matches, unless recurisive option is enabled. |
| recursive | Include the subfolders of the specified folder as well when searching for scheduled tasks. |
| folder | The folder where the scheduled task is saved. This is used for exact matches, unless recursive option is enabled. Default: '\' |
| hidden | Include hidden tasks. Default: 'false' |
| recursive | Include the subfolders of the specified folder as well when searching for scheduled tasks. Default: 'true' |
| timezone | Sets the timezone for time metrics (default is local time) |
| title | Sets the task to check. This corresonds to the title of the scheduled task, called TaskName in Powershell output. |
| title | Sets the task to check. This corresponds to the title of the scheduled task. Default: '\*' |

## Attributes

Expand Down Expand Up @@ -89,4 +90,4 @@ these can be used in filters and thresholds (along with the default attributes):
| next_run_time | Time when the registered task is next scheduled to run |
| parameters | Last actions command line parameters |
| execute | Last actions executed program |
| working_dir | Last actions working directory |
| working_directory | Last actions working directory |
4 changes: 2 additions & 2 deletions pkg/convert/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,9 @@ func BoolE(raw any) (bool, error) {
return val, nil
default:
switch strings.ToLower(fmt.Sprintf("%v", raw)) {
case "1", "enable", "enabled", "true", "yes", "on":
case "1", "enable", "enabled", "true", "t", "yes", "y", "on":
return true, nil
case "0", "disable", "disabled", "false", "no", "off":
case "0", "disable", "disabled", "false", "f", "no", "n", "off":
return false, nil
}
}
Expand Down
14 changes: 7 additions & 7 deletions pkg/snclient/check_drivesize.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (l *CheckDrivesize) Build() *CheckData {
{name: "drive_or_id", description: "Drive letter if present if not use id"},
{name: "drive_or_name", description: "Drive letter if present if not use name"},
{name: "fstype", description: "Filesystem type"},
{name: "mounted", description: "Flag whether drive is mounter (0/1)"},
{name: "mounted", description: "Flag whether drive is mounter (0/1)", unit: UBool},

{name: "free", description: "Free (human readable) bytes", unit: UByte},
{name: "free_bytes", description: "Number of free bytes", unit: UByte},
Expand All @@ -141,14 +141,14 @@ func (l *CheckDrivesize) Build() *CheckData {

{name: "media_type", description: "Windows only: numeric media type of drive"},
{name: "type", description: "Windows only: type of drive, ex.: fixed, cdrom, ramdisk, remote, removable, unknown"},
{name: "readable", description: "Windows only: flag drive is readable (0/1)"},
{name: "writable", description: "Windows only: flag drive is writable (0/1)"},
{name: "removable", description: "Windows only: flag drive is removable (0/1)"},
{name: "erasable", description: "Windows only: flag whether if drive is erasable (0/1)"},
{name: "hotplug", description: "Windows only: flag drive is hotplugable (0/1)"},
{name: "readable", description: "Windows only: flag drive is readable (0/1)", unit: UBool},
{name: "writable", description: "Windows only: flag drive is writable (0/1)", unit: UBool},
{name: "removable", description: "Windows only: flag drive is removable (0/1)", unit: UBool},
{name: "erasable", description: "Windows only: flag whether if drive is erasable (0/1)", unit: UBool},
{name: "hotplug", description: "Windows only: flag drive is hotplugable (0/1)", unit: UBool},

{name: "remote_name", description: "Windows only: the remote name of the drive, if it uses a network name"},
{name: "persistent", description: "Windows only: if the network drive is mounted as persistent (0/1)"},
{name: "persistent", description: "Windows only: if the network drive is mounted as persistent (0/1)", unit: UBool},
{name: "localised_remote_path", description: "Windows only: If the path is given as a remote path, and that remote path has an assigned logical drive," +
" this is the replaced path under that logical drive."},
},
Expand Down
33 changes: 25 additions & 8 deletions pkg/snclient/check_tasksched.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,22 @@ type CheckTasksched struct {
TaskTitle string
Folder string
Recursive bool
Hidden bool
}

const (
CheckTaskschedDefaultTaskTitle string = "*"
CheckTaskschedDefaultFolder string = "\\"
CheckTaskschedDefaultRecursive bool = true
CheckTaskschedDefaultHidden bool = false
)

func NewCheckTasksched() CheckHandler {
return &CheckTasksched{
TaskTitle: CheckTaskschedDefaultTaskTitle,
Folder: CheckTaskschedDefaultFolder,
Recursive: CheckTaskschedDefaultRecursive,
Hidden: CheckTaskschedDefaultHidden,
}
}

Expand All @@ -39,10 +42,24 @@ func (l *CheckTasksched) Build() *CheckData {
State: CheckExitOK,
},
args: map[string]CheckArgument{
"timezone": {description: "Sets the timezone for time metrics (default is local time)"},
"title": {value: &l.TaskTitle, description: "Sets the task to check. This corresonds to the title of the scheduled task, called TaskName in Powershell output."},
"folder": {value: &l.Folder, description: "The folder where the scheduled task is saved. This is used for exact matches, unless recurisive option is enabled."},
"recursive": {value: &l.Recursive, description: "Include the subfolders of the specified folder as well when searching for scheduled tasks."},
"timezone": {description: "Sets the timezone for time metrics (default is local time)"},
"title": {
value: &l.TaskTitle,
description: fmt.Sprintf("Sets the task to check. This corresponds to the title of the scheduled task. Default: '%s'", CheckTaskschedDefaultTaskTitle),
},
"folder": {
value: &l.Folder,
description: fmt.Sprintf("The folder where the scheduled task is saved. This is used for exact matches, unless recursive option is enabled. Default: '%s'",
CheckTaskschedDefaultFolder),
},
"recursive": {
value: &l.Recursive,
description: fmt.Sprintf("Include the subfolders of the specified folder as well when searching for scheduled tasks. Default: '%t'", CheckTaskschedDefaultRecursive),
},
"hidden": {
value: &l.Hidden,
description: fmt.Sprintf("Include hidden tasks. Default: '%t'", CheckTaskschedDefaultHidden),
},
},
defaultFilter: "enabled = true",
defaultCritical: "exit_code < 0",
Expand All @@ -56,24 +73,24 @@ func (l *CheckTasksched) Build() *CheckData {
{name: "application", description: "Name of the application that the task is associated with"},
{name: "comment", description: "Comment or description for the work item"},
{name: "creator", description: "Creator of the work item"},
{name: "enabled", description: "Flag whether this job is enabled (true/false)"},
{name: "enabled", description: "Flag whether this job is enabled (true/false)", unit: UBool},
{name: "exit_code", description: "The last jobs exit code"},
{name: "exit_string", description: "The last jobs exit code as string"},
{name: "folder", description: "Task folder"},
{name: "uri", description: "Fully qualified path to the task, includes folder and the task title"},
{name: "uri_clean", description: "Remove the leading backslash from the URI, only for tasks directly saved at root and not for ones saved inside folders."},
{name: "has_run", description: "True if this task has ever been executed"},
{name: "has_run", description: "True if this task has ever been executed", unit: UBool},
{name: "max_run_time", description: "Maximum length of time the task can run", unit: UDuration},
{name: "most_recent_run_time", description: "Most recent time the work item began running", unit: UDate},
{name: "priority", description: "Task priority"},
{name: "title", description: "Task title"},
{name: "hidden", description: "Indicates that the task will not be visible in the UI (true/false)"},
{name: "hidden", description: "Indicates that the task will not be visible in the UI (true/false)", unit: UBool},
{name: "missed_runs", description: "Number of times the registered task has missed a scheduled run"},
{name: "task_status", description: "Task status as string"},
{name: "next_run_time", description: "Time when the registered task is next scheduled to run", unit: UDate},
{name: "parameters", description: "Last actions command line parameters"},
{name: "execute", description: "Last actions executed program"},
{name: "working_dir", description: "Last actions working directory"},
{name: "working_directory", description: "Last actions working directory"},
},
exampleDefault: `
check_tasksched
Expand Down
105 changes: 67 additions & 38 deletions pkg/snclient/check_tasksched_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
_ "embed"
"fmt"
"os/exec"
"slices"
"strconv"
"strings"
Expand All @@ -18,16 +19,13 @@ import (
//go:embed embed/scripts/windows/scheduled_tasks.ps1
var scheduledTasksPS1 string

//nolint:funlen // function is long, but is simple, should not be dismantled
func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error {
script := scheduledTasksPS1
// if the task is not run, this date is reported
// corresponds to 1999-11-30 00:00:00 CET
// the number is unix miliseconds
const notRunDate = "/Date(943916400000)/"

// Add backslash to the beginning of the folder path if it does not exist
if l.Folder != CheckTaskschedDefaultFolder {
if !strings.HasPrefix(l.Folder, "\\") {
l.Folder = "\\" + l.Folder
}
}
func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckData) error {
l.cleanupArguments()

titleRuneBlacklist := []rune{'\\', '/', ':', '*', '?', '"', '<', '>', '|'}
if l.TaskTitle != CheckTaskschedDefaultTaskTitle {
Expand All @@ -44,33 +42,7 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD
}
}

cmd, err := powerShellCmd(
ctx, script,
PowerShellParameter{
name: "title",
parameterType: "string",
specifyDefaultValue: true,
defaultValue: CheckTaskschedDefaultTaskTitle,
specifyValue: true,
specifiedValue: l.TaskTitle,
},
PowerShellParameter{
name: "folder",
parameterType: "string",
specifyDefaultValue: true,
defaultValue: CheckTaskschedDefaultFolder,
specifyValue: true,
specifiedValue: l.Folder,
},
PowerShellParameter{
name: "recursive",
parameterType: "string",
specifyDefaultValue: true,
defaultValue: strconv.FormatBool(CheckTaskschedDefaultRecursive),
specifyValue: true,
specifiedValue: strconv.FormatBool(l.Recursive),
},
)
cmd, err := l.buildPowershellCmd(ctx)
if err != nil {
return fmt.Errorf("error when building a powershell command: %s", err.Error())
}
Expand All @@ -93,7 +65,7 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD
for index := range taskList {
task := taskList[index]
hasRun := false
if task.LastRunTime != "" {
if task.LastRunTime != notRunDate {
hasRun = true
}

Expand All @@ -118,7 +90,7 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD
"next_run_time": fmt.Sprintf("%d", l.parseDate(task.NextRunTime).Unix()),
"parameters": l.parseParameters(task.Actions),
"execute": l.parseExecuteCmd(task.Actions),
"working_dir": l.parseWorkingDir(task.Actions),
"working_directory": l.parseWorkingDir(task.Actions),
}
check.listData = append(check.listData, entry)
}
Expand All @@ -131,6 +103,63 @@ func (l *CheckTasksched) addTasks(ctx context.Context, snc *Agent, check *CheckD
return nil
}

func (l *CheckTasksched) cleanupArguments() {
// Add backslash to the beginning of the folder path if it does not exist
if l.Folder != CheckTaskschedDefaultFolder {
if !strings.HasPrefix(l.Folder, "\\") {
l.Folder = "\\" + l.Folder
}
}

// Remove backslash at the end of the folder path, if it is not exactly root: "\"
// "\Microsoft\" -> "\Microsoft"
if l.Folder != CheckTaskschedDefaultFolder && l.Folder != "\\" {
if cut, cutOk := strings.CutSuffix(l.Folder, "\\"); cutOk {
l.Folder = cut
}
}
}

func (l *CheckTasksched) buildPowershellCmd(ctx context.Context) (cmd *exec.Cmd, err error) {
cmd, err = powerShellCmd(
ctx, scheduledTasksPS1,
PowerShellParameter{
name: "title",
parameterType: "string",
specifyDefaultValue: true,
defaultValue: CheckTaskschedDefaultTaskTitle,
specifyValue: true,
specifiedValue: l.TaskTitle,
},
PowerShellParameter{
name: "folder",
parameterType: "string",
specifyDefaultValue: true,
defaultValue: CheckTaskschedDefaultFolder,
specifyValue: true,
specifiedValue: l.Folder,
},
PowerShellParameter{
name: "recursive",
parameterType: "string",
specifyDefaultValue: true,
defaultValue: strconv.FormatBool(CheckTaskschedDefaultRecursive),
specifyValue: true,
specifiedValue: strconv.FormatBool(l.Recursive),
},
PowerShellParameter{
name: "hidden",
parameterType: "string",
specifyDefaultValue: true,
defaultValue: strconv.FormatBool(CheckTaskschedDefaultHidden),
specifyValue: true,
specifiedValue: strconv.FormatBool(l.Hidden),
},
)

return cmd, err
}

func parseURIClean(uri string) string {
if strings.Count(uri, "\\") == 1 {
if cut, cutOk := strings.CutPrefix(uri, "\\"); cutOk {
Expand Down
1 change: 1 addition & 0 deletions pkg/snclient/checkdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const (
UDate
UTimestamp
UPercent
UBool
)

type CheckAttribute struct {
Expand Down
32 changes: 31 additions & 1 deletion pkg/snclient/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,8 @@ func (c *Condition) compareEmpty() bool {
// tries keyword_pct for % unit and keyword_bytes for B unit
// returns value from keyword unless found already
func (c *Condition) getVarValue(data map[string]string) (varStr string, ok bool) {
unit := c.getUnit(c.keyword)

switch {
case c.unit == "%":
varStr, ok = data[c.keyword+"_pct"]
Expand All @@ -482,6 +484,18 @@ func (c *Condition) getVarValue(data map[string]string) (varStr string, ok bool)

varStr, ok = data[c.keyword]

if ok && unit == UBool {
valBool, err := convert.BoolE(varStr)
if err != nil {
log.Errorf("condition based on non-bool value: %s", err.Error())
}
if valBool {
return "true", true
}

return "false", true
}

return varStr, ok
}

Expand Down Expand Up @@ -816,13 +830,27 @@ func (c *Condition) expandDateKeyword(str string) bool {
return false
}

//nolint:funlen // the function is long due to handling all unit types, but it is simple
func (c *Condition) expandUnitByType(str string) error {
// valid units might be "today", "thisweek", "thismonth", "thisyear" and ":utc" variants
unit := c.getUnit(c.keyword)
if unit == UDate || unit == UTimestamp {

switch unit {
case UDate, UTimestamp:
if done := c.expandDateKeyword(str); done {
return nil
}
case UBool:
// boolean units are not in the form of '<number> <unit>'
// need to handle them before regex condition checks.
newVal, err := convert.BoolE(str)
if err != nil {
return fmt.Errorf("invalid boolean value: %s", err.Error())
}
c.value = newVal

return nil
default:
}

match := reConditionValueUnit.FindStringSubmatch(str)
Expand Down Expand Up @@ -870,6 +898,8 @@ func (c *Condition) expandUnitByType(str string) error {
return nil
case UPercent:
return nil
case UBool:
return nil // handled in the switch above
case UNone:
// best effort unit expansion
return c.expandUnitByName(str)
Expand Down
25 changes: 25 additions & 0 deletions pkg/snclient/condition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,31 @@ func TestConditionStrings(t *testing.T) {
}
}

func TestConditionBool(t *testing.T) {
for _, check := range []struct {
key string
value string
condition string
expect bool
deterministic bool
}{
{"mounted", "1", "mounted = 1", true, true},
{"mounted", "0", "mounted = 1", false, true},
{"mounted", "0", "mounted = 0", true, true},
{"mounted", "1", "mounted = true", true, true},
{"mounted", "0", "mounted = true", false, true},
{"mounted", "0", "mounted = false", true, true},
} {
cond, err := NewCondition(check.condition, &[]CheckAttribute{{name: "mounted", unit: UBool}})
require.NoError(t, err)

compare := map[string]string{check.key: check.value}
res, ok := cond.Match(compare)
assert.Equalf(t, check.expect, res, "Compare(%s) -> (%v) %v", check.condition, check.value, check.expect)
assert.Equalf(t, check.deterministic, ok, "Compare(%s) -> determined: (%v) %v", check.condition, check.value, check.deterministic)
}
}

func TestConditionParseErrors(t *testing.T) {
for _, check := range []struct {
threshold string
Expand Down
Loading
Loading