From e3401ec380a20149514d0df535e269342e402302 Mon Sep 17 00:00:00 2001 From: seahi Date: Tue, 17 Jun 2025 22:06:31 +0800 Subject: [PATCH] Initial commit: Harbor batch management tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add scripts for Harbor user and project management: - register.py: Bulk user registration from Excel - delete_users.py: Complete user deletion with resource cleanup - delete_projects.py: Targeted deletion of student projects (stu01-stu49) - CLAUDE.md: Project documentation and API guidance - requirements.txt: Python dependencies 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/settings.local.json | 10 ++ .gitignore | 54 +++++++ CLAUDE.md | 110 +++++++++++++++ api.md | 5 + delete_projects.py | 271 ++++++++++++++++++++++++++++++++++++ delete_users.py | 258 ++++++++++++++++++++++++++++++++++ register.py | 98 +++++++++++++ requirements.txt | 4 + 8 files changed, 810 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 api.md create mode 100644 delete_projects.py create mode 100644 delete_users.py create mode 100644 register.py create mode 100644 requirements.txt diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..e3d6818 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(git init:*)", + "Bash(git remote add:*)", + "Bash(git add:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39357d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +env/ +.env +.venv + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Excel files (sensitive data) +*.xlsx +*.xls + +# Logs +*.log + +# Temporary files +*.tmp +*.temp \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..253b05f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,110 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Harbor container registry user management tool written in Python. It provides functionality to: +- Register/create Harbor users from Excel data (`register.py`) +- Delete Harbor users individually or in batches (`delete_users.py`, `delete_users_fixed.py`) + +The application interacts with Harbor's REST API v2.0 at `https://harbor.seahi.me`. + +## Setup and Environment + +### Dependencies Installation +```bash +pip install -r requirements.txt +``` + +### Virtual Environment Setup +```bash +# Activate existing virtual environment +source venv/bin/activate # On Unix/macOS +# or +venv\Scripts\activate # On Windows +``` + +## Core Architecture + +### Data Flow +1. **Input**: Excel file (`users.xlsx`) with user data (username, realname, password, email) +2. **Authentication**: HTTP Basic Auth with Harbor admin credentials +3. **API Operations**: REST API calls to Harbor's `/api/v2.0/users` endpoint +4. **Error Handling**: Comprehensive exception handling with Chinese language user feedback + +### Key Components + +- **Authentication**: `get_admin_credentials()` - Interactive credential collection with getpass +- **User Operations**: + - `create_harbor_user()` - User creation via POST (register.py:20) + - `delete_harbor_user()` - User deletion via DELETE (requires user ID lookup) + - `get_user_id_by_username()` - User ID resolution from username +- **Excel Processing**: `pd.read_excel()` with `skiprows=1` to handle header row +- **Connection Testing**: `test_api_connection()` validates API connectivity and auth (delete_users_fixed.py:20) +- **Interactive Modes**: `delete_users_fixed.py` provides menu-driven user interaction + +### File Structure +- `register.py` - User registration from Excel +- `delete_users.py` - Basic user deletion tool +- `delete_users_fixed.py` - Enhanced deletion tool with better error handling and interactive modes +- `users.xlsx` - Excel data source (not in version control) +- `requirements.txt` - Python dependencies + +## Common Commands + +### Environment Setup +```bash +# Activate virtual environment (required before running scripts) +source venv/bin/activate + +# Install dependencies +pip install -r requirements.txt +``` + +### Run Scripts +```bash +# Register users from Excel file +python register.py + +# Delete users (recommended enhanced version) +python delete_users_fixed.py + +# Delete users (basic version) +python delete_users.py +``` + +### Development +```bash +# Check Python syntax +python -m py_compile register.py +python -m py_compile delete_users_fixed.py + +# Run with verbose output for debugging +python -v register.py +``` + +## API Configuration + +- Harbor URL: `https://harbor.seahi.me` +- API Version: v2.0 +- Authentication: HTTP Basic Auth +- SSL Verification: Disabled (`verify=False`) +- HTTPS Warnings: Suppressed via urllib3 + +## Excel File Format + +Expected structure for `users.xlsx`: +- Row 1: Headers (skipped) +- Column 0: Username +- Column 1: Real name +- Column 2: Password +- Column 4: Email address + +## Error Handling Patterns + +- HTTP 409: User already exists (treated as success for registration) +- HTTP 401: Authentication failure +- HTTP 404: User not found +- HTTP 412: User has dependencies (cannot delete) +- Connection timeouts and network errors are handled gracefully \ No newline at end of file diff --git a/api.md b/api.md new file mode 100644 index 0000000..1f1cf5c --- /dev/null +++ b/api.md @@ -0,0 +1,5 @@ +## 用户相关 + +Base URL: harbor.seahi.me/api/v2.0 + +GET /users diff --git a/delete_projects.py b/delete_projects.py new file mode 100644 index 0000000..890f593 --- /dev/null +++ b/delete_projects.py @@ -0,0 +1,271 @@ +import requests +import getpass +import re +from urllib3.exceptions import InsecureRequestWarning +from requests.auth import HTTPBasicAuth + +# 禁用不安全HTTPS警告 +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + +HARBOR_URL = "https://harbor.seahi.me" +API_BASE = f"{HARBOR_URL}/api/v2.0" + +def get_admin_credentials(): + """获取Harbor管理员凭据""" + print("请输入Harbor管理员账号信息:") + username = input("用户名: ") + password = getpass.getpass("密码: ") + return username, password + +def test_api_connection(auth): + """测试API连接和认证""" + try: + response = requests.get(f"{API_BASE}/projects", auth=auth, verify=False, timeout=10) + if response.status_code == 401: + print("认证失败:用户名或密码错误") + return False + elif response.status_code == 200: + print("API连接成功") + return True + else: + print(f"API连接失败: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f"API连接出错: {str(e)}") + return False + +def get_all_projects(auth): + """获取所有项目列表(支持分页)""" + all_projects = [] + page = 1 + page_size = 100 # 每页获取100个项目 + + while True: + try: + params = { + 'page': page, + 'page_size': page_size + } + response = requests.get(f"{API_BASE}/projects", params=params, + auth=auth, verify=False, timeout=10) + + if response.status_code == 200: + projects = response.json() + if not projects: # 没有更多数据 + break + all_projects.extend(projects) + print(f" 已获取第 {page} 页,共 {len(projects)} 个项目") + page += 1 + else: + print(f"获取项目列表失败: {response.status_code} - {response.text}") + break + except Exception as e: + print(f"获取项目列表出错: {str(e)}") + break + + print(f"总共获取 {len(all_projects)} 个项目") + return all_projects + +def get_student_projects(auth): + """获取所有学生项目 (stu01~stu49)""" + all_projects = get_all_projects(auth) + student_projects = [] + + # 匹配 stu01 到 stu49 开头的项目名 + pattern = re.compile(r'^stu(0[1-9]|[1-4][0-9]).*') + + for project in all_projects: + project_name = project.get("name", "") + if pattern.match(project_name): + student_projects.append(project) + + return student_projects + +def get_project_repositories(auth, project_name): + """获取项目下的所有仓库(支持分页)""" + all_repositories = [] + page = 1 + page_size = 100 # 每页获取100个仓库 + + while True: + try: + params = { + 'page': page, + 'page_size': page_size + } + response = requests.get(f"{API_BASE}/projects/{project_name}/repositories", + params=params, auth=auth, verify=False, timeout=10) + + if response.status_code == 200: + repositories = response.json() + if not repositories: # 没有更多数据 + break + all_repositories.extend(repositories) + page += 1 + else: + print(f" 获取项目 {project_name} 仓库列表失败: {response.status_code}") + break + except Exception as e: + print(f" 获取项目 {project_name} 仓库列表出错: {str(e)}") + break + + return all_repositories + +def delete_repository(auth, project_name, repo_name): + """删除仓库""" + try: + # 仓库名格式为 "project_name/repository_name",需要提取仓库名部分 + if '/' in repo_name: + actual_repo_name = repo_name.split('/', 1)[1] # 取第一个/后面的部分 + else: + actual_repo_name = repo_name + + response = requests.delete(f"{API_BASE}/projects/{project_name}/repositories/{actual_repo_name}", + auth=auth, verify=False, timeout=30) + if response.status_code == 200: + print(f" ✓ 删除仓库: {repo_name}") + return True + else: + print(f" ✗ 删除仓库失败 {repo_name}: {response.status_code}") + if response.text: + print(f" 错误详情: {response.text}") + return False + except Exception as e: + print(f" ✗ 删除仓库出错 {repo_name}: {str(e)}") + return False + +def delete_project(auth, project_name): + """删除项目""" + try: + response = requests.delete(f"{API_BASE}/projects/{project_name}", + auth=auth, verify=False, timeout=30) + if response.status_code == 200: + print(f" ✓ 删除项目: {project_name}") + return True + else: + print(f" ✗ 删除项目失败 {project_name}: {response.status_code}") + if response.text: + print(f" 错误详情: {response.text}") + return False + except Exception as e: + print(f" ✗ 删除项目出错 {project_name}: {str(e)}") + return False + +def delete_student_projects_and_repositories(auth): + """删除所有学生项目和仓库""" + print("正在查找学生项目 (stu01~stu49)...") + + student_projects = get_student_projects(auth) + + if not student_projects: + print("未找到任何学生项目") + return + + print(f"找到 {len(student_projects)} 个学生项目:") + for project in student_projects: + print(f" - {project.get('name')} (ID: {project.get('project_id')})") + + # 确认删除 + print(f"\n警告:即将删除 {len(student_projects)} 个项目及其所有仓库!") + confirm = input("确认要继续吗?(输入 'DELETE' 确认): ").strip() + if confirm != 'DELETE': + print("已取消操作") + return + + success_count = 0 + total_count = len(student_projects) + + for i, project in enumerate(student_projects, 1): + project_name = project.get("name") + print(f"\n[{i}/{total_count}] 处理项目: {project_name}") + + # 获取项目下的仓库 + repositories = get_project_repositories(auth, project_name) + + if repositories: + print(f" 发现 {len(repositories)} 个仓库,开始删除...") + repo_success = 0 + for repo in repositories: + repo_name = repo.get("name") + if delete_repository(auth, project_name, repo_name): + repo_success += 1 + print(f" 仓库删除完成: {repo_success}/{len(repositories)}") + else: + print(f" 项目 {project_name} 中没有仓库") + + # 删除项目 + if delete_project(auth, project_name): + success_count += 1 + + print(f"\n" + "="*60) + print(f"删除完成!") + print(f"成功删除项目: {success_count}/{total_count}") + +def list_student_projects(auth): + """列出所有学生项目(预览模式)""" + print("正在查找学生项目 (stu01~stu49)...") + + student_projects = get_student_projects(auth) + + if not student_projects: + print("未找到任何学生项目") + return + + print(f"\n找到 {len(student_projects)} 个学生项目:") + print("-" * 80) + + for project in student_projects: + project_name = project.get("name") + project_id = project.get("project_id") + owner_name = project.get("owner_name", "N/A") + creation_time = project.get("creation_time", "N/A") + + print(f"项目: {project_name}") + print(f" ID: {project_id}") + print(f" 所有者: {owner_name}") + print(f" 创建时间: {creation_time}") + + # 获取仓库信息 + repositories = get_project_repositories(auth, project_name) + if repositories: + print(f" 仓库 ({len(repositories)}个):") + for repo in repositories: + print(f" - {repo.get('name')} (拉取次数: {repo.get('pull_count', 0)})") + else: + print(f" 仓库: 无") + print() + +def main(): + print("Harbor 学生项目删除工具") + print("=" * 40) + print("此工具专门删除 stu01~stu49 开头的项目和仓库") + + # 获取管理员凭据 + admin_username, admin_password = get_admin_credentials() + auth = HTTPBasicAuth(admin_username, admin_password) + + # 测试API连接 + if not test_api_connection(auth): + return + + while True: + print("\n请选择操作:") + print("1. 预览学生项目(不删除)") + print("2. 删除所有学生项目和仓库") + print("3. 退出") + + choice = input("请输入选项 (1-3): ").strip() + + if choice == '1': + list_student_projects(auth) + elif choice == '2': + delete_student_projects_and_repositories(auth) + break + elif choice == '3': + print("退出程序") + break + else: + print("无效选项,请重试") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/delete_users.py b/delete_users.py new file mode 100644 index 0000000..df15bf4 --- /dev/null +++ b/delete_users.py @@ -0,0 +1,258 @@ +import pandas as pd +import requests +import getpass +import sys +from urllib3.exceptions import InsecureRequestWarning +from requests.auth import HTTPBasicAuth + +# 禁用不安全HTTPS警告 +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + +HARBOR_URL = "https://harbor.seahi.me" +API_BASE = f"{HARBOR_URL}/api/v2.0" + +def get_admin_credentials(): + """获取Harbor管理员凭据""" + print("请输入Harbor管理员账号信息:") + username = input("用户名: ") + password = getpass.getpass("密码: ") + return username, password + +def test_api_connection(auth): + """测试API连接和认证""" + try: + response = requests.get(f"{API_BASE}/users", auth=auth, verify=False, timeout=10) + if response.status_code == 401: + print("认证失败:用户名或密码错误") + return False + elif response.status_code == 200: + print("API连接成功") + return True + else: + print(f"API连接失败: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f"API连接出错: {str(e)}") + return False + +def get_all_users(auth): + """获取所有用户列表""" + try: + response = requests.get(f"{API_BASE}/users", auth=auth, verify=False, timeout=10) + if response.status_code == 200: + return response.json() + else: + print(f"获取用户列表失败: {response.status_code} - {response.text}") + return None + except Exception as e: + print(f"获取用户列表出错: {str(e)}") + return None + +def get_user_by_username(auth, username): + """根据用户名获取用户信息""" + users = get_all_users(auth) + if users is None: + return None + + for user in users: + if user.get("username") == username: + return user + return None + +def get_user_projects(auth, user_id): + """获取用户拥有的所有项目""" + try: + response = requests.get(f"{API_BASE}/projects", auth=auth, verify=False, timeout=10) + if response.status_code == 200: + projects = response.json() + # 过滤出属于该用户的项目 + user_projects = [p for p in projects if p.get("owner_id") == user_id] + return user_projects + else: + print(f"获取项目列表失败: {response.status_code} - {response.text}") + return [] + except Exception as e: + print(f"获取项目列表出错: {str(e)}") + return [] + +def get_project_repositories(auth, project_name): + """获取项目下的所有仓库""" + try: + response = requests.get(f"{API_BASE}/projects/{project_name}/repositories", + auth=auth, verify=False, timeout=10) + if response.status_code == 200: + return response.json() + else: + print(f"获取项目 {project_name} 仓库列表失败: {response.status_code}") + return [] + except Exception as e: + print(f"获取项目 {project_name} 仓库列表出错: {str(e)}") + return [] + +def delete_repository(auth, project_name, repo_name): + """删除仓库""" + try: + # 仓库名称可能包含项目名,需要提取仓库名部分 + if '/' in repo_name: + repo_name = repo_name.split('/')[-1] + + response = requests.delete(f"{API_BASE}/projects/{project_name}/repositories/{repo_name}", + auth=auth, verify=False, timeout=10) + if response.status_code == 200: + print(f" ✓ 删除仓库: {project_name}/{repo_name}") + return True + else: + print(f" ✗ 删除仓库失败 {project_name}/{repo_name}: {response.status_code}") + return False + except Exception as e: + print(f" ✗ 删除仓库出错 {project_name}/{repo_name}: {str(e)}") + return False + +def delete_project(auth, project_name): + """删除项目""" + try: + response = requests.delete(f"{API_BASE}/projects/{project_name}", + auth=auth, verify=False, timeout=10) + if response.status_code == 200: + print(f" ✓ 删除项目: {project_name}") + return True + else: + print(f" ✗ 删除项目失败 {project_name}: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f" ✗ 删除项目出错 {project_name}: {str(e)}") + return False + +def delete_user_resources(auth, user): + """删除用户的所有资源(项目和仓库)""" + user_id = user.get("user_id") + username = user.get("username") + + print(f"正在清理用户 {username} 的资源...") + + # 获取用户拥有的项目 + projects = get_user_projects(auth, user_id) + if not projects: + print(f" 用户 {username} 没有拥有的项目") + return True + + print(f" 发现 {len(projects)} 个项目需要删除") + + success = True + for project in projects: + project_name = project.get("name") + print(f" 处理项目: {project_name}") + + # 获取项目下的仓库 + repositories = get_project_repositories(auth, project_name) + + # 删除所有仓库 + for repo in repositories: + repo_name = repo.get("name") + if not delete_repository(auth, project_name, repo_name): + success = False + + # 删除项目 + if not delete_project(auth, project_name): + success = False + + return success + +def delete_harbor_user(auth, username): + """删除Harbor用户及其所有资源""" + # 获取用户信息 + user = get_user_by_username(auth, username) + if user is None: + print(f"! 用户不存在: {username}") + return False + + user_id = user.get("user_id") + print(f"开始删除用户: {username} (ID: {user_id})") + + # 1. 先删除用户的所有资源 + if not delete_user_resources(auth, user): + print(f"! 清理用户 {username} 的资源时遇到问题,但继续尝试删除用户") + + # 2. 删除用户 + try: + response = requests.delete(f"{API_BASE}/users/{user_id}", + auth=auth, verify=False, timeout=10) + if response.status_code == 200: + print(f"✓ 成功删除用户: {username}") + return True + elif response.status_code == 404: + print(f"! 用户不存在: {username}") + return False + elif response.status_code == 412: + print(f"! 无法删除用户 {username}:用户仍有关联资源") + return False + else: + print(f"✗ 删除用户失败 {username}: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f"✗ 删除用户出错 {username}: {str(e)}") + return False + +def delete_users_from_excel(): + """从Excel文件中读取用户并删除""" + try: + # 读取Excel文件,跳过第一行(标题行) + df = pd.read_excel('users.xlsx', skiprows=1) + + # 获取管理员凭据 + admin_username, admin_password = get_admin_credentials() + + # 创建认证对象 + auth = HTTPBasicAuth(admin_username, admin_password) + + # 测试API连接 + if not test_api_connection(auth): + return + + print("\n开始删除用户...") + success_count = 0 + total_count = 0 + failed_users = [] + + # 遍历Excel中的用户并删除 + for _, row in df.iterrows(): + username = str(row.iloc[0]).strip() + + # 跳过空行 + if username == 'nan' or not username: + continue + + total_count += 1 + print(f"\n[{total_count}] 处理用户: {username}") + + if delete_harbor_user(auth, username): + success_count += 1 + else: + failed_users.append(username) + + print(f"\n" + "="*50) + print(f"删除完成!") + print(f"成功删除: {success_count}/{total_count} 个用户") + + if failed_users: + print(f"删除失败的用户: {', '.join(failed_users)}") + + except FileNotFoundError: + print("错误:找不到 users.xlsx 文件,请确保文件在当前目录中") + except Exception as e: + print(f"发生错误: {e}") + +def main(): + print("Harbor 用户删除工具") + print("=" * 30) + print("注意:此工具会删除用户及其所有项目和仓库!") + + confirm = input("\n确认要继续吗?(y/N): ").strip().lower() + if confirm != 'y': + print("已取消操作") + return + + delete_users_from_excel() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/register.py b/register.py new file mode 100644 index 0000000..8f08cb2 --- /dev/null +++ b/register.py @@ -0,0 +1,98 @@ +import pandas as pd +import json +import requests +import getpass +import sys +from urllib3.exceptions import InsecureRequestWarning +from requests.auth import HTTPBasicAuth + +# 禁用不安全HTTPS警告 +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + +HARBOR_URL = "https://harbor.seahi.me" + +def get_admin_credentials(): + print("请输入Harbor管理员账号信息:") + username = input("用户名: ") + password = getpass.getpass("密码: ") + return username, password + +def create_harbor_user(auth, user_data): + """创建Harbor用户""" + url = f"{HARBOR_URL}/api/v2.0/users" + headers = { + 'Content-Type': 'application/json' + } + payload = { + "username": user_data["username"], + "email": user_data["email"], + "realname": user_data["realname"], + "password": user_data["password"], + } + + try: + response = requests.post(url, json=payload, headers=headers, auth=auth, verify=False) + if response.status_code == 201: + print(f"✓ 成功创建用户: {user_data['username']}") + return True + elif response.status_code == 409: + print(f"! 用户已存在: {user_data['username']}") + return True + else: + print(f"✗ 创建用户失败 {user_data['username']}: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f"✗ 创建用户出错 {user_data['username']}: {str(e)}") + return False + +def convert_excel_to_json(): + try: + # 读取Excel文件,跳过第一行(标题行) + df = pd.read_excel('users.xlsx', skiprows=1) + + # 获取管理员凭据 + admin_username, admin_password = get_admin_credentials() + + # 创建认证对象 + auth = HTTPBasicAuth(admin_username, admin_password) + + # 测试认证 + test_response = requests.get(f"{HARBOR_URL}/api/v2.0/users", auth=auth, verify=False) + if test_response.status_code == 401: + print("认证失败:用户名或密码错误") + return + + print("\n开始创建用户...") + success_count = 0 + total_count = 0 + + # 将DataFrame转换为JSON格式并创建用户 + for _, row in df.iterrows(): + # 确保所有值都转换为字符串,并处理 NaN 值 + username = str(row.iloc[0]).strip() + realname = str(row.iloc[1]).strip() + password = str(row.iloc[2]).strip() + email = str(row.iloc[4]).strip() + + # 跳过空行 + if username == 'nan' or realname == 'nan': + continue + + user_dict = { + "username": username, + "realname": realname, + "password": password, + "email": email + } + + total_count += 1 + if create_harbor_user(auth, user_dict): + success_count += 1 + + print(f"\n完成!成功创建 {success_count}/{total_count} 个用户") + + except Exception as e: + print(f"发生错误: {e}") + +if __name__ == "__main__": + convert_excel_to_json() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..19d7331 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pandas>=1.3.0 +requests>=2.26.0 +urllib3>=1.26.7 +openpyxl>=3.0.9