check-netscaler 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ """
2
+ check_netscaler - Nagios/Icinga monitoring plugin for Citrix NetScaler ADC
3
+
4
+ A monitoring plugin for Citrix NetScaler Application Delivery Controllers
5
+ using the NITRO REST API.
6
+ """
7
+
8
+ __version__ = "2.0.0"
9
+ __author__ = "slauger"
10
+ __license__ = "MIT"
@@ -0,0 +1,10 @@
1
+ """
2
+ CLI entry point for check_netscaler
3
+ """
4
+
5
+ import sys
6
+
7
+ from check_netscaler.cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
check_netscaler/cli.py ADDED
@@ -0,0 +1,314 @@
1
+ """
2
+ Command-line interface for check_netscaler
3
+ """
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from typing import List, Optional
9
+
10
+ from check_netscaler import __version__
11
+ from check_netscaler.constants import (
12
+ DEFAULT_API_VERSION,
13
+ DEFAULT_PASSWORD,
14
+ DEFAULT_TIMEOUT,
15
+ DEFAULT_USERNAME,
16
+ STATE_UNKNOWN,
17
+ )
18
+
19
+
20
+ def create_parser() -> argparse.ArgumentParser:
21
+ """Create and configure argument parser"""
22
+ parser = argparse.ArgumentParser(
23
+ prog="check_netscaler",
24
+ description="Nagios/Icinga monitoring plugin for Citrix NetScaler ADC",
25
+ formatter_class=argparse.RawDescriptionHelpFormatter,
26
+ )
27
+
28
+ # Version
29
+ parser.add_argument(
30
+ "-V",
31
+ "--version",
32
+ action="version",
33
+ version=f"%(prog)s {__version__}",
34
+ )
35
+
36
+ # Connection arguments
37
+ parser.add_argument(
38
+ "-H",
39
+ "--hostname",
40
+ default=os.getenv("NETSCALER_HOST"),
41
+ help="Hostname or IP address of the NetScaler appliance (env: NETSCALER_HOST)",
42
+ )
43
+
44
+ parser.add_argument(
45
+ "-u",
46
+ "--username",
47
+ default=os.getenv("NETSCALER_USER", DEFAULT_USERNAME),
48
+ help=f"Username for authentication (env: NETSCALER_USER, default: {DEFAULT_USERNAME})",
49
+ )
50
+
51
+ parser.add_argument(
52
+ "-p",
53
+ "--password",
54
+ default=os.getenv("NETSCALER_PASS", DEFAULT_PASSWORD),
55
+ help=f"Password for authentication (env: NETSCALER_PASS, default: {DEFAULT_PASSWORD})",
56
+ )
57
+
58
+ parser.add_argument(
59
+ "--no-ssl",
60
+ dest="ssl",
61
+ action="store_false",
62
+ default=True,
63
+ help="Use HTTP instead of HTTPS (default: HTTPS)",
64
+ )
65
+
66
+ parser.add_argument(
67
+ "-P",
68
+ "--port",
69
+ type=int,
70
+ help="TCP port to connect to (default: 80 for HTTP, 443 for HTTPS)",
71
+ )
72
+
73
+ parser.add_argument(
74
+ "-a",
75
+ "--api",
76
+ default=DEFAULT_API_VERSION,
77
+ help=f"NITRO API version (default: {DEFAULT_API_VERSION})",
78
+ )
79
+
80
+ parser.add_argument(
81
+ "-t",
82
+ "--timeout",
83
+ type=int,
84
+ default=DEFAULT_TIMEOUT,
85
+ help=f"Connection timeout in seconds (default: {DEFAULT_TIMEOUT})",
86
+ )
87
+
88
+ # Check command arguments
89
+ parser.add_argument(
90
+ "-C",
91
+ "--command",
92
+ required=True,
93
+ choices=[
94
+ "state",
95
+ "sslcert",
96
+ "above",
97
+ "below",
98
+ "matches",
99
+ "matches_not",
100
+ "nsconfig",
101
+ "hastatus",
102
+ "servicegroup",
103
+ "hwinfo",
104
+ "interfaces",
105
+ "perfdata",
106
+ "license",
107
+ "staserver",
108
+ "ntp",
109
+ "debug",
110
+ ],
111
+ help="Check command to execute",
112
+ )
113
+
114
+ parser.add_argument(
115
+ "-o",
116
+ "--objecttype",
117
+ help="Object type to check (e.g., lbvserver, service, servicegroup)",
118
+ )
119
+
120
+ parser.add_argument(
121
+ "-n",
122
+ "--objectname",
123
+ help="Filter by specific object name",
124
+ )
125
+
126
+ parser.add_argument(
127
+ "-e",
128
+ "--endpoint",
129
+ choices=["stat", "config"],
130
+ help="API endpoint type (stat or config)",
131
+ )
132
+
133
+ # Threshold arguments
134
+ parser.add_argument(
135
+ "-w",
136
+ "--warning",
137
+ help="Warning threshold",
138
+ )
139
+
140
+ parser.add_argument(
141
+ "-c",
142
+ "--critical",
143
+ help="Critical threshold",
144
+ )
145
+
146
+ # Additional options
147
+ parser.add_argument(
148
+ "-f",
149
+ "--filter",
150
+ help="Filter objects using regular expression",
151
+ )
152
+
153
+ parser.add_argument(
154
+ "-l",
155
+ "--limit",
156
+ help="Limit check to objects matching pattern (regex)",
157
+ )
158
+
159
+ parser.add_argument(
160
+ "-L",
161
+ "--label",
162
+ help="Field name to use as label in output",
163
+ )
164
+
165
+ parser.add_argument(
166
+ "--separator",
167
+ default=".",
168
+ help="Separator for performance data labels (default: .)",
169
+ )
170
+
171
+ parser.add_argument(
172
+ "-x",
173
+ "--urlopts",
174
+ help="Additional URL options for API requests",
175
+ )
176
+
177
+ parser.add_argument(
178
+ "-v",
179
+ "--verbose",
180
+ action="count",
181
+ default=0,
182
+ help="Increase output verbosity (can be repeated)",
183
+ )
184
+
185
+ return parser
186
+
187
+
188
+ def main(args: Optional[List[str]] = None) -> int:
189
+ """Main entry point for CLI"""
190
+ parser = create_parser()
191
+ parsed_args = parser.parse_args(args)
192
+
193
+ # Validate required arguments
194
+ if not parsed_args.hostname:
195
+ parser.error("argument -H/--hostname is required (or set NETSCALER_HOST)")
196
+
197
+ try:
198
+ # Import here to avoid circular dependencies
199
+ from check_netscaler.client import NITROClient
200
+ from check_netscaler.commands.state import StateCommand
201
+ from check_netscaler.output.nagios import NagiosOutput
202
+
203
+ # Create NITRO client
204
+ client = NITROClient(
205
+ hostname=parsed_args.hostname,
206
+ username=parsed_args.username,
207
+ password=parsed_args.password,
208
+ ssl=parsed_args.ssl,
209
+ port=parsed_args.port,
210
+ timeout=parsed_args.timeout,
211
+ verify_ssl=not parsed_args.ssl, # TODO: Add --insecure flag
212
+ api_version=parsed_args.api,
213
+ )
214
+
215
+ # Execute command
216
+ with client:
217
+ if parsed_args.command == "state":
218
+ command = StateCommand(client, parsed_args)
219
+ result = command.execute()
220
+ elif parsed_args.command in ["above", "below"]:
221
+ from check_netscaler.commands.threshold import ThresholdCommand
222
+
223
+ command = ThresholdCommand(client, parsed_args)
224
+ result = command.execute()
225
+ elif parsed_args.command == "nsconfig":
226
+ from check_netscaler.commands.nsconfig import NSConfigCommand
227
+
228
+ command = NSConfigCommand(client, parsed_args)
229
+ result = command.execute()
230
+ elif parsed_args.command == "hwinfo":
231
+ from check_netscaler.commands.hwinfo import HWInfoCommand
232
+
233
+ command = HWInfoCommand(client, parsed_args)
234
+ result = command.execute()
235
+ elif parsed_args.command == "sslcert":
236
+ from check_netscaler.commands.sslcert import SSLCertCommand
237
+
238
+ command = SSLCertCommand(client, parsed_args)
239
+ result = command.execute()
240
+ elif parsed_args.command == "servicegroup":
241
+ from check_netscaler.commands.servicegroup import ServiceGroupCommand
242
+
243
+ command = ServiceGroupCommand(client, parsed_args)
244
+ result = command.execute()
245
+ elif parsed_args.command in ["matches", "matches_not"]:
246
+ from check_netscaler.commands.matches import MatchesCommand
247
+
248
+ command = MatchesCommand(client, parsed_args)
249
+ result = command.execute()
250
+ elif parsed_args.command == "staserver":
251
+ from check_netscaler.commands.staserver import STAServerCommand
252
+
253
+ command = STAServerCommand(client, parsed_args)
254
+ result = command.execute()
255
+ elif parsed_args.command == "hastatus":
256
+ from check_netscaler.commands.hastatus import HAStatusCommand
257
+
258
+ command = HAStatusCommand(client, parsed_args)
259
+ result = command.execute()
260
+ elif parsed_args.command == "license":
261
+ from check_netscaler.commands.license import LicenseCommand
262
+
263
+ command = LicenseCommand(client, parsed_args)
264
+ result = command.execute()
265
+ elif parsed_args.command == "interfaces":
266
+ from check_netscaler.commands.interfaces import InterfacesCommand
267
+
268
+ command = InterfacesCommand(client, parsed_args)
269
+ result = command.execute()
270
+ elif parsed_args.command == "perfdata":
271
+ from check_netscaler.commands.perfdata import PerfdataCommand
272
+
273
+ command = PerfdataCommand(client, parsed_args)
274
+ result = command.execute()
275
+ elif parsed_args.command == "ntp":
276
+ from check_netscaler.commands.ntp import NTPCommand
277
+
278
+ command = NTPCommand(client, parsed_args)
279
+ result = command.execute()
280
+ elif parsed_args.command == "debug":
281
+ from check_netscaler.commands.debug import DebugCommand
282
+
283
+ command = DebugCommand(client, parsed_args)
284
+ result = command.execute()
285
+ else:
286
+ print(f"UNKNOWN - Command '{parsed_args.command}' not yet implemented")
287
+ return STATE_UNKNOWN
288
+
289
+ # Format and print output
290
+ output = NagiosOutput.format_output(
291
+ status=result.status,
292
+ message=result.message,
293
+ perfdata=result.perfdata,
294
+ long_output=result.long_output,
295
+ separator=parsed_args.separator,
296
+ )
297
+ print(output)
298
+
299
+ return result.status
300
+
301
+ except KeyboardInterrupt:
302
+ print("UNKNOWN - Interrupted by user")
303
+ return STATE_UNKNOWN
304
+ except Exception as e:
305
+ print(f"UNKNOWN - Unexpected error: {e}")
306
+ if parsed_args.verbose > 0:
307
+ import traceback
308
+
309
+ traceback.print_exc()
310
+ return STATE_UNKNOWN
311
+
312
+
313
+ if __name__ == "__main__":
314
+ sys.exit(main())
@@ -0,0 +1,25 @@
1
+ """NITRO API client for NetScaler ADC"""
2
+
3
+ from check_netscaler.client.exceptions import (
4
+ NITROAPIError,
5
+ NITROAuthenticationError,
6
+ NITROConnectionError,
7
+ NITROException,
8
+ NITROPermissionError,
9
+ NITROResourceNotFoundError,
10
+ NITROTimeoutError,
11
+ )
12
+ from check_netscaler.client.nitro import NITROClient
13
+ from check_netscaler.client.session import NITROSession
14
+
15
+ __all__ = [
16
+ "NITROClient",
17
+ "NITROSession",
18
+ "NITROException",
19
+ "NITROAuthenticationError",
20
+ "NITROConnectionError",
21
+ "NITROTimeoutError",
22
+ "NITROAPIError",
23
+ "NITROResourceNotFoundError",
24
+ "NITROPermissionError",
25
+ ]
@@ -0,0 +1,48 @@
1
+ """
2
+ Exceptions for NITRO API client
3
+ """
4
+
5
+
6
+ class NITROException(Exception):
7
+ """Base exception for all NITRO client errors"""
8
+
9
+ pass
10
+
11
+
12
+ class NITROAuthenticationError(NITROException):
13
+ """Authentication failed (login error)"""
14
+
15
+ pass
16
+
17
+
18
+ class NITROConnectionError(NITROException):
19
+ """Connection to NetScaler failed"""
20
+
21
+ pass
22
+
23
+
24
+ class NITROTimeoutError(NITROException):
25
+ """Request timed out"""
26
+
27
+ pass
28
+
29
+
30
+ class NITROAPIError(NITROException):
31
+ """API returned an error response"""
32
+
33
+ def __init__(self, message: str, error_code: int = None, response: dict = None):
34
+ super().__init__(message)
35
+ self.error_code = error_code
36
+ self.response = response
37
+
38
+
39
+ class NITROResourceNotFoundError(NITROAPIError):
40
+ """Requested resource not found (404)"""
41
+
42
+ pass
43
+
44
+
45
+ class NITROPermissionError(NITROAPIError):
46
+ """Insufficient permissions (403)"""
47
+
48
+ pass
@@ -0,0 +1,195 @@
1
+ """
2
+ NITRO API client for NetScaler
3
+ """
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+ import requests
8
+
9
+ from check_netscaler.client.exceptions import (
10
+ NITROAPIError,
11
+ NITROConnectionError,
12
+ NITROPermissionError,
13
+ NITROResourceNotFoundError,
14
+ NITROTimeoutError,
15
+ )
16
+ from check_netscaler.client.session import NITROSession
17
+
18
+
19
+ class NITROClient:
20
+ """Client for NetScaler NITRO REST API"""
21
+
22
+ def __init__(
23
+ self,
24
+ hostname: str,
25
+ username: str,
26
+ password: str,
27
+ ssl: bool = True,
28
+ port: Optional[int] = None,
29
+ timeout: int = 15,
30
+ verify_ssl: bool = True,
31
+ api_version: str = "v1",
32
+ ):
33
+ """
34
+ Initialize NITRO API client
35
+
36
+ Args:
37
+ hostname: NetScaler hostname or IP address
38
+ username: Username for authentication
39
+ password: Password for authentication
40
+ ssl: Use HTTPS (default: True)
41
+ port: Custom port (default: 80 for HTTP, 443 for HTTPS)
42
+ timeout: Request timeout in seconds
43
+ verify_ssl: Verify SSL certificates (default: True)
44
+ api_version: API version (default: v1)
45
+ """
46
+ self.session = NITROSession(
47
+ hostname=hostname,
48
+ username=username,
49
+ password=password,
50
+ ssl=ssl,
51
+ port=port,
52
+ timeout=timeout,
53
+ verify_ssl=verify_ssl,
54
+ )
55
+ self.api_version = api_version
56
+
57
+ def login(self) -> None:
58
+ """Authenticate with NetScaler"""
59
+ self.session.login()
60
+
61
+ def logout(self) -> None:
62
+ """Logout from NetScaler"""
63
+ self.session.logout()
64
+
65
+ def get(
66
+ self,
67
+ resource_type: str,
68
+ resource_name: Optional[str] = None,
69
+ endpoint: str = "stat",
70
+ url_options: Optional[str] = None,
71
+ ) -> Dict[str, Any]:
72
+ """
73
+ Perform GET request to NITRO API
74
+
75
+ Args:
76
+ resource_type: Type of resource (e.g., 'lbvserver', 'service')
77
+ resource_name: Specific resource name (optional)
78
+ endpoint: API endpoint type ('stat' or 'config')
79
+ url_options: Additional URL options (e.g., 'args=detail:true')
80
+
81
+ Returns:
82
+ API response as dictionary
83
+
84
+ Raises:
85
+ NITROAPIError: If API returns an error
86
+ NITROResourceNotFoundError: If resource not found (404)
87
+ NITROPermissionError: If insufficient permissions (403)
88
+ NITROConnectionError: If connection fails
89
+ NITROTimeoutError: If request times out
90
+ """
91
+ if not self.session.is_logged_in:
92
+ raise NITROAPIError("Not logged in. Call login() first.")
93
+
94
+ # Build URL
95
+ url_parts = [self.session.base_url, endpoint, resource_type]
96
+ if resource_name:
97
+ url_parts.append(resource_name)
98
+
99
+ url = "/".join(url_parts)
100
+
101
+ if url_options:
102
+ url = f"{url}?{url_options}"
103
+
104
+ try:
105
+ response = self.session.session.get(
106
+ url,
107
+ timeout=self.session.timeout,
108
+ verify=self.session.verify_ssl,
109
+ )
110
+
111
+ # Handle HTTP errors
112
+ if response.status_code == 404:
113
+ raise NITROResourceNotFoundError(
114
+ f"Resource not found: {resource_type}"
115
+ + (f"/{resource_name}" if resource_name else "")
116
+ )
117
+
118
+ if response.status_code == 403:
119
+ raise NITROPermissionError(f"Insufficient permissions to access {resource_type}")
120
+
121
+ if response.status_code >= 400:
122
+ raise NITROAPIError(
123
+ f"API error {response.status_code}: {response.text}",
124
+ error_code=response.status_code,
125
+ )
126
+
127
+ # Parse JSON response
128
+ data = response.json()
129
+
130
+ # Check for NITRO error in response
131
+ if "errorcode" in data and data["errorcode"] != 0:
132
+ error_msg = data.get("message", "Unknown error")
133
+ error_code = data.get("errorcode")
134
+ raise NITROAPIError(
135
+ f"NITRO API error {error_code}: {error_msg}",
136
+ error_code=error_code,
137
+ response=data,
138
+ )
139
+
140
+ return data
141
+
142
+ except requests.exceptions.Timeout as e:
143
+ raise NITROTimeoutError(f"Request timed out: {e}") from e
144
+ except requests.exceptions.ConnectionError as e:
145
+ raise NITROConnectionError(f"Connection failed: {e}") from e
146
+ except requests.exceptions.RequestException as e:
147
+ raise NITROConnectionError(f"Request failed: {e}") from e
148
+
149
+ def get_stat(
150
+ self,
151
+ resource_type: str,
152
+ resource_name: Optional[str] = None,
153
+ url_options: Optional[str] = None,
154
+ ) -> Dict[str, Any]:
155
+ """
156
+ Get statistics for a resource
157
+
158
+ Args:
159
+ resource_type: Type of resource
160
+ resource_name: Specific resource name (optional)
161
+ url_options: Additional URL options
162
+
163
+ Returns:
164
+ Statistics data
165
+ """
166
+ return self.get(resource_type, resource_name, endpoint="stat", url_options=url_options)
167
+
168
+ def get_config(
169
+ self,
170
+ resource_type: str,
171
+ resource_name: Optional[str] = None,
172
+ url_options: Optional[str] = None,
173
+ ) -> Dict[str, Any]:
174
+ """
175
+ Get configuration for a resource
176
+
177
+ Args:
178
+ resource_type: Type of resource
179
+ resource_name: Specific resource name (optional)
180
+ url_options: Additional URL options
181
+
182
+ Returns:
183
+ Configuration data
184
+ """
185
+ return self.get(resource_type, resource_name, endpoint="config", url_options=url_options)
186
+
187
+ def __enter__(self):
188
+ """Context manager entry"""
189
+ self.login()
190
+ return self
191
+
192
+ def __exit__(self, exc_type, exc_val, exc_tb):
193
+ """Context manager exit"""
194
+ self.logout()
195
+ return False