Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .chloggen/42647-add-ssh-connection.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: receiver/ciscoosreceiver

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: "Add `ciscoosreceiver` to collect metrics from Cisco OS devices via SSH"

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [42647]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext: |
Supports SSH-based metric collection from Cisco devices including:
- System metrics (CPU, memory utilization)
- Interface metrics (bytes, packets, errors, status)
- Configurable scrapers for modular metric collection
- Device authentication via password or SSH key

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [user]
80 changes: 50 additions & 30 deletions receiver/ciscoosreceiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,38 +27,48 @@ The following settings are available:

### Device Configuration

Each device configuration contains device information and authentication settings following semantic conventions:

| Setting | Type | Required | Description |
|---------|------|----------|-------------|
| `host` | string | Yes | Device address in `host:port` format |
| `username` | string | Yes | SSH username for authentication |
| `password` | string | No* | Password for authentication |
| `key_file` | string | No* | Path to SSH private key file |
| `device.host.name` | string | No | Human-readable device name |
| `device.host.ip` | string | Yes | Device IP address |
| `device.host.port` | int | Yes | SSH port (typically 22) |
| `auth.username` | string | Yes | SSH username for authentication |
| `auth.password` | string | No* | Password for authentication |
| `auth.key_file` | string | No* | Path to SSH private key file |

*Either `password` or `key_file` is required, but not both.
*Either `auth.password` or `auth.key_file` is required, but not both.

### Scrapers Configuration

The scrapers are configured as groups.
The scrapers are configured as modular components. Each scraper type can be configured individually:

| Setting | Type | Description |
|---------|------|-------------|
| `bgp` | map | BGP session metrics configuration |
| `environment` | map | Temperature and power metrics configuration |
| `facts` | map | System information metrics configuration |
| `interfaces` | map | Interface status and statistics configuration |
| `optics` | map | Optical transceiver metrics configuration |

Each scraper can be enabled by simply including it in the configuration, or disabled by omitting it. Future versions may support scraper-specific configuration options within each group.

## Scrapers

The receiver supports the following scrapers:

- **BGP**: Collects BGP session information and statistics
- **Environment**: Collects temperature and power consumption metrics
- **Facts**: Collects system information (OS version, memory, CPU utilization)
- **Interfaces**: Collects interface statistics (bytes, packets, errors)
- **Optics**: Collects optical signal strength information
| `system` | map | System metrics (CPU, memory, device info) |
| `interfaces` | map | Interface statistics (bytes, packets, errors) |
| `bgp` | map | BGP session information and statistics |
| `environment` | map | Temperature and power consumption metrics |
| `optics` | map | Optical transceiver metrics |

## Metrics Collected

### Interface Metrics
- `cisco.interface.transmit.bytes` - Bytes transmitted per interface
- `cisco.interface.receive.bytes` - Bytes received per interface
- `cisco.interface.transmit.errors` - Transmit errors per interface
- `cisco.interface.receive.errors` - Receive errors per interface
- `cisco.interface.up` - Interface operational status (1=up, 0=down)

### System Metrics
- `cisco.system.cpu.utilization` - CPU utilization percentage
- `cisco.system.memory.utilization` - Memory utilization percentage

### Resource Attributes
- `cisco.device.name` - Device name
- `cisco.device.ip` - Device IP address
- `cisco.device.model` - Device model

## Example Configuration

Expand All @@ -68,15 +78,25 @@ receivers:
collection_interval: 60s
timeout: 30s
devices:
- host: "cisco-device:22"
username: "admin"
password: "password"
- device:
host:
name: "core-switch-01"
ip: "192.168.1.10"
port: 22
auth:
username: "admin"
password: "secure-password"
- device:
host:
name: "edge-router-01"
ip: "192.168.1.20"
port: 22
auth:
username: "admin"
key_file: "/path/to/ssh/key"
scrapers:
bgp:
environment:
facts:
system:
interfaces:
optics:

exporters:
debug:
Expand Down
131 changes: 87 additions & 44 deletions receiver/ciscoosreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,114 @@ package ciscoosreceiver // import "github.com/open-telemetry/opentelemetry-colle

import (
"errors"
"fmt"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/configopaque"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/xconfmap"
"go.opentelemetry.io/collector/scraper/scraperhelper"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/ciscoosreceiver/internal/metadata"
"go.uber.org/multierr"
)

// Config represents the receiver configuration
// Config defines configuration for Cisco OS receiver.
type Config struct {
scraperhelper.ControllerConfig `mapstructure:",squash"`
metadata.MetricsBuilderConfig `mapstructure:",squash"`

Devices []DeviceConfig `mapstructure:"devices"`
Scrapers ScrapersConfig `mapstructure:"scrapers"`
Devices []DeviceConfig `mapstructure:"devices"`
Scrapers map[component.Type]component.Config `mapstructure:"-"`
}

// DeviceConfig represents configuration for a single Cisco device
// DeviceConfig represents configuration for a single Cisco device using semantic conventions
type DeviceConfig struct {
Host string `mapstructure:"host"`
KeyFile string `mapstructure:"key_file"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Device DeviceInfo `mapstructure:"device"`
Auth AuthConfig `mapstructure:"auth"`
}

// DeviceInfo follows semantic conventions for device identification
type DeviceInfo struct {
// DO NOT USE unkeyed struct initialization
_ struct{} `mapstructure:"-"`

Host HostInfo `mapstructure:"host"`
}

// HostInfo contains host-specific information
type HostInfo struct {
Name string `mapstructure:"name"`
IP string `mapstructure:"ip"`
Port int `mapstructure:"port"`
}

// ScrapersConfig represents which scrapers are enabled
type ScrapersConfig struct {
BGP bool `mapstructure:"bgp"`
Environment bool `mapstructure:"environment"`
Facts bool `mapstructure:"facts"`
Interfaces bool `mapstructure:"interfaces"`
Optics bool `mapstructure:"optics"`
// AuthConfig represents authentication configuration
type AuthConfig struct {
Username string `mapstructure:"username"`
Password configopaque.String `mapstructure:"password"`
KeyFile string `mapstructure:"key_file"`
}

// Validate checks if the receiver configuration is valid
var (
_ xconfmap.Validator = (*Config)(nil)
_ confmap.Unmarshaler = (*Config)(nil)
)

// Validate checks the receiver configuration is valid
func (cfg *Config) Validate() error {
var err error

if len(cfg.Devices) == 0 {
return errors.New("at least one device must be configured")
err = errors.New("at least one device must be configured")
}

for _, device := range cfg.Devices {
if device.Host == "" {
return errors.New("device host cannot be empty")
for i, device := range cfg.Devices {
if device.Device.Host.IP == "" {
err = multierr.Append(err, fmt.Errorf("device[%d]: host.ip cannot be empty", i))
}

// Authentication validation logic:
// 1. If using key file: username + key_file (password optional)
// 2. If not using key file: username + password required
if device.KeyFile != "" {
// Key file authentication: requires username
if device.Username == "" {
return errors.New("device username cannot be empty")
}
} else {
// Password authentication: requires both username and password
if device.Username == "" {
return errors.New("device username cannot be empty")
}
if device.Password == "" {
return errors.New("device password cannot be empty")
}
if device.Device.Host.Port == 0 {
err = multierr.Append(err, fmt.Errorf("device[%d]: host.port cannot be empty", i))
}
if device.Auth.Username == "" {
err = multierr.Append(err, fmt.Errorf("device[%d]: auth.username cannot be empty", i))
}
if device.Auth.Password == "" && device.Auth.KeyFile == "" {
err = multierr.Append(err, fmt.Errorf("device[%d]: auth.password or auth.key_file must be provided", i))
}
}

// Check if at least one scraper is enabled
if !cfg.Scrapers.BGP && !cfg.Scrapers.Environment && !cfg.Scrapers.Facts && !cfg.Scrapers.Interfaces && !cfg.Scrapers.Optics {
return errors.New("at least one scraper must be enabled")
if len(cfg.Scrapers) == 0 {
err = multierr.Append(err, errors.New("must specify at least one scraper when using ciscoosreceiver"))
}

return err
}

// Unmarshal a config.Parser into the config struct.
func (cfg *Config) Unmarshal(componentParser *confmap.Conf) error {
if componentParser == nil {
return nil
}

// load the non-dynamic config normally
if err := componentParser.Unmarshal(cfg, confmap.WithIgnoreUnused()); err != nil {
return err
}

// dynamically load the individual scraper configs based on the key name
cfg.Scrapers = map[component.Type]component.Config{}

scrapersSection, err := componentParser.Sub("scrapers")
if err != nil {
return err
}

for keyStr := range scrapersSection.ToStringMap() {
key, err := component.NewType(keyStr)
if err != nil {
return fmt.Errorf("invalid scraper key name: %s", key)
}

// TODO: Implement proper scraper config unmarshaling when scraper implementations are added
// For now, store empty config for each scraper type
cfg.Scrapers[key] = nil
}

return nil
Expand Down
Loading