Initial checkin of pip wheel
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,3 +4,5 @@ lib
|
|||||||
lib64
|
lib64
|
||||||
pyvenv.cfg
|
pyvenv.cfg
|
||||||
__pycache__
|
__pycache__
|
||||||
|
dist/
|
||||||
|
kumacli.egg-info
|
||||||
|
194
LICENSE
Normal file
194
LICENSE
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of control, an entity
|
||||||
|
is "controlled by" another entity if it is controlled, directly or
|
||||||
|
indirectly, through the ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(which shall not be construed as modifying the License).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based upon (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and derivative works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control
|
||||||
|
systems, and issue tracking systems that are managed by, or on behalf
|
||||||
|
of, the Licensor for the purpose of discussing and improving the Work,
|
||||||
|
but excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to use, reproduce, modify, distribute, prepare
|
||||||
|
Derivative Works of, publicly display, publicly perform, sublicense,
|
||||||
|
and distribute the Work and such Derivative Works in Source or Object
|
||||||
|
form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, trademark, patent,
|
||||||
|
attribution and other notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright notice(s) and license terms and
|
||||||
|
conditions for use, reproduction, or distribution of Your modifications,
|
||||||
|
or for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
7. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
8. Accepting Warranty or Additional Liability. When redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same page as the copyright notice for easier identification within
|
||||||
|
third-party archives.
|
||||||
|
|
||||||
|
Copyright 2025 KumaCLI
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
5
MANIFEST.in
Normal file
5
MANIFEST.in
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
include README.md
|
||||||
|
include LICENSE
|
||||||
|
recursive-include src *.py
|
||||||
|
global-exclude __pycache__
|
||||||
|
global-exclude *.py[co]
|
46
Makefile
Normal file
46
Makefile
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
.PHONY: clean build install test help
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
help:
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " clean - Remove build artifacts and cache files"
|
||||||
|
@echo " build - Build the wheel package"
|
||||||
|
@echo " install - Install the package in development mode"
|
||||||
|
@echo " test - Run tests (if available)"
|
||||||
|
@echo " help - Show this help message"
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
@echo "Cleaning build artifacts..."
|
||||||
|
rm -rf build/
|
||||||
|
rm -rf dist/
|
||||||
|
rm -rf src/kumacli.egg-info/
|
||||||
|
rm -rf src/kumacli/__pycache__/
|
||||||
|
rm -rf src/kumacli/cmd/__pycache__/
|
||||||
|
find . -name "*.pyc" -delete
|
||||||
|
find . -name "*.pyo" -delete
|
||||||
|
find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
@echo "Clean complete."
|
||||||
|
|
||||||
|
# Build the wheel package
|
||||||
|
build: clean
|
||||||
|
@echo "Building wheel package..."
|
||||||
|
python -m build
|
||||||
|
@echo "Build complete. Artifacts in dist/"
|
||||||
|
|
||||||
|
# Install package in development mode
|
||||||
|
install:
|
||||||
|
@echo "Installing package in development mode..."
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# Test the package (placeholder for when tests are added)
|
||||||
|
test:
|
||||||
|
@echo "Running tests..."
|
||||||
|
@echo "No tests configured yet."
|
||||||
|
|
||||||
|
# Rebuild and reinstall (useful during development)
|
||||||
|
dev: clean build
|
||||||
|
@echo "Installing newly built package..."
|
||||||
|
pip uninstall kumacli -y 2>/dev/null || true
|
||||||
|
pip install dist/kumacli-*.whl
|
||||||
|
@echo "Development installation complete."
|
79
README.md
Normal file
79
README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# KumaCLI
|
||||||
|
|
||||||
|
A command-line interface for Uptime Kuma, allowing you to manage monitors and maintenance windows from the terminal.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install kumacli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Set environment variables for authentication:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export KUMA_URL="http://your-uptime-kuma-instance:3001"
|
||||||
|
export KUMA_USERNAME="your-username"
|
||||||
|
export KUMA_PASSWORD="your-password"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or pass them as command-line arguments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kumacli --url http://localhost:3001 --username admin --password password monitor list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Monitor Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all monitors
|
||||||
|
kumacli monitor list
|
||||||
|
|
||||||
|
# List monitors matching patterns
|
||||||
|
kumacli monitor list --monitor "*web*"
|
||||||
|
|
||||||
|
# List monitors in specific groups
|
||||||
|
kumacli monitor list --group "production*"
|
||||||
|
|
||||||
|
# Combine filters
|
||||||
|
kumacli monitor list --monitor "*api*" --group "web*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Maintenance Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create maintenance for specific monitors (90 minutes, starting now)
|
||||||
|
kumacli maintenance add --monitor "*nextcloud*"
|
||||||
|
|
||||||
|
# Create maintenance for groups
|
||||||
|
kumacli maintenance add --group "web*"
|
||||||
|
|
||||||
|
# Custom timing and description
|
||||||
|
kumacli maintenance add --monitor "*db*" --duration "2h" --start "2025-08-01 22:00:00" --title "Database Update"
|
||||||
|
|
||||||
|
# List all maintenances
|
||||||
|
kumacli maintenance list
|
||||||
|
|
||||||
|
# Delete specific maintenance
|
||||||
|
kumacli maintenance delete --id 5
|
||||||
|
|
||||||
|
# Delete all maintenances
|
||||||
|
kumacli maintenance delete --all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Wildcard Pattern Matching**: Use `*` wildcards for monitor and group names
|
||||||
|
- **Time Scheduling**: Flexible time parsing and duration formats (90m, 2h, 3600s)
|
||||||
|
- **Group Support**: Target entire groups of monitors
|
||||||
|
- **Environment Variables**: Store credentials securely
|
||||||
|
- **Auto-generated Descriptions**: Default descriptions list affected monitors
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Access to an Uptime Kuma instance
|
||||||
|
- uptime-kuma-api library
|
564
kuma_client.py
564
kuma_client.py
@@ -1,564 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import fnmatch
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from uptime_kuma_api import UptimeKumaApi
|
|
||||||
|
|
||||||
|
|
||||||
class KumaClient:
|
|
||||||
def __init__(self, url, username=None, password=None):
|
|
||||||
self.url = url
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
self.api = None
|
|
||||||
|
|
||||||
def parse_duration(self, duration_str):
|
|
||||||
"""Parse duration string like '90m', '1h', '3600s' into seconds"""
|
|
||||||
if not duration_str:
|
|
||||||
return 90 * 60 # Default 90 minutes in seconds
|
|
||||||
|
|
||||||
match = re.match(r"^(\d+)([smh])$", duration_str.lower())
|
|
||||||
if not match:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid duration format: {duration_str}. Use format like '90m', '1h', '3600s'"
|
|
||||||
)
|
|
||||||
|
|
||||||
value, unit = match.groups()
|
|
||||||
value = int(value)
|
|
||||||
|
|
||||||
if unit == "s":
|
|
||||||
return value
|
|
||||||
elif unit == "m":
|
|
||||||
return value * 60
|
|
||||||
elif unit == "h":
|
|
||||||
return value * 3600
|
|
||||||
|
|
||||||
raise ValueError(f"Invalid duration unit: {unit}")
|
|
||||||
|
|
||||||
def parse_start_time(self, start_str):
|
|
||||||
"""Parse start time string or use current time if None"""
|
|
||||||
if not start_str:
|
|
||||||
return datetime.utcnow()
|
|
||||||
|
|
||||||
# Try to parse ISO format first
|
|
||||||
try:
|
|
||||||
return datetime.fromisoformat(start_str.replace("Z", "+00:00")).replace(
|
|
||||||
tzinfo=None
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Try common formats
|
|
||||||
formats = [
|
|
||||||
"%Y-%m-%d %H:%M:%S",
|
|
||||||
"%Y-%m-%d %H:%M",
|
|
||||||
"%Y-%m-%d",
|
|
||||||
]
|
|
||||||
|
|
||||||
for fmt in formats:
|
|
||||||
try:
|
|
||||||
return datetime.strptime(start_str, fmt)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid start time format: {start_str}. Use ISO format or YYYY-MM-DD HH:MM:SS"
|
|
||||||
)
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
"""Connect and login to Uptime Kuma"""
|
|
||||||
try:
|
|
||||||
self.api = UptimeKumaApi(self.url)
|
|
||||||
if self.username and self.password:
|
|
||||||
result = self.api.login(self.username, self.password)
|
|
||||||
print(f"Connected to {self.url}")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to connect: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
"""Disconnect from Uptime Kuma"""
|
|
||||||
if self.api:
|
|
||||||
self.api.disconnect()
|
|
||||||
|
|
||||||
def list_monitors(self, monitor_patterns=None, group_patterns=None):
|
|
||||||
"""List all monitors or filter by patterns and/or groups"""
|
|
||||||
try:
|
|
||||||
all_monitors = self.api.get_monitors()
|
|
||||||
if not all_monitors:
|
|
||||||
print("No monitors found")
|
|
||||||
return
|
|
||||||
|
|
||||||
monitors = all_monitors[:] # Start with all monitors
|
|
||||||
|
|
||||||
# Filter by group if specified
|
|
||||||
if group_patterns:
|
|
||||||
group_monitors = self.get_monitors_in_groups(group_patterns)
|
|
||||||
if not group_monitors:
|
|
||||||
print("No monitors found in the specified groups")
|
|
||||||
return
|
|
||||||
monitor_ids = {m["id"] for m in group_monitors}
|
|
||||||
monitors = [m for m in monitors if m.get("id") in monitor_ids]
|
|
||||||
|
|
||||||
# Filter monitors by name patterns if specified
|
|
||||||
if monitor_patterns:
|
|
||||||
matched_monitors = self.find_monitors_by_pattern(monitor_patterns)
|
|
||||||
if not matched_monitors:
|
|
||||||
print("No monitors found matching the specified patterns")
|
|
||||||
return
|
|
||||||
pattern_monitor_ids = {m["id"] for m in matched_monitors}
|
|
||||||
monitors = [m for m in monitors if m.get("id") in pattern_monitor_ids]
|
|
||||||
|
|
||||||
# Display results with group information
|
|
||||||
print(
|
|
||||||
f"{'ID':<5} {'Name':<25} {'Type':<12} {'Group':<20} {'URL':<35} {'Status':<10}"
|
|
||||||
)
|
|
||||||
print("-" * 112)
|
|
||||||
|
|
||||||
for monitor in monitors:
|
|
||||||
monitor_id = monitor.get("id", "N/A")
|
|
||||||
name = monitor.get("name", "N/A")
|
|
||||||
monitor_type = monitor.get("type", "N/A")
|
|
||||||
url = monitor.get("url", "N/A")
|
|
||||||
active = "Active" if monitor.get("active") else "Inactive"
|
|
||||||
|
|
||||||
# Find parent group name
|
|
||||||
parent_id = monitor.get("parent")
|
|
||||||
parent_name = "None"
|
|
||||||
if parent_id:
|
|
||||||
parent_monitor = next(
|
|
||||||
(m for m in all_monitors if m.get("id") == parent_id), None
|
|
||||||
)
|
|
||||||
if parent_monitor:
|
|
||||||
parent_name = parent_monitor.get("name", f"Group {parent_id}")
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"{monitor_id:<5} {name:<25} {monitor_type:<12} {parent_name:<20} {url:<35} {active:<10}"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error listing monitors: {e}")
|
|
||||||
|
|
||||||
def list_maintenances(self):
|
|
||||||
"""List all maintenances"""
|
|
||||||
try:
|
|
||||||
maintenances = self.api.get_maintenances()
|
|
||||||
if not maintenances:
|
|
||||||
print("No maintenances found")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"{'ID':<5} {'Title':<30} {'Strategy':<15} {'Active':<10} {'Description':<50}"
|
|
||||||
)
|
|
||||||
print("-" * 115)
|
|
||||||
|
|
||||||
for maintenance in maintenances:
|
|
||||||
maintenance_id = maintenance.get("id", "N/A")
|
|
||||||
title = maintenance.get("title", "N/A")
|
|
||||||
strategy = maintenance.get("strategy", "N/A")
|
|
||||||
active = "Active" if maintenance.get("active") else "Inactive"
|
|
||||||
description = maintenance.get("description", "N/A")
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"{maintenance_id:<5} {title:<30} {strategy:<15} {active:<10} {description:<50}"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error listing maintenances: {e}")
|
|
||||||
|
|
||||||
def find_monitors_by_pattern(self, patterns):
|
|
||||||
"""Find monitor IDs by name patterns (case insensitive, supports wildcards)"""
|
|
||||||
try:
|
|
||||||
monitors = self.api.get_monitors()
|
|
||||||
matched_monitors = []
|
|
||||||
|
|
||||||
for pattern in patterns:
|
|
||||||
pattern_lower = pattern.lower()
|
|
||||||
for monitor in monitors:
|
|
||||||
monitor_name = monitor.get("name", "").lower()
|
|
||||||
if fnmatch.fnmatch(monitor_name, pattern_lower):
|
|
||||||
matched_monitors.append(
|
|
||||||
{"id": monitor.get("id"), "name": monitor.get("name")}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remove duplicates while preserving order
|
|
||||||
seen = set()
|
|
||||||
unique_monitors = []
|
|
||||||
for monitor in matched_monitors:
|
|
||||||
if monitor["id"] not in seen:
|
|
||||||
seen.add(monitor["id"])
|
|
||||||
unique_monitors.append(monitor)
|
|
||||||
|
|
||||||
return unique_monitors
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error finding monitors: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def find_groups_by_pattern(self, patterns):
|
|
||||||
"""Find group IDs by name patterns (case insensitive, supports wildcards)"""
|
|
||||||
try:
|
|
||||||
monitors = self.api.get_monitors()
|
|
||||||
groups = [m for m in monitors if m.get("type") == "group"]
|
|
||||||
matched_groups = []
|
|
||||||
|
|
||||||
for pattern in patterns:
|
|
||||||
pattern_lower = pattern.lower()
|
|
||||||
for group in groups:
|
|
||||||
group_name = group.get("name", "").lower()
|
|
||||||
if fnmatch.fnmatch(group_name, pattern_lower):
|
|
||||||
matched_groups.append(
|
|
||||||
{"id": group.get("id"), "name": group.get("name")}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remove duplicates while preserving order
|
|
||||||
seen = set()
|
|
||||||
unique_groups = []
|
|
||||||
for group in matched_groups:
|
|
||||||
if group["id"] not in seen:
|
|
||||||
seen.add(group["id"])
|
|
||||||
unique_groups.append(group)
|
|
||||||
|
|
||||||
return unique_groups
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error finding groups: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_monitors_in_groups(self, group_patterns):
|
|
||||||
"""Get all monitors that belong to specified groups"""
|
|
||||||
try:
|
|
||||||
monitors = self.api.get_monitors()
|
|
||||||
matched_groups = self.find_groups_by_pattern(group_patterns)
|
|
||||||
|
|
||||||
if not matched_groups:
|
|
||||||
return []
|
|
||||||
|
|
||||||
print(f"Found {len(matched_groups)} matching groups:")
|
|
||||||
for group in matched_groups:
|
|
||||||
print(f" - {group['name']} (ID: {group['id']})")
|
|
||||||
|
|
||||||
group_ids = {g["id"] for g in matched_groups}
|
|
||||||
group_members = []
|
|
||||||
|
|
||||||
for monitor in monitors:
|
|
||||||
# Check if monitor's parent is in our group list
|
|
||||||
if monitor.get("parent") in group_ids:
|
|
||||||
group_members.append(monitor)
|
|
||||||
|
|
||||||
return group_members
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error getting group members: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def add_maintenance(
|
|
||||||
self,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
start_time=None,
|
|
||||||
duration="90m",
|
|
||||||
monitor_patterns=None,
|
|
||||||
group_patterns=None,
|
|
||||||
):
|
|
||||||
"""Add a new maintenance"""
|
|
||||||
try:
|
|
||||||
# Check if we have either monitor patterns or group patterns
|
|
||||||
if not monitor_patterns and not group_patterns:
|
|
||||||
print(
|
|
||||||
"Error: Either --monitor or --group flag is required. Specify at least one pattern."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
matched_monitors = []
|
|
||||||
|
|
||||||
# Find monitors by patterns if specified
|
|
||||||
if monitor_patterns:
|
|
||||||
pattern_monitors = self.find_monitors_by_pattern(monitor_patterns)
|
|
||||||
matched_monitors.extend(pattern_monitors)
|
|
||||||
|
|
||||||
# Find monitors by groups if specified
|
|
||||||
if group_patterns:
|
|
||||||
group_monitors = self.get_monitors_in_groups(group_patterns)
|
|
||||||
# Convert to the same format as find_monitors_by_pattern
|
|
||||||
group_monitor_objs = [
|
|
||||||
{"id": m.get("id"), "name": m.get("name")} for m in group_monitors
|
|
||||||
]
|
|
||||||
matched_monitors.extend(group_monitor_objs)
|
|
||||||
|
|
||||||
# Remove duplicates while preserving order
|
|
||||||
seen = set()
|
|
||||||
unique_monitors = []
|
|
||||||
for monitor in matched_monitors:
|
|
||||||
if monitor["id"] not in seen:
|
|
||||||
seen.add(monitor["id"])
|
|
||||||
unique_monitors.append(monitor)
|
|
||||||
|
|
||||||
matched_monitors = unique_monitors
|
|
||||||
|
|
||||||
if not matched_monitors:
|
|
||||||
print(
|
|
||||||
"Error: No monitors found matching the specified patterns or groups"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Found {len(matched_monitors)} matching monitors:")
|
|
||||||
for monitor in matched_monitors:
|
|
||||||
print(f" - {monitor['name']} (ID: {monitor['id']})")
|
|
||||||
|
|
||||||
# Set default description if not provided
|
|
||||||
if not description:
|
|
||||||
monitor_names = [monitor["name"] for monitor in matched_monitors]
|
|
||||||
description = "Maintenance on:\n" + "\n".join(monitor_names)
|
|
||||||
|
|
||||||
# Parse start time and duration
|
|
||||||
start_dt = self.parse_start_time(start_time)
|
|
||||||
duration_seconds = self.parse_duration(duration)
|
|
||||||
end_dt = start_dt + timedelta(seconds=duration_seconds)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Maintenance window: {start_dt.strftime('%Y-%m-%d %H:%M:%S UTC')} - {end_dt.strftime('%Y-%m-%d %H:%M:%S UTC')} ({duration})"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create the maintenance with single strategy and date range
|
|
||||||
maintenance_data = {
|
|
||||||
"title": title,
|
|
||||||
"description": description,
|
|
||||||
"strategy": "single",
|
|
||||||
"active": True,
|
|
||||||
"dateRange": [
|
|
||||||
start_dt.strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
end_dt.strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
result = self.api.add_maintenance(**maintenance_data)
|
|
||||||
maintenance_id = result.get("maintenanceID")
|
|
||||||
|
|
||||||
# Add monitors to maintenance
|
|
||||||
if maintenance_id:
|
|
||||||
print(f"Maintenance added successfully: {result}")
|
|
||||||
|
|
||||||
# Prepare monitors list in the correct format for the API
|
|
||||||
monitors_list = [{"id": monitor["id"]} for monitor in matched_monitors]
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.api.add_monitor_maintenance(
|
|
||||||
maintenance_id, monitors_list
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"Successfully added {len(matched_monitors)} monitors to maintenance"
|
|
||||||
)
|
|
||||||
print(f"API response: {result}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: Failed to add monitors to maintenance: {e}")
|
|
||||||
print(
|
|
||||||
"This might be due to API compatibility issues or server configuration"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error adding maintenance: {e}")
|
|
||||||
|
|
||||||
def delete_maintenance(self, maintenance_id=None, delete_all=False):
|
|
||||||
"""Delete a specific maintenance or all maintenances"""
|
|
||||||
try:
|
|
||||||
if delete_all:
|
|
||||||
# Get all maintenances first
|
|
||||||
maintenances = self.api.get_maintenances()
|
|
||||||
if not maintenances:
|
|
||||||
print("No maintenances found to delete")
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Found {len(maintenances)} maintenances to delete:")
|
|
||||||
for maintenance in maintenances:
|
|
||||||
print(
|
|
||||||
f" - {maintenance.get('title', 'N/A')} (ID: {maintenance.get('id', 'N/A')})"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete all maintenances
|
|
||||||
deleted_count = 0
|
|
||||||
for maintenance in maintenances:
|
|
||||||
try:
|
|
||||||
result = self.api.delete_maintenance(maintenance.get("id"))
|
|
||||||
print(
|
|
||||||
f"Deleted maintenance '{maintenance.get('title', 'N/A')}' (ID: {maintenance.get('id')})"
|
|
||||||
)
|
|
||||||
deleted_count += 1
|
|
||||||
except Exception as e:
|
|
||||||
print(
|
|
||||||
f"Failed to delete maintenance '{maintenance.get('title', 'N/A')}': {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Successfully deleted {deleted_count} out of {len(maintenances)} maintenances"
|
|
||||||
)
|
|
||||||
|
|
||||||
elif maintenance_id:
|
|
||||||
# Delete specific maintenance
|
|
||||||
try:
|
|
||||||
# First get the maintenance info for confirmation
|
|
||||||
maintenance = self.api.get_maintenance(maintenance_id)
|
|
||||||
maintenance_title = maintenance.get("title", "N/A")
|
|
||||||
|
|
||||||
result = self.api.delete_maintenance(maintenance_id)
|
|
||||||
print(
|
|
||||||
f"Successfully deleted maintenance '{maintenance_title}' (ID: {maintenance_id})"
|
|
||||||
)
|
|
||||||
print(f"API response: {result}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Failed to delete maintenance ID {maintenance_id}: {e}")
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
"Error: Either --id or --all flag is required for delete operation"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error during maintenance deletion: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description="Uptime Kuma CLI Client")
|
|
||||||
parser.add_argument(
|
|
||||||
"--url", help="Uptime Kuma server URL (can also use KUMA_URL env var)"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--username",
|
|
||||||
help="Username for authentication (can also use KUMA_USERNAME env var)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--password",
|
|
||||||
help="Password for authentication (can also use KUMA_PASSWORD env var)",
|
|
||||||
)
|
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="resource", help="Resource to operate on")
|
|
||||||
|
|
||||||
# Monitor commands
|
|
||||||
monitor_parser = subparsers.add_parser("monitor", help="Monitor operations")
|
|
||||||
monitor_subparsers = monitor_parser.add_subparsers(
|
|
||||||
dest="monitor_action", help="Monitor actions"
|
|
||||||
)
|
|
||||||
list_monitors_parser = monitor_subparsers.add_parser(
|
|
||||||
"list", help="List all monitors"
|
|
||||||
)
|
|
||||||
list_monitors_parser.add_argument(
|
|
||||||
"--monitor",
|
|
||||||
action="append",
|
|
||||||
help="Monitor name pattern to filter by (supports wildcards, can be repeated)",
|
|
||||||
)
|
|
||||||
list_monitors_parser.add_argument(
|
|
||||||
"--group",
|
|
||||||
action="append",
|
|
||||||
help="Group name pattern to filter by (supports wildcards, can be repeated)",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Maintenance commands
|
|
||||||
maintenance_parser = subparsers.add_parser(
|
|
||||||
"maintenance", help="Maintenance operations"
|
|
||||||
)
|
|
||||||
maintenance_subparsers = maintenance_parser.add_subparsers(
|
|
||||||
dest="maintenance_action", help="Maintenance actions"
|
|
||||||
)
|
|
||||||
maintenance_subparsers.add_parser("list", help="List all maintenances")
|
|
||||||
|
|
||||||
# Delete maintenance command
|
|
||||||
delete_maintenance_parser = maintenance_subparsers.add_parser(
|
|
||||||
"delete", help="Delete maintenance(s)"
|
|
||||||
)
|
|
||||||
delete_maintenance_parser.add_argument(
|
|
||||||
"--id", type=int, help="Maintenance ID to delete"
|
|
||||||
)
|
|
||||||
delete_maintenance_parser.add_argument(
|
|
||||||
"--all", action="store_true", help="Delete all maintenances"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add maintenance command
|
|
||||||
add_maintenance_parser = maintenance_subparsers.add_parser(
|
|
||||||
"add", help="Add a new maintenance"
|
|
||||||
)
|
|
||||||
add_maintenance_parser.add_argument(
|
|
||||||
"--title",
|
|
||||||
help='Maintenance title (defaults to "Update started on <current UTC time>")',
|
|
||||||
)
|
|
||||||
add_maintenance_parser.add_argument("--description", help="Maintenance description")
|
|
||||||
add_maintenance_parser.add_argument(
|
|
||||||
"--start",
|
|
||||||
help="Start time (defaults to now, format: YYYY-MM-DD HH:MM:SS or ISO format)",
|
|
||||||
)
|
|
||||||
add_maintenance_parser.add_argument(
|
|
||||||
"--duration",
|
|
||||||
default="90m",
|
|
||||||
help="Duration (default: 90m, format: 3600s, 1h, 60m)",
|
|
||||||
)
|
|
||||||
add_maintenance_parser.add_argument(
|
|
||||||
"--monitor",
|
|
||||||
action="append",
|
|
||||||
help="Monitor name pattern to add to maintenance (supports wildcards like *NextCloud*, can be repeated)",
|
|
||||||
)
|
|
||||||
add_maintenance_parser.add_argument(
|
|
||||||
"--group",
|
|
||||||
action="append",
|
|
||||||
help="Group name pattern to add all group members to maintenance (supports wildcards, can be repeated)",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if not args.resource:
|
|
||||||
parser.print_help()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Get configuration from environment variables or command line args
|
|
||||||
url = args.url or os.getenv("KUMA_URL")
|
|
||||||
username = args.username or os.getenv("KUMA_USERNAME")
|
|
||||||
password = args.password or os.getenv("KUMA_PASSWORD")
|
|
||||||
|
|
||||||
if not url:
|
|
||||||
print(
|
|
||||||
"Error: URL is required. Provide --url or set KUMA_URL environment variable."
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
client = KumaClient(url, username, password)
|
|
||||||
|
|
||||||
if not client.connect():
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if args.resource == "monitor":
|
|
||||||
if args.monitor_action == "list":
|
|
||||||
client.list_monitors(
|
|
||||||
monitor_patterns=args.monitor, group_patterns=args.group
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
monitor_parser.print_help()
|
|
||||||
elif args.resource == "maintenance":
|
|
||||||
if args.maintenance_action == "list":
|
|
||||||
client.list_maintenances()
|
|
||||||
elif args.maintenance_action == "add":
|
|
||||||
title = (
|
|
||||||
args.title
|
|
||||||
or f"Update started on {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}"
|
|
||||||
)
|
|
||||||
client.add_maintenance(
|
|
||||||
title,
|
|
||||||
args.description,
|
|
||||||
args.start,
|
|
||||||
args.duration,
|
|
||||||
monitor_patterns=args.monitor,
|
|
||||||
group_patterns=args.group,
|
|
||||||
)
|
|
||||||
elif args.maintenance_action == "delete":
|
|
||||||
client.delete_maintenance(maintenance_id=args.id, delete_all=args.all)
|
|
||||||
else:
|
|
||||||
maintenance_parser.print_help()
|
|
||||||
finally:
|
|
||||||
client.disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
47
setup.py
Normal file
47
setup.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
with open("README.md", "r", encoding="utf-8") as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="kumacli",
|
||||||
|
version="1.0.0",
|
||||||
|
author="Uptime Kuma CLI",
|
||||||
|
description="A command-line interface for Uptime Kuma",
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="https://github.com/yourusername/kumacli",
|
||||||
|
packages=find_packages(where="src"),
|
||||||
|
package_dir={"": "src"},
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: System Administrators",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Topic :: System :: Monitoring",
|
||||||
|
"Topic :: System :: Systems Administration",
|
||||||
|
],
|
||||||
|
python_requires=">=3.8",
|
||||||
|
install_requires=[
|
||||||
|
"uptime-kuma-api>=1.0.0",
|
||||||
|
],
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"kumacli=kumacli.__main__:main",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
keywords="uptime kuma monitoring cli",
|
||||||
|
project_urls={
|
||||||
|
"Bug Reports": "https://github.com/yourusername/kumacli/issues",
|
||||||
|
"Source": "https://github.com/yourusername/kumacli",
|
||||||
|
},
|
||||||
|
)
|
11
src/kumacli/__init__.py
Normal file
11
src/kumacli/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"""
|
||||||
|
KumaCLI - A command-line interface for Uptime Kuma
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__author__ = "KumaCLI Team"
|
||||||
|
__email__ = "info@kumacli.com"
|
||||||
|
|
||||||
|
from kumacli.__main__ import main
|
||||||
|
|
||||||
|
__all__ = ["main"]
|
73
src/kumacli/__main__.py
Executable file
73
src/kumacli/__main__.py
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from kumacli.client import KumaClient
|
||||||
|
from kumacli.cmd.monitor import setup_monitor_parser, handle_monitor_command
|
||||||
|
from kumacli.cmd.maintenance import setup_maintenance_parser, handle_maintenance_command
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Uptime Kuma CLI Client")
|
||||||
|
parser.add_argument(
|
||||||
|
"--url", help="Uptime Kuma server URL (can also use KUMA_URL env var)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--username",
|
||||||
|
help="Username for authentication (can also use KUMA_USERNAME env var)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--password",
|
||||||
|
help="Password for authentication (can also use KUMA_PASSWORD env var)",
|
||||||
|
)
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest="resource", help="Resource to operate on")
|
||||||
|
|
||||||
|
# Setup command parsers
|
||||||
|
monitor_parser = setup_monitor_parser(subparsers)
|
||||||
|
maintenance_parser = setup_maintenance_parser(subparsers)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.resource:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Get configuration from environment variables or command line args
|
||||||
|
url = args.url or os.getenv("KUMA_URL")
|
||||||
|
username = args.username or os.getenv("KUMA_USERNAME")
|
||||||
|
password = args.password or os.getenv("KUMA_PASSWORD")
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
print(
|
||||||
|
"Error: URL is required. Provide --url or set KUMA_URL environment variable."
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
client = KumaClient(url, username, password)
|
||||||
|
|
||||||
|
if not client.connect():
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = False
|
||||||
|
if args.resource == "monitor":
|
||||||
|
success = handle_monitor_command(args, client)
|
||||||
|
elif args.resource == "maintenance":
|
||||||
|
success = handle_maintenance_command(args, client)
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
170
src/kumacli/client.py
Normal file
170
src/kumacli/client.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from uptime_kuma_api import UptimeKumaApi
|
||||||
|
|
||||||
|
|
||||||
|
class KumaClient:
|
||||||
|
def __init__(self, url, username=None, password=None):
|
||||||
|
self.url = url
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.api = None
|
||||||
|
|
||||||
|
def parse_duration(self, duration_str):
|
||||||
|
"""Parse duration string like '90m', '1h', '3600s' into seconds"""
|
||||||
|
if not duration_str:
|
||||||
|
return 90 * 60 # Default 90 minutes in seconds
|
||||||
|
|
||||||
|
match = re.match(r"^(\d+)([smh])$", duration_str.lower())
|
||||||
|
if not match:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid duration format: {duration_str}. Use format like '90m', '1h', '3600s'"
|
||||||
|
)
|
||||||
|
|
||||||
|
value, unit = match.groups()
|
||||||
|
value = int(value)
|
||||||
|
|
||||||
|
if unit == "s":
|
||||||
|
return value
|
||||||
|
elif unit == "m":
|
||||||
|
return value * 60
|
||||||
|
elif unit == "h":
|
||||||
|
return value * 3600
|
||||||
|
|
||||||
|
raise ValueError(f"Invalid duration unit: {unit}")
|
||||||
|
|
||||||
|
def parse_start_time(self, start_str):
|
||||||
|
"""Parse start time string or use current time if None"""
|
||||||
|
if not start_str:
|
||||||
|
return datetime.utcnow()
|
||||||
|
|
||||||
|
# Try to parse ISO format first
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(start_str.replace("Z", "+00:00")).replace(
|
||||||
|
tzinfo=None
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Try common formats
|
||||||
|
formats = [
|
||||||
|
"%Y-%m-%d %H:%M:%S",
|
||||||
|
"%Y-%m-%d %H:%M",
|
||||||
|
"%Y-%m-%d",
|
||||||
|
]
|
||||||
|
|
||||||
|
for fmt in formats:
|
||||||
|
try:
|
||||||
|
return datetime.strptime(start_str, fmt)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid start time format: {start_str}. Use ISO format or YYYY-MM-DD HH:MM:SS"
|
||||||
|
)
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Connect and login to Uptime Kuma"""
|
||||||
|
try:
|
||||||
|
self.api = UptimeKumaApi(self.url)
|
||||||
|
if self.username and self.password:
|
||||||
|
result = self.api.login(self.username, self.password)
|
||||||
|
print(f"Connected to {self.url}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to connect: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Disconnect from Uptime Kuma"""
|
||||||
|
if self.api:
|
||||||
|
self.api.disconnect()
|
||||||
|
|
||||||
|
def find_monitors_by_pattern(self, patterns):
|
||||||
|
"""Find monitor IDs by name patterns (case insensitive, supports wildcards)"""
|
||||||
|
try:
|
||||||
|
monitors = self.api.get_monitors()
|
||||||
|
matched_monitors = []
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
pattern_lower = pattern.lower()
|
||||||
|
for monitor in monitors:
|
||||||
|
monitor_name = monitor.get("name", "").lower()
|
||||||
|
if fnmatch.fnmatch(monitor_name, pattern_lower):
|
||||||
|
matched_monitors.append(
|
||||||
|
{"id": monitor.get("id"), "name": monitor.get("name")}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
seen = set()
|
||||||
|
unique_monitors = []
|
||||||
|
for monitor in matched_monitors:
|
||||||
|
if monitor["id"] not in seen:
|
||||||
|
seen.add(monitor["id"])
|
||||||
|
unique_monitors.append(monitor)
|
||||||
|
|
||||||
|
return unique_monitors
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error finding monitors: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def find_groups_by_pattern(self, patterns):
|
||||||
|
"""Find group IDs by name patterns (case insensitive, supports wildcards)"""
|
||||||
|
try:
|
||||||
|
monitors = self.api.get_monitors()
|
||||||
|
groups = [m for m in monitors if m.get("type") == "group"]
|
||||||
|
matched_groups = []
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
pattern_lower = pattern.lower()
|
||||||
|
for group in groups:
|
||||||
|
group_name = group.get("name", "").lower()
|
||||||
|
if fnmatch.fnmatch(group_name, pattern_lower):
|
||||||
|
matched_groups.append(
|
||||||
|
{"id": group.get("id"), "name": group.get("name")}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
seen = set()
|
||||||
|
unique_groups = []
|
||||||
|
for group in matched_groups:
|
||||||
|
if group["id"] not in seen:
|
||||||
|
seen.add(group["id"])
|
||||||
|
unique_groups.append(group)
|
||||||
|
|
||||||
|
return unique_groups
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error finding groups: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_monitors_in_groups(self, group_patterns):
|
||||||
|
"""Get all monitors that belong to specified groups"""
|
||||||
|
try:
|
||||||
|
monitors = self.api.get_monitors()
|
||||||
|
matched_groups = self.find_groups_by_pattern(group_patterns)
|
||||||
|
|
||||||
|
if not matched_groups:
|
||||||
|
return []
|
||||||
|
|
||||||
|
print(f"Found {len(matched_groups)} matching groups:")
|
||||||
|
for group in matched_groups:
|
||||||
|
print(f" - {group['name']} (ID: {group['id']})")
|
||||||
|
|
||||||
|
group_ids = {g["id"] for g in matched_groups}
|
||||||
|
group_members = []
|
||||||
|
|
||||||
|
for monitor in monitors:
|
||||||
|
# Check if monitor's parent is in our group list
|
||||||
|
if monitor.get("parent") in group_ids:
|
||||||
|
group_members.append(monitor)
|
||||||
|
|
||||||
|
return group_members
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting group members: {e}")
|
||||||
|
return []
|
0
src/kumacli/cmd/__init__.py
Normal file
0
src/kumacli/cmd/__init__.py
Normal file
289
src/kumacli/cmd/maintenance.py
Normal file
289
src/kumacli/cmd/maintenance.py
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from kumacli.client import KumaClient
|
||||||
|
|
||||||
|
|
||||||
|
class MaintenanceCommands:
|
||||||
|
def __init__(self, client: KumaClient):
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
def list_maintenances(self):
|
||||||
|
"""List all maintenances"""
|
||||||
|
try:
|
||||||
|
maintenances = self.client.api.get_maintenances()
|
||||||
|
if not maintenances:
|
||||||
|
print("No maintenances found")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{'ID':<5} {'Title':<30} {'Strategy':<15} {'Active':<10} {'Description':<50}"
|
||||||
|
)
|
||||||
|
print("-" * 115)
|
||||||
|
|
||||||
|
for maintenance in maintenances:
|
||||||
|
maintenance_id = maintenance.get("id", "N/A")
|
||||||
|
title = maintenance.get("title", "N/A")
|
||||||
|
strategy = maintenance.get("strategy", "N/A")
|
||||||
|
active = "Active" if maintenance.get("active") else "Inactive"
|
||||||
|
description = maintenance.get("description", "N/A")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{maintenance_id:<5} {title:<30} {strategy:<15} {active:<10} {description:<50}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error listing maintenances: {e}")
|
||||||
|
|
||||||
|
def add_maintenance(
|
||||||
|
self,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
start_time=None,
|
||||||
|
duration="90m",
|
||||||
|
monitor_patterns=None,
|
||||||
|
group_patterns=None,
|
||||||
|
):
|
||||||
|
"""Add a new maintenance"""
|
||||||
|
try:
|
||||||
|
# Check if we have either monitor patterns or group patterns
|
||||||
|
if not monitor_patterns and not group_patterns:
|
||||||
|
print(
|
||||||
|
"Error: Either --monitor or --group flag is required. Specify at least one pattern."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
matched_monitors = []
|
||||||
|
|
||||||
|
# Find monitors by patterns if specified
|
||||||
|
if monitor_patterns:
|
||||||
|
pattern_monitors = self.client.find_monitors_by_pattern(
|
||||||
|
monitor_patterns
|
||||||
|
)
|
||||||
|
matched_monitors.extend(pattern_monitors)
|
||||||
|
|
||||||
|
# Find monitors by groups if specified
|
||||||
|
if group_patterns:
|
||||||
|
group_monitors = self.client.get_monitors_in_groups(group_patterns)
|
||||||
|
# Convert to the same format as find_monitors_by_pattern
|
||||||
|
group_monitor_objs = [
|
||||||
|
{"id": m.get("id"), "name": m.get("name")} for m in group_monitors
|
||||||
|
]
|
||||||
|
matched_monitors.extend(group_monitor_objs)
|
||||||
|
|
||||||
|
# Remove duplicates while preserving order
|
||||||
|
seen = set()
|
||||||
|
unique_monitors = []
|
||||||
|
for monitor in matched_monitors:
|
||||||
|
if monitor["id"] not in seen:
|
||||||
|
seen.add(monitor["id"])
|
||||||
|
unique_monitors.append(monitor)
|
||||||
|
|
||||||
|
matched_monitors = unique_monitors
|
||||||
|
|
||||||
|
if not matched_monitors:
|
||||||
|
print(
|
||||||
|
"Error: No monitors found matching the specified patterns or groups"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(matched_monitors)} matching monitors:")
|
||||||
|
for monitor in matched_monitors:
|
||||||
|
print(f" - {monitor['name']} (ID: {monitor['id']})")
|
||||||
|
|
||||||
|
# Set default description if not provided
|
||||||
|
if not description:
|
||||||
|
monitor_names = [monitor["name"] for monitor in matched_monitors]
|
||||||
|
description = "Maintenance on:\n" + "\n".join(monitor_names)
|
||||||
|
|
||||||
|
# Parse start time and duration
|
||||||
|
start_dt = self.client.parse_start_time(start_time)
|
||||||
|
duration_seconds = self.client.parse_duration(duration)
|
||||||
|
end_dt = start_dt + timedelta(seconds=duration_seconds)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Maintenance window: {start_dt.strftime('%Y-%m-%d %H:%M:%S UTC')} - {end_dt.strftime('%Y-%m-%d %H:%M:%S UTC')} ({duration})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the maintenance with single strategy and date range
|
||||||
|
maintenance_data = {
|
||||||
|
"title": title,
|
||||||
|
"description": description,
|
||||||
|
"strategy": "single",
|
||||||
|
"active": True,
|
||||||
|
"dateRange": [
|
||||||
|
start_dt.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
end_dt.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.client.api.add_maintenance(**maintenance_data)
|
||||||
|
maintenance_id = result.get("maintenanceID")
|
||||||
|
|
||||||
|
# Add monitors to maintenance
|
||||||
|
if maintenance_id:
|
||||||
|
print(f"Maintenance added successfully: {result}")
|
||||||
|
|
||||||
|
# Prepare monitors list in the correct format for the API
|
||||||
|
monitors_list = [{"id": monitor["id"]} for monitor in matched_monitors]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.client.api.add_monitor_maintenance(
|
||||||
|
maintenance_id, monitors_list
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"Successfully added {len(matched_monitors)} monitors to maintenance"
|
||||||
|
)
|
||||||
|
print(f"API response: {result}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: Failed to add monitors to maintenance: {e}")
|
||||||
|
print(
|
||||||
|
"This might be due to API compatibility issues or server configuration"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error adding maintenance: {e}")
|
||||||
|
|
||||||
|
def delete_maintenance(self, maintenance_id=None, delete_all=False):
|
||||||
|
"""Delete a specific maintenance or all maintenances"""
|
||||||
|
try:
|
||||||
|
if delete_all:
|
||||||
|
# Get all maintenances first
|
||||||
|
maintenances = self.client.api.get_maintenances()
|
||||||
|
if not maintenances:
|
||||||
|
print("No maintenances found to delete")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(maintenances)} maintenances to delete:")
|
||||||
|
for maintenance in maintenances:
|
||||||
|
print(
|
||||||
|
f" - {maintenance.get('title', 'N/A')} (ID: {maintenance.get('id', 'N/A')})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete all maintenances
|
||||||
|
deleted_count = 0
|
||||||
|
for maintenance in maintenances:
|
||||||
|
try:
|
||||||
|
result = self.client.api.delete_maintenance(
|
||||||
|
maintenance.get("id")
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"Deleted maintenance '{maintenance.get('title', 'N/A')}' (ID: {maintenance.get('id')})"
|
||||||
|
)
|
||||||
|
deleted_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(
|
||||||
|
f"Failed to delete maintenance '{maintenance.get('title', 'N/A')}': {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Successfully deleted {deleted_count} out of {len(maintenances)} maintenances"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif maintenance_id:
|
||||||
|
# Delete specific maintenance
|
||||||
|
try:
|
||||||
|
# First get the maintenance info for confirmation
|
||||||
|
maintenance = self.client.api.get_maintenance(maintenance_id)
|
||||||
|
maintenance_title = maintenance.get("title", "N/A")
|
||||||
|
|
||||||
|
result = self.client.api.delete_maintenance(maintenance_id)
|
||||||
|
print(
|
||||||
|
f"Successfully deleted maintenance '{maintenance_title}' (ID: {maintenance_id})"
|
||||||
|
)
|
||||||
|
print(f"API response: {result}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to delete maintenance ID {maintenance_id}: {e}")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"Error: Either --id or --all flag is required for delete operation"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during maintenance deletion: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_maintenance_parser(subparsers):
|
||||||
|
"""Setup maintenance command parser"""
|
||||||
|
maintenance_parser = subparsers.add_parser(
|
||||||
|
"maintenance", help="Maintenance operations"
|
||||||
|
)
|
||||||
|
maintenance_subparsers = maintenance_parser.add_subparsers(
|
||||||
|
dest="maintenance_action", help="Maintenance actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# List maintenances command
|
||||||
|
maintenance_subparsers.add_parser("list", help="List all maintenances")
|
||||||
|
|
||||||
|
# Delete maintenance command
|
||||||
|
delete_maintenance_parser = maintenance_subparsers.add_parser(
|
||||||
|
"delete", help="Delete maintenance(s)"
|
||||||
|
)
|
||||||
|
delete_maintenance_parser.add_argument(
|
||||||
|
"--id", type=int, help="Maintenance ID to delete"
|
||||||
|
)
|
||||||
|
delete_maintenance_parser.add_argument(
|
||||||
|
"--all", action="store_true", help="Delete all maintenances"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add maintenance command
|
||||||
|
add_maintenance_parser = maintenance_subparsers.add_parser(
|
||||||
|
"add", help="Add a new maintenance"
|
||||||
|
)
|
||||||
|
add_maintenance_parser.add_argument(
|
||||||
|
"--title",
|
||||||
|
help='Maintenance title (defaults to "Update started on <current UTC time>")',
|
||||||
|
)
|
||||||
|
add_maintenance_parser.add_argument("--description", help="Maintenance description")
|
||||||
|
add_maintenance_parser.add_argument(
|
||||||
|
"--start",
|
||||||
|
help="Start time (defaults to now, format: YYYY-MM-DD HH:MM:SS or ISO format)",
|
||||||
|
)
|
||||||
|
add_maintenance_parser.add_argument(
|
||||||
|
"--duration",
|
||||||
|
default="90m",
|
||||||
|
help="Duration (default: 90m, format: 3600s, 1h, 60m)",
|
||||||
|
)
|
||||||
|
add_maintenance_parser.add_argument(
|
||||||
|
"--monitor",
|
||||||
|
action="append",
|
||||||
|
help="Monitor name pattern to add to maintenance (supports wildcards like *NextCloud*, can be repeated)",
|
||||||
|
)
|
||||||
|
add_maintenance_parser.add_argument(
|
||||||
|
"--group",
|
||||||
|
action="append",
|
||||||
|
help="Group name pattern to add all group members to maintenance (supports wildcards, can be repeated)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return maintenance_parser
|
||||||
|
|
||||||
|
|
||||||
|
def handle_maintenance_command(args, client):
|
||||||
|
"""Handle maintenance command execution"""
|
||||||
|
maintenance_commands = MaintenanceCommands(client)
|
||||||
|
|
||||||
|
if args.maintenance_action == "list":
|
||||||
|
maintenance_commands.list_maintenances()
|
||||||
|
elif args.maintenance_action == "add":
|
||||||
|
title = (
|
||||||
|
args.title
|
||||||
|
or f"Update started on {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC')}"
|
||||||
|
)
|
||||||
|
maintenance_commands.add_maintenance(
|
||||||
|
title,
|
||||||
|
args.description,
|
||||||
|
args.start,
|
||||||
|
args.duration,
|
||||||
|
monitor_patterns=args.monitor,
|
||||||
|
group_patterns=args.group,
|
||||||
|
)
|
||||||
|
elif args.maintenance_action == "delete":
|
||||||
|
maintenance_commands.delete_maintenance(
|
||||||
|
maintenance_id=args.id, delete_all=args.all
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("Unknown maintenance action. Use --help for usage information.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
108
src/kumacli/cmd/monitor.py
Normal file
108
src/kumacli/cmd/monitor.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from kumacli.client import KumaClient
|
||||||
|
|
||||||
|
|
||||||
|
class MonitorCommands:
|
||||||
|
def __init__(self, client: KumaClient):
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
def list_monitors(self, monitor_patterns=None, group_patterns=None):
|
||||||
|
"""List all monitors or filter by patterns and/or groups"""
|
||||||
|
try:
|
||||||
|
all_monitors = self.client.api.get_monitors()
|
||||||
|
if not all_monitors:
|
||||||
|
print("No monitors found")
|
||||||
|
return
|
||||||
|
|
||||||
|
monitors = all_monitors[:] # Start with all monitors
|
||||||
|
|
||||||
|
# Filter by group if specified
|
||||||
|
if group_patterns:
|
||||||
|
group_monitors = self.client.get_monitors_in_groups(group_patterns)
|
||||||
|
if not group_monitors:
|
||||||
|
print("No monitors found in the specified groups")
|
||||||
|
return
|
||||||
|
monitor_ids = {m["id"] for m in group_monitors}
|
||||||
|
monitors = [m for m in monitors if m.get("id") in monitor_ids]
|
||||||
|
|
||||||
|
# Filter monitors by name patterns if specified
|
||||||
|
if monitor_patterns:
|
||||||
|
matched_monitors = self.client.find_monitors_by_pattern(
|
||||||
|
monitor_patterns
|
||||||
|
)
|
||||||
|
if not matched_monitors:
|
||||||
|
print("No monitors found matching the specified patterns")
|
||||||
|
return
|
||||||
|
pattern_monitor_ids = {m["id"] for m in matched_monitors}
|
||||||
|
monitors = [m for m in monitors if m.get("id") in pattern_monitor_ids]
|
||||||
|
|
||||||
|
# Display results with group information
|
||||||
|
print(
|
||||||
|
f"{'ID':<5} {'Name':<25} {'Type':<12} {'Group':<20} {'URL':<35} {'Status':<10}"
|
||||||
|
)
|
||||||
|
print("-" * 112)
|
||||||
|
|
||||||
|
for monitor in monitors:
|
||||||
|
monitor_id = monitor.get("id", "N/A")
|
||||||
|
name = monitor.get("name", "N/A")
|
||||||
|
monitor_type = monitor.get("type", "N/A")
|
||||||
|
url = monitor.get("url", "N/A")
|
||||||
|
active = "Active" if monitor.get("active") else "Inactive"
|
||||||
|
|
||||||
|
# Find parent group name
|
||||||
|
parent_id = monitor.get("parent")
|
||||||
|
parent_name = "None"
|
||||||
|
if parent_id:
|
||||||
|
parent_monitor = next(
|
||||||
|
(m for m in all_monitors if m.get("id") == parent_id), None
|
||||||
|
)
|
||||||
|
if parent_monitor:
|
||||||
|
parent_name = parent_monitor.get("name", f"Group {parent_id}")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"{monitor_id:<5} {name:<25} {monitor_type:<12} {parent_name:<20} {url:<35} {active:<10}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error listing monitors: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_monitor_parser(subparsers):
|
||||||
|
"""Setup monitor command parser"""
|
||||||
|
monitor_parser = subparsers.add_parser("monitor", help="Monitor operations")
|
||||||
|
monitor_subparsers = monitor_parser.add_subparsers(
|
||||||
|
dest="monitor_action", help="Monitor actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
# List monitors command
|
||||||
|
list_monitors_parser = monitor_subparsers.add_parser(
|
||||||
|
"list", help="List all monitors"
|
||||||
|
)
|
||||||
|
list_monitors_parser.add_argument(
|
||||||
|
"--monitor",
|
||||||
|
action="append",
|
||||||
|
help="Monitor name pattern to filter by (supports wildcards, can be repeated)",
|
||||||
|
)
|
||||||
|
list_monitors_parser.add_argument(
|
||||||
|
"--group",
|
||||||
|
action="append",
|
||||||
|
help="Group name pattern to filter by (supports wildcards, can be repeated)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return monitor_parser
|
||||||
|
|
||||||
|
|
||||||
|
def handle_monitor_command(args, client):
|
||||||
|
"""Handle monitor command execution"""
|
||||||
|
monitor_commands = MonitorCommands(client)
|
||||||
|
|
||||||
|
if args.monitor_action == "list":
|
||||||
|
monitor_commands.list_monitors(
|
||||||
|
monitor_patterns=args.monitor, group_patterns=args.group
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("Unknown monitor action. Use --help for usage information.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
Reference in New Issue
Block a user