Skip to main content

A drop-in replacement for ops.pebble.Client that uses the Pebble CLI

Project description

Shimmer - shiny Pebble client

A 100% compatible drop-in replacement for ops.pebble.Client that uses the Pebble CLI tool instead of socket communication.

Overview

Shimmer provides PebbleCliClient, a class that implements the same interface as ops.pebble.Client but communicates with Pebble via CLI commands instead of socket communication. This is useful for environments with restricted socket access (such as a Rock or Juju container).

Installation

Install from PyPI

uv pip install pebble-shimmer

Install development version

git clone https://github.com/tonyandrewmeyer/shimmer
cd shimmer
uv pip install -e .

Quick Start

from shimmer import PebbleCliClient as Client

# Create a client instance:
client = Client(
    socket_path="/var/lib/pebble/default/.pebble.socket",  # Optional: for env setup
    pebble_binary="pebble",  # Path to pebble binary
    timeout=30.0,  # Default command timeout
)

# Use exactly like ops.pebble.Client:
services = client.get_services()
client.start_services(["myservice"])

# Execute commands:
process = client.exec(["echo", "hello world"])
stdout, stderr = process.wait_output()
print(stdout)  # "hello world\n"

# File operations:
client.push("/path/to/file", "content")
content = client.pull("/path/to/file").read()

# Layer management
layer = """
services:
  myservice:
    override: replace
    command: python3 -m http.server 8080
    startup: enabled
"""
client.add_layer("mylayer", layer)
client.replan_services()

Advanced Usage

Custom Binary Path

client = PebbleCliClient(pebble_binary="/usr/local/bin/pebble")

Environment Configuration

# If using custom Pebble directory:
client = PebbleCliClient(socket_path="/custom/path/.pebble.socket")

# This automatically sets:
# PEBBLE=/custom/path
# PEBBLE_SOCKET=/custom/path/.pebble.socket

Error Handling

from shimmer import APIError, ConnectionError, TimeoutError

try:
    client.start_services(["nonexistent"])
except APIError as e:
    print(f"API Error: {e.message} (code: {e.code})")
except ConnectionError:
    print("Could not connect to Pebble")
except TimeoutError:
    print("Operation timed out")

Process Execution

# Simple command:
process = client.exec(["ls", "-la"])
stdout, stderr = process.wait_output()

# With environment and options:
process = client.exec(
    ["python3", "script.py"],
    environment={"PYTHONPATH": "/app"},
    working_dir="/app",
    timeout=60.0,
    user="appuser",
)

# Streaming I/O:
process = client.exec(["cat"], stdin="Hello World\n")
stdout, stderr = process.wait_output()

Architecture

The PebbleCliClient works by:

  1. Command Translation - Converts API calls to CLI commands
  2. Output Parsing - Parses CLI output back to Python objects
  3. Error Mapping - Maps CLI errors to compatible exceptions
  4. Process Management - Handles subprocess execution and I/O

Limitations

While this client aims for 100% compatibility, there are some limitations due to CLI constraints:

  1. Performance - CLI calls have higher overhead than socket communication
  2. Concurrency - Each operation spawns a new process
  3. Streaming - Some streaming operations may be buffered
  4. Platform - Requires Pebble binary in PATH or specified location

Other minor limitations:

  • replan_services(), start_services(), stop_services(), and restart_services() are only able to return the change ID if no timeout is set.
  • notify() only supports custom notices.
  • autostart_services() is an alias for replan() (possibly we could fix this by getting the current state?)
  • ack_warnings() is yet to be implemented.
  • get_warnings() is only implemented for the 'no warnings' case; parsing a non-empty warnings list raises NotImplementedError.

get_services(), get_checks(), list_files(), get_changes(), get_change(), and get_identities() use Pebble's structured --format json output, so they return the same rich data as ops.pebble.Client (including change kind/tasks/err, check thresholds, real file ownership, and local identity user IDs). This requires a Pebble build that supports --format on read commands.

Comparison with ops.pebble.Client

Feature ops.pebble.Client PebbleCliClient
Communication Unix socket CLI commands
Performance High Moderate
Setup Requires socket access Requires binary
Compatibility Native 100% API compatible
Dependencies ops library ops library + CLI
Use Cases Production charms Testing, development, debugging

Examples

Service Management

from shimmer import PebbleCliClient as Client

client = Client()

# Get all services:
services = client.get_services()
for service in services:
    print(f"{service.name}: {service.current}")

# Start specific services:
change_id = client.start_services(["web", "db"])
print(f"Started services, change: {change_id}")

# Wait for change to complete:
change = client.wait_change(change_id)
print(f"Change {change.id} status: {change.status}")

File Management

# Create directory structure:
client.make_dir("/app/config", make_parents=True, permissions=0o755)

# Write configuration file:
config = """
server:
  port: 8080
  host: 0.0.0.0
"""
client.push("/app/config/server.yaml", config)

# Read file back:
content = client.pull("/app/config/server.yaml").read()
print(content)

# List directory contents:
files = client.list_files("/app/config")
for file in files:
    print(f"{file.name} ({file.type})")

Layer Management

# Define service layer:
layer = {
    "summary": "Web application layer",
    "description": "Defines the web application service",
    "services": {
        "webapp": {
            "override": "replace",
            "summary": "Web application",
            "command": "python3 -m uvicorn app:main --host 0.0.0.0 --port 8080",
            "startup": "enabled",
            "environment": {
                "PYTHONPATH": "/app"
            },
            "user": "webapp",
            "group": "webapp",
        }
    },
    "checks": {
        "webapp-health": {
            "override": "replace",
            "level": "alive",
            "http": {"url": "http://localhost:8080/health"},
            "period": "10s",
            "timeout": "3s",
            "threshold": 3,
        }
    }
}

# Add and apply layer:
client.add_layer("webapp", layer)
change_id = client.replan_services()

# Wait for services to start:
client.wait_change(change_id)

# Check service status:
services = client.get_services(["webapp"])
print(f"webapp status: {services[0].current}")

Command Execution

# Execute simple command:
process = client.exec(["whoami"])
stdout, stderr = process.wait_output()
print(f"Running as: {stdout.strip()}")

# Execute with service context:
process = client.exec(
    ["python3", "-c", "import os; print(os.getcwd())"],
    service_context="webapp"
)
stdout, stderr = process.wait_output()
print(f"Service working directory: {stdout.strip()}")

# Execute interactive command:
process = client.exec(["python3", "-c", "print(input('Name: '))"])
process.stdin.write("Alice\n")
process.stdin.close()
stdout, stderr = process.wait_output()
print(f"Output: {stdout.strip()}")

Health Checks

# Get all checks:
checks = client.get_checks()
for check in checks:
    print(f"{check.name}: {check.status} ({check.level})")

# Start specific checks:
started = client.start_checks(["webapp-health"])
print(f"Started checks: {started}")

# Monitor check status:
import time
for _ in range(5):
    checks = client.get_checks(names=["webapp-health"])
    if checks:
        check = checks[0]
        print(f"Check status: {check.status}, failures: {check.failures}")
    time.sleep(2)

Notice Management

# Get recent notices:
notices = client.get_notices()
for notice in notices:
    print(f"{notice.type}: {notice.key} (occurred: {notice.occurrences})")

# Create custom notice:
notice_id = client.notify(
    type="custom",
    key="myapp.com/deployment",
    data={"version": "1.2.3", "environment": "production"}
)
print(f"Created notice: {notice_id}")

# Get specific notice:
notice = client.get_notice(notice_id)
print(f"Notice data: {notice.last_data}")

Troubleshooting

Common Issues

  1. "Pebble binary not found"

    # Specify full path to pebble
    client = PebbleCliClient(pebble_binary="/snap/bin/pebble")
    
  2. "Permission denied"

    # Ensure user has access to Pebble directory
    sudo chown -R $USER:$USER $PEBBLE
    
  3. "Connection error"

    # Check if Pebble is running
    import subprocess
    result = subprocess.run(["pebble", "version"], capture_output=True)
    print(result.stdout)
    

Debugging

Enable debug logging to see CLI commands:

import logging
logging.basicConfig(level=logging.DEBUG)

# Commands will be logged before execution:
client = PebbleCliClient()
services = client.get_services()

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Changelog

See CHANGELOG.md for version history and changes.

Related Projects

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pebble_shimmer-1.0.0b1.tar.gz (38.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pebble_shimmer-1.0.0b1-py3-none-any.whl (22.0 kB view details)

Uploaded Python 3

File details

Details for the file pebble_shimmer-1.0.0b1.tar.gz.

File metadata

  • Download URL: pebble_shimmer-1.0.0b1.tar.gz
  • Upload date:
  • Size: 38.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pebble_shimmer-1.0.0b1.tar.gz
Algorithm Hash digest
SHA256 bcb6693c5c854db2b1437e7fbaa0206fb01289fffbf11c7b78a515719be98f2c
MD5 28ef073ed7e5566c4e19bde843ebaf33
BLAKE2b-256 af2152d186c113cb4ecea5f6dc57e969fff0c681696c46b34e0fc11ac422b39d

See more details on using hashes here.

Provenance

The following attestation bundles were made for pebble_shimmer-1.0.0b1.tar.gz:

Publisher: publish.yaml on tonyandrewmeyer/shimmer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pebble_shimmer-1.0.0b1-py3-none-any.whl.

File metadata

File hashes

Hashes for pebble_shimmer-1.0.0b1-py3-none-any.whl
Algorithm Hash digest
SHA256 4ef903be9827524693dc58e9ad7e1c9fa053d38c7b40b4f47ce5a44d8b85e3c0
MD5 9f823d434b74779c6d4e0cf474f95793
BLAKE2b-256 59de1b2f4434a48d4a4d6ababe43567be109c43a5f78a758ee208637dcac18f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for pebble_shimmer-1.0.0b1-py3-none-any.whl:

Publisher: publish.yaml on tonyandrewmeyer/shimmer

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page