Herald Daemon is designed as a lightweight task dispatcher. It could be used to arrange the server maintenance jobs, which is able to control the tasks for a single server and remote servers. Job scripts could be located on any git server.
The Herald Daemon builds the Herald workflow from a YAML configuration file. It also provides some common Herald components.
Download binary file from the release page.
If you would like to build from source, first install Go and setup the workspace, then use the following command to install Herald Daemon.
$ go get -u github.com/heraldgo/heralddWrite a configuration file and run the Herald Daemon:
$ heraldd -config config.ymlPress Ctrl+C to exit.
The workflow is defined in a single YAML file.
The configuration consists of following sections:
- log
- plugin
- trigger
- selector
- executor
- preset
- router
If no output specified, the log will go to stderr.
log:
  level: DEBUG
  output: /var/log/heraldd/heraldd.logThe configuration structure for trigger, selector and executor are quite similar. Take selector as an example:
selector:
  selector1_name:
    type: selector_type
    param1: value1
    param2: value2
  selector2_name:
    type: selector_typeThe name for the same component must be unique. Each component should have its type, which could be omitted if it is the same as the component name. All remaining parameters are passed to component. The parameters vary among different component types.
It is not necessary to write configuration for all components. They could be specified by the type name in router directly, which will use their default parameters.
The preset section includes a map which includes some common params
which could be used in router.
preset:
  preset1:
    key1: value1
    key2: value2
  preset2:
    key3: value3router:
  router1_name:
    trigger: trigger1_name
    selector: selector1_name
    task:
      task1_name: executor1_name
      task2_name:
        executor: executor2_name
        select_param:
          preset: [preset1, preset2]
          key1: value1
        job_param: preset2
    select_param:
      key2: value2
    job_param:
      preset: preset3
      key3: value3
  router2_name:
    trigger: trigger1_name
    selector: selector2_name
    task:
      task3_name: executor2_name
    job_param: [preset1, preset3, preset4]select_param will be passed to the selector and job_param will be
added to the execution param of executor. Both params could be specified
in the route level and the task level. Each param could specify the
preset value which could be a string or a list of strings.
The final param for each task is the combination of preset and inline params from both router level and task level. In case there are conflicts, the priority is:
Task inline > Task preset[0] > Task preset[1] > ... > Router inline > Router preset[0] > Router preset[1] > ...
The name preset is reserved and should not be used as param name.
If no task specific params are needed, the executor name could be used
directly as a string.
If inline params are absent, the preset name could be specified as
a string or slice of strings directly.
This is an example which print the param every 2 seconds.
trigger:
  every2s:
    type: tick
    interval: 2
router:
  print_param_every2s:
    trigger: every2s
    selector: all
    task:
      print_param: printThis is an example which run uptime command on wednesday morning.
trigger:
  wednesday_morning:
    type: cron
    cron: '30 6 * * 3'
executor:
  local_command:
    type: local
    work_dir: /var/lib/heraldd/work
router:
  uptime_wednesday_morning:
    trigger: wednesday_morning
    selector: all
    task:
      run_local: local_command
    job_param:
      cmd: uptime
  print_result:
    trigger: exe_done
    selector: match_map
    task:
      print_result: print
    select_param:
      match_key: router
      match_value: uptime_wednesday_morning
    job_param:
      print_key: trigger_param/resultexe_done trigger could be used to get the job execution result.
The match_map selector here only accepts previous job which
comes from uptime_wednesday_morning router.
You can put common params in the preset section, which could
be referenced in router.
trigger:
  every5s:
    type: tick
    interval: 5
executor:
  local_command:
    type: local
    work_dir: /var/lib/heraldd/work
preset:
  hostname:
    cmd: hostname
  df:
    cmd: df
    arg: [-hT]
  uptime:
    cmd: uptime
router:
  run_every5s:
    trigger: every5s
    selector: all
    task:
      hostname:
        executor: local_command
        job_param:
          preset: hostname
      df:
        executor: local_command
        job_param:
          preset: df
      uptime:
        executor: local_command
        job_param:
          preset: uptime
  print_result:
    trigger: exe_done
    selector: match_map
    task:
      print_result: print
    select_param:
      match_key: router
      match_value: run_every5s
    job_param:
      print_key: [trigger_param/task, trigger_param/result/exit_code, trigger_param/result/output]You can combine different triggers, executors and selectors in the routers.
trigger:
  every2s:
    type: tick
    interval: 2
  wednesday_morning:
    type: cron
    cron: '30 6 * * 3'
  every_evening:
    type: cron
    cron: '0 18 * * *'
executor:
  local_command:
    type: local
    work_dir: /var/lib/heraldd/work
  remote_command:
    type: http_remote
    host: https://example.com/
    secret: yyyyyyyyyyyyyyyy
    data_dir: /var/lib/heraldd/data
preset:
  common_script_repo:
    git_repo: https://github.com/heraldgo/herald-script
    git_username: user
    git_password: pass
router:
  print_param_every2s:
    trigger: every2s
    selector: all
    task:
      print_param: print
  ls_wednesday_morning:
    trigger: wednesday_morning
    selector: all
    task:
      run_local_ls: local_command
    job_param:
      cmd: ls
      arg: /
  run_every_evening:
    trigger: every_evening
    selector: all
    task:
      hostname:
        executor: remote_command
        job_param:
          cmd: hostname
      df:
        executor: local_command
        job_param:
          cmd: df
          arg: [-hT]
      uptime:
        executor: local_command
        job_param:
          cmd: uptime
      print_param: print
  doit_remote_every_evening:
    trigger: every_evening
    selector: all
    task:
      doit_locally: local_command
      doit_remotely: remote_command
    job_param:
      preset: common_script_repo
      cmd: doit.shTrigger defines when the job workflow should start. Herald Daemon provides the following triggers.
This is an internal trigger name, not a type.
It is automatically activated after any job is done.
Do NOT define a trigger with the same name.
You can use exe_done trigger directly in the router.
The "trigger param" for exe_done is the result of last job, which
looks like:
{
  "id": "F60CFC6A-2FDE-248D-6C35-C3EFD484014F",
  "trigger_id": "A8D875BC-5875-3BA7-EECB-F829A341F78E",
  "router": "router_name",
  "trigger": "trigger_name",
  "selector": "selector_name",
  "task": "task_name",
  "executor": "executor_name",
  "trigger_param": {},
  "select_param": {},
  "job_param": {},
  "success": true,
  "error": "",
  "result": {},
}The "trigger param" above is just the job common
information plus the job execution result.
The content of result depends on the execution,
and varies among executors.
exe_done can be used to check the status of a job exeuction.
router:
  run_every5s:
    trigger: every5s
    selector: all
    task:
      hostname: local_command
    job_param:
      cmd: hostname
  print_result:
    trigger: exe_done
    selector: match_map
    task:
      print_result: print
    select_param:
      match_key: router
      match_value: run_every5sWith exe_done you can also build a task chain with proper selector.
router:
  step1:
    trigger: every_morning
    selector: all
    task:
      step1: print
  step2:
    trigger: exe_done
    selector: match_map
    task:
      step2: print
    select_param:
      match_key: router
      match_value: step1
  step3:
    trigger: exe_done
    selector: match_map
    task:
      step3: print
    select_param:
      match_key: router
      match_value: step2Do NOT use all selector with exe_done trigger, which will lead to
a dead loop.
A trigger activated periodically. The unit for the interval is second.
trigger:
  every2s:
    type: tick
    interval: 2"cron" builds a trigger with cron syntax. It uses the cron library.
trigger:
  cron:
    cron: '30 6 * * *'You can add second field if option with_seconds is true.
trigger:
  cron_every2s:
    type: cron
    cron: '*/2 * * * * *'
    with_seconds: true"http" is trigger which will create a http server. The trigger will be activated when it receives proper http request.
trigger:
  manual:
    type: http
    host: 127.0.0.1
    port: 8123You must POST with a json body to the server to activate the trigger:
$ curl -i -H "Content-Type:application/json" -X POST -d '{"clean":"old_files"}' localhost:8123The json body will be parsed as the "trigger param".
This trigger is suitable for doing some manual actions.
"http" trigger can also listen on unix socket, which could use nginx as the reverse proxy.
trigger:
  http:
    unix_socket: /var/run/heraldd/http.sockThere is no authority control for this trigger, so it is not a good idea to open it globally.
The selector check the "trigger param" and "job param" to determine whether or not to proceed with the job execution.
Pass all the situation.
router:
  print_param_every2s:
    trigger: every2s
    selector: all
    task:
      print_param: printOnly pass when specified key and value match in trigger param. Nested keys are seperated by "/".
router:
  print_result:
    trigger: exe_done
    selector: match_map
    task:
      print_result: print
    select_param:
      match_key: router
      match_value: uptime_wednesday_morningIf match_value is absent, it will only check the existence of
the match_key.
except_map is the opposite of match_map. It will NOT pass
if specified key and value are matched.
router:
  print_result:
    trigger: exe_done
    selector: except_map
    task:
      print_result: print
    select_param:
      except_key: router
      except_value: print_resultIf except_value is absent, it will fail when except_key exists.
In case the selection is complex and no internal selector is available,
external selector provides a way to write your own program as selector.
It will call an external program which sets json format
of "trigger param" and "job param" as environment variables.
The default variable names are HERALD_TRIGGER_PARAM and
HERALD_SELECT_PARAM, which could be configured in selector options
trigger_param_env and select_param_env individually.
The selector will pass if the exit code is 0.
selector:
  xxx:
    type: external
    program: /selector/xxx.py
    #trigger_param_env: HERALD_TRIGGER_PARAM
    #select_param_env: HERALD_SELECT_PARAM
router:
  print_result:
    trigger: exe_done
    selector: xxx
    task:
      print_result: print
    select_param:
      key: valueThis is an example of program written in python:
#!/usr/bin/env python
import sys
import json
trigger_param = json.loads(os.environ['HERALD_TRIGGER_PARAM'])
select_param = json.loads(os.environ['HERALD_SELECT_PARAM'])
if trigger_param.get('key') != select_param.get('key'):
    sys.exit(1)  # Do not pass
sys.exit(0)This is what the execution param looks like.
{
  "id": "F60CFC6A-2FDE-248D-6C35-C3EFD484014F",
  "trigger_id": "A8D875BC-5875-3BA7-EECB-F829A341F78E",
  "router": "router_name",
  "trigger": "trigger_name",
  "selector": "selector_name",
  "task": "task_name",
  "executor": "executor_name",
  "trigger_param": {},
  "select_param": {},
  "job_param": {}
}trigger_param comes from the trigger. job_param is combined
from router and task.
Do nothing. Could be used for debug purpose.
Print the job param to log.
router:
  print:
    trigger: ttt
    selector: all
    task:
      print_it: print
    job_param:
      print_key: [trigger, trigger_param/result]If the option print_key is set as job param,
the print executor will only print specified keys.
Run command on the local server.
Make sure work_dir is set properly, which will keep the git repo
and used as the command current work directory.
executor:
  local_command:
    type: local
    work_dir: /var/lib/heraldd/work
router:
  run_cmd:
    trigger: ttt
    selector: all
    task:
      run_cmd: local_command
    job_param:
      cmd: uptime
  run_git:
    trigger: ttt
    selector: all
    task:
      run_git: local_command
    job_param:
      git_repo: https://github.com/heraldgo/herald-script.git
      cmd: run/doit.sh
  check_env:
    trigger: ttt
    selector: all
    task:
      run_git: local_command
    job_param:
      cmd: printenv
      arg: ['TEST_SET_ENV', 'TEST_ANOTHER_ENV']
      env:
        TEST_SET_ENV: 'Herald daemon'
        TEST_ANOTHER_ENV: 'This is another env'
  print_result:
    trigger: exe_done
    selector: match_map
    task:
      print_result: print
    select_param:
      match_key: executor
      match_value: local_command
    job_param:
      print_key: [trigger_param/result]Here are the params used by local executor.
- cmd. The command to be executed. If- git_repois set, the- cmdwill be relative to the- git_repo.
- arg. Argument(s) which will be passed to the command. The- argcould be a list of strings. If it is a string, it will be used as a single argument. Do NOT write multiple arguments in the same string.
- env. This is a map which will be set as environment variables for the command.
- param_env_name. The name of the environment variable which includes- jsonformat of all execution parameters. The default name is- HERALD_EXECUTE_PARAM.
- ignore_param_env. Set to- trueif you do not want to set the- HERALD_EXECUTE_PARAMenvironment variable.
- background. If set to- true, the command will run in background and return immediately. You are not able to get the result of the command anymore.
- git_repo. The executor will try to load the git repo and run- cmdfrom it. Only use- git_repowhich you can trust.
- git_username. The username for authentication.
- git_password. The password for authentication. If- git_passwordis not empty,- git_ssh_keyand- git_ssh_key_filewill be ignored.
- git_ssh_key. The string of ssh key if you are using ssh protocol. if- git_ssh_keyis not empty,- git_ssh_key_filewill be ignored.
- git_ssh_key_file. The file path of ssh key. If it is empty, it will try to find one under- ~/.ssh.
- git_ssh_key_password. Provide the password in case the ssh key is encrypted.
- git_branch. Remote branch for the git repo.
All the trigger and job params could be found in
HERALD_EXECUTE_PARAM environment variable.
The multiline git_ssh_key could be written like this:
router:
  router1:
    trigger: trigger1
    selector: all
    task:
      run_cmd: local_command
    job_param:
      cmd: test/run_script.sh
      git_repo: git@github.com:heraldd/herald-script.git
      git_ssh_key: |
        -----BEGIN RSA PRIVATE KEY-----
        MIIEpAIBAAKCAQEAn5aGCdbBNBOawhMJ2/SoKVoAAL5tRN5MJzrJGob09p6MC/dc
        AZoMH5YOQEpoBaZOg8smh9GlqGSE2LKmWreNrqZk/+0w5XUnNhcQ/I6MY2u2l5fb
        iYx5FgclExsNH+Y3EUyv1LVfRuIRRLg7WH1snoKYsmteAVwVIZGtFgBs4AzhTsn9
        k3mW9FZF90DuEbsrRu6up8SiDobF4t2IDZKU/wIDAQABAoIBAQCVh1YkFeKFRvE0
        ct5EB+Mgi8GA8Ow1IQy9nSkc/+K6ySdzdtvwbER7u/+yYYVB9eePOWPq0pajRzvq
        Rsn0KhRI1oPAAKBV/wU0ezxhR7dm2GAHfjQnl0VFTICCfFA52V0zimUdqquRIPUJ
        bubkD58K2S6DuVOJ1DB3VNRI4qvCGu8D1N+iS9o0l07NtKzSITFNlRQvdk5OSPZJ
        fWtxuU3SfXAw8Y2cPM1j1SECgYEAovzjGRhZv+lXh51YBU+7XBM7iUMswslzNiNh
        eV36dfDzhwqQKF3AGtX0nMWkSruS8s8AzqBuNfmF5/O7H0nrvIShMA73gJ8eHLPe
        8aFzMaPX4uvt7ZAFJDfXU1Eqbb4T/W0oBtQJopI9n5r7Ry9/eghCqZBMabRpsVvp
        9v9dVW0CgYAl87ZqIwr/JwiqnDuxvS2E+3hYK3pWjFAl9/OKi1JbS94/ZmyLO3l5
        +bnhEXkB/wUHF59yPVu4M9JOg67ugNmW8gRAQ7SbRoGtdfUdC2zV4JstGqf+PJlM
        5xiJ2wxJsq+hct1OpbzjmzXBRrDUOevASvCsTGdfhG+Neqnm1IJUNA==
        -----END RSA PRIVATE KEY-----The RFC4716-format ssh key is not supported currently. If you are encounting
problems with encrypted key, try to generate key in old PEM format with
-m PEM:
$ ssh-keygen -m PEMThe result of the local executor is like:
{
  "exit_code": 0,
  "output": "",
  "file": {
    "file1": "/full/path/of/file1.dat",
    "file2": "/full/path/of/file2.dat"
  },
  "key1": "value1",
  "key2": "value2"
}If the standard output of the command could be converted to json,
it will be merged into the result, or it will be directly put in output.
If you would like to get the result, add a router triggered by
exe_done and check the trigger_param.
http_remote provide the way to execute job on a remote server.
It must be used together with the
Herald Runner.
data_dir is used to keep output files from the remote execution.
secret must be exactly the same with Herald Runner or
the request will be rejected.
secret is used for SHA256 HMAC signature of the request body.
executor:
  remote_command:
    type: http_remote
    host: https://example.com/
    secret: yyyyyyyyyyyyyyyy
    data_dir: /var/lib/heraldd/data
router:
  run_cmd:
    trigger: ttt
    selector: all
    task:
      run_cmd: remote_command
    job_param:
      cmd: hostname
  run_git:
    trigger: ttt
    selector: all
    task:
      run_git: remote_command
    job_param:
      git_repo: https://github.com/heraldgo/herald-script.git
      cmd: run/doit.sh
  print_result:
    trigger: exe_done
    selector: match_map
    task:
      print_result: print
    select_param:
      match_key: executor
      match_value: remote_command
    job_param:
      print_key: trigger_param/resultThe job param for http_remote is exactly the same as local, so you
can run the same task with both local and http_remote.
If
git_ssh_key_fileis specified, it will try to load the ssh key file from the Herald Runner server, not the Herald Daemon server. If you would like to use the ssh key from the Herald Daemon server, write the content ingit_ssh_key.
If the job need output files, the output json of the command
must include file part. These files will be validated by SHA256
checksum.
{
  "file": {
    "file1": "/full/path/of/file1.dat",
    "file2": "/full/path/of/file2.dat"
  },
  "key1": "value1",
  "key2": "value2"
}Then these files will be transferred back to the Herald Daemon server
and kept in data_dir.
The final result will also include these files with local path.
{
  "file": {
    "file1": "/data_dir/job_id/file1/file1.dat",
    "file2": "/data_dir/job_id/file2/file2.dat"
  },
  "key1": "value1",
  "key2": "value2"
}Herald Daemon has provided some internal triggers, selectors and executors. If you are not satisfied with them, you can also define your own ones in the form of plugin to meet your requirements.
The extended components should be implemented as a Go plugin which is built with:
$ go build --buildmode=pluginTake trigger as example, there must be one function CreateTrigger exported:
type triggerExample struct {}
func (tgr *triggerExample) Run(ctx context.Context, sendParam func(map[string]interface{})) {
	...
}
func CreateTrigger(typeName string, param map[string]interface{}) (interface{}, error) {
	if typeName == "trigger_example" {
		return &triggerExample{}, nil
	}
	return nil, fmt.Errorf(`Trigger "%s" is not in this plugin`, typeName)
}
CreateTriggerreturnsinterface{}instead ofHerald.Triggerin order not to introduce extra import in the plugin. So it is OK that the plugin does not importheraldpackage, which may reduce the possibility of version inconsistency between plugin and Herald Daemon.
Define a type name for each trigger, which will be used in the
configuration.
CreateTrigger function should return a trigger instance with
the trigger type and initialize it with the param argument.
If it is not able to create a corresponding trigger,
it should return an error. The returned trigger instance must implement the
Herald.Trigger interface.
It is similar to selector
and executor, which need
to export CreateSelector or CreateExecutor function.
A single plugin could include all or only part of the three kinds of
components (trigger, selector and executor).
The plugin files are specified in the configuration file. More than one plugins could be added.
plugin:
  - /usr/lib/heraldd/plugin/herald-gogshook.so
  - /usr/lib/heraldd/plugin/herald-plugin.soHerald Daemon will try to find a type of component first in the order of plugin list and then from the internal ones. Once the specified component is found, it will stop further searching.
There is one optional method for each components, SetLogger.
If you would like to share the logger with Herald Daemon, you can implement this function:
func (c *component) SetLogger(logger interface{}) {
	c.logger = logger
}The logger could be considered as a Herald.Logger interface.