Initial checkin of pip wheel
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,3 +4,5 @@ lib
|
||||
lib64
|
||||
pyvenv.cfg
|
||||
__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