opswalrus is a tool that runs scripts against hosts. It’s kind of like Ansible, but aims to be simpler to use.
You have two options:
❯ gem install opswalrus
❯ ops version
1.0.13
The path to whatever directory you store your age identity files in should be used in place of $HOME/.secrets/age
in the following shell alias:
❯ alias ops='docker run --rm -it -v $HOME/.secrets/age:/root/.secrets -e OPSWALRUS_AGE_IDS="/root/.secrets/*.priv" -v $HOME/.ssh:/root/.ssh -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}/:/workdir ghcr.io/opswalrus/ops'
❯ ops version
1.0.13
> ops run core/host info
{
success: true,
host: {
name: "davidlinux",
os: "Ubuntu 23.04 (lunar)",
kernel: "Linux 6.2.0-1007-lowlatency x86_64 GNU/Linux",
}
}
An ops package is a directory containing a package.yml (or package.yaml) file.
A package file looks like this:
author: David Ellis
license: MIT
version: 1.0.0
dependencies:
core: davidkellis/ops_core
apt: davidkellis/ops_apt
An ops package may also consist of ops files, arranged in an arbitrary directory structure.
An ops file looks like this:
params:
ops_username: string
ops_ssh_public_key: string
hostname: string
output:
success: boolean,
error: string?
imports:
core: core # core references the bundled core package referenced in the package.yaml
svc: service # service references the bundled service package referenced in the package.yaml
...
desc "create the admin group if it doens't exist"
core.create_group name: "admin"
desc "set up passwordless sudo for admin group users"
core.replace_line file: "/etc/sudoers",
pattern: "^%admin",
line: "%admin ALL=(ALL) NOPASSWD: ALL",
verify: "/usr/sbin/visudo -cf %s"
desc "create the ops user and make it an admin user"
core.create_user name: params.ops_username
desc "set up authorized key for id_ansible ssh key (root user)"
core.ssh.add_authorized_key user: "root", key: ops_ssh_public_key
desc "set up authorized key for id_ansible ssh key (ops user)"
core.ssh.add_authorized_key user: ops_username, key: ops_ssh_public_key
desc "disable password authentication for root"
core.replace_line file: '/etc/ssh/sshd_config',
pattern: '^#?PermitRootLogin',
line: 'PermitRootLogin prohibit-password'
desc "restart sshd"
svc.restart name: "sshd"
{
success: true,
}
An ops file is broken up into two parts. The first part is an optional YAML block that describes the structure of the expected input parameters, the structure of the expected JSON output message, and the package dependencies that the script needs in order to run.
The YAML block is concluded with an elipsis, ...
, on a line by itself.
The YAML block and its associated trailing elipsis may be omitted.
Following the elipsis that concludes the YAML block is a block of Ruby code. The block of Ruby is executed with a number of methods, constants, and libraries that are available as a kind of domain specific language (DSL). This DSL makes writing ops scripts feel very much like writing standard bash shell scripts.
Ops file imports are a mapping consisting of a local name and a corresponding package reference.
When an ops file is run, the ops runtime will first bundle up the invoked ops file as well as all package dependencies and place the bundle of associated ops packages and ops files into an ops bundle directory.
The bundle directory is named ops_bundle, and contains everything needed to run the specified ops file on either the local host or a remote host.
The ops command will place the bundle directory in the directory from which the ops command is being run. So, if pwd
returns /home/david/foo
and the ops command is run from within that directory, then the bundle directory will be placed
at /home/david/foo/ops_bundle
.
The one exception to the normal bundle directory placement rule described in the previous paragraph is when the ops command is being run from within a directory that is contained within a package directory. In that case, the bundle directory will be placed inside the package directory. So, for example, if the directory structure looks like:
❯ tree pkg
pkg
├── apt
│ ├── install.ops
│ └── update.ops
├── core
│ ├── echo.ops
│ ├── host
│ │ ├── info.ops
│ │ └── info.rb
│ ├── package.yaml
│ ├── ssh_copy_id.ops
│ ├── touch.ops
│ └── whoami.ops
├── hardening
│ └── package.yaml
├── motd
│ ├── motd.ops
│ └── package.yaml
└── service
└── restart.ops
and the pwd
command returns pkg/core/host
, and the ops command is run from within pkg/core/host
, then the bundle
directory will be placed at pkg/core/ops_bundle
.
A bundle directory contains all the dependencies for a given ops file invocation. There are two possible cases:
In case (1), when the ops file being invoked is part of a package, we’ll call it P, then the bundle directory will contain a copy of the package directory associated with P, as well as all of the package directories associated with all transitive package dependencies of P. For example, if the ops file foo.ops is contained within the package directory for the Bar package, and if the Bar package depends on the core package and the service package, then the directory structure of the bundle directory would be:
❯ tree ops_bundle
ops_bundle
├── Bar
│ └── foo.ops
├── core
│ ├── echo.ops
│ ├── host
│ │ ├── info.ops
│ │ └── info.rb
│ ├── package.yaml
│ ├── ssh_copy_id.ops
│ ├── touch.ops
│ └── whoami.ops
└── service
└── restart.ops
In case (2), when the ops file being invoked is not part of a package, then the bundle directory will contain a copy of the package directories associated with all transitive package dependencies of the ops file being invoked. Additionally, the deepest nested directory containing all of the transitive ops file dependencies of the ops file being invoked will be copied to the bundle directory.
When the ops command bundles and runs an ops file, the rules that the runtime uses to resolve references to other ops files is as follows; assume the following sample project directory structure:
Project directory structure:
davidinfra
├── caddy
│ ├── install
│ │ └── debian.ops
│ └── install.ops
├── hosts.yaml
├── main.ops
├── prepare_host
│ ├── all.ops
│ ├── hostname.ops
│ └── ssh.ops
└── roles
└── web.ops
Corresponding bundle directory structure:
davidinfra
├── ops_bundle
│ ├── core
│ │ └──...
│ └── davidinfra
│ ├── caddy
│ │ ├── install
│ │ │ ├── debian.ops
│ │ │ └── install.ops
│ │ └── restart.ops
│ ├── hosts.yaml
│ ├── main.ops
│ ├── prepare_host
│ │ ├── all.ops
│ │ ├── hostname.ops
│ │ └── ssh.ops
│ └── roles
│ └── web.ops
├── caddy
│ ├── install
│ │ └── debian.ops
│ │ └── install.ops
│ └── restart.ops
├── hosts.yaml
├── main.ops
├── prepare_host
│ ├── all.ops
│ ├── hostname.ops
│ └── ssh.ops
└── roles
└── web.ops
The import and symbol resolution rules are as follows:
caddy.restart(...)
hostname(...)
caddy.install(...)
as a shorthand syntax for caddy.install.install(...)
debian(...)
, and reference other files or subpackages within the caddy/install directory