Skip to content

Device Connections (Capability Proxies & Plugins)

This guide starts a short series on Neops’ capability-based connection system. Instead of a global get_connection() pool API, you build a typed proxy by inheriting capability interfaces, and the system resolves an appropriate plugin for the device and connection parameters.

If you only read one page, read this one and then jump to:


Prerequisites (quick)

You’ll be more comfortable with this system if you know:

  • Python ABCs (abc.ABC) and @abstractmethod
  • multiple inheritance / method resolution order (MRO)

If those are new, it’s still workable—just keep the examples close and start with the context manager usage in 31-connection-methods.md.


Glossary (terms used throughout)

  • Resolution: selecting a plugin based on platform/type/library + required capabilities (happens when you call MyProxy.get_connection(...))
  • Delegation: proxy forwarding a capability method call to the plugin implementation (happens when you call proxy.get_version(), proxy.get_status(), etc.)
  • Fallback: what happens when the plugin does not implement a capability method (default: raise NotImplementedForThisPlatform; proxies can override)
  • Best-match: when no plugin implements all required capabilities, choose the one that implements the most (warn loudly)

Note: These terms are used consistently throughout the 30–36 docs in this series.


Quick start example

This is a minimal end-to-end example showing the complete flow (proxy → resolve plugin → connect → call capability method):

import neops_worker_sdk.connection.plugins  # noqa: F401

from neops_worker_sdk.connection.capabilities import DeviceInfoCapability
from neops_worker_sdk.connection.proxy import ConnectionProxy
from neops_worker_sdk.workflow import Device


class DeviceInfoProxy(ConnectionProxy, DeviceInfoCapability):
    pass


device = Device(
    id=1,
    platform={"id": 1, "name": "ios", "short_name": "ios"},
    ip="192.168.1.1",
    username="admin",
    password="secret",
    skip_if_identifier_exists=False,
    identifier_fields=[],
)

with DeviceInfoProxy.connect(device, "ssh", "scrapli") as proxy:
    version = proxy.get_version()
    print(version)

Mental model

Think of the system as two layers plus one “front door”:

  • BaseConnection: a thin wrapper around a third‑party library connection (Netmiko, Scrapli, httpx, ncclient, …). It owns connect/disconnect/is_alive and performs lazy imports. See 34-writing-base-plugins.md.
  • ConnectionPlugin: platform‑aware factory + implementation point for capabilities. A plugin creates a BaseConnection and can implement capability methods (e.g. get_version()).
  • ConnectionProxy: the user-facing API. A proxy declares required capabilities via inheritance and delegates calls to the resolved plugin.

The three core building blocks

1) Capability interfaces

Capabilities are abstract interfaces that define the methods you want to call.

from abc import abstractmethod

from neops_worker_sdk.connection.capabilities import CapabilityInterface


class DeviceInfoCapability(CapabilityInterface):
    @abstractmethod
    def get_version(self) -> dict[str, str | None]:
        ...

See 33-writing-capabilities-and-proxies.md for guidance on designing capabilities and composing them into proxies.

2) Proxies (what your code uses)

Proxies declare what they need (capabilities) and provide a clean, typed API.

from neops_worker_sdk.connection.proxy import ConnectionProxy


class DeviceInfoProxy(ConnectionProxy, DeviceInfoCapability):
    pass

3) Plugins (what gets resolved at runtime)

Plugins declare what they provide (capabilities) and how to create/manage a raw connection.

from neops_worker_sdk.connection.capabilities import DeviceInfoCapability
from neops_worker_sdk.connection.registry import register_connection_plugin
from neops_worker_sdk.connection.types import ConnectionPlugin


@register_connection_plugin()
class IOSCiscoScrapliPlugin(ConnectionPlugin, DeviceInfoCapability):
    platform = "ios"
    connection_type = "ssh"
    connection_library = "scrapli"

    # _create_connection() creates the BaseConnection wrapper (see 34-writing-base-plugins.md)
    # get_version() implements DeviceInfoCapability (platform/library specific)

What happens when you call a proxy method

flowchart TD
  userCode["UserCode"] --> proxyConnect["MyProxy.connect(...)"]
  proxyConnect --> resolvePlugin["registry.resolve_plugin(...)"]
  resolvePlugin --> pluginInstance["Plugin(device)"]
  pluginInstance --> initConn["plugin.initialize_connection()"]
  initConn --> baseConn["BaseConnection.connect()"]
  userCode --> proxyCall["proxy.some_capability_method()"]
  proxyCall --> delegate["ConnectionProxy.__getattribute__ delegates"]
  delegate --> pluginMethod["plugin.some_capability_method()"]
  delegate --> fallbackRaise["Fallback raises NotImplementedForThisPlatform"]

Note: Mermaid diagrams render in MkDocs (with the configured Mermaid fence) and on GitHub. Some editors (including VS Code’s built-in Markdown preview) may require a Mermaid/Markdown preview extension to render them.

Key points:

  • Resolution is capability-aware: the proxy requires capabilities; the plugin must implement them (or you get fallback behavior; see 32-method-resolution-priority.md).
  • Fallback methods are auto-generated: if a plugin doesn’t implement a capability method, the proxy raises NotImplementedForThisPlatform with rich context. See 33-writing-capabilities-and-proxies.md.
  • Lazy dependency errors surface on first use: base plugins import third‑party libraries inside connect(). Missing dependencies raise ConnectionCreationError when the plugin is used.

Connection types and libraries (current conventions)

The system uses string identifiers:

  • Connection types: "ssh", "api", "netconf", "restconf" (and any custom types you define)
  • Connection libraries: "netmiko", "scrapli", "httpx", "ncclient" (and custom libraries)
  • Platforms: the device platform short name (e.g. "ios", "nxos", "iosxr")

Note: Platform is derived from device.platform.short_name if present, otherwise from device.platform if it’s a string.


Where to put “custom methods”

This series focuses on proxies/capabilities/plugins. In the current architecture:

There is no separate “register_connection_method” registry in the current connection module; capabilities and plugins are the intended extension points.


Further reading

  • docs/development/36-capability-proxy-architecture.md (design rationale and deeper internals)
  • neops_worker_sdk/connection/draft_usage.py (runnable, end-to-end examples)