├── .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 |  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 |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 |This is the backend API server for VyManager.
38 |If you're seeing this page, it means the backend is running correctly!
39 |API endpoints available:
48 |17 | Monitor cache performance and manage cache settings for both frontend and backend systems. 18 |
19 | 20 |Loading Poweroff page...
94 |Shut down your VyOS router
105 |Loading Reboot page...
94 |Reboot your VyOS router
105 |Manage the client-side cache
117 |Manage the server-side cache
134 |39 | Loading essential data and preparing application... 40 |
41 |64 | {connectionError} 65 |
66 |Troubleshooting steps:
70 |59 | {JSON.stringify(config[key], null, 2)} 60 |61 |
{interfaceItem.config.description}
92 |97 | {interfaceItem.config.disable ? "Disabled" : "Enabled"} 98 |
99 |103 | {interfaceItem.config.mtu || "Default"} 104 |
105 |