Skip to content

Commit a1c490c

Browse files
[AI-5346] Add Proxmox integration (DataDog#20428)
* add proxmox integration * remove dash * update changelog * validate ci * update labeler * Add process signatures * Add version metadata check * add e2e test * dont catch exception * [AI-5550] Collect resources from cluster/resources endpoint (DataDog#20533) * add count metrics * add more tags * Add support for up and count metrics * add support for proxmox tags * stop check if endpoint not accessible * update metadata and assert * add resource metrics * Update proxmox/datadog_checks/proxmox/check.py Co-authored-by: NouemanKHAL <noueman.khalikine@datadoghq.com> * Update proxmox/datadog_checks/proxmox/constants.py Co-authored-by: NouemanKHAL <noueman.khalikine@datadoghq.com> * rename tag to resource_name * Revert "add resource metrics" This reverts commit c293da3. --------- Co-authored-by: NouemanKHAL <noueman.khalikine@datadoghq.com> * Collect resource and performance metrics [AI-5549] (DataDog#20631) * add count metrics * add more tags * Add support for up and count metrics * add support for proxmox tags * stop check if endpoint not accessible * update metadata and assert * add resource metrics * add all metrics * refactor * remove elif * add status tag * lint * add metric to check * Add proxmox log instructions (DataDog#20680) * update logs * update unit test * test --------- Co-authored-by: NouemanKHAL <noueman.khalikine@datadoghq.com>
1 parent 9d3e835 commit a1c490c

30 files changed

Lines changed: 2482 additions & 0 deletions

File tree

.codecov.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,10 @@ coverage:
534534
target: 75
535535
flags:
536536
- prometheus
537+
Proxmox:
538+
target: 75
539+
flags:
540+
- proxmox
537541
ProxySQL:
538542
target: 75
539543
flags:
@@ -1482,6 +1486,11 @@ flags:
14821486
paths:
14831487
- prometheus/datadog_checks/prometheus
14841488
- prometheus/tests
1489+
proxmox:
1490+
carryforward: true
1491+
paths:
1492+
- proxmox/datadog_checks/proxmox
1493+
- proxmox/tests
14851494
proxysql:
14861495
carryforward: true
14871496
paths:

.github/workflows/config/labeler.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,8 @@ integration/process:
525525
- process/**/*
526526
integration/prometheus:
527527
- prometheus/**/*
528+
integration/proxmox:
529+
- proxmox/**/*
528530
integration/proxysql:
529531
- proxysql/**/*
530532
integration/pulsar:

.github/workflows/test-all.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3154,6 +3154,26 @@ jobs:
31543154
minimum-base-package: ${{ inputs.minimum-base-package }}
31553155
pytest-args: ${{ inputs.pytest-args }}
31563156
secrets: inherit
3157+
jf882f09:
3158+
uses: ./.github/workflows/test-target.yml
3159+
with:
3160+
job-name: Proxmox
3161+
target: proxmox
3162+
platform: linux
3163+
runner: '["ubuntu-22.04"]'
3164+
repo: "${{ inputs.repo }}"
3165+
python-version: "${{ inputs.python-version }}"
3166+
standard: ${{ inputs.standard }}
3167+
latest: ${{ inputs.latest }}
3168+
agent-image: "${{ inputs.agent-image }}"
3169+
agent-image-py2: "${{ inputs.agent-image-py2 }}"
3170+
agent-image-windows: "${{ inputs.agent-image-windows }}"
3171+
agent-image-windows-py2: "${{ inputs.agent-image-windows-py2 }}"
3172+
test-py2: ${{ inputs.test-py2 }}
3173+
test-py3: ${{ inputs.test-py3 }}
3174+
minimum-base-package: ${{ inputs.minimum-base-package }}
3175+
pytest-args: ${{ inputs.pytest-args }}
3176+
secrets: inherit
31573177
jbb39de2:
31583178
uses: ./.github/workflows/test-target.yml
31593179
with:

proxmox/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# CHANGELOG - Proxmox
2+
3+
<!-- towncrier release notes start -->
4+

proxmox/README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Agent Check: Proxmox
2+
3+
## Overview
4+
5+
This check monitors [Proxmox][1] through the Datadog Agent.
6+
7+
Include a high level overview of what this integration does:
8+
- What does your product do (in 1-2 sentences)?
9+
- What value will customers get from this integration, and why is it valuable to them?
10+
- What specific data will your integration monitor, and what's the value of that data?
11+
12+
## Setup
13+
14+
Follow the instructions below to install and configure this check for an Agent running on a host. For containerized environments, see the [Autodiscovery Integration Templates][3] for guidance on applying these instructions.
15+
16+
### Installation
17+
18+
The Proxmox check is included in the [Datadog Agent][2] package.
19+
No additional installation is needed on your server.
20+
21+
### Configuration
22+
23+
1. Edit the `proxmox.d/conf.yaml` file, in the `conf.d/` folder at the root of your Agent's configuration directory to start collecting your proxmox performance data. See the [sample proxmox.d/conf.yaml][4] for all available configuration options.
24+
25+
2. [Restart the Agent][5].
26+
27+
### Validation
28+
29+
[Run the Agent's status subcommand][6] and look for `proxmox` under the Checks section.
30+
31+
## Data Collected
32+
33+
### Metrics
34+
35+
See [metadata.csv][7] for a list of metrics provided by this integration.
36+
37+
### Logs
38+
39+
To collect logs from all of your Proxmox services:
40+
41+
1. Enable log collection in your `datadog.yaml` file:
42+
43+
```yaml
44+
logs_enabled: true
45+
```
46+
47+
2. Uncomment and edit the logs configuration block in your `proxmox.d/conf.yaml` file. For example:
48+
49+
```yaml
50+
logs:
51+
- type: journald
52+
source: proxmox
53+
include_units:
54+
- pveproxy.service
55+
- pvedaemon.service
56+
- pve-firewall.service
57+
- pve-ha-crm.service
58+
- pve-ha-lrm.service
59+
- pvescheduler.service
60+
- pvestatd.service
61+
- qmeventd.service
62+
```
63+
64+
### Events
65+
66+
The Proxmox integration does not include any events.
67+
68+
### Service Checks
69+
70+
The Proxmox integration does not include any service checks.
71+
72+
See [service_checks.json][8] for a list of service checks provided by this integration.
73+
74+
## Troubleshooting
75+
76+
Need help? Contact [Datadog support][9].
77+
78+
79+
[1]: **LINK_TO_INTEGRATION_SITE**
80+
[2]: https://app.datadoghq.com/account/settings/agent/latest
81+
[3]: https://docs.datadoghq.com/agent/kubernetes/integrations/
82+
[4]: https://github.com/DataDog/integrations-core/blob/master/proxmox/datadog_checks/proxmox/data/conf.yaml.example
83+
[5]: https://docs.datadoghq.com/agent/guide/agent-commands/#start-stop-and-restart-the-agent
84+
[6]: https://docs.datadoghq.com/agent/guide/agent-commands/#agent-status-and-information
85+
[7]: https://github.com/DataDog/integrations-core/blob/master/proxmox/metadata.csv
86+
[8]: https://github.com/DataDog/integrations-core/blob/master/proxmox/assets/service_checks.json
87+
[9]: https://docs.datadoghq.com/help/
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Proxmox
2+
files:
3+
- name: proxmox.yaml
4+
options:
5+
- template: init_config
6+
options:
7+
- template: init_config/default
8+
- template: init_config/http
9+
- template: instances
10+
options:
11+
- name: proxmox_server
12+
display_priority: 5
13+
description: |
14+
Proxmox API endpoint.
15+
value:
16+
example: http://localhost:8006
17+
type: string
18+
required: true
19+
- template: instances/default
20+
- template: instances/http
21+
overrides:
22+
headers.display_priority: 4
23+
headers.enabled: true
24+
headers.description: |
25+
Headers to use for every request. An Authorization header including the Proxmox API token is required
26+
for authentication for the REST API.
27+
You can alternatively use the `auth_token` option.
28+
29+
headers.value.example:
30+
Authorization: "PVEAPIToken=<USER>@<REALM>!<TOKEN_ID>=<YOUR_TOKEN>"
31+
auth_token.display_priority: 3
32+
- template: logs
33+
example:
34+
- type: journald
35+
source: proxmox
36+
include_units:
37+
- pveproxy.service
38+
- pvedaemon.service
39+
- pve-firewall.service
40+
- pve-ha-crm.service
41+
- pve-ha-lrm.service
42+
- pvescheduler.service
43+
- pvestatd.service
44+
- qmeventd.service

proxmox/changelog.d/20428.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Initial Release
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# (C) Datadog, Inc. 2025-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
__version__ = '0.0.1'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# (C) Datadog, Inc. 2025-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
from .__about__ import __version__
5+
from .check import ProxmoxCheck
6+
7+
__all__ = ['__version__', 'ProxmoxCheck']
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# (C) Datadog, Inc. 2025-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
from requests.exceptions import ConnectionError, HTTPError, InvalidURL, JSONDecodeError, Timeout
5+
6+
from datadog_checks.base import AgentCheck
7+
from datadog_checks.proxmox.config_models import ConfigMixin
8+
9+
from .constants import (
10+
NODE_RESOURCE,
11+
OK_STATUS,
12+
PERF_METRIC_NAME,
13+
RESOURCE_COUNT_METRICS,
14+
RESOURCE_METRIC_NAME,
15+
RESOURCE_TYPE_MAP,
16+
VM_RESOURCE,
17+
)
18+
19+
20+
class ProxmoxCheck(AgentCheck, ConfigMixin):
21+
__NAMESPACE__ = 'proxmox'
22+
23+
def __init__(self, name, init_config, instances):
24+
super(ProxmoxCheck, self).__init__(name, init_config, instances)
25+
self.check_initializations.append(self._parse_config)
26+
self.all_resources = {}
27+
28+
def _parse_config(self):
29+
self.base_tags = [f"proxmox_server:{self.config.proxmox_server}"]
30+
if self.config.tags:
31+
self.base_tags.extend(self.config.tags)
32+
33+
def _submit_resource_metrics(self, resource, tags, hostname):
34+
for metric_name, metric_name_remapped in RESOURCE_METRIC_NAME.items():
35+
metric_value = resource.get(metric_name)
36+
metric_method = self.count if metric_name in RESOURCE_COUNT_METRICS else self.gauge
37+
if metric_value is not None:
38+
metric_method(f'{metric_name_remapped}', metric_value, tags=tags, hostname=hostname)
39+
40+
def _get_vm_hostname(self, vm_id, vm_name, node):
41+
try:
42+
url = f"{self.config.proxmox_server}/nodes/{node}/qemu/{vm_id}/agent/get-host-name"
43+
hostname_response = self.http.get(url)
44+
hostname_json = hostname_response.json()
45+
except (HTTPError, InvalidURL, ConnectionError, Timeout, JSONDecodeError) as e:
46+
self.log.info(
47+
"Failed to get hostname for vm %s on node %s; endpoint: %s; %s",
48+
vm_id,
49+
node,
50+
self.config.proxmox_server,
51+
e,
52+
)
53+
hostname_json = {}
54+
hostname = hostname_json.get("data", {}).get("result", {}).get("host-name", vm_name)
55+
return hostname
56+
57+
def _collect_performance_metrics(self):
58+
metrics_response = self.http.get(f"{self.config.proxmox_server}/cluster/metrics/export")
59+
metrics_response_json = metrics_response.json()
60+
metrics = metrics_response_json.get('data', {}).get('data', [])
61+
62+
for metric in metrics:
63+
resource_id = metric.get('id')
64+
resource = self.all_resources.get(resource_id, {})
65+
metric_value = metric.get('value')
66+
metric_name = metric.get('metric')
67+
metric_type = metric.get('type')
68+
metric_name_remapped = PERF_METRIC_NAME.get(metric_name)
69+
hostname = resource.get('hostname')
70+
tags = resource.get('tags', [])
71+
if not resource or metric_name_remapped is None:
72+
self.log.debug(
73+
"Invalid metric entry found; metric name: %s, resource id: %s", metric_name_remapped, resource_id
74+
)
75+
continue
76+
77+
metric_method = self.count if metric_type == 'derive' else self.gauge
78+
metric_method(metric_name_remapped, metric_value, tags=tags, hostname=hostname)
79+
80+
def _collect_resource_metrics(self):
81+
resources_response = self.http.get(f"{self.config.proxmox_server}/cluster/resources")
82+
resources_response_json = resources_response.json()
83+
resources = resources_response_json.get("data", [])
84+
85+
external_tags = []
86+
all_resources = {}
87+
88+
for resource in resources:
89+
resource_type = resource.get('type')
90+
node = resource.get('node')
91+
resource_type_remapped = RESOURCE_TYPE_MAP.get(resource_type, resource_type)
92+
resource_id = resource.get('id')
93+
resource_name = resource.get('name')
94+
if resource_name is None:
95+
# some resource types don't have a name attribute
96+
resource_name = resource.get(resource.get('type', ''))
97+
98+
resource_tags = {
99+
f'proxmox_type:{resource_type_remapped}',
100+
f'proxmox_name:{resource_name}',
101+
f'proxmox_id:{resource_id}',
102+
}
103+
104+
proxmox_tags = resource.get('tags')
105+
if proxmox_tags:
106+
proxmox_tags = proxmox_tags.split(';')
107+
resource_tags.update(proxmox_tags)
108+
109+
if node and resource_type_remapped != 'node':
110+
resource_tags.add(f'proxmox_node:{node}')
111+
112+
pool = resource.get('pool')
113+
if pool and resource_type_remapped != 'pool':
114+
resource_tags.add(f'proxmox_pool:{pool}')
115+
116+
self.gauge(
117+
f'{resource_type_remapped}.count',
118+
1,
119+
tags=self.base_tags + list(resource_tags),
120+
)
121+
122+
status = resource.get("status")
123+
status = 1 if status in OK_STATUS else 0
124+
125+
hostname = None
126+
127+
if (resource_type_remapped == VM_RESOURCE or resource_type_remapped == NODE_RESOURCE) and status == 0:
128+
# don't collect information about powered off VMs and nodes
129+
continue
130+
elif resource_type_remapped == VM_RESOURCE and status == 1:
131+
vm_id = resource.get('vmid')
132+
hostname = self._get_vm_hostname(vm_id, resource_name, node)
133+
elif resource_type_remapped == NODE_RESOURCE:
134+
hostname = node
135+
136+
tags = []
137+
if hostname is None:
138+
tags = self.base_tags + list(resource_tags)
139+
else:
140+
external_tags.append((hostname, {self.__NAMESPACE__: self.base_tags + list(resource_tags)}))
141+
142+
all_resources[resource_id] = {'resource_type': resource_type_remapped, 'tags': tags, 'hostname': hostname}
143+
144+
if resource_type_remapped != "pool":
145+
# pools don't have a status attribute
146+
self.gauge(
147+
f'{resource_type_remapped}.up',
148+
status,
149+
tags,
150+
hostname=hostname,
151+
)
152+
self._submit_resource_metrics(resource, tags, hostname)
153+
154+
self.all_resources = all_resources
155+
self.set_external_tags(external_tags)
156+
157+
def check(self, _):
158+
try:
159+
response = self.http.get(f"{self.config.proxmox_server}/version")
160+
response.raise_for_status()
161+
162+
response_json = response.json()
163+
version = response_json.get("data", {}).get("version")
164+
self.set_metadata('version', version)
165+
self.gauge("api.up", 1, tags=self.base_tags + ['proxmox_status:up'])
166+
167+
except (HTTPError, InvalidURL, ConnectionError, Timeout, JSONDecodeError) as e:
168+
self.log.error(
169+
"Encountered an Exception when hitting the Proxmox API %s: %s", self.config.proxmox_server, e
170+
)
171+
self.gauge("api.up", 0, tags=self.base_tags + ['proxmox_status:down'])
172+
raise
173+
174+
self._collect_resource_metrics()
175+
self._collect_performance_metrics()

0 commit comments

Comments
 (0)