Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ bwh node add production --api-key <API_KEY> --veid <VEID>

# Basic operations
bwh info # View server details
bwh rate-limit # Check API rate limit status
bwh start/stop/restart # Power management
bwh usage --period 7d # Check usage statistics
bwh abuse suspensions # Show suspension details
bwh notifications list # Show notification preferences
bwh snapshot create "backup-name" # Create snapshots
bwh iso images # List available ISO images
bwh iso mount ubuntu-20.04.iso # Mount ISO for rescue/install
Expand Down Expand Up @@ -98,12 +101,16 @@ backups, err := c.ListBackups(ctx)

**Server Management**: `GetServiceInfo`, `GetLiveServiceInfo`, `Start`, `Stop`, `Restart`, `Kill`, `SetHostname`, `ReinstallOS`, `ResetRootPassword`, `MountISO`, `UnmountISO`

**Monitoring**: `GetRawUsageStats`, `GetBasicServiceInfo`, audit log access
**Monitoring**: `GetRawUsageStats`, `GetAuditLog`, `GetRateLimitStatus`

**Backup & Recovery**: `CreateSnapshot`, `RestoreSnapshot`, `DeleteSnapshot`, backup management

**Migration**: `GetMigrateLocations`, `StartMigration` (use `StartMigrationWithTimeout` for custom timeouts)

**Security & Abuse**: `GetSuspensionDetails`, `GetPolicyViolations`

**Notifications**: `GetNotificationPreferences`

**Network**: SSH key management, IP/reverse DNS configuration, IPv6 subnet management, private IPv4 management

*Complete API reference*: View [pkg/client documentation](./pkg/client) or run `go doc github.com/strahe/bwh/pkg/client` for all available methods.
Expand Down Expand Up @@ -187,7 +194,7 @@ Add to your Continue configuration:
### Configuration Notes

- **Custom Config**: Use `--config /path/to/config.yaml` to specify a config file
- **Multiple Instances**: The server automatically uses your default instance from configuration
- **Multiple Instances**: The server uses the configured default instance, or `--instance <name>` as the MCP session default. Tool-level `instance` arguments override it.
- **Integration**: Add to existing MCP config files without replacing other servers

### Available MCP Tools (Read-only)
Expand All @@ -199,6 +206,14 @@ Add to your Continue configuration:
- **backup_list**: List backups (`instance?`, `os_contains?`, `since?`, `until?`, `sort_by?`, `order?`, `limit?`)
- **vps_audit_get**: Get audit logs (`instance?`, `since?`, `until?`, `limit?`, `ip_contains?`, `type?`)
- **iso_list**: List available and mounted ISO images (`instance?`)
- **ssh_keys_get**: Get SSH public keys (`instance?`, `full?`)
- **os_templates_get**: List OS templates (`instance?`)
- **rate_limit_get**: Get API rate limit status (`instance?`)
- **migration_locations_get**: List migration locations (`instance?`)
- **private_ip_available_get**: List available private IPv4 addresses (`instance?`)
- **abuse_suspensions_get**: Get suspension details (`instance?`)
- **abuse_policy_get**: Get policy violations (`instance?`)
- **notification_preferences_get**: Get notification preferences (`instance?`)

All MCP tools are safe, read-only operations that won't modify your VPS configuration or data.

Expand Down Expand Up @@ -228,6 +243,8 @@ iso Manage ISO images for VPS boot
reinstall Reinstall VPS operating system (WARNING: destroys all data)
usage Display detailed VPS usage statistics
audit Display audit log entries
abuse Display suspension details and policy violations
notifications Display KiwiVM notification preferences
reset-password Reset the root password
snapshot Manage VPS snapshots
backup Manage VPS backups
Expand Down
21 changes: 19 additions & 2 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ bwh node add production --api-key <API_KEY> --veid <VEID>

# 基本操作
bwh info # 查看服务器详情
bwh rate-limit # 检查 API 限制状态
bwh start/stop/restart # 电源管理
bwh usage --period 7d # 检查使用统计
bwh abuse suspensions # 查看暂停详情
bwh notifications list # 查看通知偏好
bwh snapshot create "备份名称" # 创建快照
bwh iso images # 列出可用 ISO 镜像
bwh iso mount ubuntu-20.04.iso # 挂载 ISO 用于救援/安装
Expand Down Expand Up @@ -98,12 +101,16 @@ backups, err := c.ListBackups(ctx)

**服务器管理**: `GetServiceInfo`、`GetLiveServiceInfo`、`Start`、`Stop`、`Restart`、`Kill`、`SetHostname`、`ReinstallOS`、`ResetRootPassword`、`MountISO`、`UnmountISO`

**监控**: `GetRawUsageStats`、`GetBasicServiceInfo`、审计日志访问
**监控**: `GetRawUsageStats`、`GetAuditLog`、`GetRateLimitStatus`

**备份和恢复**: `CreateSnapshot`、`RestoreSnapshot`、`DeleteSnapshot`、备份管理

**迁移**: `GetMigrateLocations`、`StartMigration`(支持 `StartMigrationWithTimeout` 自定义超时)

**安全与 abuse**: `GetSuspensionDetails`、`GetPolicyViolations`

**通知**: `GetNotificationPreferences`

**网络**: SSH 密钥管理、IP/反向 DNS 配置、IPv6 子网管理、私有 IPv4 管理

*完整 API 参考*: 查看 [pkg/client 文档](./pkg/client) 或运行 `go doc github.com/strahe/bwh/pkg/client` 获取所有可用方法。
Expand Down Expand Up @@ -187,7 +194,7 @@ claude mcp add bwh -- bwh mcp serve
### 配置说明

- **自定义配置**: 使用 `--config /path/to/config.yaml` 指定配置文件
- **多实例**: 服务器自动使用配置中的默认实例
- **多实例**: 服务器使用配置中的默认实例,或使用 `--instance <name>` 作为 MCP 会话默认实例。工具级 `instance` 参数可覆盖该默认值。
- **集成**: 添加到现有 MCP 配置文件中,不替换其他服务器

### 可用工具
Expand All @@ -199,6 +206,14 @@ claude mcp add bwh -- bwh mcp serve
- **backup_list**: 列出备份 (`instance?`, `os_contains?`, `since?`, `until?`, `sort_by?`, `order?`, `limit?`)
- **vps_audit_get**: 获取审计日志 (`instance?`, `since?`, `until?`, `limit?`, `ip_contains?`, `type?`)
- **iso_list**: 列出可用和已挂载的 ISO 镜像 (`instance?`)
- **ssh_keys_get**: 获取 SSH 公钥 (`instance?`, `full?`)
- **os_templates_get**: 列出系统模板 (`instance?`)
- **rate_limit_get**: 获取 API 限制状态 (`instance?`)
- **migration_locations_get**: 列出迁移位置 (`instance?`)
- **private_ip_available_get**: 列出可用私有 IPv4 地址 (`instance?`)
- **abuse_suspensions_get**: 获取暂停详情 (`instance?`)
- **abuse_policy_get**: 获取策略违规 (`instance?`)
- **notification_preferences_get**: 获取通知偏好 (`instance?`)

所有 MCP 工具都是安全的只读操作,不会修改您的 VPS 配置或数据。

Expand Down Expand Up @@ -228,6 +243,8 @@ iso 管理 VPS 启动用 ISO 镜像
reinstall 重装 VPS 操作系统(警告:摧毁所有数据)
usage 显示详细 VPS 使用统计
audit 显示审计日志条目
abuse 显示暂停详情与策略违规
notifications 显示 KiwiVM 通知偏好
reset-password 重置 root 密码
snapshot 管理 VPS 快照
backup 管理 VPS 备份
Expand Down
133 changes: 133 additions & 0 deletions cmd/bwh/abuse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package main

import (
"context"
"fmt"
"sort"
"time"

"github.com/strahe/bwh/pkg/client"
"github.com/urfave/cli/v3"
)

var abuseCmd = &cli.Command{
Name: "abuse",
Usage: "inspect abuse suspensions and policy violations",
Commands: []*cli.Command{
abuseSuspensionsCmd,
abusePolicyCmd,
},
}

var abuseSuspensionsCmd = &cli.Command{
Name: "suspensions",
Usage: "show service suspension details",
Action: func(ctx context.Context, cmd *cli.Command) error {
bwhClient, resolvedName, err := createBWHClient(cmd)
if err != nil {
return err
}

fmt.Printf("Getting suspension details for instance: %s\n", resolvedName)
resp, err := bwhClient.GetSuspensionDetails(ctx)
if err != nil {
return fmt.Errorf("failed to get suspension details: %w", err)
}

displaySuspensionDetails(resp)
return nil
},
}

var abusePolicyCmd = &cli.Command{
Name: "policy",
Usage: "show active policy violations",
Action: func(ctx context.Context, cmd *cli.Command) error {
bwhClient, resolvedName, err := createBWHClient(cmd)
if err != nil {
return err
}

fmt.Printf("Getting policy violations for instance: %s\n", resolvedName)
resp, err := bwhClient.GetPolicyViolations(ctx)
if err != nil {
return fmt.Errorf("failed to get policy violations: %w", err)
}

displayPolicyViolations(resp)
return nil
},
}

func displaySuspensionDetails(resp *client.SuspensionDetailsResponse) {
fmt.Printf("\n🚫 SUSPENSION DETAILS\n")
fmt.Printf(" Suspensions (YTD): %d\n", resp.SuspensionCount)
fmt.Printf(" Abuse Points : %d / %d\n", resp.TotalAbusePoints, resp.MaxAbusePoints)

if len(resp.Suspensions) == 0 {
fmt.Printf("\nNo active suspension issues found.\n")
return
}

records := append([]client.SuspensionRecord{}, resp.Suspensions...)
sort.Slice(records, func(i, j int) bool {
return records[i].RecordID < records[j].RecordID
})

fmt.Printf("\nOutstanding Issues (%d):\n", len(records))
for i, record := range records {
fmt.Printf("\n[%d] Case #%d\n", i+1, record.RecordID)
fmt.Printf(" Flag : %s\n", record.Flag)
fmt.Printf(" Soft Resolve: %s\n", yesNo(record.IsSoft == 1))
fmt.Printf(" Abuse Points: %d\n", record.AbusePoints)
if record.EvidenceRecordID != 0 {
fmt.Printf(" Evidence ID : %d\n", record.EvidenceRecordID)
if text := resp.Evidence[fmt.Sprintf("%d", record.EvidenceRecordID)]; text != "" {
fmt.Printf(" Evidence : %s\n", summarizeText(text, 120))
}
}
}
}

func displayPolicyViolations(resp *client.PolicyViolationsResponse) {
fmt.Printf("\n⚠️ POLICY VIOLATIONS\n")
fmt.Printf(" Abuse Points: %d / %d\n", resp.TotalAbusePoints, resp.MaxAbusePoints)

if len(resp.PolicyViolations) == 0 {
fmt.Printf("\nNo active policy violations found.\n")
return
}

records := append([]client.PolicyViolationRecord{}, resp.PolicyViolations...)
sort.Slice(records, func(i, j int) bool {
return records[i].Timestamp > records[j].Timestamp
})

fmt.Printf("\nActive Violations (%d):\n", len(records))
for i, record := range records {
fmt.Printf("\n[%d] Case #%d\n", i+1, record.RecordID)
fmt.Printf(" Flag : %s\n", record.Flag)
fmt.Printf(" Soft Resolve: %s\n", yesNo(record.IsSoft == 1))
fmt.Printf(" Abuse Points: %d\n", record.AbusePoints)
if record.Timestamp > 0 {
fmt.Printf(" Created : %s\n", time.Unix(record.Timestamp, 0).Format("2006-01-02 15:04:05"))
}
if record.SuspendAt > 0 {
fmt.Printf(" Suspend At : %s\n", time.Unix(record.SuspendAt, 0).Format("2006-01-02 15:04:05"))
}
if record.EvidenceData != "" {
fmt.Printf(" Evidence : %s\n", summarizeText(record.EvidenceData, 120))
}
}
}

func summarizeText(s string, maxLen int) string {
if maxLen <= 0 {
return "..."
}
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return string(runes[:maxLen]) + "..."
}
13 changes: 12 additions & 1 deletion cmd/bwh/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -176,8 +177,9 @@ func displayDetailedInfo(info *client.LiveServiceInfo, instanceName string) {
}

if len(info.IPNullroutes) > 0 {
nullrouteIPs := sortedNullrouteIPs(info.IPNullroutes)
fmt.Printf(" ⚠️ DDoS Protection : %d IP(s) currently null-routed\n", len(info.IPNullroutes))
fmt.Printf(" Null-routed IPs : %s\n", strings.Join(info.IPNullroutes, ", "))
fmt.Printf(" Null-routed IPs : %s\n", strings.Join(nullrouteIPs, ", "))
}

if info.IPv6SitTunnelEndpoint != "" {
Expand Down Expand Up @@ -444,6 +446,15 @@ func displaySecurityInfo(info *client.ServiceInfo) {
}
}

func sortedNullrouteIPs(nullroutes client.IPNullroutes) []string {
ips := make([]string, 0, len(nullroutes))
for ip := range nullroutes {
ips = append(ips, ip)
}
sort.Strings(ips)
return ips
}

// formatBool converts boolean to readable yes/no
func formatBool(b bool) string {
if b {
Expand Down
2 changes: 2 additions & 0 deletions cmd/bwh/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ func main() {
reinstallCmd,
usageCmd,
auditCmd,
abuseCmd,
notificationsCmd,
resetPasswordCmd,
snapshotCmd,
backupCmd,
Expand Down
89 changes: 89 additions & 0 deletions cmd/bwh/notifications.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"context"
"fmt"
"sort"
"time"

"github.com/strahe/bwh/pkg/client"
"github.com/urfave/cli/v3"
)

var notificationsCmd = &cli.Command{
Name: "notifications",
Usage: "inspect KiwiVM notification preferences",
Commands: []*cli.Command{
notificationsListCmd,
},
}

var notificationsListCmd = &cli.Command{
Name: "list",
Usage: "list KiwiVM notification preferences",
Action: func(ctx context.Context, cmd *cli.Command) error {
bwhClient, resolvedName, err := createBWHClient(cmd)
if err != nil {
return err
}

fmt.Printf("Getting notification preferences for instance: %s\n", resolvedName)
resp, err := bwhClient.GetNotificationPreferences(ctx)
if err != nil {
return fmt.Errorf("failed to get notification preferences: %w", err)
}

displayNotificationPreferences(resp)
return nil
},
}

func displayNotificationPreferences(resp *client.NotificationPreferencesResponse) {
fmt.Printf("\n📧 NOTIFICATION PREFERENCES\n")
if resp.NotificationEmail != "" {
fmt.Printf(" Email: %s\n", resp.NotificationEmail)
}

if len(resp.EmailPreferences) == 0 {
fmt.Printf("\nNo notification preferences found.\n")
return
}

categories := make([]string, 0, len(resp.EmailPreferences))
for category := range resp.EmailPreferences {
categories = append(categories, category)
}
sort.Strings(categories)

for _, category := range categories {
prefs := resp.EmailPreferences[category]
ids := make([]string, 0, len(prefs))
for id := range prefs {
ids = append(ids, id)
}
sort.Strings(ids)

fmt.Printf("\n%s\n", category)
for _, id := range ids {
pref := prefs[id]
fmt.Printf(" • %s\n", id)
fmt.Printf(" Status : %s\n", enabledStatus(pref.IsEnabled))
if pref.FriendlyDescription != "" {
fmt.Printf(" Description: %s\n", pref.FriendlyDescription)
}
if pref.ChangedTimestamp > 0 {
fmt.Printf(" Updated : %s\n", time.Unix(pref.ChangedTimestamp, 0).Format("2006-01-02 15:04:05"))
}
if pref.SValue != "" {
fmt.Printf(" Value : %s\n", pref.SValue)
}
}
}
}

func enabledStatus(value int) string {
if value == 1 {
return "✅ Enabled"
}
return "❌ Disabled"
}
Loading
Loading