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 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.
  • get_notices() cannot include the last_occurred, last_data, repeat_after, or expire_after fields
  • get_changes() cannot include the kind, tasks, err, ready_time, or data fields, and guesses at the ready field
  • autostart_services() is an alias for replan() (possibly we could fix this by getting the current state?)
  • wait_change() is yet to be implemented
  • ack_warnings() is yet to be implemented
  • get_warnings() is only implemented for the 'no warnings' case
  • list_file() looks up the user and group IDs locally, which is very likely to be wrong
  • get_identities() is unable to get the user ID for local identities

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.0a4.tar.gz (26.0 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.0a4-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for pebble_shimmer-1.0.0a4.tar.gz
Algorithm Hash digest
SHA256 39bdf37faba90482518a9ad27c7af74d85378ce7ee35c1ab694fa720a4f04118
MD5 0ebeb169e9f5c13bc1292e9165d74725
BLAKE2b-256 b7b70e87f6b1da34485bd0b42731248710c37945a64146c7840b1cf41cd91538

See more details on using hashes here.

Provenance

The following attestation bundles were made for pebble_shimmer-1.0.0a4.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.0a4-py3-none-any.whl.

File metadata

File hashes

Hashes for pebble_shimmer-1.0.0a4-py3-none-any.whl
Algorithm Hash digest
SHA256 36f2ba7ba1d09896609ce838571975c06f72c809c4eb81f64886e9d8efb07779
MD5 e80358f2bb212ebf0c454d24fb071006
BLAKE2b-256 a931a2f3aaf39fb3cba1fd17f8ca8c738ff363250f977314d3d2a0cf66f78730

See more details on using hashes here.

Provenance

The following attestation bundles were made for pebble_shimmer-1.0.0a4-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