Compare commits
28 Commits
feat_docs
...
4fa1e6e4ef
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fa1e6e4ef | |||
| 06c48f2927 | |||
| 54c0e8a46b | |||
| 7b4dcdd1d8 | |||
| 72d0aa61cd | |||
| e37a84d77d | |||
| 2d6ca2b0bd | |||
| df2b9e7624 | |||
| 46d1270648 | |||
| 5cbf066ccf | |||
| 515e244592 | |||
| fb4322c040 | |||
| f376d8684b | |||
| d0439394cf | |||
| a91632028c | |||
| bccdcf2ca3 | |||
| 18e3a93a38 | |||
| cfb553c975 | |||
| 1466623070 | |||
| c527d40721 | |||
| 781c096abf | |||
| c94fbd4b48 | |||
| 1f00713c1e | |||
| 12cf423550 | |||
| f99726d3b7 | |||
| 13932e572f | |||
| 20120785bd | |||
| 5af4c963ea |
32
README.md
32
README.md
@@ -62,6 +62,38 @@ gokill should run as daemon. config should be read from /etc/somename/config.jso
|
||||
]
|
||||
```
|
||||
|
||||
## nix support
|
||||
|
||||
gokill enjoys full nix support. gokill exposes a nix flakes that outputs a gokill package, a nixosModule and more.
|
||||
That means you can super easily incorporate gokill into your existing nixosConfigurations.
|
||||
Here is a small example config:
|
||||
|
||||
``` nix
|
||||
{
|
||||
services.gokill.enable = true;
|
||||
services.gokill.triggers = [
|
||||
{
|
||||
type = "EthernetDisconnect";
|
||||
name = "MainTrigger";
|
||||
options = {
|
||||
interfaceName = "eth1";
|
||||
};
|
||||
actions = [
|
||||
{
|
||||
type = "Command";
|
||||
options = {
|
||||
command = "echo hello world";
|
||||
};
|
||||
stage = 1;
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
This will automatically configure and enable a systemd running gokill as root user in the background
|
||||
|
||||
## actions
|
||||
- [x] shutdown
|
||||
- [ ] wipe ram
|
||||
|
||||
@@ -45,7 +45,7 @@ func (a StagedActions) executeInternal(f func(Action)) {
|
||||
err := <-a.ActionChan
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error occured on Stage %d: %s", idx+1, err)
|
||||
fmt.Printf("Error occured on Stage %d: %s\n", idx+1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,7 @@ func GetAllActions() []DocumentedAction {
|
||||
Printer{},
|
||||
TimeOut{},
|
||||
Command{},
|
||||
ShellScript{},
|
||||
Shutdown{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func (p Printer) GetExample() string {
|
||||
return `
|
||||
{
|
||||
type: "Print",
|
||||
"options: {
|
||||
"options": {
|
||||
"message": "Hello World!"
|
||||
}
|
||||
}
|
||||
|
||||
119
actions/shell_script.go
Normal file
119
actions/shell_script.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"os"
|
||||
|
||||
"unknown.com/gokill/internal"
|
||||
)
|
||||
|
||||
type ShellScript struct {
|
||||
Path string `json:"path"`
|
||||
ActionChan ActionResultChan
|
||||
}
|
||||
|
||||
func isExecutableFile(path string) bool {
|
||||
fi, err := os.Lstat(path)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Test executing Shellscript Failed.")
|
||||
return false
|
||||
}
|
||||
|
||||
mode := fi.Mode()
|
||||
|
||||
//TODO: should check if current user can execute
|
||||
if mode&01111 == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c ShellScript) DryExecute() {
|
||||
fmt.Printf("Test Executing ShellScript:\n%s\n", c.Path)
|
||||
|
||||
_, err := os.Open(c.Path)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Test executing Shellscript Failed.")
|
||||
c.ActionChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
if !isExecutableFile(c.Path) {
|
||||
fmt.Println("Test executing Shellscript Failed.")
|
||||
c.ActionChan <- fmt.Errorf("File is not executable: %s", c.Path)
|
||||
return
|
||||
}
|
||||
|
||||
c.ActionChan <- nil
|
||||
}
|
||||
|
||||
func (c ShellScript) Execute() {
|
||||
if !isExecutableFile(c.Path) {
|
||||
fmt.Println("Test executing Shellscript Failed.")
|
||||
c.ActionChan <- fmt.Errorf("File is not executable: %s", c.Path)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("/bin/sh", c.Path)
|
||||
|
||||
stdout, err := cmd.Output()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
c.ActionChan <- err
|
||||
}
|
||||
|
||||
fmt.Println(string(stdout[:]))
|
||||
c.ActionChan <- nil
|
||||
}
|
||||
|
||||
func CreateShellScript(config internal.ActionConfig, c ActionResultChan) (ShellScript, error) {
|
||||
result := ShellScript{}
|
||||
|
||||
err := json.Unmarshal(config.Options, &result)
|
||||
|
||||
if err != nil {
|
||||
return ShellScript{}, err
|
||||
}
|
||||
|
||||
if result.Path == "" {
|
||||
return ShellScript{}, internal.OptionMissingError{"path"}
|
||||
}
|
||||
|
||||
result.ActionChan = c
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (cc ShellScript) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
|
||||
return CreateShellScript(config, c)
|
||||
}
|
||||
|
||||
func (p ShellScript) GetName() string {
|
||||
return "ShellScript"
|
||||
}
|
||||
|
||||
func (p ShellScript) GetDescription() string {
|
||||
return "Executes the given shell script."
|
||||
}
|
||||
|
||||
func (p ShellScript) GetExample() string {
|
||||
return `
|
||||
{
|
||||
"type": "ShellScript",
|
||||
"options": {
|
||||
"path": "/path/to/file.sh"
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func (p ShellScript) GetOptions() []internal.ConfigOption {
|
||||
return []internal.ConfigOption{
|
||||
{"path", "string", "path to script to execute", ""},
|
||||
}
|
||||
}
|
||||
@@ -3,23 +3,24 @@ package actions
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"encoding/json"
|
||||
|
||||
"unknown.com/gokill/internal"
|
||||
)
|
||||
|
||||
type Shutdown struct {
|
||||
Timeout string `json:"time"`
|
||||
ActionChan ActionResultChan
|
||||
}
|
||||
|
||||
func (s Shutdown) DryExecute() {
|
||||
fmt.Printf("shutdown -h %s\n", s.Timeout)
|
||||
fmt.Println("Test Shutdown executed...")
|
||||
|
||||
s.ActionChan <- nil
|
||||
|
||||
}
|
||||
|
||||
func (s Shutdown) Execute() {
|
||||
if err := exec.Command("shutdown", "-h", "now").Run(); err != nil {
|
||||
if err := exec.Command("shutdown", "-h", s.Timeout).Run(); err != nil {
|
||||
fmt.Println("Failed to initiate shutdown:", err)
|
||||
}
|
||||
|
||||
@@ -29,7 +30,16 @@ func (s Shutdown) Execute() {
|
||||
}
|
||||
|
||||
func (s Shutdown) Create(config internal.ActionConfig, c ActionResultChan) (Action, error) {
|
||||
return Shutdown{c}, nil
|
||||
var result Shutdown
|
||||
err := json.Unmarshal(config.Options, &result)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Parsing Shutdown options failed.")
|
||||
return Shutdown{}, err
|
||||
}
|
||||
|
||||
result.ActionChan = c
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (p Shutdown) GetName() string {
|
||||
@@ -44,10 +54,20 @@ func (p Shutdown) GetExample() string {
|
||||
return `
|
||||
{
|
||||
"type": "Shutdown",
|
||||
"options": {
|
||||
"time": "+5" //wait 5 minutes before shutdown
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func (p Shutdown) GetOptions() []internal.ConfigOption {
|
||||
return []internal.ConfigOption{}
|
||||
return []internal.ConfigOption{
|
||||
{
|
||||
Name: "time",
|
||||
Type: "string",
|
||||
Description: "TIME parameter passed to shutdown as follows ```shutdown -h TIME```",
|
||||
Default: "now",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,33 @@ type Command struct {
|
||||
ActionChan ActionResultChan
|
||||
}
|
||||
|
||||
func isCommandAvailable(name string) bool {
|
||||
cmd := exec.Command("/bin/sh", "-c", "command -v "+name)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c Command) DryExecute() {
|
||||
fmt.Printf("Test Executing Command:\n%s ", c.Command)
|
||||
fmt.Printf("Test Executing Command:\n%s\n", c.Command)
|
||||
command, _, err := c.splitCommandString()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error during argument parsing of command '%s'\n", c.Command)
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
isAvailable := isCommandAvailable(command)
|
||||
|
||||
if !isAvailable {
|
||||
fmt.Printf("Command %s not found\n", command)
|
||||
c.ActionChan <- fmt.Errorf("Command %s not found!", command)
|
||||
return
|
||||
}
|
||||
|
||||
c.ActionChan <- nil
|
||||
}
|
||||
|
||||
@@ -48,6 +73,7 @@ func (c Command) Execute() {
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
c.ActionChan <- err
|
||||
}
|
||||
|
||||
fmt.Println(string(stdout[:]))
|
||||
|
||||
@@ -15,9 +15,7 @@ Actions have the following syntax:
|
||||
"type": "SomeAction",
|
||||
"options": { //each action defines its own options
|
||||
"firstOption": "someValue",
|
||||
"Stage": 2 //this (positive) number defines the order of multiple actions
|
||||
"stage": 2 //this (positive) number defines the order of multiple actions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To get a list of all actions and their options from the commandline run ``` gokill -d ```
|
||||
|
||||
@@ -17,5 +17,3 @@ Triggers have the following syntax:
|
||||
"actions": [] //list actions that should be executed here
|
||||
}
|
||||
```
|
||||
|
||||
To get a list of all triggers and their options from the commandline run ```gokill -d```
|
||||
|
||||
121
flake.nix
121
flake.nix
@@ -44,101 +44,41 @@
|
||||
|
||||
packages.x86_64-linux.default = self.packages.x86_64-linux.gokill;
|
||||
|
||||
nixosModules.gokill = { config, lib, pkgs, ... }:
|
||||
let
|
||||
cfg = config.services.gokill;
|
||||
configFile = pkgs.writeText "config.json" ''${cfg.extraConfig}'';
|
||||
gokill-pkg = self.packages.x86_64-linux.gokill;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.gokill = {
|
||||
enable = lib.mkOption {
|
||||
default = false;
|
||||
type = lib.types.bool;
|
||||
description = lib.mdDoc ''
|
||||
Enables gokill daemon
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = lib.mdDoc ''
|
||||
gokill config.json
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
systemd.services.gokill = {
|
||||
description = "gokill daemon";
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${gokill-pkg}/bin/gokill -c ${configFile}";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
|
||||
wantedBy = [ "default.target" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
nixosModules.gokill = import ./nixos-modules/gokill.nix { self = self; };
|
||||
|
||||
packages.x86_64-linux.testVm =
|
||||
let
|
||||
nixos = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
specialArgs = { inherit self; };
|
||||
modules = [
|
||||
self.nixosModules.gokill
|
||||
{
|
||||
services.gokill.enable = true;
|
||||
services.gokill.extraConfig = ''
|
||||
[
|
||||
{
|
||||
"type": "Timeout",
|
||||
"name": "custom timeout",
|
||||
"options": {
|
||||
"duration": 30
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "Print",
|
||||
"options": {
|
||||
"message": "Stage 1 triggered. Waiting 25 seconds"
|
||||
},
|
||||
"stage": 1
|
||||
},
|
||||
{
|
||||
"type": "Timeout",
|
||||
"options": {
|
||||
"duration": 20
|
||||
},
|
||||
"stage": 1
|
||||
},
|
||||
{
|
||||
"type": "Timeout",
|
||||
"options": {
|
||||
"duration": 5
|
||||
},
|
||||
"stage": 2
|
||||
},
|
||||
{
|
||||
"type": "Print",
|
||||
"options": {
|
||||
"message": "Shutdown in 5 seconds..."
|
||||
},
|
||||
"stage": 2
|
||||
},
|
||||
{
|
||||
"type": "Shutdown",
|
||||
"options": {
|
||||
},
|
||||
"stage": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
'';
|
||||
services.gokill.triggers = [
|
||||
{
|
||||
type = "Timeout";
|
||||
name = "custom timeout";
|
||||
options = {
|
||||
duration = 10;
|
||||
};
|
||||
actions = [
|
||||
{
|
||||
type = "Timeout";
|
||||
options = {
|
||||
duration = 5;
|
||||
};
|
||||
stage = 1;
|
||||
}
|
||||
{
|
||||
type = "Shutdown";
|
||||
options = {
|
||||
};
|
||||
stage = 2;
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
users.users.root.password = "root";
|
||||
virtualisation.vmVariant.virtualisation.graphics = false;
|
||||
}
|
||||
@@ -160,5 +100,14 @@
|
||||
program = builtins.toString (nixpkgs.legacyPackages."x86_64-linux".writeScript "docs" ''
|
||||
${pkgs.python3}/bin/python3 -m http.server --directory ${self.packages."x86_64-linux".docs}/share/doc'');
|
||||
};
|
||||
|
||||
checks = forAllSystems (system: let
|
||||
checkArgs = {
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
inherit self;
|
||||
};
|
||||
in {
|
||||
gokill = import ./test/test.nix checkArgs;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
73
nixos-modules/gokill.nix
Normal file
73
nixos-modules/gokill.nix
Normal file
@@ -0,0 +1,73 @@
|
||||
flake: { config, lib, pkgs, self, ... }:
|
||||
let
|
||||
cfg = config.services.gokill;
|
||||
configFile = pkgs.writeText "config.json" (builtins.toJSON cfg.triggers);
|
||||
gokill-pkg = self.packages.x86_64-linux.gokill;
|
||||
in
|
||||
{
|
||||
options = with lib; {
|
||||
services.gokill = {
|
||||
enable = mkOption {
|
||||
default = false;
|
||||
type = types.bool;
|
||||
description = mdDoc ''
|
||||
Enables gokill daemon
|
||||
'';
|
||||
};
|
||||
|
||||
triggers = mkOption {
|
||||
description = "list of triggers";
|
||||
default = [];
|
||||
type = with types; types.listOf ( submodule {
|
||||
options = {
|
||||
type = mkOption {
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
options = mkOption {
|
||||
type = types.attrs;
|
||||
};
|
||||
|
||||
actions = mkOption {
|
||||
description = "list of actions";
|
||||
type = with types; types.listOf ( submodule {
|
||||
options = {
|
||||
type = mkOption {
|
||||
type = types.str;
|
||||
};
|
||||
|
||||
options = mkOption {
|
||||
type = types.attrs;
|
||||
};
|
||||
|
||||
stage = mkOption {
|
||||
type = types.int;
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
systemd.services.gokill = {
|
||||
description = "gokill daemon";
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${gokill-pkg}/bin/gokill -c ${configFile}";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
|
||||
wantedBy = [ "default.target" ];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
21
test/lib.nix
Normal file
21
test/lib.nix
Normal file
@@ -0,0 +1,21 @@
|
||||
# tests/lib.nix
|
||||
# based on https://blog.thalheim.io/2023/01/08/how-to-use-nixos-testing-framework-with-flakes/
|
||||
# The first argument to this function is the test module itself
|
||||
test:
|
||||
# These arguments are provided by `flake.nix` on import, see checkArgs
|
||||
{ pkgs, self}:
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
# this imports the nixos library that contains our testing framework
|
||||
nixos-lib = import (pkgs.path + "/nixos/lib") {};
|
||||
in
|
||||
(nixos-lib.runTest {
|
||||
hostPkgs = pkgs;
|
||||
# This speeds up the evaluation by skipping evaluating documentation (optional)
|
||||
defaults.documentation.enable = lib.mkDefault false;
|
||||
# This makes `self` available in the NixOS configuration of our virtual machines.
|
||||
# This is useful for referencing modules or packages from your own flake
|
||||
# as well as importing from other flakes.
|
||||
node.specialArgs = { inherit self; };
|
||||
imports = [ test ];
|
||||
}).config.result
|
||||
40
test/test.nix
Normal file
40
test/test.nix
Normal file
@@ -0,0 +1,40 @@
|
||||
(import ./lib.nix) {
|
||||
name = "gokill-base-test";
|
||||
nodes = {
|
||||
node1 = { self, pkgs, ... }: {
|
||||
imports = [ self.nixosModules.gokill ];
|
||||
|
||||
services.gokill = {
|
||||
enable = true;
|
||||
triggers = [
|
||||
{
|
||||
type = "Timeout";
|
||||
name = "custom timeout";
|
||||
options = {
|
||||
duration = 3;
|
||||
};
|
||||
actions = [
|
||||
{
|
||||
type = "Command";
|
||||
options = {
|
||||
command = "echo hello world";
|
||||
};
|
||||
stage = 2;
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import time
|
||||
start_all() # wait for our service to start
|
||||
node1.wait_for_unit("gokill")
|
||||
time.sleep(4)
|
||||
output = node1.succeed("journalctl -u gokill.service | tail -n 2 | head -n 1")
|
||||
# Check if our webserver returns the expected result
|
||||
assert "hello world" in output
|
||||
'';
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package triggers
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"unknown.com/gokill/actions"
|
||||
@@ -17,7 +17,7 @@ type EthernetDisconnect struct {
|
||||
}
|
||||
|
||||
func isEthernetConnected(deviceName string) bool {
|
||||
content, err := ioutil.ReadFile(fmt.Sprintf("/sys/class/net/%s/operstate", deviceName))
|
||||
content, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/operstate", deviceName))
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -95,7 +95,7 @@ func (p EthernetDisconnect) GetExample() string {
|
||||
"options": {
|
||||
"interfaceName": "eth0",
|
||||
"waitTillConnected": true
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
]
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (p TimeOut) GetExample() string {
|
||||
"name": "Example Trigger",
|
||||
"options": {
|
||||
"duration": 5
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
]
|
||||
}
|
||||
|
||||
@@ -85,7 +85,10 @@ func (p UsbDisconnect) GetName() string {
|
||||
}
|
||||
|
||||
func (p UsbDisconnect) GetDescription() string {
|
||||
return "Triggers when given usb drive is disconnected"
|
||||
return `
|
||||
Triggers when given usb drive is disconnected.
|
||||
Currently it simply checks that the file /dev/disk/by-id/$deviceId exists.
|
||||
`
|
||||
}
|
||||
|
||||
func (p UsbDisconnect) GetExample() string {
|
||||
@@ -96,7 +99,7 @@ func (p UsbDisconnect) GetExample() string {
|
||||
"options": {
|
||||
"deviceId": "ata-Samsung_SSD_860_EVO_1TB_S4AALKWJDI102",
|
||||
"waitTillConnected": true
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user