{"id":15103561,"url":"https://github.com/skx/marionette","last_synced_at":"2025-08-16T05:31:50.185Z","repository":{"id":42231199,"uuid":"253405744","full_name":"skx/marionette","owner":"skx","description":"Something like puppet, for the localhost only.","archived":false,"fork":false,"pushed_at":"2024-06-10T14:21:05.000Z","size":416,"stargazers_count":90,"open_issues_count":3,"forks_count":8,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-12-04T20:48:24.541Z","etag":null,"topics":["automation","golang","golang-application","puppet","sysadmin"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"skx","custom":"https://steve.fi/donate/"}},"created_at":"2020-04-06T05:42:12.000Z","updated_at":"2024-12-02T13:23:13.000Z","dependencies_parsed_at":"2024-06-20T15:52:19.729Z","dependency_job_id":null,"html_url":"https://github.com/skx/marionette","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fmarionette","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fmarionette/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fmarionette/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skx%2Fmarionette/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skx","download_url":"https://codeload.github.com/skx/marionette/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230013016,"owners_count":18159602,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","golang","golang-application","puppet","sysadmin"],"created_at":"2024-09-25T19:40:29.499Z","updated_at":"2024-12-16T19:12:38.440Z","avatar_url":"https://github.com/skx.png","language":"Go","funding_links":["https://github.com/sponsors/skx","https://steve.fi/donate/"],"categories":[],"sub_categories":[],"readme":"[![Go Report Card](https://goreportcard.com/badge/github.com/skx/marionette)](https://goreportcard.com/report/github.com/skx/marionette)\n[![license](https://img.shields.io/github/license/skx/marionette.svg)](https://github.com/skx/marionette/blob/master/LICENSE)\n[![Release](https://img.shields.io/github/release/skx/marionette.svg)](https://github.com/skx/marionette/releases/latest)\n\n* [marionette](#marionette)\n* [Installation \u0026amp; Usage](#installation--usage)\n* [Rule Definition](#rule-definition)\n  * [Dependency Management](#dependency-management)\n  * [Conditionals](#conditionals)\n  * [Examples](#examples)\n  * [Misc. Features](#misc-features)\n    * [Command Execution](#command-execution)\n    * [File Inclusion](#include-files)\n    * [Pre-Declared Variables](#pre-declared-variables)\n    * [Outputs](#outputs)\n* [Module Types](#module-types)\n   * [directory](#directory)\n   * [docker](#docker)\n   * [edit](#edit)\n   * [file](#file)\n   * [git](#git)\n   * [group](#group)\n   * [http](#http)\n     * [Outputs](#http-outputs)\n   * [link](#link)\n   * [log](#log)\n   * [package](#package)\n   * [shell](#shell)\n     * [Outputs](#shell-outputs)\n   * [sql](#sql)\n   * [user](#user)\n* [Future Plans](#future-plans)\n  * [See also](#see-also)\n* [Github Setup](#github-setup)\n\n\n\n\n# marionette\n\n`marionette` is a simple command-line application which is designed to carry out system automation tasks.  It was designed to resemble the well-known configuration-management application [puppet](https://puppet.com/), albeit in a much simpler and more minimal manner.\n\n`marionette` contains a small number of built-in modules, providing enough primitives to turn a blank virtual machine into a host running a few services:\n\n* Cloning git repositories.\n* Creating/modifying files/directories.\n* Pulling Docker images from public container-registries.\n* Installing and removing packages.\n  * Debian GNU/Linux, and CentOS are supported, using `apt-get`, `dpkg`, and `yum` as appropriate.\n* Executing shell commands.\n* Making HTTP-requests.\n\nIn the future it is possible that more modules will be added, but this will require users to file bug-reports requesting them, contribute code, or the author realizing something is necessary.\n\n\n\n\n# Installation \u0026 Usage\n\nBinaries for several systems are available upon our [download page](https://github.com/skx/marionette/releases).  If you prefer to use something more recent you can install directly from the repository via:\n\n```\ngo install github.com/skx/marionette@latest\n```\n\nThe main application can then be launched with the path to a set of rules, which it will then try to apply:\n\n```\nmarionette [flags] ./rules.txt ./rules2.txt ... ./rulesN.txt\n```\n\nThe following flags are supported:\n\n* `-debug`\n  * Show many low-level details when executing the supplied rules-file(s).\n* `-verbose`\n  * Show extra details when executing the supplied rules-file(s).\n* `-version`\n  * Show the released version number, and exit.\n\nTypically a user would run with `-verbose`, and a developer might examine the output produced when `-debug` is specified.\n\nIn addition to the general-purpose flags `-dp` and `-dl` exist for developers, to dump the output of the parser and lexer, respectively.\n\n\n\n\n# Rule Definition\n\nThe general form of our rules looks like this:\n\n```\n$MODULE [triggered] {\n            name  =\u003e \"NAME OF RULE\",\n            arg_1 =\u003e \"Value 1 ... \",\n            arg_2 =\u003e [ \"array values\", \"are fine\" ],\n            arg_3 =\u003e \"Value 3 .. \",\n}\n```\n\nEach rule starts by declaring the type of module which is being invoked, then there is a block containing \"`key =\u003e value`\" sections.  Different modules will accept/expect different keys to configure themselves.  (Unknown arguments will generally be ignored.)\n\nA rule may also contain an optional `triggered` attribute.  Rules which contain the `triggered` modifier are not executed unless explicitly invoked by another rule - think of it as a \"handler\" if you're used to `ansible`.\n\nHere is an example rule which executes a shell-command:\n\n```\n\n\n\n\n# Run a command, unconditionally\nshell {\n        command =\u003e \"uptime \u003e /tmp/uptime.txt\"\n}\n\n```\n\nAnother simple example to illustrate the available syntax might look like the following, which ensures that I have `~/bin/` owned by myself:\n\n```\ndirectory {\n             target  =\u003e \"/home/${USER}/bin\",\n             mode    =\u003e \"0755\",\n             owner   =\u003e \"${USER}\",\n             group   =\u003e \"${USER}\",\n}\n```\n\nThere are four magical keys which can be supplied to all modules:\n\n| Name     | Usage                                                            |\n|----------|------------------------------------------------------------------|\n| `require`| This is used for [dependency management](#dependency-management) |\n| `notify` | This is used for [dependency management](#dependency-management) |\n| `if`     | This is used to make a rule [conditional](#conditionals)         |\n| `unless` | This is used to make a rule [conditional](#conditionals)         |\n\n\n\n## Dependency Management\n\nThere are two keys which can be used to link rules together, to handle dependencies:\n\n* `require`\n  * This key contains either a single rule-name, or a list of any rule-names, which must be executed before _this_ one.\n* `notify`\n  * A list of any number of rules which should be notified, if the given rule resulted in a state-change.\n\n**Note** You only need to give rules names to link them for the purpose of managing dependencies.\n\nImagine we wanted to create a new directory, and write a file there.  We could do that with a pair of rules:\n\n* One to create a directory.\n* One to generate the output file.\n\nWe could wing-it and write the rules in the logical order, but it would be far better to link the two rules explicitly.\n\nThere are two ways we could implement this.  The simplest way would be this:\n\n```\nshell { command   =\u003e \"uptime \u003e /tmp/blah/uptime\",\n        require   =\u003e \"Create /tmp/blah\" }\n\ndirectory{ name   =\u003e \"Create /tmp/blah\",\n           target =\u003e \"/tmp/blah\" }\n```\n\n\nThe alternative would have been to have the directory-creation trigger the shell-execution rule via an explicit notification:\n\n```\n\n\n\n\n# This command will notify the \"Test\" rule, if it creates the directory\n\n\n\n\n# because it was not already present.\ndirectory{ target =\u003e \"/tmp/blah\",\n           notify =\u003e \"Test\"\n}\n\n\n\n\n# Run the command, when triggered/notified.\nshell triggered { name         =\u003e \"Test\",\n                  command      =\u003e \"uptime \u003e /tmp/blah/uptime\",\n}\n```\n\nThe difference in these two approaches is how often things run:\n\n* In the first case we always run `uptime \u003e /tmp/blah/uptime`\n  * We just make sure that _before_ that the directory has been created.\n* In the second case we run the command only once.\n  * We run it only after the directory is created.\n  * Because the directory-creation triggers the notification only when the rule changes (i.e. the directory goes from being \"absent\" to \"present\").\n\nYou'll note that any rule which is followed by the token `triggered` will __only__ be executed when it is triggered by name.  If there is no `notify` key referring to that rule it will __never__ be executed.\n\n\n\n## Conditionals\n\nRules may be made conditional, via the magical keys `if` and `unless`.\n\nThe following example runs a command, using `apt-get`, only if a specific file exists upon the filesystem:\n\n```\nshell { name    =\u003e \"Upgrade\",\n        command =\u003e \"apt-get dist-upgrade --yes --force-yes\",\n        if      =\u003e exists(\"/usr/bin/apt-get\") }\n```\n\nFor the reverse, running a rule unless something is true, we can use the `unless` key:\n\n```\nlet arch = `/usr/bin/arch`\n\nfile { name   =\u003e \"Create file\",\n       target =\u003e \"/tmp/foo\",\n       unless =\u003e equal( \"x86_64\", \"${arch}\" ) }\n```\n\nHere we see that we've used two functions `equal` and `exists`, these are both built-in functions which do what you'd expect.\n\nThe following list shows all the built-in functions that you may use (but only within the `if` or `unless` keys):\n\n* `contains(haystack, needle)`\n  * Returns true if the first string contains the second.\n* `exists( /some/file )`\n  * Return true if the specified file/directory exists.\n* `equal( foo, bar )`\n  * Return true if the two values are identical.\n* `nonempty(string|variable)`\n  * Return true if the string/variable is non-empty.\n  * `set` is a synonym.\n* `prompt(string|variable)`\n  * Prompt the user for input, at run-time, and return it.\n* `on_path(string|variable)`\n  * Return true if a binary with the given name is on the users' PATH.\n* `empty(string|variable)`\n  * Return true if the string/variable is empty (i.e. has zero length).\n  * `unset` is a synonym.\n* `success(string)`\n  * Returns true if the command `string` is executed and returns a non-error exit-code (i.e. 0).\n  * Output is discarded, and not captured.\n* `failure(string)`\n  * Returns true if the command `string` is executed and returns an error exit-code (i.e. non-zero 0).\n  * Output is discarded, and not captured.\n\nMore conditional primitives may be added if they appear to be necessary, or if users request them.\n\nConditionals may also be applied to variable assignments and file inclusion:\n\n```\n\n\n\n\n# Include a file of rules, on a per-arch basis\ninclude \"x86_64.rules\" if equal( \"${ARCH}\",\"x86_64\" )\ninclude \"i386.rules\"   if equal( \"${ARCH}\",\"i386\" )\n\n\n\n\n# Setup a ${cmd} to download something, depending on what is present.\nlet cmd = \"curl --output ${dst} ${url}\" if on_path(\"curl\")\nlet cmd = \"wget -O ${dst} ${url}\"       if on_path(\"wget\")\n```\n\nIn addition to these conditional functions the following primitives are built in, and may be freely used:\n\n* `field(txt,index)`\n  * Split the given text on whitespace, and return the specified field by index.\n  * 0 is the first field, 1 is the second, etc.\n* `gt(a,b)`\n  * Return true if a\u003eb\n* `gte(a,b)`\n  * Return true if a\u003e=b\n* `lt(a,b)`\n  * Return true if a\u003cb\n* `lte(a,b)`\n  * Return true if a\u003c=b\n* `len(txt)`\n  * Return the length of the given value.\n* `lower(txt)`\n  * Converts the given string to lower-case.\n* `matches(text, regexp)`\n  * Return true if the text matches the specified regular expression.\n* `rand(min,max,seed)`\n  * Return a random integer between min and max. Optionally set a seed value.\n* `md5sum(txt)`\n  * Returns the MD5-digest of the given value.\n* `sha1sum(txt)`\n  * Returns the SHA1-digest of the given value.\n* `upper(txt)`\n  * Converts the given string to upper-case.\n* `newer(file1, file2)`\n  * Returns true if file1 has a newer modification time than file2.\n* `older(file1, file2)`\n  * Returns true if file1 has an older modification time than file2.\n\n\n\n## Examples\n\nYou can find a small set of example recipes beneath the examples directory:\n\n* [examples/](examples/)\n\n\n\n## Misc. Features\n\n\n### Command Execution\n\nBackticks can be used to execute commands, in variable-assignments and in parameters to rules.\n\nFor example we might determine the system architecture like this:\n\n```\nlet arch = `/usr/bin/arch`\n\nshell { name    =\u003e \"Show arch\",\n        command =\u003e \"echo We are running on an ${arch} system\" }\n```\n\nHere `${arch}` expands to the output of the command, as you would expect, with any trailing newline removed.\n\n\u003e **Note** `${ARCH}` is available by default, as noted in the [pre-declared variables](#pre-declared-variables) section.  This was just an example of command-execution.\n\nUsing commands inside parameter values is also supported:\n\n```\nfile { name    =\u003e \"set-todays-date\",\n       target  =\u003e \"/tmp/today\",\n       content =\u003e `/usr/bin/date` }\n```\n\nThe commands executed with the backticks have any embedded variables expanded _before_ they run, so this works as you'd expect:\n\n```\nlet fmt   = \"+%Y\"\n\nfile { name    =\u003e \"set-todays-date\",\n       target  =\u003e \"/tmp/today\",\n       content =\u003e `/bin/date ${fmt}` }\n```\n\n\n### Include Files\n\nYou can break large rule-files into pieces, and include them in each other:\n\n```\n\n\n\n\n# main.in\n\nlet prefix=\"/etc/marionette\"\n\ninclude \"foo.in\"\ninclude \"${prefix}/test.in\"\n```\n\nTo simplify your recipe writing including other files may be made conditional,\njust like our rules:\n\n```\n\n\n\n\n# main.in\n\ninclude \"x86_64.rules\" if equal( \"${ARCH}\",\"x86_64\" )\ninclude \"i386.rules\"   if equal( \"${ARCH}\",\"i386\" )\n```\n\n\n### Pre-Declared Variables\n\nThe following variables are available by default:\n\n| Name          | Value                                                 |\n|---------------|-------------------------------------------------------|\n| `${ARCH}`     | The system architecture (as taken from `sys.GOARCH`). |\n| `${HOMEDIR}`  | The home directory of the user running marionette.    |\n| `${HOSTNAME}` | The hostname of the local system.                     |\n| `${OS}`       | The operating system name (as taken from `sys.GOOS`). |\n| `${USERNAME}` | The username of user running marionette.              |\n\nThere are additionally two \"magic\" variables available which will always have values based upon the current rule-file being processed, whether that is a file specified upon the command-line, or as a result of an `include` statement:\n\n| Name              | Value                                                            |\n|-------------------|------------------------------------------------------------------|\n| `${INCLUDE_DIR}`  | The absolute directory path of the current file being processed. |\n| `${INCLUDE_FILE}` | The absolute path of the current file being processed.           |\n\n\n### Outputs\n\nSome modules will set \"outputs\" after they've executed, and those outputs will be documented explicitly in the later list of available modules.\n\nWhen a module creates an output it will be available for subsequent modules to use, prefixed with the name of the rule which created it.\n\nHere is an example showing the use of the `stdout` output which the [shell](#shell) module produces:\n\n```\n\n\n\n\n# Run a command - This will produce \"${user.stdout}\" and \"${user.stderr}\"\n\n\n\n\n# variables which can be used later.\nshell {\n           name =\u003e \"user\",\n        command =\u003e \"/usr/bin/whoami\"\n}\n\n\nlog {\n    message =\u003e \"STEVE!\",\n    if      =\u003e equal( \"${user.stdout}\", \"skx\" )\n}\nlog {\n    message =\u003e \"ROOT!\",\n    if      =\u003e equal( \"${user.stdout}\", \"root\" )\n}\n```\n\n**NOTE:** The output `stdout` is available here as `${user.stdout}` - the \"`user.`\" prefix comes from the name of the rule which invoked the shell.  So here we'd be able to process the result of `${kernel-version.output}`\n\n```\nshell {\n          name =\u003e \"kernel-version\",\n          command =\u003e \"uname -r\",\n}\n```\n\n\n\n\n# Module Types\n\nOur primitives are implemented in 100% pure golang, and are included with our binary, these are now described briefly:\n\n\n\n## `directory`\n\nThe directory module allows you to create a directory, or change the permissions of one.\n\nExample usage:\n\n```\ndirectory {  name    =\u003e \"My home should have a binary directory\",\n             target  =\u003e \"/home/steve/bin\",\n             mode    =\u003e \"0755\",\n}\n```\n\nValid parameters are:\n\n* `target` is a mandatory parameter, and specifies the directory, or directories, to operate upon.\n* `owner` - Username of the owner, e.g. \"root\".\n* `group` - Groupname of the owner, e.g. \"root\".\n* `mode` - The mode to set, e.g. \"0755\".\n* `state` - Set the state of the directory.\n  * `state =\u003e \"absent\"` remove it.\n  * `state =\u003e \"present\"` create it (this is the default).\n\n\n\n## `docker`\n\nThis module allows fetching a container from a remote registry.\n\n```\ndocker { image =\u003e \"alpine:latest\" }\n```\n\nThe following keys are supported:\n\n* `image` - The image/images to fetch.\n* `force`\n  * If this is set to `true` then we fetch the image even if it appears to be available locally already.\n\n**NOTE**: We don't support private registries, or the use of authentication.\n\n\n\n## `edit`\n\nThis module allows minor edits to be applied to a file:\n\n* Removing lines matching a given regular expression.\n* Appending a line to the file if missing.\n\n```\nedit { name =\u003e \"Remove my VPN hosts\",\n       target =\u003e \"/etc/hosts\",\n       remove_lines =\u003e \"\\.vpn\" }\n```\n\nThe following keys are supported:\n\n* `target` - Mandatory filename to edit.\n* `remove_lines` - Remove any lines of the file matching the specified regular expression.\n* `append_if_missing` - Append the given text if not already present in the file.\n* `search`\n* `replace`\n  * If both `search` and `replace` are non-empty then they will be used to update the content of the specified file.\n  * `search` is treated as a regular expression, for added flexibility.\n\nAn example of changing a file might look like this:\n\n```\nedit { target  =\u003e \"/etc/ssh/sshd_config\",\n       search  =\u003e \"^PasswordAuthentication\",\n       replace =\u003e \"# PasswordAuthentication\",\n}\n```\n\n\n\n## `fail`\n\nThe fail-module is designed to terminate processing, if you find a situation where the local\nenvironment doesn't match your requirements.  For example:\n\n```\nlet path = `which useradd`\n\nfail {\n   message =\u003e \"I can't find a working useradd binary to use\",\n   if      =\u003e empty(path)\n}\n```\n\nThe only valid parameter is `message`.\n\nSee also [log](#log), which will log a message but then continue execution.\n\n\n\n## `file`\n\nThe file module allows a file to be created, from a local file, or via a remote HTTP-source.\n\nExample usage:\n\n```\nfile {  name       =\u003e \"fetch file\",\n        target     =\u003e \"/tmp/steve.txt\",\n        source_url =\u003e \"https://steve.fi/\",\n}\n\nfile {  name     =\u003e \"write file\",\n        target   =\u003e \"/tmp/name.txt\",\n        content  =\u003e \"My name is Steve\",\n}\n```\n\n`target` is a mandatory parameter, and specifies the file to be operated upon.\n\nThere are four ways a file can be created:\n\n* `content` - Specify the content inline.\n* `source_url` - The file contents are fetched from a remote URL.\n* `source` - Content is copied from the existing path.\n* `template` - Content is produced by rendering a template from a path.\n\nOther valid parameters are:\n\n* `owner` - Username of the owner, e.g. \"root\".\n* `group` - Groupname of the owner, e.g. \"root\".\n* `mode` - The mode to set, e.g. \"0755\".\n* `state` - Set the state of the file.\n  * `state =\u003e \"absent\"` remove it.\n  * `state =\u003e \"present\"` create it (this is the default).\n\nWhere `template` is used, the template file is rendered using the\n[`text/template`](https://pkg.go.dev/text/template) Go package\n\n\n\n## `git`\n\nClone a remote repository to a local directory.\n\nExample usage:\n\n```\ngit { path =\u003e \"/tmp/xxx\",\n      repository =\u003e \"https://github.com/src-d/go-git\",\n}\n```\n\nValid parameters are:\n\n* `repository` Contain the HTTP/HTTPS repository to clone.\n* `path` - The location we'll clone to.\n* `branch` - The branch to switch to, or be upon.\n  * A missing branch will not be created.\n\nIf this module is used to `notify` another then it will trigger such a\nnotification if either:\n\n* The repository wasn't present, and had to be cloned.\n* The repository was updated.  (i.e. Remote changes were pulled in.)\n\n\n\n## `group`\n\nThe group module allows you to add or remove local Unix groups to your system.\n\nExample:\n\n```\ngroup { group =\u003e \"sysadmin\",\n        state =\u003e \"present\" }\n```\n\n* `elevate` is an optional parameter, which should contain the path to \"sudo\", or similar program to grant root-privileges.\n* `group` is a mandatory parameter.\n* `state` should be one of `absent` or `present`, depending upon whether you want to add or remove the group.\n\n\n\n## `http`\n\nThe `http` module allows you to make HTTP requests.\n\nExample:\n\n```\nhttp { url =\u003e \"https://api.github.com/user\",\n       method =\u003e \"PATCH\",\n       headers =\u003e [\n         \"Authorization: token ${AUTH_TOKEN}\",\n         \"Accept: application/vnd.github.v3+json\",\n       ],\n       body =\u003e \"{\\\"name\\\":\\\"new name\\\"}\",\n       expect =\u003e \"200\" }\n```\n\nValid parameters are:\n\n* `url` is a mandatory parameter, and specifies the URL to make the HTTP request to.\n* `method` - If this is set, the HTTP request will use this method, otherwise a `GET` request is made.\n* `headers` - If this is set, the headers provided will be sent with the request.\n* `body` - If this is set, this will be the body of the HTTP request.\n* `expect` - If this is set, an error will be triggered if the response status code does not match the expected status code.\n  * If `expect` is not set, an error will be triggered for any non 2xx response status code.\n\nThe `http` module is always regarded as having made a change on a successful request.\n\n\n### `http` Outputs\n\nThe following [outputs](#outputs) will be set:\n\n* `body`\n  * The body returned from the request.\n* `code`\n  * The HTTP-status code of the response.\n* `status`\n  * The HTTP-status line of the response.\n\n\n\n## `link`\n\nThe `link` module allows you to create a symbolic link.\n\nExample usage:\n\n```\nlink { name =\u003e \"Symlink test\",\n       source =\u003e \"/etc/passwd\",  target =\u003e \"/tmp/password.txt\" }\n```\n\nValid parameters are:\n\n* `target` is a mandatory parameter, and specifies the location of the symlink to create.\n* `source` is a mandatory parameter, and specifies the item the symlink should point to.\n\n\n\n## `log`\n\nThe `log` module allows you to output a message, but continue execution.\n\nExample usage:\n\n```\nlog { message =\u003e \"I'm ${USER} running on ${HOSTNAME}\" }\n```\n\nThe only valid parameter is `message`, which may be either a single value, or an array of values.\n\nSee also [fail](#fail), which will log a message but then terminate execution.\n\n\n\n## `package`\n\nThe package-module allows you to install or remove a package from your system, via the execution of `apt-get`, `dpkg`, and `yum`, as appropriate.\n\nExample usage:\n\n```\n\n\n\n\n# Install a single package\npackage { name    =\u003e \"Install bash\",\n          package =\u003e \"bash\",\n          state   =\u003e \"installed\",\n        }\n\n\n\n\n# Uninstall a series of packages\npackage { package =\u003e [ \"nano\", \"vim-tiny\", \"nvi\" ],\n          state =\u003e \"absent\" }\n```\n\nValid parameters are:\n\n* `elevate` is an optional parameter, which should contain the path to \"sudo\", or similar program to grant root-privileges.\n* `package` is a mandatory parameter, containing the package, or list of packages.\n* `state` - Should be one of `installed` or `absent`, depending upon whether you want to install or uninstall the named package(s).\n* `update` - If this is set to `true` then the system will be updated prior to installation.\n  * In the case of a Debian system, for example, `apt-get update` will be executed.\n\n\n\n## `sql`\n\nThe SQL-module allows you to run arbitrary SQL against a database.  Two parameters are required `driver` and `dsn`, which are used to open the database connection.  For example:\n\n```\nsql {\n       driver =\u003e \"sqlite3\",\n       dsn    =\u003e \"/tmp/sql.db\",\n\n       # Memory based-SQLite could be used like so:\n       #  dsn   =\u003e \"/tmp/foo.db?mode=memory\",\n\n       # MySQL connection would look like this:\n       #\n       #   driver =\u003e \"mysql\",\n       #   dsn    =\u003e \"user:password@(127.0.0.1:3306)/\",\n}\n```\n\nWe support the following drivers:\n\n* `mysql`\n* `postgres`\n* `sqlite3`\n\nTo specify the query to run you should set one of the following two parameters:\n\n* `sql`\n  * Literal SQL to execute.\n* `sql_file`\n  * A file to read and execute in one execution.\n\nNOTE: You may find you need to append `multiStatements=true` to your DSN to ensure correct operation when reading SQL from a file.\n\n\n\n## `shell`\n\nThe shell module allows you to run shell-commands, complete with redirection and pipes.\n\nExample:\n\n```\nshell { name    =\u003e \"I touch your file.\",\n        command =\u003e \"touch /tmp/blah/test.me\"\n      }\n```\n\n`command` is the only mandatory parameter.  Multiple values can be specified:\n\n```\nshell { name    =\u003e \"restart the aplication\",\n        command =\u003e [\n                     \"systemctl stop  foo.service\",\n                     \"systemctl start foo.service\",\n                   ]\n      }\n```\n\nBy default commands are executed directly, unless they contain redirection-characters (\"\u003e\", or \"\u003c\"), or the use of a pipe (\"|\").  If special characters are used then we instead invoke the command via `/bin/bash`:\n\n* `bash -c \"${command}\"`\n\nYou may specify `shell =\u003e true` to force the use of a shell, despite the lack of redirection/pipe characters:\n\n```\nshell { shell   =\u003e true,\n        command =\u003e \"sed .. /etc/file.txt\"\n      }\n```\n\n\n### `shell` Outputs\n\nThe following [outputs](#outputs) will be set:\n\n* `stdout`\n  * Anything the command wrote to STDOUT.\n* `stderr`\n  * Anything the command wrote to STDERR.\n\n\n\n## `user`\n\nThe user module allows you to add or remove local Unix users to your system.\n\nExample:\n\n```\nuser { login =\u003e \"steve\",\n       state =\u003e \"present\" }\n```\n\n* `elevate` is an optional parameter, which should contain the path to \"sudo\", or similar program to grant root-privileges.\n* `login` is a mandatory parameter.\n* `shell` is an optional parameter to use for the users' shell.\n* `state` should be one of `absent` or `present`, depending upon whether you want to add or remove the user.\n\n\n\n\n# Future Plans\n\n* Gathering \"facts\" about the local system, and storing them as variables would be useful.\n  * At the moment we just have a small number of [pre-declared variables](#pre-declared-variables).\n\n\n\n## See Also\n\n* There are some brief notes on our implementation contained within [HACKING.md](HACKING.md).\n  * This gives an overview of the structure.\n* There are some brief-notes on [fuzz-testing the parser](FUZZING.md).\n  * This should ensure that there is no input that can crash our application.\n\n\n\n## Github Setup\n\nThis repository is configured to run tests upon every commit, and when\npull-requests are created/updated.  The testing is carried out via\n[.github/run-tests.sh](.github/run-tests.sh) which is used by the\n[github-action-tester](https://github.com/skx/github-action-tester) action.\n\nReleases are automated in a similar fashion via [.github/build](.github/build),\nand the [github-action-publish-binaries](https://github.com/skx/github-action-publish-binaries) action.\n\n\nSteve\n--\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskx%2Fmarionette","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskx%2Fmarionette","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskx%2Fmarionette/lists"}