├── .dockerignore ├── .gitattributes ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── backend ├── .env.example ├── .gitignore ├── cache.py ├── client.py ├── debug_env.py ├── endpoints │ ├── __init__.py │ ├── config_file.py │ ├── configure.py │ ├── generate.py │ ├── graphql.py │ ├── image.py │ ├── poweroff.py │ ├── reboot.py │ ├── reset.py │ ├── retrieve.py │ └── show.py ├── example.py ├── main.py ├── requirements.txt ├── start.sh ├── static │ └── .gitkeep ├── templates │ ├── dashboard.html │ └── index.html └── utils.py ├── container ├── backend │ └── Containerfile ├── env_file_compose.yaml ├── environment_vars_compose.yaml └── frontend │ └── Containerfile ├── docs ├── README-container.md ├── README-routing.md └── VyosAPIINDEX.md ├── frontend ├── .env-example ├── .gitignore ├── SETUP.md ├── app │ ├── advanced │ │ └── page.tsx │ ├── containers │ │ └── page.tsx │ ├── dashboard │ │ ├── cache │ │ │ └── page.tsx │ │ └── page.tsx │ ├── firewall │ │ ├── groups │ │ │ └── page.tsx │ │ └── page.tsx │ ├── globals.css │ ├── interfaces │ │ └── page.tsx │ ├── layout.tsx │ ├── nat │ │ └── page.tsx │ ├── page.tsx │ ├── power │ │ ├── poweroff │ │ │ └── page.tsx │ │ └── reboot │ │ │ └── page.tsx │ ├── routing │ │ └── page.tsx │ ├── services │ │ ├── dhcp │ │ │ └── page.tsx │ │ ├── https │ │ │ └── page.tsx │ │ ├── ntp │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── ssh │ │ │ └── page.tsx │ ├── system │ │ └── page.tsx │ ├── utils.tsx │ └── vpn │ │ └── page.tsx ├── components.json ├── components │ ├── cache │ │ ├── dashboard.tsx │ │ └── init-loading.tsx │ ├── config-display.tsx │ ├── config-editor.tsx │ ├── config-tree.tsx │ ├── interfaces-list.tsx │ ├── providers │ │ ├── cache-provider.tsx │ │ └── index.tsx │ ├── status-badge.tsx │ ├── theme-provider.tsx │ ├── theme-toggle.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── progress.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── skeleton.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── example-data │ └── config.json ├── hooks │ └── use-cached-data.ts ├── lib │ ├── api.ts │ ├── cache-init.ts │ ├── cache.ts │ ├── hooks │ │ └── use-cached-data.ts │ └── utils.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── robots.txt ├── start.sh ├── tailwind.config.js └── tsconfig.json ├── image.png ├── index.html └── nginx.conf.example /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | .github 5 | 6 | # Docker 7 | .docker 8 | docker-compose*.yml 9 | Dockerfile* 10 | .dockerignore 11 | 12 | # Node.js 13 | frontend/node_modules 14 | frontend/.pnp 15 | frontend/.pnp.js 16 | frontend/npm-debug.log* 17 | frontend/yarn-debug.log* 18 | frontend/yarn-error.log* 19 | frontend/.npm 20 | 21 | # Python 22 | __pycache__/ 23 | *.py[cod] 24 | *$py.class 25 | *.so 26 | .Python 27 | venv/ 28 | ENV/ 29 | env/ 30 | .venv 31 | .pytest_cache/ 32 | .coverage 33 | htmlcov/ 34 | .tox/ 35 | 36 | # Environment 37 | .env 38 | !.env.example 39 | 40 | # Documentation 41 | README*.md 42 | LICENSE 43 | CODE_OF_CONDUCT.md 44 | 45 | # Build outputs 46 | frontend/.next/ 47 | frontend/out/ 48 | 49 | # Development/IDE specific 50 | .vscode/ 51 | .idea/ 52 | *.sublime-project 53 | *.sublime-workspace 54 | .DS_Store 55 | Thumbs.db -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Convert everything to LF endings (to prevent issues on Windows deployments, which uses CRLF) 2 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: MydsiIversen 4 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: MadsZBC 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | Remember you can also give logs! 40 | https://github.com/Community-VyProjects/VyManager/wiki 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: MadsZBC 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Commands in question** 20 | Any related commands and/or system details from Vyos would be helpfull. Take a look at Wiki https://github.com/Community-VyProjects/VyManager/wiki 21 | 22 | 23 | **Additional context** 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environment files 2 | /.env 3 | /.env.* 4 | !/.env.example 5 | 6 | # Python artifacts 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | *.so 11 | .Python 12 | venv/ 13 | ENV/ 14 | env/ 15 | .venv 16 | .pytest_cache/ 17 | .coverage 18 | htmlcov/ 19 | .tox/ 20 | 21 | # Node/Next.js artifacts 22 | node_modules/ 23 | .next/ 24 | out/ 25 | .npm 26 | .yarn/ 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # Docker artifacts 32 | .docker/ 33 | docker-compose.override.yml 34 | docker-volumes/ 35 | 36 | # System files 37 | .DS_Store 38 | Thumbs.db 39 | *.swp 40 | *.swo 41 | .idea/ 42 | .vscode/ 43 | *.sublime-project 44 | *.sublime-workspace 45 | 46 | # Project-specific 47 | /static.zip 48 | /static -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | mydsi@mydsi.cc. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VyManager 2 | 3 | Modern web interface to make configuring, deploying and monitoring VyOS routers easier 4 | 5 | ## VyOS Version Support 6 | Currently being developed for: 7 | - VyOS 1.4-sagitta **(full)** 8 | - VyOS 1.5-circinus **(partial)** 9 | 10 | **[Skip to Configuration and Installation](https://github.com/Community-VyProjects/VyManager#configuration)** 11 | 12 | **Feel free to add a star ⭐ to our project if you like to use it!** 13 | 14 | [💭 Feel free to join our official Discord community](https://discord.gg/k9SSkK7wPQ) 15 | 16 | **[Live Demo (limited uptime during some periods)](https://vymanager.vyprojects.org)** 17 | 18 | ![alt text](image.png) 19 | 20 | --- 21 | 22 | ## Features 23 | 24 | - **Dashboard Overview**: View system info, interfaces, and services at a glance 25 | - **Configuration Management**: Browse and edit VyOS configurations through a user-friendly interface 26 | - **Interface Management**: See details of all network interfaces and edit their properties 27 | - **Firewall Management**: Configure firewall rules, policies, and address groups 28 | - **Routing**: Manage static routes, dynamic routing protocols, and view routing tables 29 | - **NAT**: Configure source and destination NAT rules 30 | - **VPN**: Manage VPN configurations and monitor connections 31 | - **Services**: Configure DHCP, DNS, NTP, and SSH services 32 | - **Containers**: Manage and monitor containers (Podman) 33 | - **Modern UI**: Built with Next.js, React, TypeScript, and Tailwind CSS for a responsive experience 34 | - **Dark Mode**: Optimized dark interface for reduced eye strain 35 | 36 | ## Architecture 37 | 38 | This project consists of two main components: 39 | 40 | 1. **Backend API**: Python-based FastAPI application that interfaces with VyOS CLI to manage configurations 41 | 2. **Frontend**: Next.js application built with React, TypeScript, and Shadcn UI components 42 | 43 | ## Prerequisites 44 | 45 | - Node.js 18+ for the frontend (only for manual install) 46 | - Python 3.11+ for the backend (only for manual install) 47 | - VyOS router with API access enabled (see configuration for securely enabling API access) 48 | - Docker and Docker Compose or Podman and Podman Compose (optional, for containerized deployment) 49 | 50 | --- 51 | 52 | ## Configuration 53 | 54 | Before you start, ensure you're connected to the VyOS router via the terminal/shell. You need to do follow both VyOS router setup and Environment values. 55 | 56 | ### Step 1) Setup VyOS routers: 57 | Setup the HTTPS REST API in your VyOS router(s), using the following CLI commands: 58 | 59 | 1. Start configuration mode: 60 | ``` conf ``` 61 | 62 | 2. Create a HTTPS key: 63 | >💡Security Notice: replace KEY with a really secure key, it's like a password! You will need to enter this password in your backend .env file in the next steps! 64 | ``` set service https api keys id fastapi key KEY ``` 65 | 66 | 3. (only required on VyOS 1.5 and above) Enable the REST functionality: 67 | ``` set service https api rest ``` 68 | 69 | 4. (optional) Enable GraphQL functionality: 70 | ``` set service https api graphql ``` 71 | 72 | 5. Save your changes in CLI (run these **two** commands chronologically): 73 | ``` commit ```, then ``` save ``` 74 | 75 | 76 | ### Step 2) Environment values: 77 | Next you will need to configure your environment configuration files, make sure you configure both .env files in /frontend and /backend! 78 | For each one, you can find an example .env configuration file in the belonging directories. 79 | 80 | 1) Configuration in /backend path: 81 | Create a `.env` file in the root directory with the following configuration: 82 | 83 | ``` 84 | VYOS_HOST=your-vyos-router-ip 85 | VYOS_API_KEY=your-api-key 86 | VYOS_HTTPS=true 87 | TRUST_SELF_SIGNED=true # For self-signed certificates 88 | ENVIRONMENT=production # or development 89 | ``` 90 | 91 | 2) Configuration in /frontend directory: 92 | Create a `.env` file in the /frontend directory with the following configuration: 93 | ``` 94 | NEXT_PUBLIC_API_URL=http://localhost:3001 95 | ``` 96 | 97 | --- 98 | 99 | ## Installation 100 | 101 | ### Using Containers (Recommended) 102 | 103 | The easiest way to run the application is using either Docker Compose or Podman Compose, for such make sure you have both docker and docker-compose or podman and podman-compose installed: 104 | 105 | #### Docker 106 | ```bash 107 | # Create a .env file with your VyOS router configuration 108 | # See .env.example for required variables 109 | 110 | cd container 111 | 112 | # Build and start the container 113 | docker-compose -f env_file_compose.yaml up -d 114 | 115 | # View logs 116 | docker-compose -f env_file_compose.yaml logs -f 117 | ``` 118 | #### Podman 119 | ```bash 120 | # Create a .env file with your VyOS router configuration 121 | # See .env.example for required variables 122 | 123 | cd container 124 | 125 | # Build and start the container 126 | podman compose -f env_file_compose.yaml up -d 127 | 128 | # View logs 129 | podman compose -f env_file_compose.yaml logs -f 130 | ``` 131 | 132 | For more detailed Docker and Podman instructions, see [README-docker.md](docs/README-container.md). 133 | 134 | ### Manual Installation 135 | 136 | #### Backend 137 | 138 | ```bash 139 | # Install Python dependencies 140 | pip install -r requirements.txt 141 | 142 | # Configure your VyOS connection in .env file 143 | # See .env.example for required variables 144 | 145 | # Run the backend server 146 | uvicorn main:app --host 0.0.0.0 --port 3001 147 | ``` 148 | 149 | #### Frontend 150 | 151 | ```bash 152 | # Navigate to the frontend directory 153 | cd frontend 154 | 155 | # Install dependencies 156 | npm install 157 | 158 | # Start the development server 159 | npm run dev 160 | 161 | # Build for production 162 | npm run build 163 | npm start 164 | ``` 165 | 166 | ## Accessing the Application 167 | 168 | - Frontend: http://localhost:3000 169 | - Backend API: http://localhost:3001 170 | 171 | - Frontend (Development mode - live refresh): http://localhost:8005 172 | 173 | ## License 174 | 175 | This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for details. 176 | 177 | ## Contributing 178 | 179 | Contributions are welcome! Please feel free to submit a Pull Request. 180 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # VyOS API Configuration 2 | VYOS_HOST=IP 3 | VYOS_API_KEY=KEY 4 | VYOS_HTTPS=true 5 | # VYOS_PORT=443 # Uncomment to specify a non-default port 6 | # VYOS_API_URL=https://vyos-router.example.com/retrieve # Optional, only needed for compatibility with old code 7 | 8 | # SSL certificate settings 9 | # CERT_PATH=/path/to/certificate.pem # Optional, path to custom certificate 10 | TRUST_SELF_SIGNED=true # Set to true to accept self-signed certificates (not recommended for production) 11 | 12 | # Application settings 13 | ENVIRONMENT=development # Set to 'production' for production mode 14 | 15 | # Security 16 | CERT_PATH= # For custom certificate path, leave empty to use system certs 17 | 18 | # Application Settings 19 | BACKEND_PORT=3001 20 | HOST=0.0.0.0 21 | WORKERS=4 # Number of worker processes for unicorn (typically 2-4 times the number of CPU cores) -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Environment files 2 | /.env 3 | /.env.* 4 | !/.env.example 5 | 6 | # Python artifacts 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | *.so 11 | .Python 12 | venv/ 13 | ENV/ 14 | env/ 15 | .venv 16 | .pytest_cache/ 17 | .coverage 18 | htmlcov/ 19 | .tox/ 20 | 21 | # Node/Next.js artifacts 22 | node_modules/ 23 | .next/ 24 | out/ 25 | .npm 26 | .yarn/ 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # Docker artifacts 32 | .docker/ 33 | docker-compose.override.yml 34 | docker-volumes/ 35 | 36 | # System files 37 | .DS_Store 38 | Thumbs.db 39 | *.swp 40 | *.swo 41 | .idea/ 42 | .vscode/ 43 | *.sublime-project 44 | *.sublime-workspace 45 | 46 | # Project-specific 47 | /static.zip 48 | # /static -------------------------------------------------------------------------------- /backend/cache.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import functools 4 | from typing import Dict, Any, Optional, Callable, Tuple, Union, TypeVar, cast 5 | from datetime import datetime, timedelta 6 | 7 | T = TypeVar('T') 8 | 9 | class Cache: 10 | """ 11 | A simple in-memory cache with TTL support. 12 | """ 13 | _instance = None 14 | 15 | def __new__(cls): 16 | if cls._instance is None: 17 | cls._instance = super(Cache, cls).__new__(cls) 18 | cls._instance._init() 19 | return cls._instance 20 | 21 | def _init(self): 22 | self._cache: Dict[str, Tuple[Any, float]] = {} 23 | self._hit_count = 0 24 | self._miss_count = 0 25 | self._creation_time = time.time() 26 | 27 | def get(self, key: str) -> Optional[Any]: 28 | """ 29 | Get a value from the cache. 30 | 31 | Args: 32 | key: Cache key 33 | 34 | Returns: 35 | Cached value or None if not found or expired 36 | """ 37 | if key not in self._cache: 38 | self._miss_count += 1 39 | return None 40 | 41 | value, expiry = self._cache[key] 42 | 43 | # Check if the value has expired 44 | if expiry < time.time(): 45 | self._miss_count += 1 46 | del self._cache[key] 47 | return None 48 | 49 | self._hit_count += 1 50 | return value 51 | 52 | def set(self, key: str, value: Any, ttl: int = 60) -> None: 53 | """ 54 | Set a value in the cache. 55 | 56 | Args: 57 | key: Cache key 58 | value: Value to cache 59 | ttl: Time to live in seconds (default: 60) 60 | """ 61 | expiry = time.time() + ttl 62 | self._cache[key] = (value, expiry) 63 | 64 | def delete(self, key: str) -> bool: 65 | """ 66 | Delete a value from the cache. 67 | 68 | Args: 69 | key: Cache key 70 | 71 | Returns: 72 | True if the key was deleted, False otherwise 73 | """ 74 | if key in self._cache: 75 | del self._cache[key] 76 | return True 77 | return False 78 | 79 | def clear(self) -> None: 80 | """Clear all cached values.""" 81 | self._cache.clear() 82 | 83 | def delete_pattern(self, pattern: str) -> int: 84 | """ 85 | Delete all keys matching a pattern. 86 | 87 | Args: 88 | pattern: Pattern to match 89 | 90 | Returns: 91 | Number of keys deleted 92 | """ 93 | keys_to_delete = [k for k in self._cache.keys() if pattern in k] 94 | for key in keys_to_delete: 95 | del self._cache[key] 96 | return len(keys_to_delete) 97 | 98 | def stats(self) -> Dict[str, Any]: 99 | """ 100 | Get cache statistics. 101 | 102 | Returns: 103 | Dictionary with cache statistics 104 | """ 105 | total_requests = self._hit_count + self._miss_count 106 | hit_rate = self._hit_count / total_requests if total_requests > 0 else 0 107 | 108 | return { 109 | "items": len(self._cache), 110 | "hits": self._hit_count, 111 | "misses": self._miss_count, 112 | "hit_rate": hit_rate, 113 | "uptime": time.time() - self._creation_time 114 | } 115 | 116 | # Create a singleton cache instance 117 | cache = Cache() 118 | 119 | def cached(ttl: int = 60, key_prefix: str = "") -> Callable[[Callable[..., T]], Callable[..., T]]: 120 | """ 121 | Decorator for caching function results. 122 | 123 | Args: 124 | ttl: Time to live in seconds (default: 60) 125 | key_prefix: Prefix for the cache key (default: "") 126 | 127 | Returns: 128 | Decorated function 129 | """ 130 | def decorator(func: Callable[..., T]) -> Callable[..., T]: 131 | @functools.wraps(func) 132 | async def async_wrapper(*args: Any, **kwargs: Any) -> T: 133 | # Create a unique key based on the function name, args, and kwargs 134 | key_parts = [key_prefix or func.__name__] 135 | 136 | # Add positional arguments to the key 137 | if args: 138 | key_parts.append("_".join(str(arg) for arg in args)) 139 | 140 | # Add keyword arguments to the key (sorted to ensure consistent order) 141 | if kwargs: 142 | key_parts.append("_".join(f"{k}={v}" for k, v in sorted(kwargs.items()))) 143 | 144 | # Create the final key 145 | cache_key = ":".join(key_parts) 146 | 147 | # Try to get the value from the cache 148 | cached_value = cache.get(cache_key) 149 | if cached_value is not None: 150 | return cached_value 151 | 152 | # Call the function and cache the result 153 | result = await func(*args, **kwargs) 154 | cache.set(cache_key, result, ttl) 155 | return result 156 | 157 | @functools.wraps(func) 158 | def sync_wrapper(*args: Any, **kwargs: Any) -> T: 159 | # Create a unique key based on the function name, args, and kwargs 160 | key_parts = [key_prefix or func.__name__] 161 | 162 | # Add positional arguments to the key 163 | if args: 164 | key_parts.append("_".join(str(arg) for arg in args)) 165 | 166 | # Add keyword arguments to the key (sorted to ensure consistent order) 167 | if kwargs: 168 | key_parts.append("_".join(f"{k}={v}" for k, v in sorted(kwargs.items()))) 169 | 170 | # Create the final key 171 | cache_key = ":".join(key_parts) 172 | 173 | # Try to get the value from the cache 174 | cached_value = cache.get(cache_key) 175 | if cached_value is not None: 176 | return cached_value 177 | 178 | # Call the function and cache the result 179 | result = func(*args, **kwargs) 180 | cache.set(cache_key, result, ttl) 181 | return result 182 | 183 | # Return the appropriate wrapper based on whether the function is async or not 184 | if asyncio.iscoroutinefunction(func): 185 | return cast(Callable[..., T], async_wrapper) 186 | else: 187 | return cast(Callable[..., T], sync_wrapper) 188 | 189 | return decorator 190 | 191 | def invalidate_cache(pattern: str = "") -> None: 192 | """ 193 | Invalidate cache entries matching a pattern. 194 | 195 | Args: 196 | pattern: Pattern to match (default: "" which matches all keys) 197 | """ 198 | if pattern: 199 | cache.delete_pattern(pattern) 200 | else: 201 | cache.clear() 202 | 203 | import asyncio -------------------------------------------------------------------------------- /backend/debug_env.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from dotenv import load_dotenv 4 | 5 | # Load environment variables from .env file 6 | print("Loading environment variables from .env file...") 7 | load_dotenv() 8 | 9 | # Print environment variables 10 | print(f"VYOS_API_KEY: {os.getenv('VYOS_API_KEY')}") 11 | print(f"VYOS_HOST: {os.getenv('VYOS_HOST')}") 12 | print(f"VYOS_API_URL: {os.getenv('VYOS_API_URL')}") 13 | print(f"VYOS_HTTPS: {os.getenv('VYOS_HTTPS')}") 14 | print(f"TRUST_SELF_SIGNED: {os.getenv('TRUST_SELF_SIGNED')}") 15 | 16 | # Check if .env file exists 17 | import pathlib 18 | current_dir = pathlib.Path(__file__).parent 19 | env_file = current_dir / ".env" 20 | print(f".env file exists: {env_file.exists()}") 21 | 22 | if env_file.exists(): 23 | print("Contents of .env file:") 24 | with open(env_file, "r") as f: 25 | print(f.read()) 26 | 27 | print("Direct environment variables (not from .env):") 28 | print(f"VYOS_API_KEY from os.environ: {os.environ.get('VYOS_API_KEY')}") -------------------------------------------------------------------------------- /backend/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | # Endpoints package for VyOS API wrapper -------------------------------------------------------------------------------- /backend/endpoints/config_file.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any, Optional 2 | from utils import PathBuilder, make_api_request 3 | 4 | 5 | class ConfigFileEndpoint(PathBuilder): 6 | """ 7 | Endpoint handler for the /config-file endpoint. 8 | 9 | This endpoint is used to save or load configuration files. 10 | """ 11 | 12 | def __init__(self, client, path: List[str] = None): 13 | """ 14 | Initialize a new ConfigFileEndpoint. 15 | 16 | Args: 17 | client: The parent VyOSClient instance 18 | path: Optional initial path elements 19 | """ 20 | super().__init__(client, path) 21 | self.endpoint = "/config-file" 22 | 23 | async def save(self, file: Optional[str] = None) -> Dict[str, Any]: 24 | """ 25 | Save the running configuration to a file. 26 | 27 | Args: 28 | file: The path of the file to save to (optional, default is /config/config.boot) 29 | 30 | Returns: 31 | The API response 32 | """ 33 | # Prepare data for API request 34 | data = { 35 | "op": "save" 36 | } 37 | 38 | # Add the file path if provided 39 | if file: 40 | data["file"] = file 41 | 42 | # Make the API request 43 | return await make_api_request(self._client, self.endpoint, data) 44 | 45 | async def load(self, file: str) -> Dict[str, Any]: 46 | """ 47 | Load a configuration file. 48 | 49 | Args: 50 | file: The path of the file to load 51 | 52 | Returns: 53 | The API response 54 | """ 55 | # Prepare data for API request 56 | data = { 57 | "op": "load", 58 | "file": file 59 | } 60 | 61 | # Make the API request 62 | return await make_api_request(self._client, self.endpoint, data) 63 | 64 | async def __call__(self, operation: str, file: Optional[str] = None) -> Dict[str, Any]: 65 | """ 66 | Execute a config-file operation. 67 | 68 | Args: 69 | operation: The operation to perform ('save' or 'load') 70 | file: The path of the file to save to or load from 71 | 72 | Returns: 73 | The API response 74 | """ 75 | if operation.lower() == "save": 76 | return await self.save(file) 77 | elif operation.lower() == "load": 78 | if not file: 79 | raise ValueError("File path is required for 'load' operation") 80 | return await self.load(file) 81 | else: 82 | raise ValueError(f"Invalid config-file operation: {operation}. Must be 'save' or 'load'.") -------------------------------------------------------------------------------- /backend/endpoints/configure.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any, Union 2 | from utils import PathBuilder, make_api_request 3 | 4 | 5 | class ConfigureOperation(PathBuilder): 6 | """ 7 | Base class for configure operations (set, delete, comment). 8 | 9 | This class is not meant to be used directly, but extended by specific 10 | operation classes. 11 | """ 12 | 13 | def __init__(self, client, operation: str, path: List[str] = None): 14 | """ 15 | Initialize a new ConfigureOperation. 16 | 17 | Args: 18 | client: The parent VyOSClient instance 19 | operation: The operation type ('set', 'delete', or 'comment') 20 | path: Optional initial path elements 21 | """ 22 | super().__init__(client, path) 23 | self.endpoint = "/configure" 24 | self.operation = operation 25 | 26 | async def __call__(self, *args) -> Dict[str, Any]: 27 | """ 28 | Execute a configure operation with the current path. 29 | 30 | Args: 31 | *args: Additional path elements or values to append 32 | 33 | Returns: 34 | The API response 35 | """ 36 | # Build the full path 37 | path = self._path.copy() 38 | 39 | # Append any additional arguments as path elements 40 | for arg in args: 41 | if arg is not None: 42 | path.append(str(arg)) 43 | 44 | # Prepare data for API request 45 | data = { 46 | "op": self.operation, 47 | "path": path 48 | } 49 | 50 | # Make the API request 51 | return await make_api_request(self._client, self.endpoint, data) 52 | 53 | 54 | class ConfigureBatchOperation: 55 | """ 56 | Class to handle batch configuration operations. 57 | """ 58 | 59 | def __init__(self, client): 60 | """ 61 | Initialize a new ConfigureBatchOperation. 62 | 63 | Args: 64 | client: The parent VyOSClient instance 65 | """ 66 | self._client = client 67 | self._operations = [] 68 | 69 | def add_operation(self, operation: str, path: List[str]) -> 'ConfigureBatchOperation': 70 | """ 71 | Add an operation to the batch. 72 | 73 | Args: 74 | operation: The operation type ('set', 'delete', or 'comment') 75 | path: The configuration path 76 | 77 | Returns: 78 | The ConfigureBatchOperation instance for chaining 79 | """ 80 | self._operations.append({ 81 | "op": operation, 82 | "path": path 83 | }) 84 | return self 85 | 86 | def set(self, path: List[str]) -> 'ConfigureBatchOperation': 87 | """ 88 | Add a 'set' operation to the batch. 89 | 90 | Args: 91 | path: The configuration path 92 | 93 | Returns: 94 | The ConfigureBatchOperation instance for chaining 95 | """ 96 | return self.add_operation("set", path) 97 | 98 | def delete(self, path: List[str]) -> 'ConfigureBatchOperation': 99 | """ 100 | Add a 'delete' operation to the batch. 101 | 102 | Args: 103 | path: The configuration path 104 | 105 | Returns: 106 | The ConfigureBatchOperation instance for chaining 107 | """ 108 | return self.add_operation("delete", path) 109 | 110 | def comment(self, path: List[str]) -> 'ConfigureBatchOperation': 111 | """ 112 | Add a 'comment' operation to the batch. 113 | 114 | Args: 115 | path: The configuration path 116 | 117 | Returns: 118 | The ConfigureBatchOperation instance for chaining 119 | """ 120 | return self.add_operation("comment", path) 121 | 122 | async def execute(self) -> Dict[str, Any]: 123 | """ 124 | Execute all operations in the batch. 125 | 126 | Returns: 127 | The API response 128 | """ 129 | # Make the API request with the batched operations 130 | return await make_api_request(self._client, "/configure", self._operations) 131 | 132 | 133 | class SetOperation(ConfigureOperation): 134 | """Endpoint handler for the 'set' operation on the /configure endpoint.""" 135 | 136 | def __init__(self, client, path: List[str] = None): 137 | """Initialize a new SetOperation.""" 138 | super().__init__(client, "set", path) 139 | 140 | 141 | class DeleteOperation(ConfigureOperation): 142 | """Endpoint handler for the 'delete' operation on the /configure endpoint.""" 143 | 144 | def __init__(self, client, path: List[str] = None): 145 | """Initialize a new DeleteOperation.""" 146 | super().__init__(client, "delete", path) 147 | 148 | 149 | class CommentOperation(ConfigureOperation): 150 | """Endpoint handler for the 'comment' operation on the /configure endpoint.""" 151 | 152 | def __init__(self, client, path: List[str] = None): 153 | """Initialize a new CommentOperation.""" 154 | super().__init__(client, "comment", path) 155 | 156 | 157 | class ConfigureEndpoint(PathBuilder): 158 | """ 159 | Endpoint handler for the /configure endpoint. 160 | 161 | This endpoint is used to make configuration changes on VyOS. 162 | It supports 'set', 'delete', and 'comment' operations, as well as 163 | batch operations. 164 | """ 165 | 166 | def __init__(self, client, path: List[str] = None): 167 | """ 168 | Initialize a new ConfigureEndpoint. 169 | 170 | Args: 171 | client: The parent VyOSClient instance 172 | path: Optional initial path elements 173 | """ 174 | super().__init__(client, path) 175 | self.endpoint = "/configure" 176 | 177 | # Initialize operation handlers 178 | self.set = SetOperation(client) 179 | self.delete = DeleteOperation(client) 180 | self.comment = CommentOperation(client) 181 | 182 | def batch(self) -> ConfigureBatchOperation: 183 | """ 184 | Create a new batch operation. 185 | 186 | Returns: 187 | A new ConfigureBatchOperation instance 188 | """ 189 | return ConfigureBatchOperation(self._client) 190 | 191 | async def __call__(self, 192 | operations: List[Dict[str, Union[str, List[str]]]]) -> Dict[str, Any]: 193 | """ 194 | Execute a batch of configure operations. 195 | 196 | Args: 197 | operations: A list of operation dictionaries, each with 'op' and 'path' keys 198 | 199 | Returns: 200 | The API response 201 | """ 202 | # Make the API request with the provided operations 203 | return await make_api_request(self._client, self.endpoint, operations) -------------------------------------------------------------------------------- /backend/endpoints/generate.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any 2 | from utils import PathBuilder, make_api_request 3 | 4 | 5 | class GenerateEndpoint(PathBuilder): 6 | """ 7 | Endpoint handler for the /generate endpoint. 8 | 9 | This endpoint is used to generate various resources on VyOS, 10 | such as PKI keys, certificates, etc. 11 | """ 12 | 13 | def __init__(self, client, path: List[str] = None): 14 | """ 15 | Initialize a new GenerateEndpoint. 16 | 17 | Args: 18 | client: The parent VyOSClient instance 19 | path: Optional initial path elements 20 | """ 21 | super().__init__(client, path) 22 | self.endpoint = "/generate" 23 | 24 | async def __call__(self, *args) -> Dict[str, Any]: 25 | """ 26 | Execute a generate operation with the current path. 27 | 28 | Args: 29 | *args: Additional path elements to append 30 | 31 | Returns: 32 | The API response 33 | """ 34 | # Build the full path 35 | path = self._path.copy() 36 | 37 | # Append any additional arguments as path elements 38 | for arg in args: 39 | if arg is not None: 40 | path.append(str(arg)) 41 | 42 | # Prepare data for API request 43 | data = { 44 | "op": "generate", 45 | "path": path 46 | } 47 | 48 | # Make the API request 49 | return await make_api_request(self._client, self.endpoint, data) -------------------------------------------------------------------------------- /backend/endpoints/graphql.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Optional 2 | import aiohttp 3 | from pydantic import BaseModel 4 | import json 5 | import ssl 6 | 7 | class GraphQLQuery(BaseModel): 8 | """Model for GraphQL query data""" 9 | query: str 10 | 11 | class GraphQLEndpoint: 12 | """Endpoint for handling GraphQL queries.""" 13 | 14 | def __init__(self, client): 15 | """ 16 | Initialize the GraphQL endpoint. 17 | 18 | Args: 19 | client: VyOSClient instance 20 | """ 21 | self.client = client 22 | self.base_url = f"{'https' if client.https else 'http'}://{client.host}/graphql" 23 | if client.trust_self_signed: 24 | self.ssl_context = ssl.create_default_context() 25 | self.ssl_context.check_hostname = False 26 | self.ssl_context.verify_mode = ssl.CERT_NONE 27 | else: 28 | self.ssl_context = True 29 | print(f"GraphQL endpoint initialized with base URL: {self.base_url}") 30 | 31 | async def operation(self, name: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: 32 | """ 33 | Execute a GraphQL operation. 34 | 35 | Args: 36 | name: The name of the operation (e.g., 'ShowContainerContainer', 'ShowImageContainer') 37 | data: Optional dictionary of additional data parameters (e.g., {'intf_name': 'eth0'}) 38 | 39 | Returns: 40 | Dict containing the operation response 41 | """ 42 | # Start with the required API key 43 | operation_data = {"key": self.client.api_key} 44 | 45 | # Add any additional data parameters 46 | if data: 47 | operation_data.update(data) 48 | 49 | # Convert the data dictionary to a string of key-value pairs 50 | data_str = ", ".join(f'{k}: "{v}"' for k, v in operation_data.items()) 51 | 52 | query = """ 53 | { 54 | %s(data: {%s}) { 55 | success 56 | errors 57 | data { 58 | result 59 | } 60 | } 61 | } 62 | """ % (name, data_str) 63 | return await self.query(query) 64 | 65 | async def query(self, query: str) -> Dict[str, Any]: 66 | """ 67 | Execute a GraphQL query. 68 | 69 | Args: 70 | query: The GraphQL query string 71 | 72 | Returns: 73 | Dict containing the query response 74 | """ 75 | print(f"Executing GraphQL query: {query}") 76 | 77 | headers = { 78 | 'Content-Type': 'application/json', 79 | 'X-API-Key': self.client.api_key 80 | } 81 | 82 | payload = { 83 | "query": query, 84 | "variables": {} 85 | } 86 | 87 | print(f"GraphQL request payload: {payload}") 88 | print(f"GraphQL request headers: {headers}") 89 | 90 | try: 91 | async with aiohttp.ClientSession() as session: 92 | async with session.post( 93 | self.base_url, 94 | headers=headers, 95 | json=payload, 96 | ssl=self.ssl_context 97 | ) as response: 98 | print(f"GraphQL response status: {response.status}") 99 | 100 | if response.status != 200: 101 | error_text = await response.text() 102 | print(f"GraphQL error response: {error_text}") 103 | return { 104 | "success": False, 105 | "error": f"GraphQL request failed with status {response.status}", 106 | "data": None 107 | } 108 | 109 | data = await response.json() 110 | print(f"GraphQL response data: {data}") 111 | 112 | # Check for GraphQL errors 113 | if data.get("errors"): 114 | return { 115 | "success": False, 116 | "error": str(data["errors"]), 117 | "data": None 118 | } 119 | 120 | return { 121 | "success": True, 122 | "error": None, 123 | "data": data.get("data") 124 | } 125 | except Exception as e: 126 | print(f"GraphQL query error: {str(e)}") 127 | import traceback 128 | print(f"Traceback: {traceback.format_exc()}") 129 | return { 130 | "success": False, 131 | "error": str(e), 132 | "data": None 133 | } -------------------------------------------------------------------------------- /backend/endpoints/image.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any, Optional 2 | from utils import PathBuilder, make_api_request 3 | 4 | 5 | class ImageEndpoint(PathBuilder): 6 | """ 7 | Endpoint handler for the /image endpoint. 8 | 9 | This endpoint is used to manage VyOS system images. 10 | """ 11 | 12 | def __init__(self, client, path: List[str] = None): 13 | """ 14 | Initialize a new ImageEndpoint. 15 | 16 | Args: 17 | client: The parent VyOSClient instance 18 | path: Optional initial path elements 19 | """ 20 | super().__init__(client, path) 21 | self.endpoint = "/image" 22 | 23 | async def add(self, url: str) -> Dict[str, Any]: 24 | """ 25 | Add a new VyOS image from a URL. 26 | 27 | Args: 28 | url: The URL to download the image from 29 | 30 | Returns: 31 | The API response 32 | """ 33 | # Prepare data for API request 34 | data = { 35 | "op": "add", 36 | "url": url 37 | } 38 | 39 | # Make the API request 40 | return await make_api_request(self._client, self.endpoint, data) 41 | 42 | async def delete(self, name: str) -> Dict[str, Any]: 43 | """ 44 | Delete a VyOS image by name. 45 | 46 | Args: 47 | name: The name of the image to delete 48 | 49 | Returns: 50 | The API response 51 | """ 52 | # Prepare data for API request 53 | data = { 54 | "op": "delete", 55 | "name": name 56 | } 57 | 58 | # Make the API request 59 | return await make_api_request(self._client, self.endpoint, data) 60 | 61 | async def __call__(self, operation: str, **kwargs) -> Dict[str, Any]: 62 | """ 63 | Execute an image operation. 64 | 65 | Args: 66 | operation: The operation to perform ('add' or 'delete') 67 | **kwargs: Additional parameters for the operation 68 | 69 | Returns: 70 | The API response 71 | """ 72 | if operation.lower() == "add": 73 | return await self.add(kwargs.get("url")) 74 | elif operation.lower() == "delete": 75 | return await self.delete(kwargs.get("name")) 76 | else: 77 | raise ValueError(f"Invalid image operation: {operation}. Must be 'add' or 'delete'.") -------------------------------------------------------------------------------- /backend/endpoints/poweroff.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any 2 | from utils import PathBuilder, make_api_request 3 | 4 | 5 | class PoweroffEndpoint(PathBuilder): 6 | """ 7 | Endpoint handler for the /poweroff endpoint. 8 | 9 | This endpoint is used to power off the VyOS system. 10 | """ 11 | 12 | def __init__(self, client, path: List[str] = None): 13 | """ 14 | Initialize a new PoweroffEndpoint. 15 | 16 | Args: 17 | client: The parent VyOSClient instance 18 | path: Optional initial path elements 19 | """ 20 | super().__init__(client, path) 21 | self.endpoint = "/poweroff" 22 | 23 | async def __call__(self, now: bool = True) -> Dict[str, Any]: 24 | """ 25 | Execute a poweroff operation. 26 | 27 | Args: 28 | now: Whether to power off immediately (always True for this API) 29 | 30 | Returns: 31 | The API response 32 | """ 33 | # Prepare data for API request 34 | data = { 35 | "op": "poweroff", 36 | "path": ["now"] 37 | } 38 | 39 | # Make the API request 40 | return await make_api_request(self._client, self.endpoint, data) -------------------------------------------------------------------------------- /backend/endpoints/reboot.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any 2 | from utils import PathBuilder, make_api_request 3 | 4 | 5 | class RebootEndpoint(PathBuilder): 6 | """ 7 | Endpoint handler for the /reboot endpoint. 8 | 9 | This endpoint is used to reboot the VyOS system. 10 | """ 11 | 12 | def __init__(self, client, path: List[str] = None): 13 | """ 14 | Initialize a new RebootEndpoint. 15 | 16 | Args: 17 | client: The parent VyOSClient instance 18 | path: Optional initial path elements 19 | """ 20 | super().__init__(client, path) 21 | self.endpoint = "/reboot" 22 | 23 | async def __call__(self, now: bool = True) -> Dict[str, Any]: 24 | """ 25 | Execute a reboot operation. 26 | 27 | Args: 28 | now: Whether to reboot immediately (always True for this API) 29 | 30 | Returns: 31 | The API response 32 | """ 33 | # Prepare data for API request 34 | data = { 35 | "op": "reboot", 36 | "path": ["now"] 37 | } 38 | 39 | # Make the API request 40 | return await make_api_request(self._client, self.endpoint, data) -------------------------------------------------------------------------------- /backend/endpoints/reset.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any 2 | from utils import PathBuilder, make_api_request 3 | 4 | 5 | class ResetEndpoint(PathBuilder): 6 | """ 7 | Endpoint handler for the /reset endpoint. 8 | 9 | This endpoint is used to reset various components in VyOS, 10 | such as BGP neighbors, interfaces, etc. 11 | """ 12 | 13 | def __init__(self, client, path: List[str] = None): 14 | """ 15 | Initialize a new ResetEndpoint. 16 | 17 | Args: 18 | client: The parent VyOSClient instance 19 | path: Optional initial path elements 20 | """ 21 | super().__init__(client, path) 22 | self.endpoint = "/reset" 23 | 24 | async def __call__(self, *args) -> Dict[str, Any]: 25 | """ 26 | Execute a reset operation with the current path. 27 | 28 | Args: 29 | *args: Additional path elements to append 30 | 31 | Returns: 32 | The API response 33 | """ 34 | # Build the full path 35 | path = self._path.copy() 36 | 37 | # Append any additional arguments as path elements 38 | for arg in args: 39 | if arg is not None: 40 | path.append(str(arg)) 41 | 42 | # Prepare data for API request 43 | data = { 44 | "op": "reset", 45 | "path": path 46 | } 47 | 48 | # Make the API request 49 | return await make_api_request(self._client, self.endpoint, data) -------------------------------------------------------------------------------- /backend/endpoints/retrieve.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any, Optional 2 | from utils import PathBuilder, make_api_request 3 | 4 | 5 | class RetrieveEndpoint(PathBuilder): 6 | """ 7 | Endpoint handler for the /retrieve endpoint. 8 | 9 | This endpoint is used to get configuration data from VyOS. 10 | """ 11 | 12 | def __init__(self, client, path: List[str] = None): 13 | """ 14 | Initialize a new RetrieveEndpoint. 15 | 16 | Args: 17 | client: The parent VyOSClient instance 18 | path: Optional initial path elements 19 | """ 20 | super().__init__(client, path) 21 | self.endpoint = "/retrieve" 22 | 23 | async def __call__(self, *args) -> Dict[str, Any]: 24 | """ 25 | Execute a retrieve operation with the current path. 26 | 27 | Args: 28 | *args: Additional path elements to append 29 | 30 | Returns: 31 | The API response 32 | """ 33 | # Build the full path 34 | path = self._path.copy() 35 | 36 | # Append any additional arguments as path elements 37 | for arg in args: 38 | if arg is not None: 39 | path.append(str(arg)) 40 | 41 | # Prepare data for API request 42 | data = { 43 | "op": "showConfig", 44 | "path": path 45 | } 46 | 47 | # Make the API request 48 | return await make_api_request(self._client, self.endpoint, data) 49 | 50 | async def exists(self, path: Optional[List[str]] = None) -> Dict[str, Any]: 51 | """ 52 | Check if a configuration path exists. 53 | 54 | Args: 55 | path: The path to check, or None to use the current path 56 | 57 | Returns: 58 | The API response 59 | """ 60 | # Use the provided path or the current path 61 | check_path = path if path is not None else self._path 62 | 63 | # Prepare data for API request 64 | data = { 65 | "op": "exists", 66 | "path": check_path 67 | } 68 | 69 | # Make the API request 70 | return await make_api_request(self._client, self.endpoint, data) 71 | 72 | async def return_values(self, path: Optional[List[str]] = None) -> Dict[str, Any]: 73 | """ 74 | Get the values of a multi-valued node. 75 | 76 | Args: 77 | path: The path to check, or None to use the current path 78 | 79 | Returns: 80 | The API response 81 | """ 82 | # Use the provided path or the current path 83 | values_path = path if path is not None else self._path 84 | 85 | # Prepare data for API request 86 | data = { 87 | "op": "returnValues", 88 | "path": values_path 89 | } 90 | 91 | # Make the API request 92 | return await make_api_request(self._client, self.endpoint, data) 93 | 94 | 95 | class ShowConfigEndpoint(RetrieveEndpoint): 96 | """ 97 | Specialized endpoint for the showConfig operation on the /retrieve endpoint. 98 | 99 | This is a convenience class that automatically sets the operation to "showConfig". 100 | """ 101 | 102 | def __init__(self, client, path: List[str] = None): 103 | """ 104 | Initialize a new ShowConfigEndpoint. 105 | 106 | Args: 107 | client: The parent VyOSClient instance 108 | path: Optional initial path elements 109 | """ 110 | super().__init__(client, path) 111 | # This endpoint uses the /retrieve endpoint with showConfig operation 112 | self.endpoint = "/retrieve" 113 | 114 | async def __call__(self, *args) -> Dict[str, Any]: 115 | """ 116 | Execute a showConfig operation with the current path. 117 | 118 | Args: 119 | *args: Additional path elements to append 120 | 121 | Returns: 122 | The API response 123 | """ 124 | # Reuse the parent class implementation 125 | return await super().__call__(*args) -------------------------------------------------------------------------------- /backend/endpoints/show.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any 2 | from utils import PathBuilder, make_api_request 3 | 4 | 5 | class ShowEndpoint(PathBuilder): 6 | """ 7 | Endpoint handler for the /show endpoint. 8 | 9 | This endpoint is used to execute operational mode show commands. 10 | """ 11 | 12 | def __init__(self, client, path: List[str] = None): 13 | """ 14 | Initialize a new ShowEndpoint. 15 | 16 | Args: 17 | client: The parent VyOSClient instance 18 | path: Optional initial path elements 19 | """ 20 | super().__init__(client, path) 21 | self.endpoint = "/show" 22 | 23 | async def __call__(self, *args) -> Dict[str, Any]: 24 | """ 25 | Execute a show operation with the current path. 26 | 27 | Args: 28 | *args: Additional path elements to append 29 | 30 | Returns: 31 | The API response 32 | """ 33 | # Build the full path 34 | path = self._path.copy() 35 | 36 | # Append any additional arguments as path elements 37 | for arg in args: 38 | if arg is not None: 39 | path.append(str(arg)) 40 | 41 | # Prepare data for API request 42 | data = { 43 | "op": "show", 44 | "path": path 45 | } 46 | 47 | # Make the API request 48 | return await make_api_request(self._client, self.endpoint, data) -------------------------------------------------------------------------------- /backend/example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from client import VyOSClient 4 | from utils import VyOSAPIError 5 | 6 | 7 | async def main(): 8 | # Create a client instance 9 | # Method 1: Direct instantiation 10 | vyos = VyOSClient( 11 | host="vyos-router.example.com", 12 | api_key="YOUR_API_KEY", 13 | https=True, 14 | trust_self_signed=True # Only for testing 15 | ) 16 | 17 | # Method 2: From environment variables 18 | # vyos = VyOSClient.from_env() 19 | 20 | # Method 3: From URL 21 | # vyos = VyOSClient.from_url("https://vyos-router.example.com", "YOUR_API_KEY") 22 | 23 | try: 24 | # Example 1: Get system configuration 25 | print("\n--- Example 1: Get system configuration ---") 26 | result = await vyos.showConfig.system() 27 | print_result(result) 28 | 29 | # Example 2: Check if a configuration path exists 30 | print("\n--- Example 2: Check if a configuration path exists ---") 31 | result = await vyos.retrieve.exists(["service", "https", "api"]) 32 | print_result(result) 33 | 34 | # Example 3: Get all interfaces 35 | print("\n--- Example 3: Get all interfaces ---") 36 | result = await vyos.showConfig.interfaces() 37 | print_result(result) 38 | 39 | # Example 4: Show system image 40 | print("\n--- Example 4: Show system image ---") 41 | result = await vyos.show.system.image() 42 | print_result(result) 43 | 44 | # Example 5: Configure an interface (set operation) 45 | print("\n--- Example 5: Configure an interface ---") 46 | result = await vyos.configure.set.interfaces.dummy.dum0.address("192.168.99.1/24") 47 | print_result(result) 48 | 49 | # Example 6: Delete a configuration node 50 | print("\n--- Example 6: Delete a configuration node ---") 51 | result = await vyos.configure.delete.interfaces.dummy.dum0.address("192.168.99.1/24") 52 | print_result(result) 53 | 54 | # Example 7: Batch configuration 55 | print("\n--- Example 7: Batch configuration ---") 56 | batch = vyos.configure.batch() 57 | batch.set(["interfaces", "dummy", "dum1", "address", "10.0.0.1/24"]) 58 | batch.set(["interfaces", "dummy", "dum1", "description", "Test interface"]) 59 | result = await batch.execute() 60 | print_result(result) 61 | 62 | # Example 8: Generate a wireguard key-pair 63 | print("\n--- Example 8: Generate a wireguard key-pair ---") 64 | result = await vyos.generate.pki.wireguard("key-pair") 65 | print_result(result) 66 | 67 | # Example 9: Save configuration 68 | print("\n--- Example 9: Save configuration ---") 69 | result = await vyos.config_file.save() 70 | print_result(result) 71 | 72 | except VyOSAPIError as e: 73 | print(f"API Error: {e.message}") 74 | if e.status_code: 75 | print(f"Status code: {e.status_code}") 76 | except Exception as e: 77 | print(f"Unexpected error: {str(e)}") 78 | 79 | 80 | def print_result(result): 81 | """Print the API result in a readable format.""" 82 | if isinstance(result, dict): 83 | if "data" in result: 84 | print("Success:", result.get("success", False)) 85 | if isinstance(result["data"], (dict, list)): 86 | print("Data:", json.dumps(result["data"], indent=2)) 87 | else: 88 | print("Data:", result["data"]) 89 | if result.get("error"): 90 | print("Error:", result["error"]) 91 | else: 92 | print(json.dumps(result, indent=2)) 93 | else: 94 | print(result) 95 | 96 | 97 | if __name__ == "__main__": 98 | asyncio.run(main()) -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | # Core web framework 2 | fastapi==0.115.12 3 | uvicorn==0.34.2 4 | aiohttp==3.11.18 5 | jinja2==3.1.6 6 | 7 | # Uvicorn adapters for better performance 8 | websockets==15.0.1 9 | httptools==0.6.4 10 | uvloop==0.21.0 ; sys_platform != 'win32' # uvloop doesn't support Windows 11 | 12 | # HTTP client 13 | httpx==0.28.1 14 | 15 | # Utilities 16 | python-multipart==0.0.20 17 | pydantic==2.11.4 18 | certifi==2025.4.26 19 | python-dotenv==1.0.0 -------------------------------------------------------------------------------- /backend/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BACKEND_LOG=${BACKEND_LOG:-backend.log} 4 | 5 | # Function for logging with timestamp 6 | log() { 7 | local message="[$(date +'%Y-%m-%d %H:%M:%S')] $1" 8 | echo "$message" | tee -a "$BACKEND_LOG" 9 | } 10 | 11 | # Check if .env file exists 12 | if [ ! -f .env ]; then 13 | log "WARNING: No .env file found. will use environment variables and default instead." 14 | else 15 | # Load environment variables from .env 16 | log "INFO: Reading environment variables from .env file" 17 | export $(grep -v '^#' .env | grep -E '^\s*[A-Za-z_][A-Za-z0-9_]*=' | xargs) 18 | fi 19 | 20 | # Default values if not provided in .env 21 | BACKEND_PORT=${BACKEND_PORT:-3001} 22 | HOST=${HOST:-0.0.0.0} 23 | WORKERS=${WORKERS:-1} # Default to 1 worker to avoid duplicate processes 24 | LOG_LEVEL=${LOG_LEVEL:-info} 25 | ENVIRONMENT=${ENVIRONMENT:-development} 26 | BACKEND_LOG=${BACKEND_LOG:-backend.log} 27 | 28 | # Check if python is installed and version is 3.8 or higher 29 | python_version=$(python --version | grep -oP '\d+\.\d+') 30 | python_major_version=$(echo "$python_version" | cut -d '.' -f 1) 31 | python_minor_version=$(echo "$python_version" | cut -d '.' -f 2) 32 | if [ -z "$python_version" ]; then 33 | log "ERROR: Python is not installed or not found in PATH" 34 | exit 1 35 | fi 36 | if [ "$python_major_version" -lt 3 ] || { [ "$python_major_version" -eq 3 ] && [ "$python_minor_version" -lt 8 ]; }; then 37 | log "ERROR: Python version ($python_version) may not be compatible" 38 | log "Recommended: Python 3.8 or higher" 39 | exit 1 40 | fi 41 | 42 | # Execute uvicorn command 43 | log "==============================================================" 44 | log "Starting VyManager in $ENVIRONMENT mode..." 45 | log "Host: $HOST" 46 | log "Backend Port: $BACKEND_PORT" 47 | log "Workers: $WORKERS" 48 | log "Log Level: $LOG_LEVEL" 49 | log "==============================================================" 50 | 51 | if [ "$ENVIRONMENT" = "production" ]; then 52 | python -m uvicorn main:app --host $HOST --port $BACKEND_PORT --workers $WORKERS --log-level $LOG_LEVEL 2>&1 | tee -a $BACKEND_LOG 53 | else 54 | python -m uvicorn main:app --host $HOST --port $BACKEND_PORT --reload --log-level $LOG_LEVEL 2>&1 | tee -a $BACKEND_LOG 55 | fi -------------------------------------------------------------------------------- /backend/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Community-VyProjects/VyManager/ca1a8994f30becba0f9f455b15a23c4a81863ad0/backend/static/.gitkeep -------------------------------------------------------------------------------- /backend/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | VyManager Dashboard 7 | 8 | 9 | 38 | 39 | 40 |
41 |
42 |

VyManager Dashboard

43 |
44 | v{{ app_version }} 45 | {{ vyos_host }} 46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 |

Cache Statistics

54 |
55 |
56 |
57 |
58 |
{{ cache_data.items }}
59 |
Cached Items
60 |
61 |
62 |
{{ cache_data.hits }}
63 |
Cache Hits
64 |
65 |
66 |
{{ cache_data.misses }}
67 |
Cache Misses
68 |
69 |
70 |
{{ cache_data.hit_rate }}
71 |
Hit Rate
72 |
73 |
74 |
75 | 76 | 77 | 78 |
79 |
80 | 83 |
84 |
85 |
86 | 87 |
88 |
89 |
90 |
91 |

System Status

92 |
93 |
94 |

App is running in {{ 'Production' if is_production else 'Development' }} mode.

95 |

Connected to VyOS router at {{ vyos_host }}

96 |

Cache will automatically refresh every 30-60 seconds depending on the endpoint.

97 |
98 |
99 |
100 |
101 |
102 | 103 | 104 | 144 | 145 | -------------------------------------------------------------------------------- /backend/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | VyManager 7 | 11 | 32 | 33 | 34 |

VyManager

35 | 36 |
37 |

This is the backend API server for VyManager.

38 |

If you're seeing this page, it means the backend is running correctly!

39 |
40 | 41 | 45 | 46 |
47 |

API endpoints available:

48 | 54 |
55 | 56 | -------------------------------------------------------------------------------- /container/backend/Containerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/python:3.13.3-bookworm AS backend 2 | 3 | # Set the working directory 4 | COPY ./backend /app 5 | WORKDIR /app 6 | 7 | # Set shell to bash 8 | RUN ln -sf /bin/bash /bin/sh 9 | 10 | # Install dependencies 11 | RUN python -m venv venv 12 | RUN source venv/bin/activate 13 | RUN pip3 install -r requirements.txt 14 | 15 | # Environment variables 16 | ENV BACKEND_PORT=3001 17 | ENV HOST=:: 18 | ENV ENVIRONMENT="development" 19 | ENV API_KEY="" 20 | ENV VYOS_HOST="" 21 | ENV VYOS_API_URL="" 22 | ENV CERT_PATH="" 23 | ENV TRUST_SELF_SIGNED="false" 24 | ENV HTTPS="true" 25 | 26 | # Setup port 27 | EXPOSE ${BACKEND_PORT} 28 | 29 | # Set the entrypoint 30 | RUN chmod +x start.sh 31 | ENTRYPOINT [ "./start.sh" ] 32 | -------------------------------------------------------------------------------- /container/env_file_compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | vymanager-backend: 3 | build: 4 | context: .. 5 | dockerfile: container/backend/Containerfile 6 | ports: 7 | - "3001:3001" 8 | volumes: 9 | # Sync time between container and host 10 | - /etc/localtime:/etc/localtime:ro 11 | # Mapping of the local .env file to the container, change ../backend/.env to the correct path 12 | - ../backend/.env:/app/.env:ro 13 | 14 | vymanager-frontend: 15 | build: 16 | context: .. 17 | dockerfile: container/frontend/Containerfile 18 | ports: 19 | - "3000:3000" 20 | volumes: 21 | # Sync time between container and host 22 | - /etc/localtime:/etc/localtime:ro 23 | # Mapping of the local .env file to the container, change ../frontend/.env to the correct path 24 | - ../frontend/.env:/app/.env:ro 25 | depends_on: 26 | - vymanager-backend -------------------------------------------------------------------------------- /container/environment_vars_compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | vymanager-backend: 3 | build: 4 | context: .. 5 | dockerfile: container/backend/Containerfile 6 | ports: 7 | - "3001:3001" 8 | volumes: 9 | - /etc/localtime:/etc/localtime:ro 10 | environment: 11 | - VYOS_HOST=VYOS_IP_HERE 12 | - VYOS_HTTPS=true 13 | - VYOS_API_KEY=VYOS_API_KEY_HERE 14 | - CERT_PATH= # Optional, path to custom certificate 15 | - TRUST_SELF_SIGNED=true # Set to true to accept self-signed certificates (not recommended for production) 16 | - ENVIRONMENT=production # Set to 'production' for production mode 17 | - BACKEND_PORT=3001 18 | - WORKERS=4 19 | 20 | vymanager-frontend: 21 | build: 22 | context: .. 23 | dockerfile: container/frontend/Containerfile 24 | ports: 25 | - "3000:3000" 26 | volumes: 27 | - /etc/localtime:/etc/localtime:ro 28 | environment: 29 | - NEXT_PUBLIC_API_URL=http://localhost:3001 30 | - NODE_ENV=production # Set to 'production' for production mode 31 | - FRONTEND_PORT=3000 32 | 33 | depends_on: 34 | - vymanager-backend -------------------------------------------------------------------------------- /container/frontend/Containerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/node:18-alpine AS frontend-builder 2 | 3 | # Copy frontend source code 4 | COPY ./frontend/ /app 5 | 6 | # Set working directory for frontend 7 | WORKDIR /app 8 | 9 | # Install frontend dependencies 10 | RUN npm ci 11 | 12 | ENV NEXT_PUBLIC_API_URL=http://localhost:3001 13 | ENV NODE_ENV=production 14 | ENV FRONTEND_PORT=3000 15 | 16 | EXPOSE ${FRONTEND_PORT} 17 | 18 | RUN chmod +x start.sh 19 | 20 | ENTRYPOINT [ "./start.sh" ] -------------------------------------------------------------------------------- /docs/README-container.md: -------------------------------------------------------------------------------- 1 | # Running VyOS API Manager with Container 2 | 3 | This document describes how to run the VyOS API Manager using Docker or Podman, which includes both the FastAPI backend and Next.js frontend. 4 | 5 | ## Prerequisites 6 | 7 | - Docker or Podman installed on your system 8 | - Docker Compose or Podman Compose installed on your system (optional but recommended) 9 | 10 | ## Configuration 11 | 12 | Before building the container image, make sure you have a proper configuration: 13 | 14 | 1. Create a `.env` file in the /backend directory with your VyOS router connection details: 15 | 16 | ``` 17 | VYOS_HOST=your-vyos-router-ip 18 | VYOS_API_KEY=your-api-key 19 | VYOS_HTTPS=true 20 | TRUST_SELF_SIGNED=true # Set to true if your VyOS router uses a self-signed certificate 21 | ENVIRONMENT=production # or development 22 | ``` 23 | 24 | 2. Create a `.env` file in the /frontend directory with the following configuration: 25 | ``` 26 | NEXT_PUBLIC_API_URL=http://localhost:3001 27 | ``` 28 | 29 | ## Build and Run Using Compose (Recommended) 30 | 31 | The simplest way to run the application is using Compose: 32 | 33 | 34 | #### Docker Compose 35 | ```bash 36 | 37 | cd container 38 | 39 | # Build and start the container 40 | docker-compose -f env_file_compose.yaml up -d 41 | 42 | # View logs 43 | docker-compose -f env_file_compose.yaml logs -f 44 | 45 | # Stop the container 46 | docker-compose -f env_file_compose.yaml down 47 | ``` 48 | 49 | #### Podman Compose 50 | ```bash 51 | 52 | cd container 53 | 54 | # Build and start the container 55 | podman compose -f env_file_compose.yaml up -d 56 | 57 | # View logs 58 | podman compose -f env_file_compose.yaml logs -f 59 | 60 | # Stop the container 61 | podman compose -f env_file_compose.yaml down 62 | ``` 63 | 64 | 65 | ## Build and Run Using Docker or Podman directly 66 | 67 | If you prefer to use Docker or Podman commands directly, the example below is with docker, but works the same for podman, simply change `docker` to `podman`: 68 | *Note: If you are getting an error like "sd-bus call: Interactive authentication required.: Permission denied" make sure to use sudo while running the commands.* 69 | 70 | ```bash 71 | 72 | cd container 73 | 74 | # Build the Docker images 75 | docker build -f ./backend/Containerfile -t vymanager-backend . 76 | docker build -f ./frontend/Containerfile -t vymanager-frontend . 77 | 78 | # Run the Docker containers 79 | docker run -p 3000:3000 -v ../backend/.env:/app/.env:ro --name vymanager-backend vymanager-backend 80 | docker run -p 3001:3001 -v ../frontend/.env:/app/.env:ro --name vymanager-frontend vymanager-frontend 81 | 82 | # View logs 83 | docker logs -f vymanager-backend 84 | docker logs -f vymanager-frontend 85 | 86 | # Stop the container 87 | docker stop vymanager-frontend 88 | docker stop vymanager-backend 89 | ``` 90 | 91 | ## Accessing the Application 92 | 93 | After starting the container: 94 | 95 | - The Next.js frontend is available at: http://localhost:3000 96 | - The FastAPI backend API is available at: http://localhost:3001 97 | 98 | ## Production Deployment Considerations 99 | 100 | For production deployments, consider the following: 101 | 102 | 1. Use a reverse proxy like Nginx to handle SSL termination 103 | 2. Set proper CORS settings in the FastAPI app 104 | 3. Use Docker Swarm or Kubernetes for orchestration 105 | 4. Set up proper logging and monitoring 106 | 5. Configure backups for any persistent data 107 | 108 | ## Troubleshooting 109 | 110 | If you encounter issues: 111 | 112 | 1. Check the logs: `docker-compose -f env_file_compose.yaml logs` or `docker logs vymanager-frontend` or `docker logs vymanager-backend` 113 | 2. Verify your `.env` configuration 114 | 3. Ensure your VyOS router is accessible from the Docker container 115 | 4. For connection issues, test if your VyOS API is working correctly outside the container -------------------------------------------------------------------------------- /docs/README-routing.md: -------------------------------------------------------------------------------- 1 | # Network Routing Table Feature 2 | 3 | This feature provides a detailed view of the network routing table from the VyOS router. It displays routes from all configured VRFs (Virtual Routing and Forwarding instances) and allows filtering and inspection of route details. 4 | 5 | ## Files 6 | 7 | The feature consists of the following components: 8 | 9 | 1. **Backend API**: 10 | - `routes.py`: Contains the API endpoint to fetch and parse routing information from VyOS 11 | 12 | 2. **Frontend**: 13 | - `templates/routing.html`: The HTML template for the routing table page 14 | - `static/js/routing.js`: JavaScript functionality for the routing table 15 | - `static/css/routing.css`: Styling for the routing table 16 | 17 | ## How to Use 18 | 19 | 1. Access the routing table by: 20 | - Clicking the "Routing Table" button in the Network tab 21 | - Directly navigating to `/routing` URL 22 | 23 | 2. The routing table displays: 24 | - VRF tabs for each routing domain 25 | - Destination networks 26 | - Routing protocols (static, connected, BGP, etc.) 27 | - Next hop information 28 | - Route uptime and status 29 | 30 | 3. Features: 31 | - Filter routes by typing in the search box 32 | - View detailed route information by clicking the info button 33 | - Switch between different VRFs using the VRF tabs 34 | - Refresh the data with the refresh button 35 | 36 | ## API Endpoint 37 | 38 | The routing data is fetched from: 39 | ``` 40 | GET /api/routes 41 | ``` 42 | 43 | Optional parameters: 44 | - `vrf`: Specific VRF to query (default: "all") 45 | - `force_refresh`: Boolean to force a refresh from the server 46 | 47 | ## Technical Implementation 48 | 49 | The routing table uses the following VyOS operational command: 50 | ``` 51 | show ip route vrf all json 52 | ``` 53 | 54 | The API parses the JSON response and organizes the routes by VRF, making sure the "default" VRF is shown first followed by other VRFs in alphabetical order. 55 | 56 | ## Troubleshooting 57 | 58 | If the routing table doesn't load: 59 | 60 | 1. Check that the VyOS API is accessible 61 | 2. Verify that the API key has permissions to view routing information 62 | 3. Check the browser console for JavaScript errors 63 | 4. Check the server logs for API errors 64 | 65 | ## Future Enhancements 66 | 67 | Potential future enhancements include: 68 | 69 | 1. Route tracing/testing functionality 70 | 2. BGP route filtering and attribute visualization 71 | 3. IPv6 routing table support 72 | 4. Route statistics and history tracking -------------------------------------------------------------------------------- /frontend/.env-example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_URL=http://localhost:3001 -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Next.js build output 2 | .next/ 3 | out/ 4 | 5 | # Dependencies 6 | node_modules/ 7 | .pnp/ 8 | .pnp.js 9 | 10 | # Testing 11 | coverage/ 12 | 13 | # Environment files 14 | .env 15 | .env.* 16 | !.env.example 17 | 18 | # Debug logs 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | .pnpm-debug.log* 23 | 24 | # Misc 25 | .DS_Store 26 | *.pem 27 | Thumbs.db 28 | 29 | # Editor directories and files 30 | .idea/ 31 | .vscode/ 32 | *.suo 33 | *.ntvs* 34 | *.njsproj 35 | *.sln 36 | *.sw? 37 | 38 | # Typescript 39 | *.tsbuildinfo 40 | next-env.d.ts -------------------------------------------------------------------------------- /frontend/SETUP.md: -------------------------------------------------------------------------------- 1 | # VyManager Frontend Setup Guide 2 | 3 | ## API Configuration 4 | 5 | The frontend needs to connect to the backend API server. By default, it will use `http://localhost:3001/api` as the base URL. 6 | 7 | ### Setting a Custom API URL 8 | 9 | If your backend is running on a different host or port, create a `.env.local` file in the `frontend` directory with the following content: 10 | 11 | ``` 12 | # API URL for the backend server 13 | NEXT_PUBLIC_API_URL=http://your-backend-host:port/api 14 | ``` 15 | 16 | For example, if your backend is running on port 3001: 17 | 18 | ``` 19 | NEXT_PUBLIC_API_URL=http://localhost:3001/api 20 | ``` 21 | 22 | ## Troubleshooting Connection Issues 23 | 24 | If you see the "Connection Error" screen when starting the application, check the following: 25 | 26 | 1. Make sure the backend server is running: 27 | ```bash 28 | # From the project root 29 | python -m uvicorn main:app --host 0.0.0.0 --port 3001 30 | ``` 31 | 32 | 2. Verify the API URL in your browser console: 33 | - Open the browser's developer tools (F12) 34 | - Look for network errors in the console 35 | - Check if the requests are going to the correct URL 36 | 37 | 3. If you're using a custom API URL, verify it's correctly set in `.env.local` 38 | 39 | 4. If running in development mode, restart the Next.js development server after changing the `.env.local` file: 40 | ```bash 41 | npm run dev 42 | # or 43 | yarn dev 44 | ``` 45 | 46 | ## Cache Configuration 47 | 48 | The frontend implements a caching system to reduce API calls. You can customize the cache TTL (Time To Live) values by adding the following to your `.env.local` file: 49 | 50 | ``` 51 | # Cache configuration (values in seconds) 52 | NEXT_PUBLIC_CACHE_TTL_CONFIG=30 53 | NEXT_PUBLIC_CACHE_TTL_ROUTES=60 54 | NEXT_PUBLIC_CACHE_TTL_DHCP=60 55 | NEXT_PUBLIC_CACHE_TTL_SYSTEM=10 56 | ``` -------------------------------------------------------------------------------- /frontend/app/dashboard/cache/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CacheDashboard } from '@/components/cache/dashboard'; 3 | 4 | export const metadata = { 5 | title: 'Cache Dashboard | VyManager', 6 | description: 'Monitor and manage the caching system', 7 | }; 8 | 9 | export default function CacheDashboardPage() { 10 | return ( 11 |
12 |
13 |

Cache Management

14 |
15 | 16 |

17 | Monitor cache performance and manage cache settings for both frontend and backend systems. 18 |

19 | 20 | 21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 213 94% 50%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import type { Metadata } from 'next' 3 | import { Inter } from 'next/font/google' 4 | import { ThemeProvider } from '@/components/theme-provider' 5 | import { Toaster } from '@/components/ui/toaster' 6 | import { Providers } from '@/components/providers' 7 | import { CacheInitializer } from '@/components/cache/init-loading' 8 | 9 | const inter = Inter({ subsets: ['latin'] }) 10 | 11 | export const metadata: Metadata = { 12 | title: 'VyManager', 13 | description: 'Modern web interface to make configuring, deploying and monitoring VyOS routers easier', 14 | } 15 | 16 | export default function RootLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode 20 | }) { 21 | return ( 22 | 23 | 24 | 30 | 31 | 32 | {children} 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } -------------------------------------------------------------------------------- /frontend/app/power/poweroff/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { toast } from "@/components/ui/use-toast"; 6 | import { Loader2, RefreshCw } from "lucide-react"; 7 | import { executeSavingMethod } from "@/app/utils"; 8 | 9 | export default function PoweroffPage() { 10 | const [isLoading, setIsLoading] = useState(true); 11 | const [isRefreshing, setIsRefreshing] = useState(false); 12 | const [config, setConfig] = useState(null); 13 | 14 | const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; 15 | 16 | const executePoweroff = async () => { 17 | try { 18 | const response = await fetch(`${apiUrl}/api/poweroff`, { 19 | method: 'POST' 20 | }); 21 | 22 | if (!response.ok) { 23 | throw new Error( 24 | `Server returned ${response.status} ${response.statusText}` 25 | ); 26 | } 27 | 28 | const data = await response.json(); 29 | 30 | if (data.success === true) { 31 | toast({ 32 | variant: "default", 33 | title: "Poweroff signal received", 34 | description: "VyOS has received the poweroff command", 35 | }); 36 | } else { 37 | throw new Error(data.error || "Poweroff unsuccessful"); 38 | } 39 | } catch (error) { 40 | console.error("Error sending poweroff command:", error); 41 | toast({ 42 | variant: "destructive", 43 | title: "Error sending poweroff command", 44 | description: 45 | error instanceof Error ? error.message : "An unknown error occurred", 46 | }); 47 | } finally { 48 | setIsLoading(false); 49 | } 50 | }; 51 | 52 | const fetchConfig = async () => { 53 | executeSavingMethod(); 54 | setIsLoading(true); 55 | try { 56 | const response = await fetch(`${apiUrl}/api/config`); 57 | 58 | if (!response.ok) { 59 | throw new Error( 60 | `Server returned ${response.status} ${response.statusText}` 61 | ); 62 | } 63 | 64 | const data = await response.json(); 65 | 66 | if (data.success === true && data.data) { 67 | setConfig(data.data); 68 | } else { 69 | throw new Error(data.error || "Failed to load configuration"); 70 | } 71 | } catch (error) { 72 | console.error("Error fetching configuration:", error); 73 | toast({ 74 | variant: "destructive", 75 | title: "Error loading configuration", 76 | description: 77 | error instanceof Error ? error.message : "An unknown error occurred", 78 | }); 79 | } finally { 80 | setIsLoading(false); 81 | } 82 | }; 83 | 84 | useEffect(() => { 85 | Promise.all([fetchConfig()]); 86 | }, []); 87 | 88 | if (isLoading) { 89 | return ( 90 |
91 |
92 | 93 |

Loading Poweroff page...

94 |
95 |
96 | ); 97 | } 98 | 99 | return ( 100 |
101 |
102 |
103 |

Poweroff

104 |

Shut down your VyOS router

105 |
106 |
107 | 114 |
115 |
116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /frontend/app/power/reboot/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { toast } from "@/components/ui/use-toast"; 6 | import { Loader2, RefreshCw } from "lucide-react"; 7 | import { executeSavingMethod } from "@/app/utils"; 8 | 9 | export default function PoweroffPage() { 10 | const [isLoading, setIsLoading] = useState(true); 11 | const [isRefreshing, setIsRefreshing] = useState(false); 12 | const [config, setConfig] = useState(null); 13 | 14 | const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; 15 | 16 | const executeReboot = async () => { 17 | try { 18 | const response = await fetch(`${apiUrl}/api/reboot`, { 19 | method: 'POST' 20 | }); 21 | 22 | if (!response.ok) { 23 | throw new Error( 24 | `Server returned ${response.status} ${response.statusText}` 25 | ); 26 | } 27 | 28 | const data = await response.json(); 29 | 30 | if (data.success === true) { 31 | toast({ 32 | variant: "default", 33 | title: "Reboot signal received", 34 | description: "VyOS has received the reboot command", 35 | }); 36 | } else { 37 | throw new Error(data.error || "Poweroff unsuccessful"); 38 | } 39 | } catch (error) { 40 | console.error("Error sending reboot command:", error); 41 | toast({ 42 | variant: "destructive", 43 | title: "Error sending reboot command", 44 | description: 45 | error instanceof Error ? error.message : "An unknown error occurred", 46 | }); 47 | } finally { 48 | setIsLoading(false); 49 | } 50 | }; 51 | 52 | const fetchConfig = async () => { 53 | executeSavingMethod(); 54 | setIsLoading(true); 55 | try { 56 | const response = await fetch(`${apiUrl}/api/config`); 57 | 58 | if (!response.ok) { 59 | throw new Error( 60 | `Server returned ${response.status} ${response.statusText}` 61 | ); 62 | } 63 | 64 | const data = await response.json(); 65 | 66 | if (data.success === true && data.data) { 67 | setConfig(data.data); 68 | } else { 69 | throw new Error(data.error || "Failed to load configuration"); 70 | } 71 | } catch (error) { 72 | console.error("Error fetching configuration:", error); 73 | toast({ 74 | variant: "destructive", 75 | title: "Error loading configuration", 76 | description: 77 | error instanceof Error ? error.message : "An unknown error occurred", 78 | }); 79 | } finally { 80 | setIsLoading(false); 81 | } 82 | }; 83 | 84 | useEffect(() => { 85 | Promise.all([fetchConfig()]); 86 | }, []); 87 | 88 | if (isLoading) { 89 | return ( 90 |
91 |
92 | 93 |

Loading Reboot page...

94 |
95 |
96 | ); 97 | } 98 | 99 | return ( 100 |
101 |
102 |
103 |

Reboot

104 |

Reboot your VyOS router

105 |
106 |
107 | 114 |
115 |
116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /frontend/app/utils.tsx: -------------------------------------------------------------------------------- 1 | import { toast } from "@/components/ui/use-toast"; 2 | 3 | export async function executeSavingMethod() { 4 | const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; 5 | const response = await fetch(`${apiUrl}/api/check-unsaved-changes`); 6 | var unsavedChanges = false; 7 | 8 | if (!response.ok) { 9 | throw new Error( 10 | `Server returned ${response.status} ${response.statusText}` 11 | ); 12 | } 13 | 14 | const data = await response.json(); 15 | 16 | if (data.success === true && data.data !== null) { 17 | unsavedChanges = data.data; 18 | } else { 19 | throw new Error( 20 | data.error || "Failed to confirm if there are unsaved changes" 21 | ); 22 | } 23 | 24 | if (unsavedChanges === true) { 25 | // Only continue when there are unsaved changes 26 | var savingMethod = sessionStorage.getItem("savingMethod") || "confirmation"; 27 | 28 | if (savingMethod === "direct") { 29 | const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; 30 | const response = await fetch(`${apiUrl}/api/config-file/save`, { 31 | method: "POST", 32 | headers: { 33 | accept: "application/json", 34 | }, 35 | }); 36 | var unsavedChanges = false; 37 | 38 | if (!response.ok) { 39 | throw new Error( 40 | `Server returned ${response.status} ${response.statusText}` 41 | ); 42 | } 43 | 44 | const data = await response.json(); 45 | 46 | if (data.success === true) { 47 | toast({ 48 | title: "Configuration saved successfully", 49 | description: `Current saving method is: ${savingMethod}`, 50 | }); 51 | 52 | // Because the saving method is direct, immediately mark unsavedChanges as false 53 | const responseState = await fetch( 54 | `${apiUrl}/api/set-unsaved-changes/false`, 55 | { 56 | method: "POST", 57 | headers: { 58 | accept: "application/json", 59 | }, 60 | } 61 | ); 62 | 63 | if (!responseState.ok) { 64 | throw new Error( 65 | `Server returned ${responseState.status} ${responseState.statusText}` 66 | ); 67 | } 68 | } else { 69 | throw new Error(data.error || "Failed to save configuration"); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /frontend/components/cache/dashboard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; 5 | import { Button } from '@/components/ui/button'; 6 | import { Separator } from '@/components/ui/separator'; 7 | import { useCacheContext } from '@/components/providers/cache-provider'; 8 | import { Progress } from '@/components/ui/progress'; 9 | import { RefreshCw, Trash2, Server } from 'lucide-react'; 10 | import { api } from '@/lib/api'; 11 | import { useToast } from '@/components/ui/use-toast'; 12 | 13 | export function CacheDashboard() { 14 | const { stats, isInitialized, isInitializing, refreshCache, clearCache } = useCacheContext(); 15 | const { toast } = useToast(); 16 | 17 | const handleClearBackendCache = async (pattern?: string) => { 18 | try { 19 | const response = await api.clearCache(pattern); 20 | toast({ 21 | title: 'Cache Cleared', 22 | description: response.message, 23 | variant: 'default', 24 | }); 25 | } catch (error) { 26 | toast({ 27 | title: 'Error', 28 | description: error instanceof Error ? error.message : 'Failed to clear backend cache', 29 | variant: 'destructive', 30 | }); 31 | } 32 | }; 33 | 34 | const handleClearFrontendCache = (pattern?: string) => { 35 | clearCache(pattern); 36 | toast({ 37 | title: 'Frontend Cache Cleared', 38 | description: pattern ? `Cleared ${pattern} entries` : 'Cleared all entries', 39 | variant: 'default', 40 | }); 41 | }; 42 | 43 | const handleRefreshCache = async () => { 44 | try { 45 | await refreshCache(); 46 | toast({ 47 | title: 'Cache Refreshed', 48 | description: 'All cache entries have been refreshed', 49 | variant: 'default', 50 | }); 51 | } catch (error) { 52 | toast({ 53 | title: 'Error', 54 | description: error instanceof Error ? error.message : 'Failed to refresh cache', 55 | variant: 'destructive', 56 | }); 57 | } 58 | }; 59 | 60 | if (isInitializing) { 61 | return ( 62 | 63 | 64 | Cache Initialization 65 | Initializing cache with essential data... 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | } 73 | 74 | if (!isInitialized) { 75 | return ( 76 | 77 | 78 | Cache Not Initialized 79 | The cache has not been initialized yet. 80 | 81 | 82 | 83 | 84 | 85 | ); 86 | } 87 | 88 | const hitRatePercent = Math.round(stats.hitRate * 100); 89 | const formattedUptime = formatUptime(stats.uptime); 90 | 91 | return ( 92 | 93 | 94 | 95 | Cache Statistics 96 | 100 | 101 | Performance metrics for the frontend cache system 102 | 103 | 104 |
105 | 106 | 107 | 108 | 109 |
110 | 111 | 112 | 113 |
114 |
115 |

Frontend Cache

116 |

Manage the client-side cache

117 |
118 | 122 | 125 | 128 |
129 |
130 | 131 |
132 |

Backend Cache

133 |

Manage the server-side cache

134 |
135 | 139 | 142 | 145 |
146 |
147 |
148 |
149 | 150 | Cache uptime: {formattedUptime} 151 | 152 |
153 | ); 154 | } 155 | 156 | // Helper components 157 | function StatCard({ title, value }: { title: string; value: string | number }) { 158 | return ( 159 |
160 |
{value}
161 |
{title}
162 |
163 | ); 164 | } 165 | 166 | // Helper functions 167 | function formatUptime(seconds: number): string { 168 | if (seconds < 60) { 169 | return `${seconds} seconds`; 170 | } 171 | 172 | if (seconds < 3600) { 173 | const minutes = Math.floor(seconds / 60); 174 | return `${minutes} minute${minutes !== 1 ? 's' : ''}`; 175 | } 176 | 177 | const hours = Math.floor(seconds / 3600); 178 | const minutes = Math.floor((seconds % 3600) / 60); 179 | 180 | return `${hours} hour${hours !== 1 ? 's' : ''} ${minutes} minute${minutes !== 1 ? 's' : ''}`; 181 | } -------------------------------------------------------------------------------- /frontend/components/cache/init-loading.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | import { useCacheContext } from '@/components/providers/cache-provider'; 5 | import { Progress } from '@/components/ui/progress'; 6 | import { Button } from '@/components/ui/button'; 7 | import { refreshCache } from '@/lib/cache-init'; 8 | 9 | export function CacheInitializer({ children }: { children: React.ReactNode }) { 10 | const { isInitialized, isInitializing } = useCacheContext(); 11 | const [connectionError, setConnectionError] = useState(null); 12 | const [retrying, setRetrying] = useState(false); 13 | 14 | // Handle retry button click 15 | const handleRetry = async () => { 16 | setRetrying(true); 17 | setConnectionError(null); 18 | 19 | try { 20 | await refreshCache(); 21 | } catch (error) { 22 | setConnectionError( 23 | error instanceof Error 24 | ? error.message 25 | : 'Failed to connect to the backend server. Please check if the server is running.' 26 | ); 27 | } finally { 28 | setRetrying(false); 29 | } 30 | }; 31 | 32 | if (isInitializing) { 33 | return ( 34 |
35 |
36 |
37 |

Initializing VyManager

38 |

39 | Loading essential data and preparing application... 40 |

41 |
42 | 43 | 44 | 45 |
    46 |
  • ✓ Connection established
  • 47 |
  • ✓ Loading configuration data
  • 48 |
  • ✓ Loading routing information
  • 49 |
  • ⋯ Loading device information
  • 50 |
51 |
52 |
53 | ); 54 | } 55 | 56 | // Show error message if there's a connection error 57 | if (connectionError) { 58 | return ( 59 |
60 |
61 |
62 |

Connection Error

63 |

64 | {connectionError} 65 |

66 |
67 | 68 |
69 |

Troubleshooting steps:

70 |
    71 |
  1. Ensure the backend server is running (python -m uvicorn main:app --host 0.0.0.0 --port 3001)
  2. 72 |
  3. Check network connectivity between your browser and the server
  4. 73 |
  5. Verify the API URL in the frontend configuration is correct
  6. 74 |
75 |
76 | 77 |
78 | 85 |
86 |
87 |
88 | ); 89 | } 90 | 91 | return <>{children}; 92 | } -------------------------------------------------------------------------------- /frontend/components/config-display.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 2 | import { ScrollArea } from "@/components/ui/scroll-area"; 3 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 4 | import { AlertCircle } from "lucide-react"; 5 | 6 | interface ConfigDisplayProps { 7 | config: any; 8 | } 9 | 10 | export function ConfigDisplay({ config }: ConfigDisplayProps) { 11 | if (!config) { 12 | return ( 13 | 14 | 15 | No Configuration Data Available 16 | 17 | Unable to connect to VyOS router. Please check your connection settings and ensure the VyOS router is accessible. 18 | 19 | 20 | ); 21 | } 22 | 23 | const sections = [ 24 | { key: "interfaces", title: "Network Interfaces" }, 25 | { key: "system", title: "System Configuration" }, 26 | { key: "service", title: "Services" }, 27 | { key: "firewall", title: "Firewall Rules" }, 28 | { key: "nat", title: "NAT Settings" }, 29 | { key: "protocols", title: "Routing Protocols" }, 30 | { key: "vpn", title: "VPN Configuration" }, 31 | ]; 32 | 33 | const hasSections = sections.some(section => config[section.key] && Object.keys(config[section.key]).length > 0); 34 | 35 | if (!hasSections) { 36 | return ( 37 | 38 | 39 | Limited Configuration Data 40 | 41 | The VyOS router returned configuration data, but none of the expected sections were found. 42 | Please check your VyOS router configuration. 43 | 44 | 45 | ); 46 | } 47 | 48 | return ( 49 |
50 | {sections.map(({ key, title }) => 51 | config[key] && Object.keys(config[key]).length > 0 ? ( 52 | 53 | 54 | {title} 55 | 56 | 57 | 58 |
59 |                   {JSON.stringify(config[key], null, 2)}
60 |                 
61 |
62 |
63 |
64 | ) : null 65 | )} 66 |
67 | ); 68 | } -------------------------------------------------------------------------------- /frontend/components/config-editor.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import { Button } from "@/components/ui/button" 5 | import { toast } from "@/components/ui/use-toast" 6 | import { AlertCircle, Check, Copy, Save } from "lucide-react" 7 | import { 8 | Alert, 9 | AlertDescription, 10 | AlertTitle, 11 | } from "@/components/ui/alert" 12 | import { cn } from "@/lib/utils" 13 | 14 | interface ConfigEditorProps { 15 | config: any 16 | } 17 | 18 | export function ConfigEditor({ config }: ConfigEditorProps) { 19 | const [configText, setConfigText] = useState('') 20 | const [isValidJson, setIsValidJson] = useState(true) 21 | const [errorMessage, setErrorMessage] = useState('') 22 | 23 | useEffect(() => { 24 | setConfigText(JSON.stringify(config, null, 2)) 25 | }, [config]) 26 | 27 | const handleTextChange = (e: React.ChangeEvent) => { 28 | const newText = e.target.value 29 | setConfigText(newText) 30 | 31 | try { 32 | JSON.parse(newText) 33 | setIsValidJson(true) 34 | setErrorMessage('') 35 | } catch (error) { 36 | setIsValidJson(false) 37 | setErrorMessage((error as Error).message) 38 | } 39 | } 40 | 41 | const handleSave = () => { 42 | if (!isValidJson) { 43 | toast({ 44 | variant: "destructive", 45 | title: "Invalid JSON", 46 | description: "Please fix the errors before saving" 47 | }) 48 | return 49 | } 50 | 51 | try { 52 | const parsedConfig = JSON.parse(configText) 53 | 54 | // Would connect to API to save changes 55 | toast({ 56 | title: "Configuration saved", 57 | description: "Your changes have been applied" 58 | }) 59 | } catch (error) { 60 | toast({ 61 | variant: "destructive", 62 | title: "Error saving configuration", 63 | description: (error as Error).message 64 | }) 65 | } 66 | } 67 | 68 | const handleCopy = () => { 69 | navigator.clipboard.writeText(configText) 70 | toast({ 71 | title: "Copied to clipboard", 72 | description: "Configuration has been copied to your clipboard" 73 | }) 74 | } 75 | 76 | return ( 77 |
78 |
79 |

Edit Configuration

80 |
81 | 85 | 89 |
90 |
91 | 92 | {!isValidJson && ( 93 | 94 | 95 | Invalid JSON 96 | 97 | {errorMessage} 98 | 99 | 100 | )} 101 | 102 |
103 |