arc (Automatic Remote Controller) is an automation tool that uses Lua for scripting. It executes tasks on the local or remote systems via SSH with a flexible API for managing configurations, files, and commands across multiple servers.
-
Install Rust
-
Install arc using Cargo:
cargo install arc-automationThis will compile and install the arc binary to the Cargo bin directory (usually ~/.cargo/bin/).
Please make sure you have the required dependencies installed:
Fedora
sudo dnf group install development-tools
sudo dnf install openssl-develUbuntu / Debian
sudo apt install build-essential libssl-devMacOS
- Install Homebrew
brew install opensslInitialize a new arc project with type definitions for LSP support:
arc init /path/to/projectThis command creates the project structure, type definitions for code completion and type checking, and a basic arc.lua file with example tasks.
-- Define a target system
targets.systems["web-server"] = {
address = "192.168.1.100",
user = "root",
}
-- Define a simple task
tasks["hello"] = {
handler = function(system)
local result = system:run_command("echo 'Hello from ' $(hostname)")
print(result.stdout)
end
}Run the task:
arc run -s web-server -t helloSee the examples directory for more complete usage examples. You can also explore the available commands using arc --help, arc run --help etc.
Targets define the systems where tasks will be executed. There are two types: individual systems and groups.
Systems can be either remote (accessed via SSH) or local (running on the arc host machine).
Remote systems represent individual servers with SSH connection details.
targets.systems["frontend-server"] = {
address = "192.168.1.100",
user = "root",
port = 22, -- optional, defaults to 22
}
targets.systems["api-server"] = {
address = "192.168.1.101",
user = "deploy",
port = 2222,
}Authentication is handled through the SSH agent. The host's public key must be present in the remote server's authorized_keys.
Local systems execute tasks on the machine where arc is running.
targets.systems["localhost"] = {
type = "local"
}Local systems use the same API as remote systems but operations execute locally instead of over SSH. The address, port, and user properties return nil for local systems.
Groups organize multiple systems.
targets.groups["web-servers"] = {
members = {"frontend-server", "api-server"}
}
targets.groups["prod"] = {
members = {"prod-web-1", "prod-db-1"}
}Tasks define operations to execute on target systems. Tasks execute in definition order on each system.
tasks["install_nginx"] = {
handler = function(system)
local result = system:run_command("apt install nginx -y")
if result.exit_code ~= 0 then
error("Failed to install nginx: " .. result.stderr)
end
end,
tags = {"nginx", "setup"},
}
tasks["configure_nginx"] = {
requires = {"install_nginx"},
handler = function(system)
local config = system:file("/etc/nginx/nginx.conf")
config.content = "..."
end,
tags = {"nginx"},
}See Tasks API for all available fields.
arc uses a restricted LuaJIT environment. The following standard library modules are available:
- Modules (
require) - String Manipulation (
string.format,string.match,string.gsub, etc.) - Table Manipulation (
table.insert,table.remove,table.sort, etc.) - Mathematical Functions (
math.floor,math.random, etc.)
Not available: io, os, debug, coroutine. Use the provided arc APIs (system:run_command(), system:file(), env.get(), etc.) instead.
The global print() function is an alias for log.info().
Tasks are defined by assigning to the global tasks table. Tasks execute in definition order on each system.
-
handler: Function that implements the task logic- Parameters:
system- The system object to operate on - Returns: Optional result value accessible via
tasks["name"].result
- Parameters:
-
tags(optional): Array of tags for filtering tasks. Tasks are automatically tagged with their name and source file path components (e.g.,modules/web/nginx.luaadds tags:modules,web,nginx). -
targets(optional): Array of group or system names where this task should run. If omitted, runs on all systems. -
requires(optional): Array of tags this task requires. Tasks with matching tags are included when this task is selected. Resolved transitively. -
when(optional): Guard predicate that determines if the task should run- Returns:
boolean- Iffalse, task is skipped
- Returns:
-
on_fail(optional): Behavior when this task fails"continue": Proceed to next task"skip_system": Skip remaining tasks for this system"abort"(default): Halt execution entirely
-
important(optional): Iftrue, always runs regardless of tag filters,--no-reqs, andskip_system
result: Return value from handler (nil if failed/skipped)state:"success","failed", or"skipped"error: Error message if failed (nil otherwise)
Example:
tasks["check_nginx"] = {
handler = function(system)
return system:run_command("which nginx").exit_code == 0
end
}
tasks["install_nginx"] = {
requires = {"check_nginx"},
when = function()
return tasks["check_nginx"].result == false
end,
handler = function(system)
local result = system:run_command("apt install nginx -y")
if result.exit_code ~= 0 then
error("Failed: " .. result.stderr)
end
end,
on_fail = "skip_system",
}Requires affect which tasks run, not when. Tasks always execute in definition order. If a task requires something defined later, the required task runs after the requiring task.
The system object represents a connection to a target system (remote or local) and is passed to task handlers.
name: The name of the system as defined intargets.systemstype: The type of system -"remote"or"local"address: The IP address of the system (nil for local systems)port: The SSH port of the system (nil for local systems)user: The SSH user used to connect to the system (nil for local systems)
-
run_command(cmd): Execute a command on the system- Parameters:
cmd(string) - The command to execute - Returns: A table with
stdout,stderr, andexit_code
- Parameters:
-
file(path): Get a File object representing a file on the system- Parameters:
path(string) - Path to the file - Returns: A File object
- Parameters:
-
directory(path): Get a Directory object representing a directory on the system- Parameters:
path(string) - Path to the directory - Returns: A Directory object
- Parameters:
Example:
tasks["check_service"] = {
handler = function(system)
log.info("Checking nginx on " .. system.name)
local result = system:run_command("systemctl status nginx")
return result.exit_code == 0
end
}The File object represents a file on a target system and provides access to file content, metadata, and operations.
path: Path to the file (can be read and set; setting the path moves the file)file_name: The name of the file without the directory path (can be read and set)content: Content of the file as binary string (can be read and set)permissions: File permissions (can be read and set as numeric mode; returnsnilif file doesn't exist)
-
exists(): Check if file exists- Returns:
boolean-trueif file exists,falseotherwise
- Returns:
-
metadata(): Get file metadata- Returns: A table with file metadata (see Metadata Structure), or
nilif file doesn't exist
- Returns: A table with file metadata (see Metadata Structure), or
-
remove(): Remove the file -
directory(): Get the directory containing this file- Returns: A Directory object, or
nilif at root path
- Returns: A Directory object, or
Example:
tasks["configure_nginx"] = {
handler = function(system)
local config_file = system:file("/etc/nginx/sites-available/default")
config_file.content = "server {\n listen 80 default_server;\n root /var/www/html;\n}"
config_file.permissions = tonumber("644", 8)
local metadata = config_file:metadata()
if metadata and metadata.size then
log.info("File size: " .. metadata.size .. " bytes")
end
end
}
tasks["manage_file"] = {
handler = function(system)
-- Create, move and then delete a file
local file = system:file("/path/to/file.txt")
file.content = "New content" -- Write to file
file.permissions = tonumber("755", 8) -- Set permissions
file.path = "/new-path/to/renamed-file.txt" -- Rename file
file:remove() -- Delete file
end
}The Directory object represents a directory on a target system and provides access to directory operations and contents.
path: Path to the directory (can be read and set; setting the path renames the directory)file_name: The name of the directory without the parent path (can be read and set)permissions: Directory permissions (can be read and set as numeric mode; returnsnilif directory doesn't exist)
create(): Create the directory (including any missing ancestor directories)remove(): Remove the directoryexists(): Check if directory exists- Returns:
boolean-trueif directory exists,falseotherwise
- Returns:
metadata(): Get directory metadata- Returns: A table with directory metadata (see Metadata Structure), or
nilif directory doesn't exist
- Returns: A table with directory metadata (see Metadata Structure), or
parent(): Get the parent directory- Returns: A Directory object representing the parent directory, or
nilif at root path
- Returns: A Directory object representing the parent directory, or
entries(): Get directory entries- Returns: Array of File and Directory objects representing the directory contents
Example:
tasks["setup_directory"] = {
handler = function(system)
-- Create directory structure
local app_dir = system:directory("/var/www/myapp")
app_dir:create()
-- Set permissions
app_dir.permissions = tonumber("755", 8)
end
}
tasks["list_configs"] = {
handler = function(system)
-- Iterate through directory contents
local dir = system:directory("/etc/nginx/sites-available")
for _, entry in ipairs(dir:entries()) do
-- Each entry is either a File or Directory object
local metadata = entry:metadata()
if metadata then
print(entry.path .. " (" .. metadata.type .. ")")
if metadata.type == "file" and metadata.size then
print(" Size: " .. metadata.size .. " bytes")
end
end
end
end
}
tasks["manage_directory"] = {
handler = function(system)
-- Create, move and then delete a directory
local dir = system:directory("/path/to/dir")
dir:create() -- Create directory
dir.path = "/path/to/renamed-dir" -- Rename directory
dir:remove() -- Delete directory
end
}The metadata() method on File or Directory objects returns a table with the following fields, or nil if the file/directory doesn't exist:
path: Path to the file or directorysize: Size in bytes (number, ornilif unavailable)permissions: Permission mode (number, ornilif unavailable)type: Type of the item ("file", "directory", or "unknown")uid: User ID of the owner (number, ornil; alwaysnilon local systems)gid: Group ID of the owner (number, ornil; alwaysnilon local systems)accessed: Last access time as a Unix timestamp (number, ornilif unavailable)modified: Last modification time as a Unix timestamp (number, ornilif unavailable)
Example:
tasks["check_metadata"] = {
handler = function(system)
local file = system:file("/etc/hostname")
local metadata = file:metadata()
if metadata then
log.info("File size: " .. (metadata.size or "unknown"))
log.info("File type: " .. metadata.type)
log.info("File permissions: " .. (metadata.permissions or "unknown"))
log.info("Last modified: " .. (metadata.modified or "unknown"))
else
log.info("File does not exist")
end
end
}The env module provides access to environment variables. arc automatically loads variables from .env files in the project directory. Variables defined in the .env file take precedence over already defined ones.
get(var_name): Get the value of an environment variable- Parameters:
var_name(string) - Name of the environment variable - Returns: Value of the environment variable (string) or nil if not set
- Parameters:
Example:
tasks["deploy_app"] = {
handler = function(system)
local app_version = env.get("APP_VERSION") or "latest"
local deploy_path = env.get("DEPLOY_PATH") or "/var/www"
system:run_command("docker pull myapp:" .. app_version)
system:run_command("docker run -d -v " .. deploy_path .. ":/app myapp:" .. app_version)
end
}The global host object provides functions for interacting with the local system where arc is running. It has the same interface as the system object but operates on the local machine and its working directory is the directory where arc.lua is located.
-
run_command(cmd): Execute a command on the local system- Parameters:
cmd(string) - The command to execute - Returns: A table with
stdout,stderr, andexit_code
- Parameters:
-
file(path): Get a File object representing a file on the local system- Parameters:
path(string) - Path to the file - Returns: A File object
- Parameters:
-
directory(path): Get a Directory object representing a directory on the local system- Parameters:
path(string) - Path to the directory - Returns: A Directory object
- Parameters:
Example:
tasks["deploy_from_local"] = {
handler = function(system)
-- Read a local template
local config_template = host:file("templates/nginx.conf").content
-- Write to remote system
local remote_config = system:file("/etc/nginx/nginx.conf")
remote_config.content = config_template
-- Restart service
system:run_command("systemctl restart nginx")
end
}
tasks["backup_to_local"] = {
handler = function(system)
-- Read from remote
local remote_config = system:file("/etc/app/config.json").content
-- Save locally
local backup_dir = host:directory("backups/" .. system.name)
backup_dir:create()
local backup_file = host:file("backups/" .. system.name .. "/config.json")
backup_file.content = remote_config
end
}The format module provides utilities for working with JSON data.
-
to_json(value): Convert a Lua value to JSON- Parameters:
value(any) - Value to convert - Returns: JSON string
- Parameters:
-
to_json_pretty(value): Convert a Lua value to pretty-printed JSON- Parameters:
value(any) - Value to convert - Returns: JSON string
- Parameters:
-
from_json(json_string): Parse a JSON string to a Lua value- Parameters:
json_string(string) - JSON string to parse - Returns: Parsed Lua value
- Parameters:
Example:
tasks["manage_json_config"] = {
handler = function(system)
-- Read a JSON configuration file
local config_file = system:file("/etc/myapp/config.json")
local config = format.from_json(config_file.content)
-- Modify configuration
config.debug = true
config.log_level = "info"
-- Write back to the file
config_file.content = format.to_json_pretty(config)
end
}
tasks["update_api_config"] = {
handler = function(system)
-- Get current config from an API
local result = system:run_command("curl -s http://localhost:8080/api/config")
local api_config = format.from_json(result.stdout)
-- Update configuration
api_config.settings.cache_ttl = 3600
-- Send updated config back to API
local json_config = format.to_json(api_config)
system:run_command('curl -X POST -H "Content-Type: application/json" -d \'' .. json_config .. '\' http://localhost:8080/api/config')
end
}The template module provides template rendering capabilities using the Tera template engine.
render(template_content, context): Render a template with given context- Parameters:
template_content(string) - Template contentcontext(table) - Variables to use for template rendering
- Returns: Rendered template as string
- Parameters:
Example:
tasks["configure_web_server"] = {
handler = function(system)
-- Load a template from a local file
local template_content = host:file("templates/nginx.conf.template").content
-- Define context variables
local context = {
worker_processes = 4,
worker_connections = 1024,
server_name = system.name .. ".example.com",
document_root = "/var/www/" .. system.name,
environment = env.get("ENVIRONMENT") or "production"
}
-- Render and deploy configuration
local config = template.render(template_content, context)
system:file("/etc/nginx/nginx.conf").content = config
-- Validate and reload
local validation = system:run_command("nginx -t")
if validation.exit_code == 0 then
system:run_command("systemctl reload nginx")
else
error("Nginx configuration is invalid: " .. validation.stderr)
end
end
}The log module provides logging functions at various severity levels.
-
debug(value): Log a debug message- Parameters:
value(any) - Value to log
- Parameters:
-
info(value): Log an info message- Parameters:
value(any) - Value to log
- Parameters:
-
warn(value): Log a warning message- Parameters:
value(any) - Value to log
- Parameters:
-
error(value): Log an error message- Parameters:
value(any) - Value to log
- Parameters:
Example:
tasks["provision_database"] = {
handler = function(system)
log.info("Provisioning database on " .. system.name)
local check = system:run_command("which psql")
log.debug("which psql exit code: " .. check.exit_code)
if check.exit_code ~= 0 then
log.warn("PostgreSQL not found, attempting to install")
local install_result = system:run_command("apt-get install -y postgresql")
if install_result.exit_code ~= 0 then
error("Failed to install PostgreSQL: " .. install_result.stderr)
end
end
log.info("PostgreSQL is available")
end
}arc provides Language Server Protocol (LSP) support for Lua code editing with autocomplete, type checking, and inline documentation.
-
Install the Lua Language Server for the editor being used.
-
Initialize an arc project to generate type definitions:
arc init /path/to/projectThe init command creates .luarc.json and type definition files that enable the Lua Language Server to recognize arc's API types.
Contributions are welcome! Please feel free to submit any issue or pull request.
This project is licensed under the MIT License - see the LICENSE file for details.