Skip to content

Commit ac882d2

Browse files
committed
First import
1 parent 9d3ce19 commit ac882d2

File tree

2 files changed

+310
-0
lines changed

2 files changed

+310
-0
lines changed

README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# nagios-plugin-azure-resource
2+
3+
Nagios plugin to monitor Microsoft Azure resource objects.
4+
5+
## Authors
6+
7+
Mohamed El Morabity <melmorabity -(at)- fedoraproject.org>
8+
9+
## Requirements
10+
11+
The plugin is written in Python 2. It requires the following libraries:
12+
13+
* [pynag](https://pypi.python.org/pypi/pynag)
14+
* [msrestazure](https://pypi.python.org/pypi/msrestazure) >= 0.4.15
15+
16+
## Usage
17+
18+
check_azure_resource.py -C CLIENT -S SECRET -T TENANT -R RESOURCE -M METRIC [-D DIMENSION -V DIMENSION-VALUE] [-H HOST] [-T TIMEOUT]
19+
20+
### Options
21+
22+
-h, --help
23+
24+
Show help message and exit
25+
26+
-C CLIENT, --client=CLIENT
27+
28+
Azure client ID
29+
30+
-S SECRET, --secret=SECRET
31+
32+
Azure client secret
33+
34+
-T TENANT, --tenant=TENANT
35+
36+
Azure tenant ID
37+
38+
-R RESOURCE, --resource=RESOURCE
39+
40+
Azure resource ID (in the following format: `/subscriptions/<subscriptionId>/resourceGroups/<resourceGroupName>/providers/<resourceProviderNamespace>/<resourceType>/<resourceName>`)
41+
42+
-M METRIC, --metric=METRIC
43+
44+
Metric (see https://docs.microsoft.com/en-us/azure/monitoring-and-diagnostics/monitoring-supported-metrics for a list of all metrics available for each resource type)
45+
46+
-D DIMENSION, --dimension=DIMENSION
47+
48+
Metric dimension
49+
50+
-V DIMENSION-VALUE, --dimension-value=DIMENSION-VALUE
51+
52+
Metric dimension value
53+
54+
-H HOST, --host=HOST
55+
56+
Alternative Azure Management URL (for Azure national cloud instances like Germany or China)
57+
58+
-t TIMEOUT, --timeout=TIMEOUT
59+
60+
Connection Timeout
61+
62+
-c CRITICAL, --critical=CRITICAL
63+
64+
Critical Threshhold
65+
66+
-w WARNING, --warning=WARNING
67+
68+
Warn Threshhold
69+
70+
## Examples
71+
72+
$ ./check_azure_resource.py \
73+
--client=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX \
74+
--secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
75+
--tenant=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX \
76+
--resource /subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVirtualMachine \
77+
--metric 'Percentage CPU' \
78+
--warning 50 --critical 75
79+
OK: Percentage CPU 3.865 percent | 'Percentage CPU'=3.865%;50;75;;
80+
81+
$ ./check_azure_resource.py \
82+
--client=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX \
83+
--secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \
84+
--tenant=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX \
85+
--resource /subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/myResourceGroup/providers/Microsoft.Sql/servers/myDBServer \
86+
--metric storage_used \
87+
--dimension DatabaseResourceId \
88+
--dimension-value /subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/myResourceGroup/providers/Microsoft.Sql/servers/myDBServer/databases/myDB \
89+
--warning 25000000000 --critical 50000000000
90+
WARNING: Storage used 36783194112.0 bytes | 'storage_used'=36783194112.0B;25000000000;50000000000;;

check_azure_resource.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright (C) 2018 Mohamed El Morabity <melmorabity@fedoraproject.com>
5+
#
6+
# This module is free software: you can redistribute it and/or modify it under the terms of the GNU
7+
# General Public License as published by the Free Software Foundation, either version 3 of the
8+
# License, or (at your option) any later version.
9+
#
10+
# This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
11+
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
# General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License along with this program. If not,
15+
# see <http://www.gnu.org/licenses/>.
16+
17+
18+
from msrest.exceptions import ClientException
19+
from msrest.service_client import ServiceClient
20+
21+
from msrestazure.azure_active_directory import ServicePrincipalCredentials
22+
from msrestazure.azure_configuration import AzureConfiguration
23+
from msrestazure.azure_exceptions import CloudError
24+
import msrestazure.tools
25+
26+
from pynag import Plugins
27+
from pynag.Plugins import simple as Plugin
28+
29+
from requests.exceptions import HTTPError
30+
31+
32+
def _call_arm_rest_api(client, path, api_version, method='GET', body=None, query=None,
33+
headers=None, timeout=None):
34+
"""Launch an Azure REST API request."""
35+
36+
request = getattr(client, method.lower())(
37+
url=path, params=dict(query or {}, **{'api-version': api_version})
38+
)
39+
response = client.send(
40+
request=request, content=body,
41+
headers=dict(headers or {}, **{'Content-Type': 'application/json; charset=utf-8'}),
42+
timeout=timeout
43+
)
44+
45+
try:
46+
response.raise_for_status()
47+
except HTTPError:
48+
# msrestazure.azure_exceptions.CloudError constructor provides a nice way to extract
49+
# Azure errors from request responses
50+
raise CloudError(response)
51+
52+
try:
53+
result = response.json()
54+
except ValueError:
55+
result = response.text
56+
57+
return result
58+
59+
60+
class NagiosAzureResourceMonitor(Plugin):
61+
"""Implements functionalities to grab metrics from Azure resource objects."""
62+
63+
DEFAULT_AZURE_SERVICE_HOST = 'management.azure.com'
64+
_AZURE_METRICS_API = '2017-05-01-preview'
65+
_AZURE_METRICS_UNIT_SYMBOLS = {'Percent': '%', 'Bytes': 'B', 'Seconds': 's'}
66+
67+
def __init__(self, *args, **kwargs):
68+
Plugin.__init__(self, *args, **kwargs)
69+
70+
self._set_cli_options()
71+
72+
def _set_cli_options(self):
73+
"""Define command line options."""
74+
75+
self.add_arg('C', 'client', 'Azure client ID')
76+
self.add_arg('S', 'secret', 'Azure client secret')
77+
self.add_arg('T', 'tenant', 'Azure tenant ID')
78+
79+
self.add_arg('R', 'resource', 'Azure resource ID')
80+
self.add_arg('M', 'metric', 'Metric')
81+
self.add_arg('D', 'dimension', 'Metric dimension', required=None)
82+
self.add_arg('V', 'dimension-value', 'Metric dimension value', required=None)
83+
84+
def activate(self):
85+
"""Parse out all command line options and get ready to process the plugin."""
86+
Plugin.activate(self)
87+
88+
if not msrestazure.tools.is_valid_resource_id(self['resource']):
89+
self.parser.error('invalid resource ID')
90+
91+
if bool(self['dimension']) != bool(self['dimension-value']):
92+
self.parser.error('--dimension and --dimension-value must be used together')
93+
94+
# Set up Azure Resource Management URL
95+
if self['host'] is None:
96+
self['host'] = self.DEFAULT_AZURE_SERVICE_HOST
97+
98+
# Set up timeout
99+
if self['timeout'] is not None:
100+
try:
101+
self['timeout'] = float(self['timeout'])
102+
if self['timeout'] < 0:
103+
raise ValueError
104+
except ValueError as ex:
105+
self.parser.error('Invalid timeout')
106+
107+
# Authenticate to ARM
108+
azure_management_url = 'https://{}'.format(self['host'])
109+
try:
110+
credentials = ServicePrincipalCredentials(client_id=self['client'],
111+
secret=self['secret'],
112+
tenant=self['tenant'])
113+
self._client = ServiceClient(credentials, AzureConfiguration(azure_management_url))
114+
except ClientException as ex:
115+
self.nagios_exit(Plugins.UNKNOWN, str(ex.inner_exception or ex))
116+
117+
try:
118+
self._metric_definitions = self._get_metric_definitions()
119+
except CloudError as ex:
120+
self.nagios_exit(Plugins.UNKNOWN, ex.message)
121+
122+
metric_ids = [m['name']['value'] for m in self._metric_definitions]
123+
if self['metric'] not in metric_ids:
124+
self.parser.error(
125+
'Unknown metric {} for specified resource. ' \
126+
'Supported metrics are: {}'.format(self['metric'], ', '.join(metric_ids))
127+
)
128+
self._metric_properties = self._get_metric_properties()
129+
130+
dimension_ids = [d['value'] for d in self._metric_properties.get('dimensions', [])]
131+
if self._is_dimension_required() and self['dimension'] is None:
132+
self.parser.error(
133+
'Dimension required for metric {}. ' \
134+
'Supported dimensions are: {}'.format(self['metric'], ', '.join(dimension_ids))
135+
)
136+
if self['dimension'] is not None and self['dimension'] not in dimension_ids:
137+
self.parser.error(
138+
'Unknown dimension {} for metric {}. ' \
139+
'Supported dimensions are: {}'.format(self['dimension'], self['metric'],
140+
', '.join(dimension_ids))
141+
)
142+
143+
def _get_metric_definitions(self):
144+
"""Get all available metric definitions for the Azure resource object."""
145+
146+
path = '{}/providers/Microsoft.Insights/metricDefinitions'.format(self['resource'])
147+
metrics = _call_arm_rest_api(self._client, path, self._AZURE_METRICS_API,
148+
timeout=self['timeout'])
149+
150+
return metrics['value']
151+
152+
def _get_metric_properties(self):
153+
"""Get metric properties."""
154+
155+
for metric in self._metric_definitions:
156+
if metric['name']['value'] == self['metric']:
157+
return metric
158+
159+
return None
160+
161+
def _is_dimension_required(self):
162+
"""Check whether an additional metric is required for a given metric ID."""
163+
164+
return self._metric_properties['isDimensionRequired']
165+
166+
def _get_metric_value(self):
167+
"""Get latest metric value available for the Azure resource object."""
168+
169+
query = {'metric': self['metric']}
170+
if self['dimension'] is not None:
171+
query['$filter'] = "{} eq '{}'".format(self['dimension'], self['dimension-value'])
172+
173+
path = '{}/providers/Microsoft.Insights/metrics/{}'.format(self['resource'],
174+
self['metric'])
175+
176+
try:
177+
metric_values = _call_arm_rest_api(self._client, path,
178+
self._AZURE_METRICS_API, query=query,
179+
timeout=self['timeout'])
180+
metric_values = metric_values['value'][0]['timeseries']
181+
except CloudError as ex:
182+
self.nagios_exit(Plugins.UNKNOWN, ex.message)
183+
184+
if not metric_values:
185+
return None
186+
187+
aggregation_type = self._metric_properties['primaryAggregationType'].lower()
188+
# Get the latest value available
189+
for value in metric_values[0]['data'][::-1]:
190+
if aggregation_type in value:
191+
return value[aggregation_type]
192+
193+
def check_metric(self):
194+
"""Check if the metric value is within the threshold range, and exits with status code,
195+
message and perfdata.
196+
"""
197+
198+
value = self._get_metric_value()
199+
if value is None:
200+
message = 'No value available for metric {}'.format(self['metric'])
201+
if self['dimension'] is not None:
202+
message += ' and dimension {}'.format(self['dimension'])
203+
self.nagios_exit(Plugins.UNKNOWN, message)
204+
205+
status = Plugins.check_threshold(value, warning=self['warning'], critical=self['critical'])
206+
207+
unit = self._AZURE_METRICS_UNIT_SYMBOLS.get(self._metric_properties['unit'])
208+
self.add_perfdata(self._metric_properties['name']['value'], value, uom=unit,
209+
warn=self['warning'], crit=self['critical'])
210+
211+
self.nagios_exit(status,
212+
'{} {} {}'.format(self._metric_properties['name']['localizedValue'],
213+
value,
214+
self._metric_properties['unit'].lower()))
215+
216+
217+
if __name__ == '__main__':
218+
PLUGIN = NagiosAzureResourceMonitor()
219+
PLUGIN.activate()
220+
PLUGIN.check_metric()

0 commit comments

Comments
 (0)