├── .gitignore ├── Dockerfile ├── README.md ├── __init__.py ├── config.json ├── main.py ├── python_scripts ├── add_context.py ├── command_utils.py ├── delete_images.py ├── download_csv.py ├── download_images.py ├── get_images.py ├── installed_images.py ├── live_search_form.py └── misc_utils.py ├── renovate.json ├── requirements.txt ├── routers ├── __init__.py ├── changelogs.py ├── downloads.py ├── installed.py ├── request_delete.py └── request_download.py └── web_app └── src ├── components ├── alerts.html ├── confirmDeleteModal.html ├── confirmDownloadModal.html ├── dashboard.html ├── footer.html ├── head.html └── navbar.html ├── pages ├── changelogs.html ├── download.html ├── help.html ├── index.html └── installed.html └── static ├── images ├── Homepage - ishare2.png └── favicon.ico ├── scripts ├── app.js ├── handleButtonsModals.js └── search.js └── styles └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | bin/ 94 | lib64 95 | pyvenv.cfg 96 | 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | .vscode/settings.json 135 | 136 | # ignore downloaded files 137 | csv/* 138 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an existing base image 2 | FROM python:3.11-alpine 3 | 4 | # Set the working directory in the container to /app 5 | WORKDIR /app 6 | 7 | # Copy the requirements.txt file to the container 8 | COPY requirements.txt . 9 | 10 | # Install the required packages 11 | RUN pip install -r requirements.txt 12 | 13 | # Copy the rest of the files to the container 14 | COPY . . 15 | 16 | # Specify the command to run when the container starts 17 | CMD ["python", "main.py"] 18 | 19 | # Port to expose 20 | EXPOSE 5000 21 | 22 | # Volume to mount 23 | VOLUME /opt/unetlab/ 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ishare2 GUI 2 | 3 | ishare2 GUI is a web interface for the ishare2 project, designed to provide a graphical user experience for managing and downloading bin, QEMU, Dynamips, and Docker images for network emulators. It leverages the capabilities of the [ishare2 CLI](https://github.com/pnetlabrepo/ishare2), executing its commands under the hood to simplify image management for network administrators. 4 | 5 | ## Table of Contents 6 | 7 | - [Features](#features) 8 | - [Screenshots](#screenshots) 9 | - [Install from ishare2 (Coming soon)](#install-from-ishare2-coming-soon) 10 | - [Installation (as root)](#installation-as-root) 11 | - [Prerequisites](#prerequisites) 12 | - [Change to root user](#change-to-root-user) 13 | - [Clone the repository](#clone-the-repository) 14 | - [Create a virtual environment](#create-a-virtual-environment) 15 | - [Install dependencies](#install-dependencies) 16 | - [Run the application](#run-the-application) 17 | - [Run the application uvicorn](#run-the-application-uvicorn) 18 | - [Docker container (Experimental)](#docker-container-experimental) 19 | - [Build Docker image](#build-docker-image) 20 | - [Load image](#load-image) 21 | - [Run Docker](#run-docker) 22 | - [Run Docker (Detached mode)](#run-docker-detached-mode) 23 | 24 | ## Features 25 | 26 | - Manage bin, QEMU, Dynamips, and Docker images 27 | - Download bin, QEMU, Dynamips, and Docker images 28 | - Frontend for ishare2 CLI 29 | 30 | ## Screenshots 31 | 32 | ![alt ishare2-GUI's Homepage](web_app/src/static/images/Homepage%20-%20ishare2.png) 33 | 34 | The ishare2 GUI is currently under development and may not be stable yet. It has only been tested on PNetLab, but it may be adaptable to work on other network emulators. 35 | 36 | The ishare2 GUI is designed to make it easy for you to manage your network emulation environment by providing a simple, intuitive interface. You can use the "Manage" dropdown in the navbar to access different management options, such as managing your bin images, QEMU images, Dynamips images, or Docker images. Additionally, the "Download" option in the sub-menu gives you access to the different image types that you can download. 37 | 38 | The ishare2 GUI has a clear and modern look and feel, with a logo and favicon representing the tool, and a user-friendly interface that makes it easy to navigate. You can find more information about the project on the [ishare2-GUI](https://github.com/ishare2-org/ishare2-gui) GitHub page, or reach out to the support team on [Telegram](https://t.me/unetlab_cloud). 39 | 40 | So if you're looking for a tool that can help you manage and download the images you need for your network emulation environment, consider checking out ishare2 GUI! 41 | 42 | Other useful chats tho not directly associated: 43 | [PNetLab Group Chat](https://t.me/pnetlab) 44 | 45 | ## Install from ishare2 (Coming soon) 46 | 47 | ```bash 48 | placeholder 49 | ``` 50 | 51 | ## Installation (as root) 52 | 53 | ishare2 GUI needs to be run as root, as it needs to access the /opt/unetlab directory to manage the images. 54 | You can install ishare2 GUI on your system following these steps: 55 | 56 | ### Prerequisites 57 | 58 | - Python 3.8 or higher 59 | - pip 60 | - virtualenv 61 | 62 | ```bash 63 | sudo apt-get install python3 python3-pip python3-venv -y 64 | ``` 65 | 66 | ### Change to root user 67 | 68 | ```bash 69 | sudo su 70 | ``` 71 | 72 | ### Clone the repository 73 | 74 | Choose a directory where you want to clone the repository, and then clone it. It is recommended to clone the repository in the /opt/ishare2/gui/ directory. However, you can clone it anywhere you want inside the root's home directory. 75 | You can clone the repository using the following command: 76 | 77 | ```bash 78 | git clone https://github.com/ishare2-org/ishare2-web-gui.git /opt/ishare2/gui/ 79 | cd /opt/ishare2/gui/ 80 | ``` 81 | 82 | ### Create a virtual environment 83 | 84 | ```bash 85 | python3 -m venv venv 86 | source venv/bin/activate 87 | ``` 88 | 89 | ### Install dependencies 90 | 91 | ```bash 92 | pip install -r requirements.txt 93 | ``` 94 | 95 | ### Run the application 96 | 97 | ```bash 98 | python3 main.py 99 | ``` 100 | 101 | The application will be available at 102 | 103 | ### Run the application uvicorn 104 | 105 | ```bash 106 | uvicorn main:app --reload 107 | ``` 108 | 109 | The application will be available at 110 | 111 | ## Docker container (Experimental) 112 | 113 | ### Build Docker image 114 | 115 | ```bash 116 | git clone https://github.com/ishare2-org/ishare2-web-gui.git 117 | cd ishare2-web-gui 118 | sudo docker build 119 | docker build - < Dockerfile 120 | ``` 121 | 122 | ### Load image 123 | 124 | ```bash 125 | sudo docker load -i /path/to/ishare2.tar 126 | ``` 127 | 128 | ### Run Docker 129 | 130 | ```bash 131 | sudo docker run -p 5000:5000 -v /opt/unetlab:/opt/unetlab -it ishare 132 | ``` 133 | 134 | ### Run Docker (Detached mode) 135 | 136 | ```bash 137 | sudo docker run -d -p 5000:5000 -v /opt/unetlab:/opt/unetlab -it ishare 138 | ``` 139 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishare2-org/ishare2-web-gui/3c30aa842a100b7ce33596cfa35bb799f216bcb0/__init__.py -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "host": "0.0.0.0", 4 | "port": 5000 5 | }, 6 | "credentials": { 7 | "bin_sheet_id": 0, 8 | "qemu_sheet_id": 647866092, 9 | "dynamips_sheet_id": 1118397071, 10 | "google_sheet_id": "2PACX-1vR-RCxKCkhzzW2ZDDQOUIAxq2QTPwcCJZXuFyGD0hvXDC60TMU0_Yx1fx02S1_H9h7ZU52X-hAVC37h", 11 | "csv_path": "csv" 12 | }, 13 | "constants": { 14 | "ishare2_version": "https://raw.githubusercontent.com/pnetlabrepo/ishare2/main/version", 15 | "changelog_md_file": "https://raw.githubusercontent.com/pnetlabrepo/ishare2/main/CHANGELOG.md", 16 | "changelog_gui_md_file": "https://raw.githubusercontent.com/pnetlabrepo/ishare2/main/gui/CHANGELOG.md", 17 | "help_md_file": "https://raw.githubusercontent.com/pnetlabrepo/ishare2/main/HELP.md" 18 | }, 19 | "current_installation": { 20 | "ishare2_version": "" 21 | }, 22 | "metatags": { 23 | "sitename": "ishare2 GUI", 24 | "title": "ishare2 GUI", 25 | "url": "https://github.com/ishare2-org/ishare2-gui", 26 | "type": "website", 27 | "description": "ishare2 GUI is a web interface for ishare2 project. It allows you to download and manage bin, QEMU, Dynamips and Docker images.", 28 | "keywords": "ishare2, gui, web, interface, bin, qemu, dynamips, docker, images, download, manage", 29 | "image": "Homepage - ishare2.png", 30 | "author": "ishare2", 31 | "author_link": "https://t.me/unetlab_cloud", 32 | "favicon": "favicon.ico", 33 | "logo": "logo.png", 34 | "logo_text": "ishare2 GUI", 35 | "logo_link": "/" 36 | }, 37 | "social_media": { 38 | "links": { 39 | "github": "https://github.com/ishare-org/ishare2-gui", 40 | "telegram": "https://t.me/unetlab_cloud", 41 | "support": "https://t.me/pnetlab" 42 | }, 43 | "icons": { 44 | "github": "fa-github", 45 | "telegram": "fa-telegram", 46 | "support": "fa-telegram" 47 | } 48 | }, 49 | "navbar": { 50 | "brand": "ishare2 GUI", 51 | "menu": [ 52 | { 53 | "title": "Home", 54 | "icon": "fa-home", 55 | "href": "/" 56 | }, 57 | { 58 | "title": "GitHub", 59 | "icon": "fa-github", 60 | "href": "https://github.com/ishare2-org/ishare2-gui" 61 | }, 62 | { 63 | "title": "Changelog", 64 | "icon": "fa-history", 65 | "href": "/changelogs/ishare2-gui" 66 | } 67 | ], 68 | "dropdown": { 69 | "title": "Manage", 70 | "icon": "fa-cog", 71 | "menu": [ 72 | { 73 | "title": "Download bin images", 74 | "href": "/download/bin" 75 | }, 76 | { 77 | "title": "Download QEMU images", 78 | "href": "/download/qemu" 79 | }, 80 | { 81 | "title": "Download Dynamips images", 82 | "href": "/download/dynamips" 83 | } 84 | ], 85 | "sub_menu": [ 86 | { 87 | "title": "Manage bin images", 88 | "href": "/installed/bin" 89 | }, 90 | { 91 | "title": "Manage QEMU images", 92 | "href": "/installed/qemu" 93 | }, 94 | { 95 | "title": "Manage Dynamips images", 96 | "href": "/installed/dynamips" 97 | }, 98 | { 99 | "title": "Manage Docker images", 100 | "href": "/installed/docker" 101 | } 102 | ] 103 | } 104 | }, 105 | "footer": { 106 | "title": "ishare2 GUI", 107 | "text": "ishare2 is a free and open source project. It is not affiliated with Cisco Systems, Inc. or any other company. All trademarks are the property of their respective owners.", 108 | "company": "ishare2", 109 | "company_link": "https://github.com/pnetlabrepo/ishare2", 110 | "menu": [ 111 | { 112 | "title": "Home", 113 | "link": "/" 114 | }, 115 | { 116 | "title": "Get in touch", 117 | "link": "https://t.me/unetlab_cloud" 118 | }, 119 | { 120 | "title": "Changelog", 121 | "link": "/changelogs/gui" 122 | } 123 | ] 124 | } 125 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uvicorn 3 | from fastapi import FastAPI, Request 4 | from fastapi.responses import HTMLResponse 5 | from fastapi.staticfiles import StaticFiles 6 | from fastapi.templating import Jinja2Templates 7 | from fastapi.middleware.cors import CORSMiddleware 8 | 9 | from python_scripts.add_context import add_context 10 | from python_scripts.command_utils import _run_command 11 | from python_scripts.download_csv import download_csv 12 | from python_scripts.misc_utils import ( 13 | get_config, 14 | get_help_content, 15 | get_version, 16 | install_ishare2, 17 | relicense, 18 | ) 19 | 20 | from routers import ( 21 | changelogs, 22 | downloads, 23 | installed, 24 | request_delete, 25 | request_download, 26 | ) 27 | 28 | download_csv() 29 | 30 | app = FastAPI( 31 | title="ishare2 API", 32 | version=os.getenv("API_VERSION", "0.0.1"), 33 | debug=False, #Change to False in production 34 | ) 35 | 36 | origins = os.getenv("ALLOWED_ORIGINS", "*").split(",") 37 | 38 | app.add_middleware( 39 | CORSMiddleware, 40 | allow_origins=origins, 41 | allow_credentials=True, 42 | allow_methods=["*"], 43 | allow_headers=["*"], 44 | ) 45 | 46 | app.mount( 47 | "/images", StaticFiles(directory=os.getenv("IMAGES_DIR", "./web_app/src/static/images")), name="images" 48 | ) 49 | app.mount( 50 | "/styles.css", 51 | StaticFiles(directory=os.getenv( 52 | "STYLES_DIR", "./web_app/src/static/styles")), 53 | name="styles", 54 | ) 55 | app.mount( 56 | "/scripts", 57 | StaticFiles(directory=os.getenv( 58 | "SCRIPTS_DIR", "./web_app/src/static/scripts")), 59 | name="scripts", 60 | ) 61 | 62 | 63 | templates = Jinja2Templates( 64 | directory=os.getenv("TEMPLATES_DIR", "./web_app/src")) 65 | 66 | routes = [downloads, request_delete, request_download, installed, changelogs] 67 | 68 | for route in routes: 69 | app.include_router(route.router) 70 | 71 | 72 | @app.get("/", response_class=HTMLResponse, tags=["Root"]) 73 | async def root(request: Request): 74 | data = { 75 | "title": "Homepage - ishare2", 76 | } 77 | context = add_context({}, request, data) 78 | return templates.TemplateResponse("/pages/index.html", context) 79 | 80 | 81 | @app.get("/relicense", tags=["Extras"]) 82 | async def get_relicensed(request: Request): 83 | return relicense() 84 | 85 | 86 | @app.get("/install/ishare2", tags=["Extras"]) 87 | async def get_install_ishare2(request: Request): 88 | return install_ishare2() 89 | 90 | 91 | if __name__ == "__main__": 92 | config = get_config() 93 | HOST = config["api"]["host"] 94 | PORT = config["api"]["port"] 95 | 96 | uvicorn.run( 97 | "main:app", 98 | host=HOST, 99 | port=PORT, 100 | reload=False, # Change to False in production 101 | workers=4, 102 | ) 103 | -------------------------------------------------------------------------------- /python_scripts/add_context.py: -------------------------------------------------------------------------------- 1 | from .misc_utils import get_config, get_version, iol_license, ishare2_cli_version, is_root 2 | import requests 3 | 4 | 5 | def add_context(context: dict, request, data: dict): 6 | config = get_config() 7 | URL_ISHARE2_VERSION = config["constants"]["ishare2_version"] 8 | context["data"] = data 9 | context["is_root"] = is_root() 10 | context["request"] = request 11 | context["ishare_version"] = requests.get(URL_ISHARE2_VERSION).text 12 | context["emulator_version"] = get_version() 13 | context["iol_license"] = iol_license() 14 | context["ishare2_cli_version"] = ishare2_cli_version() 15 | context["footer_title"] = config["footer"]["title"] 16 | context["footer_text"] = config["footer"]["text"] 17 | context["footer_company"] = config["footer"]["company"] 18 | context["footer_company_link"] = config["footer"]["company_link"] 19 | context["github_link"] = config["social_media"]["links"]["github"] 20 | context["github_icon"] = config["social_media"]["icons"]["github"] 21 | context["telegram_link"] = config["social_media"]["links"]["telegram"] 22 | context["telegram_icon"] = config["social_media"]["icons"]["telegram"] 23 | context["support_link"] = config["social_media"]["links"]["support"] 24 | context["support_icon"] = config["social_media"]["icons"]["support"] 25 | context["footer_menu"] = config["footer"]["menu"] 26 | context["navbar"] = config["navbar"] 27 | context["nav_items"] = context["navbar"]["menu"] 28 | context["dropdown"] = config["navbar"]["dropdown"] 29 | context["dropdown_menu"] = context["dropdown"]["menu"] 30 | context["dropdown_submenu"] = context["dropdown"]["sub_menu"] 31 | context["metatags"] = config["metatags"] 32 | 33 | return context 34 | -------------------------------------------------------------------------------- /python_scripts/command_utils.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | 4 | def _run_command(command): 5 | p = subprocess.run(command, shell=True, 6 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 7 | return p.returncode, p.stdout.decode().strip(), p.stderr.decode().strip() 8 | -------------------------------------------------------------------------------- /python_scripts/delete_images.py: -------------------------------------------------------------------------------- 1 | from .command_utils import _run_command 2 | 3 | NOT_FOUND_MSG = "The image has not been found in the system." 4 | DELETED_SUCCESS_MSG = "The image has been deleted successfully from the system." 5 | UNETLAB_PATH = "/opt/unetlab/addons" 6 | 7 | 8 | def delete_image(id, image_type): 9 | if image_type == "bin": 10 | CHECK_FILE_EXISTS_COMMAND = f'ls {UNETLAB_PATH}/iol/bin/{id}' 11 | DELETE_COMMAND = f'rm -rf {UNETLAB_PATH}/iol/bin/{id}' 12 | 13 | elif image_type == "dynamips": 14 | CHECK_FILE_EXISTS_COMMAND = f'ls {UNETLAB_PATH}/dynamips/{id}' 15 | DELETE_COMMAND = f'rm -rf {UNETLAB_PATH}/dynamips/{id}' 16 | elif image_type == "qemu": 17 | CHECK_FILE_EXISTS_COMMAND = f'ls {UNETLAB_PATH}/qemu/{id}' 18 | DELETE_COMMAND = f'rm -rf {UNETLAB_PATH}/qemu/{id}' 19 | elif image_type == "docker": 20 | CHECK_FILE_EXISTS_COMMAND = f'docker images | grep {id}' 21 | DELETE_COMMAND = f'docker rmi {id}' 22 | else: 23 | # handle an invalid image_type value here 24 | raise ValueError("Invalid image_type specified") 25 | 26 | retcode, stdout, stderr = _run_command(CHECK_FILE_EXISTS_COMMAND) 27 | if retcode != 0: 28 | return { 29 | "name": id, 30 | "status": 1, 31 | "message": NOT_FOUND_MSG 32 | } 33 | 34 | retcode, stdout, stderr = _run_command(DELETE_COMMAND) 35 | if retcode == 0: 36 | return { 37 | "name": id, 38 | "status": 0, 39 | "message": DELETED_SUCCESS_MSG 40 | } 41 | else: 42 | return { 43 | "name": id, 44 | "status": 1, 45 | "message": stderr 46 | } 47 | -------------------------------------------------------------------------------- /python_scripts/download_csv.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os.path 3 | from .misc_utils import get_config, downloader 4 | 5 | 6 | def download_csv(): 7 | config = get_config() 8 | CSV_PATH = config["credentials"]["csv_path"] 9 | GOOGLE_SHEETS_ID = config["credentials"]["google_sheet_id"] 10 | BIN_ID = config["credentials"]["bin_sheet_id"] 11 | QEMU_ID = config["credentials"]["qemu_sheet_id"] 12 | DYNAMIPS_ID = config["credentials"]["dynamips_sheet_id"] 13 | 14 | URL_BIN_FILE = f"https://docs.google.com/spreadsheets/d/e/{GOOGLE_SHEETS_ID}/pub?gid={BIN_ID}&single=true&output=csv" 15 | URL_QEMU_FILE = f"https://docs.google.com/spreadsheets/d/e/{GOOGLE_SHEETS_ID}/pub?gid={QEMU_ID}&single=true&output=csv" 16 | URL_DYNAMIPS_FILE = f"https://docs.google.com/spreadsheets/d/e/{GOOGLE_SHEETS_ID}/pub?gid={DYNAMIPS_ID}&single=true&output=csv" 17 | 18 | BIN_CSV = os.path.join(CSV_PATH, "bin.csv") 19 | QEMU_CSV = os.path.join(CSV_PATH, "qemu.csv") 20 | DYNAMIPS_CSV = os.path.join(CSV_PATH, "dynamips.csv") 21 | 22 | if not os.path.exists(CSV_PATH): 23 | os.makedirs(CSV_PATH) 24 | 25 | # Check if the files exist 26 | if os.path.exists(BIN_CSV) and os.path.exists(QEMU_CSV) and os.path.exists(DYNAMIPS_CSV): 27 | # Check if files are older than 30 minutes 28 | if os.path.getmtime(BIN_CSV) < time.time() - 1800: 29 | downloader(URL_BIN_FILE, BIN_CSV) 30 | if os.path.getmtime(QEMU_CSV) < time.time() - 1800: 31 | downloader(URL_QEMU_FILE, QEMU_CSV) 32 | if os.path.getmtime(DYNAMIPS_CSV) < time.time() - 1800: 33 | downloader(URL_DYNAMIPS_FILE, DYNAMIPS_CSV) 34 | # If files don't exist, download them 35 | else: 36 | downloader(URL_BIN_FILE, BIN_CSV) 37 | downloader(URL_QEMU_FILE, QEMU_CSV) 38 | downloader(URL_DYNAMIPS_FILE, DYNAMIPS_CSV) 39 | -------------------------------------------------------------------------------- /python_scripts/download_images.py: -------------------------------------------------------------------------------- 1 | from .command_utils import _run_command 2 | PERMISSIONS_APPLIED = "Fix permissions command has been applied" 3 | ALREADY_EXISTS = "already exists in server" 4 | NOT_FOUND_MSG = "The image has not been found in the system." 5 | 6 | 7 | def download_image(id, image_type): 8 | if image_type == "bin": 9 | COMMAND = f'ishare2 pull bin {id}' 10 | 11 | elif image_type == "dynamips": 12 | COMMAND = f'ishare2 pull dynamips {id}' 13 | elif image_type == "qemu": 14 | COMMAND = f'ishare2 pull qemu {id}' 15 | elif image_type == "docker": 16 | COMMAND = f'docker pull {id}' 17 | else: 18 | # handle an invalid image_type value here 19 | raise ValueError("Invalid image_type specified") 20 | 21 | retcode, stdout, stderr = _run_command(COMMAND) 22 | if PERMISSIONS_APPLIED in stdout: 23 | return { 24 | "id": id, 25 | "status": retcode, 26 | "message": "Image has been downloaded successfully.", 27 | "type": image_type 28 | } 29 | if ALREADY_EXISTS in stdout: 30 | return { 31 | "id": id, 32 | "status": retcode, 33 | "message": "Image already exists in the server and cannot be downloaded twice.", 34 | "type": image_type 35 | } 36 | if retcode != 0: 37 | return { 38 | "id": id, 39 | "status": retcode, 40 | "message": f"Image could not be downloaded. Error: {stderr}", 41 | "type": image_type 42 | } 43 | -------------------------------------------------------------------------------- /python_scripts/get_images.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import csv 3 | from collections import defaultdict 4 | from .misc_utils import get_config 5 | from python_scripts.download_csv import download_csv 6 | 7 | 8 | def get_images(image_type): 9 | config = get_config() 10 | CSV_PATH = config["credentials"]["csv_path"] 11 | 12 | if image_type == "bin": 13 | CSV_FILE = os.path.join(CSV_PATH, "bin.csv") 14 | elif image_type == "dynamips": 15 | CSV_FILE = os.path.join(CSV_PATH, "dynamips.csv") 16 | elif image_type == "qemu": 17 | CSV_FILE = os.path.join(CSV_PATH, "qemu.csv") 18 | return get_qemu_list(CSV_FILE) 19 | else: 20 | # handle an invalid image_type value here 21 | raise ValueError("Invalid image_type specified") 22 | 23 | if not os.path.exists(CSV_FILE) or os.stat(CSV_FILE).st_size == 0: 24 | try: 25 | download_csv() 26 | except Exception as e: 27 | raise Exception(f"Error downloading CSV file: {e}") 28 | 29 | if not os.path.exists(CSV_FILE) or os.stat(CSV_FILE).st_size == 0: 30 | raise Exception(f"CSV file {CSV_FILE} is empty or does not exist") 31 | 32 | with open(CSV_FILE, "r") as f: 33 | csv_file = csv.reader(f, delimiter=',') 34 | next(csv_file) 35 | 36 | final_list = [] 37 | for row in csv_file: 38 | unit = row[3][:2] if len(row[3]) > 2 else row[3] 39 | final_list.append({ 40 | "name": row[1], 41 | "link": row[2], 42 | "size": float(row[3]), 43 | "unit": row[4], 44 | "type": image_type 45 | }) 46 | return final_list 47 | 48 | 49 | def get_qemu_list(qemu_csv): 50 | QEMU_CSV = qemu_csv 51 | 52 | if not os.path.exists(QEMU_CSV) or os.stat(QEMU_CSV).st_size == 0: 53 | try: 54 | download_csv() 55 | except Exception as e: 56 | raise Exception(f"Error downloading CSV file: {e}") 57 | 58 | if not os.path.exists(QEMU_CSV) or os.stat(QEMU_CSV).st_size == 0: 59 | raise Exception(f"CSV file {CSV_FILE} is empty or does not exist") 60 | 61 | with open(QEMU_CSV, "r") as f: 62 | csv_file = csv.reader(f, delimiter=',') 63 | header = next(csv_file) 64 | data_no_headers = list(csv_file) 65 | 66 | final_list = [] 67 | for element in data_no_headers: 68 | d = defaultdict(str) 69 | d["foldername"] = element[1] 70 | d["size"] = float(element[2]) 71 | d["unit"] = element[3] 72 | d["filename1"] = element[4] 73 | d["filelink1"] = element[5] 74 | d["filename2"] = element[6] 75 | d["filelink2"] = element[7] 76 | d["filename3"] = element[8] 77 | d["filelink3"] = element[9] 78 | d["filename4"] = element[10] 79 | d["filelink4"] = element[11] 80 | d["filename5"] = element[12] 81 | d["filelink5"] = element[13] 82 | d["filename6"] = element[14] 83 | d["filelink6"] = element[15] 84 | d["type"] = "qemu" 85 | final_list.append(d) 86 | return final_list 87 | -------------------------------------------------------------------------------- /python_scripts/installed_images.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .command_utils import _run_command 3 | 4 | 5 | def get_installed_images(image_type): 6 | if image_type == "bin": 7 | COMMAND = "ls -l /opt/unetlab/addons/iol/bin/ | awk '{ print $9 }'" 8 | elif image_type == "dynamips": 9 | COMMAND = "ls -l /opt/unetlab/addons/dynamips/ | awk '{ print $9 }'" 10 | elif image_type == "qemu": 11 | COMMAND = "ls -l /opt/unetlab/addons/qemu/ | awk '{ print $9 }'" 12 | elif image_type == "docker": 13 | COMMAND = "ishare2 installed docker" 14 | else: 15 | return [None, "Error: Invalid image type provided."] 16 | 17 | retcode, stdout, stderr = _run_command(COMMAND) 18 | if retcode != 0: 19 | return [] 20 | 21 | if image_type != "docker": 22 | result = [] 23 | files = stdout.strip().split("\n") 24 | for file_name in files: 25 | result.append({"name": file_name}) 26 | return result 27 | 28 | data = stdout.strip().split("\n") 29 | if "0 docker images found in server" in data[-1]: 30 | return [] 31 | data = data[3:-2] 32 | final_list = [] 33 | for sub in data: 34 | image = sub.split() 35 | dictionary = { 36 | "repository_name": image[0], 37 | "tag": image[1], 38 | "image_id": image[2], 39 | "created": image[3] + " " + image[4] + " " + image[5], 40 | "size": image[6][:-2], 41 | "unit": image[6][-2:] 42 | } 43 | final_list.append(dictionary) 44 | return [final_list] 45 | 46 | 47 | def is_valid_file(file_name): 48 | return file_name.endswith((".image", ".bin")) or os.path.isdir(file_name) 49 | -------------------------------------------------------------------------------- /python_scripts/live_search_form.py: -------------------------------------------------------------------------------- 1 | from .ishare2_search_bin import get_bin_list 2 | from .ishare2_search_dynamips import get_dynamips_list 3 | from .ishare2_search_qemu import get_qemu_list 4 | 5 | 6 | def live_search(q): 7 | results = [] 8 | 9 | results += live_search_bin_image(q) 10 | results += live_search_dynamips_image(q) 11 | results += live_search_qemu_image(q) 12 | 13 | return results 14 | 15 | 16 | def live_search_bin_image(q): 17 | data = get_bin_list() 18 | return [ 19 | { 20 | "name": value["name"], 21 | "size": value["size"], 22 | "unit": value["unit"], 23 | "id": value["id"], 24 | } for value in data if q in value["name"] 25 | ] 26 | 27 | 28 | def live_search_dynamips_image(q): 29 | data = get_dynamips_list() 30 | return [ 31 | { 32 | "name": value["name"], 33 | "size": value["size"], 34 | "unit": value["unit"], 35 | "id": value["id"], 36 | } for value in data if q in value["name"] 37 | ] 38 | 39 | 40 | def live_search_qemu_image(q): 41 | data = get_qemu_list() 42 | return [ 43 | { 44 | "name": value["foldername"], 45 | "size": value["size"], 46 | "unit": value["unit"], 47 | "id": value["id"], 48 | } for value in data if q in value["foldername"] 49 | ] 50 | -------------------------------------------------------------------------------- /python_scripts/misc_utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import os.path 4 | from .command_utils import _run_command 5 | 6 | 7 | def is_root(): 8 | retcode, stdout, stderr = _run_command("whoami") 9 | if retcode == 0 and stdout == "root": 10 | return "root" 11 | else: 12 | return stdout 13 | 14 | 15 | def relicense(): 16 | command = "ishare2 relicense" 17 | retcode, stdout, stderr = _run_command(command) 18 | if retcode == 0 and "Done" in stdout: 19 | command = "cat /opt/unetlab/addons/iol/bin/iourc" 20 | retcode, stdout, stderr = _run_command(command) 21 | if retcode == 0: 22 | license_value = stdout[10:-2] 23 | return { 24 | "status": 0, 25 | "message": "Relicense command has been applied successfully", 26 | "license_value": license_value 27 | } 28 | else: 29 | return { 30 | "status": 1, 31 | "message": "Failed to retrieve license value: {}".format(stderr), 32 | } 33 | else: 34 | return { 35 | "status": 1, 36 | "message": "Relicense command failed: {}".format(stderr), 37 | } 38 | 39 | 40 | def get_config(): 41 | with open('config.json', 'r') as f: 42 | return json.load(f) 43 | 44 | 45 | def get_version(): 46 | retcode, stdout, stderr = _run_command( 47 | "dpkg -l | grep pnetlab | head -n 1") 48 | if retcode == 0 and stdout != "": 49 | return "v{}".format(stdout.split()[2]) 50 | else: 51 | return "N/A" 52 | 53 | 54 | def ishare2_cli_version(): 55 | if not os.path.exists("/usr/sbin/ishare2"): 56 | if os.path.exists("/usr/sbin/ishare2_version"): 57 | retcode, stdout, stderr = _run_command( 58 | "rm /usr/sbin/ishare2_version") 59 | return "N/A" 60 | retcode, stdout, stderr = _run_command( 61 | "cat /usr/sbin/ishare2_version") 62 | if retcode == 0 and stdout != "": 63 | return "{}".format(stdout) 64 | else: 65 | return "N/A" 66 | 67 | 68 | def iol_license(): 69 | retcode, stdout, stderr = _run_command( 70 | "cat /opt/unetlab/addons/iol/bin/iourc") 71 | if retcode == 0 and stdout != "": 72 | return stdout 73 | else: 74 | return "N/A" 75 | 76 | 77 | def install_ishare2(): 78 | command = "curl -o /usr/sbin/ishare2 https://raw.githubusercontent.com/pnetlabrepo/ishare2/main/ishare2 > /dev/null 2>&1 && chmod +x /usr/sbin/ishare2 && ishare2" 79 | retcode, stdout, stderr = _run_command(command) 80 | if retcode == 0 and "Done" in stdout: 81 | return { 82 | "status": 0, 83 | "message": "ishare2 has been installed successfully", 84 | } 85 | else: 86 | return { 87 | "status": 1, 88 | "message": "ishare2 installation failed: {}. {}".format(stderr, stdout), 89 | } 90 | 91 | 92 | def get_changelog_content(changelog_type): 93 | if changelog_type == "ishare2-cli": 94 | return get_config()["constants"]["changelog_md_file"] 95 | elif changelog_type == "ishare2-gui": 96 | return get_config()["constants"]["changelog_gui_md_file"] 97 | else: 98 | raise Exception("Invalid changelog type", changelog_type) 99 | 100 | 101 | def get_help_content(): 102 | return get_config()["constants"]["help_md_file"] 103 | 104 | 105 | def get_social_content(): 106 | return get_config()["social"] 107 | 108 | 109 | def downloader(url, file_name): 110 | with open(file_name, "wb") as file: 111 | response = requests.get(url) 112 | file.write(response.content) 113 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | anyio<=3.7.0 2 | certifi<=2023.5.7 3 | charset-normalizer<=3.1.0 4 | click<=8.1.3 5 | fastapi<=0.103.1 6 | h11<=0.14.0 7 | idna<=3.4 8 | Jinja2<=3.1.2 9 | MarkupSafe<=2.1.3 10 | pydantic<=1.10.9 11 | requests<=2.31.0 12 | sniffio<=1.3.0 13 | starlette<=0.28.0 14 | typing_extensions<=4.6.3 15 | urllib3<=2.0.5 16 | uvicorn<=0.23.2 17 | -------------------------------------------------------------------------------- /routers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishare2-org/ishare2-web-gui/3c30aa842a100b7ce33596cfa35bb799f216bcb0/routers/__init__.py -------------------------------------------------------------------------------- /routers/changelogs.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | from fastapi.templating import Jinja2Templates 3 | from python_scripts.misc_utils import get_changelog_content 4 | from python_scripts.add_context import add_context 5 | import os 6 | 7 | router = APIRouter() 8 | 9 | templates = Jinja2Templates( 10 | directory=os.getenv("TEMPLATES_DIR", "web_app/src")) 11 | 12 | CHANGELOGS = ["ishare2-cli", "ishare2-gui"] 13 | 14 | for changelog in CHANGELOGS: 15 | @router.get(f"/changelogs/{changelog}/", tags=["changelogs"]) 16 | async def changelogs(request: Request, chagelog=changelog): 17 | data = { 18 | "title": f"Changelog - {changelog}", 19 | "url": get_changelog_content(chagelog) 20 | } 21 | context = {} 22 | context = add_context(context, request, data) 23 | return templates.TemplateResponse("pages/changelogs.html", context) 24 | -------------------------------------------------------------------------------- /routers/downloads.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | from fastapi.templating import Jinja2Templates 3 | from python_scripts.add_context import add_context 4 | from python_scripts.get_images import get_images 5 | from python_scripts.get_images import get_qemu_list 6 | import os 7 | 8 | router = APIRouter() 9 | 10 | templates = Jinja2Templates( 11 | directory=os.getenv("TEMPLATES_DIR", "web_app/src")) 12 | 13 | IMAGE_TYPES = ["bin", "dynamips", "qemu", "docker"] 14 | 15 | 16 | for image_type in IMAGE_TYPES: 17 | @router.get(f"/download/{image_type}", tags=["download"]) 18 | async def get_image_type(request: Request, image_type=image_type): 19 | data = { 20 | "title": f"{image_type} images - ishare2", 21 | "command": get_images(image_type), 22 | "type": image_type 23 | } 24 | context = {} 25 | context = add_context(context, request, data) 26 | return templates.TemplateResponse("pages/download.html", context) 27 | -------------------------------------------------------------------------------- /routers/installed.py: -------------------------------------------------------------------------------- 1 | import os 2 | from fastapi import Request, APIRouter 3 | from fastapi.templating import Jinja2Templates 4 | from python_scripts.installed_images import get_installed_images 5 | from python_scripts.add_context import add_context 6 | 7 | router = APIRouter() 8 | 9 | templates = Jinja2Templates( 10 | directory=os.getenv("TEMPLATES_DIR", "web_app/src")) 11 | 12 | IMAGE_TYPES = ["bin", "dynamips", "qemu", "docker"] 13 | for image_type in IMAGE_TYPES: 14 | @router.get(f"/installed/{image_type}", tags=["Get installed images"]) 15 | async def get_installed_image_type(request: Request, image_type=image_type): 16 | data = { 17 | "title": f"Installed {image_type} images - ishare2", 18 | "command": get_installed_images(image_type), 19 | "type": image_type 20 | } 21 | context = {} 22 | context = add_context(context, request, data) 23 | return templates.TemplateResponse("pages/installed.html", context) 24 | -------------------------------------------------------------------------------- /routers/request_delete.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from python_scripts.delete_images import delete_image 3 | 4 | router = APIRouter() 5 | IMAGE_TYPES = ["bin", "dynamips", "qemu", "docker"] 6 | 7 | for image_type in IMAGE_TYPES: 8 | @router.get(f"/delete/{image_type}/{{id}}", tags=["Delete images"]) 9 | async def delete_image_type(id, image_type=image_type): 10 | result = delete_image(id, image_type) 11 | return result 12 | -------------------------------------------------------------------------------- /routers/request_download.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from python_scripts.download_images import download_image 3 | 4 | router = APIRouter() 5 | 6 | IMAGE_TYPES = ["bin", "dynamips", "qemu", "docker"] 7 | 8 | 9 | for image_type in IMAGE_TYPES: 10 | @router.get(f"/download/{image_type}/{{id}}", tags=["Download images"]) 11 | async def download_image_type(id, image_type=image_type): 12 | result = download_image(id, image_type) 13 | return result 14 | -------------------------------------------------------------------------------- /web_app/src/components/alerts.html: -------------------------------------------------------------------------------- 1 | {% if is_root != 'root' %} 2 | 6 | {% endif %} -------------------------------------------------------------------------------- /web_app/src/components/confirmDeleteModal.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web_app/src/components/confirmDownloadModal.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web_app/src/components/dashboard.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Server info

4 |

Emulator

5 | {% if emulator_version != 'N/A' %} 6 |

Server is running {{ emulator_version }} emulator.

7 | {% else %} 8 |

{{ emulator_version }}: Server is not running an 9 | emulator. Or it is 10 | unknown. 11 |

12 | {% endif %} 13 |

ishare2 CLI version

14 | {% if ishare2_cli_version != 'N/A' %} 15 |

Server is running {{ ishare2_cli_version }} version of ishare2 CLI.

16 | {% else %} 17 |

{{ ishare2_cli_version }}: ishare2 CLI is not 18 | installed on the 19 | server or it was not found. Are you running as root?
20 | Click the button below to install it.

21 | 24 | {% endif %} 25 |
26 |
27 |
28 |
29 |

What can you do from this panel?

30 |

Manage your images, download new ones, install them or delete the ones you no longer need.

31 |
32 |
33 | 34 |
35 |
36 |
BIN Images
37 |
38 |
39 |

Download and install Cisco IOS images for use with network simulation 40 | software 41 | such as PNetLab or
eve-ng.

42 | 43 | Download images 44 | 46 | View installed 47 |
48 |
49 |
50 |
51 | 52 |
53 |
54 |
QEMU Images
55 |
56 |
57 |

Download and install QEMU images for use with network simulation 58 | software such 59 | as PNetLab or
eve-ng.

60 | 61 | Download images 62 | 64 | View installed 65 |
66 |
67 |
68 |
69 |
70 |
71 |
DYNAMIPS Images
72 |
73 |
74 |

Download and install Dynamips images for use with network simulation 75 | software 76 | such as PNetLab or
eve-ng. 77 |

78 | 79 | Download images 80 | 82 | View installed 83 |
84 |
85 |
86 |
87 |
88 |
89 |
Generate License (iourc file)
90 |
91 |
92 |

Click the "License" button to automatically generate a license for iol 93 | images. No further interaction is needed after clicking the button.

94 | 95 |
96 | 98 |
99 | 102 |
103 |
104 |
105 |
106 |
107 | 108 |
109 | -------------------------------------------------------------------------------- /web_app/src/components/footer.html: -------------------------------------------------------------------------------- 1 |
2 | {% if request.url.path.startswith('/download/') %} 3 | 4 | {% endif %} 5 | 6 | 19 | 20 | 37 | 38 |
-------------------------------------------------------------------------------- /web_app/src/components/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ data.title }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /web_app/src/components/navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web_app/src/pages/changelogs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include "components/head.html" %} 4 | 5 | 6 | 7 | {% include "components/navbar.html" %} 8 |
9 | {% include 'components/alerts.html' %} 10 |
11 |
12 | {% include 'components/footer.html' %} 13 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /web_app/src/pages/download.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include "components/head.html" %} 4 | 5 | 6 | {% include "components/navbar.html" %} 7 | 8 |
9 |
10 | {% include 'components/alerts.html' %} 11 |
12 |
13 |

Available {{ data.type }} images to download

14 |
15 |
16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for value in data.command %} 30 | 31 | 32 | 46 | {% if value.type == "qemu" %} 47 | 48 | {% else %} 49 | 50 | {% endif %} 51 | 52 | 53 | {% endfor %} 54 | 55 |
18 | Available {{ data.type }} images for download 19 |
#Actions Name Size
{{loop.index}} 33 | 39 | 45 | {{value.foldername}}{{value.name}}{{value.size}} {{value.unit}}
56 |
57 |
58 | 59 | {% include "components/confirmDownloadModal.html" %} {% include 60 | "components/confirmDeleteModal.html" %} 61 |
62 |
63 |
64 |
65 | {% include 'components/footer.html' %} 66 | 67 | 68 | -------------------------------------------------------------------------------- /web_app/src/pages/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block head %} {% include "components/head.html" %} {% endblock %} 4 | 5 | 6 | {% include 'components/alerts.html' %} 7 | {% include "components/navbar.html" %} 8 | 9 |
10 |
11 | 19 | PNETLAB platform Group 20 | 21 | ... 22 | members 23 | 24 |
25 | {% include 'components/footer.html' %} 26 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /web_app/src/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block head %} {% include "components/head.html" %} {% endblock %} 4 | 5 | 6 | {% include "components/navbar.html" %} 7 | 8 |
9 |
10 | {% include 'components/alerts.html' %} 11 |
12 |
13 |

Panel Dashboard

14 | {% include 'components/dashboard.html' %} 15 |
16 |
17 |
18 |
19 | 20 | {% include 'components/footer.html' %} 21 | 22 | 23 | -------------------------------------------------------------------------------- /web_app/src/pages/installed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block head %} {% include "components/head.html" %} {% endblock %} 4 | 5 | 6 | {% include "components/navbar.html" %} 7 | 8 |
9 |
10 | {% include 'components/alerts.html' %} 11 |
12 |
13 |

Installed {{ data.type }} images

14 |
15 | {% if data.command == [] %} 16 |

No {{ data.type }} images available.

17 | {% else %} 18 |
19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% for value in data.command %} 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 | 37 |
21 | List of installed {{ data.type }} images 22 |
#Name
{{loop.index}}{{value.name}}
38 |
39 | {% endif %} 40 |
41 |
42 |
43 |
44 | {% include 'components/footer.html' %} 45 | 46 | 47 | -------------------------------------------------------------------------------- /web_app/src/static/images/Homepage - ishare2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishare2-org/ishare2-web-gui/3c30aa842a100b7ce33596cfa35bb799f216bcb0/web_app/src/static/images/Homepage - ishare2.png -------------------------------------------------------------------------------- /web_app/src/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ishare2-org/ishare2-web-gui/3c30aa842a100b7ce33596cfa35bb799f216bcb0/web_app/src/static/images/favicon.ico -------------------------------------------------------------------------------- /web_app/src/static/scripts/app.js: -------------------------------------------------------------------------------- 1 | const year = document.getElementById("year"); 2 | year.textContent = new Date().getFullYear(); 3 | 4 | 5 | document.addEventListener('keydown', (event) => { 6 | if (event.key === 'k' && (event.ctrlKey || event.metaKey)) { 7 | event.preventDefault(); 8 | document.querySelector('.form-inline.my-2.my-lg-3 input[type="search"]').focus(); 9 | } 10 | }); 11 | const searchInput = document.querySelector('.form-inline.my-2.my-lg-3 input[type="search"]'); 12 | document.addEventListener('keydown', (event) => { 13 | if (event.key === 'Escape' && document.activeElement === searchInput) { 14 | event.preventDefault(); 15 | searchInput.blur(); 16 | } 17 | }); 18 | 19 | let headerCells = document.querySelectorAll('th'); 20 | headerCells.forEach(cell => { 21 | cell.addEventListener('click', function () { 22 | sortTable(cell.cellIndex); 23 | }); 24 | }); 25 | 26 | function sortTable(columnIndex) { 27 | let table = document.querySelector('table'); 28 | let rows = Array.from(table.rows).slice(1); 29 | 30 | // Sort the rows based on the content of the selected column 31 | rows.sort(function (a, b) { 32 | let valueA = a.cells[columnIndex].textContent; 33 | let valueB = b.cells[columnIndex].textContent; 34 | 35 | // Check if the values are numbers 36 | valueA = isNaN(parseFloat(valueA)) ? valueA.toLowerCase() : parseFloat(valueA); 37 | valueB = isNaN(parseFloat(valueB)) ? valueB.toLowerCase() : parseFloat(valueB); 38 | 39 | if (valueA < valueB) { 40 | return -1; 41 | } 42 | if (valueA > valueB) { 43 | return 1; 44 | } 45 | return 0; 46 | }); 47 | 48 | // Remove the current rows from the table 49 | rows.forEach(row => { 50 | table.deleteRow(row.rowIndex); 51 | }); 52 | 53 | // Re-add the sorted rows to the table 54 | rows.forEach(row => { 55 | table.appendChild(row); 56 | }); 57 | updateTableStyles(); 58 | } 59 | 60 | 61 | function updateTableStyles() { 62 | let table = document.querySelector('table'); 63 | let rows = Array.from(table.rows).slice(1); 64 | 65 | // Remove the existing styles from the table rows 66 | rows.forEach(row => { 67 | row.classList.remove('table-active'); 68 | row.classList.remove('table-secondary'); 69 | }); 70 | 71 | // Add the new styles to the table rows 72 | rows.forEach((row, index) => { 73 | if (index % 2 === 0) { 74 | row.classList.add('table-active'); 75 | } else { 76 | row.classList.add('table-secondary'); 77 | } 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /web_app/src/static/scripts/handleButtonsModals.js: -------------------------------------------------------------------------------- 1 | $('#confirmDownloadModal').on('show.bs.modal', function (event) { 2 | let button = $(event.relatedTarget) // Button that triggered the modal 3 | let id = button.data('index') // Extract info from data-* attributes 4 | let name = button.data('name') ? button.data('name') : button.data('foldername') // Extract info from data-* attributes 5 | let type = button.data('type') // Extract info from data-* attributes 6 | let size = button.data('size') // Extract info from data-* attributes 7 | let unit = button.data('size-unit') // Extract info from data-* attributes 8 | // If necessary, you could initiate an AJAX request here (and then do the updating in a callback). 9 | // Update the modal's content. We'll use jQuery here, but you could use a data binding library or other methods instead. 10 | let modal = $(this) 11 | // modal.find('.modal-title').text('New message to ' + recipient) 12 | // modal.find('.modal-body input').val(recipient) 13 | modal.find('#image-name').val(name) 14 | modal.find('#image-type').val(type) 15 | modal.find('#image-size').val(size + ' ' + unit) 16 | modal.find('#ishare-command').val('ishare2 pull ' + type + ' ' + id) 17 | modal.find('.btn-success').click(function () { 18 | downloadImage(id, name, type) 19 | }) 20 | }) 21 | $('#confirmDeleteModal').on('show.bs.modal', function (event) { 22 | let button = $(event.relatedTarget) // Button that triggered the modal 23 | let id = button.data('index') // Extract info from data-* attributes 24 | let name = button.data('name') ? button.data('name') : button.data('foldername') // Extract info from data-* attributes 25 | let type = button.data('type') // Extract info from data-* attributes 26 | let size = button.data('size') // Extract info from data-* attributes 27 | let unit = button.data('size-unit') // Extract info from data-* attributes 28 | // If necessary, you could initiate an AJAX request here (and then do the updating in a callback). 29 | // Update the modal's content. We'll use jQuery here, but you could use a data binding library or other methods instead. 30 | let modal = $(this) 31 | // modal.find('.modal-title').text('New message to ' + recipient) 32 | // modal.find('.modal-body input').val(recipient) 33 | modal.find('#image-name').val(name) 34 | modal.find('#image-type').val(type) 35 | modal.find('#image-size').val(size + ' ' + unit) 36 | modal.find('#ishare-command').val('ishare2 pull ' + type + ' ' + id) 37 | modal.find('.btn-danger').click(function () { 38 | deleteImage(id, name, type) 39 | }) 40 | }) 41 | 42 | let isAjaxRunning = false; // track whether an AJAX request is running 43 | 44 | window.onbeforeunload = function () { 45 | if (isAjaxRunning) { 46 | return "Dude, are you sure you want to leave? Think of the kittens!"; 47 | } 48 | }; 49 | 50 | function downloadImage(id, name, type) { 51 | isAjaxRunning = true; 52 | $.ajax({ 53 | url: '/download/' + type + '/' + id, 54 | type: 'GET', 55 | 56 | success: function (result) { 57 | let msgclass; 58 | result.status > 0 ? msgclass = "error" : msgclass = "success"; 59 | $.notify(result.message, { 60 | position: "bottom right", 61 | style: "bootstrap", 62 | className: msgclass, 63 | autoHide: true, 64 | clickToHide: true, 65 | autoHideDelay: 3000 66 | }); 67 | } 68 | }); 69 | isAjaxRunning = false; 70 | } 71 | 72 | function deleteImage(id, name, type) { 73 | isAjaxRunning = true; 74 | $.ajax({ 75 | url: '/delete/' + type + '/' + name, 76 | type: 'GET', 77 | success: function (result) { 78 | let msgclass; 79 | result.status > 0 ? msgclass = "error" : msgclass = "success"; 80 | $.notify(result.message, { 81 | position: "bottom right", 82 | style: "bootstrap", 83 | className: msgclass, 84 | autoHide: true, 85 | clickToHide: true, 86 | autoHideDelay: 3000 87 | }); 88 | } 89 | }); 90 | isAjaxRunning = false; 91 | } 92 | -------------------------------------------------------------------------------- /web_app/src/static/scripts/search.js: -------------------------------------------------------------------------------- 1 | const inputBox = document.getElementById("searchTerm") 2 | inputBox.addEventListener("keyup", searchTable); 3 | 4 | function searchTable(event) { 5 | if (event) { 6 | event.preventDefault(); 7 | } 8 | const tables = document.querySelectorAll("table") 9 | if (tables.length <= 0 || tables.length === undefined) { 10 | return; 11 | } 12 | 13 | const searchTerm = document.getElementById("searchTerm").value.trim().toLowerCase(); 14 | 15 | const urlParams = new URLSearchParams(window.location.search); 16 | urlParams.set("searchTerm", searchTerm); 17 | window.history.replaceState({}, "", `${window.location.pathname}?${urlParams}`); 18 | 19 | const tableRows = document.querySelectorAll("table tbody tr"); 20 | const similarWords = { 21 | "windows": ["win-", "winserver"], 22 | "windows 7": ["win-7"], 23 | "windows 10": ["win-10"], 24 | "cisco": ["vios"] 25 | // Add more search terms and their similar words here 26 | }; 27 | 28 | tableRows.forEach(row => { 29 | const rowCells = row.querySelectorAll("td"); 30 | 31 | let hideRow = true; 32 | rowCells.forEach(cell => { 33 | if (cell.cellIndex === 1) { 34 | return; 35 | } 36 | 37 | const cellText = cell.textContent.trim().toLowerCase(); 38 | let words = [searchTerm, ...(similarWords[searchTerm] || [])]; 39 | 40 | // Add the search term and its similar words with "-" separated 41 | words = words.concat(words.map(word => word.replace(/ /g, '-'))); 42 | 43 | words.forEach(word => { 44 | if (cellText.indexOf(word) !== -1) { 45 | hideRow = false; 46 | } 47 | }); 48 | }); 49 | 50 | row.style.display = hideRow ? "none" : "table-row"; 51 | }); 52 | } 53 | 54 | 55 | 56 | const urlParams = new URLSearchParams(window.location.search); 57 | const searchTerm = urlParams.get("searchTerm"); 58 | if (searchTerm) { 59 | document.getElementById("searchTerm").value = searchTerm; 60 | searchTable(); 61 | } 62 | 63 | const visibleRows = document.querySelectorAll("table tbody tr:not([style='display: none;'])"); 64 | console.log(visibleRows.length); 65 | const table = document.querySelector("table"); 66 | if (table !== null) { 67 | if (visibleRows.length === 0) { 68 | const noResultsRow = document.createElement("tr"); 69 | const noResultsCell = document.createElement("td"); 70 | noResultsCell.colSpan = "4"; 71 | noResultsCell.textContent = "No results found"; 72 | noResultsCell.style.textAlign = "center"; 73 | noResultsRow.appendChild(noResultsCell); 74 | table.querySelector("tbody").appendChild(noResultsRow); 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /web_app/src/static/styles/styles.css: -------------------------------------------------------------------------------- 1 | /* color variables */ 2 | :root { 3 | --darker: #222246; 4 | --medium-darker: #222250; 5 | --main-dark: #2c2c61; 6 | --light: #fff; 7 | } 8 | 9 | @media (min-width: 576px) { 10 | .bd-title { 11 | font-size: 3rem; 12 | } 13 | } 14 | .bd-title { 15 | margin-top: 1.3rem; 16 | margin-bottom: 0.8rem; 17 | font-weight: 300; 18 | font-weight: 500 !important; 19 | font-size: 5rem !important; 20 | } 21 | 22 | * { 23 | font-family: sans-serif; 24 | box-sizing: border-box; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | .nav-item { 30 | font-weight: 700; 31 | } 32 | 33 | .bg-black { 34 | background-color: var(--darker); 35 | } 36 | 37 | .fa-sort:hover { 38 | cursor: pointer; 39 | } 40 | 41 | main { 42 | background: var(--main-dark); 43 | color: var(--light); 44 | font-size: 16px; 45 | line-height: 1.5; 46 | font-weight: 400; 47 | overflow-x: hidden; 48 | min-height: 70vh; 49 | padding: 2rem; 50 | } 51 | 52 | table { 53 | width: 100%; 54 | } 55 | 56 | th { 57 | text-transform: uppercase; 58 | } 59 | 60 | footer { 61 | bottom: 0; 62 | left: 0; 63 | right: 0; 64 | background: var(--darker); 65 | width: 100vw; 66 | padding-top: 40px; 67 | color: #fff; 68 | } 69 | 70 | .footer-content { 71 | display: flex; 72 | align-items: center; 73 | justify-content: center; 74 | flex-direction: column; 75 | text-align: center; 76 | padding: 0 2.2rem; 77 | } 78 | 79 | .footer-content h3 { 80 | font-size: 2.1rem; 81 | font-weight: 500; 82 | line-height: 3rem; 83 | } 84 | 85 | .footer-content p { 86 | max-width: 500px; 87 | margin: 10px auto; 88 | line-height: 28px; 89 | font-size: 14px; 90 | color: #cacdd2; 91 | } 92 | 93 | .socials { 94 | list-style: none; 95 | display: flex; 96 | align-items: center; 97 | justify-content: center; 98 | margin: 1rem 0 3rem 0; 99 | } 100 | 101 | .socials li { 102 | margin: 0 10px; 103 | } 104 | 105 | .socials a { 106 | text-decoration: none; 107 | color: #fff; 108 | padding: 5px; 109 | } 110 | 111 | .socials a i { 112 | font-size: 1.1rem; 113 | width: 20px; 114 | transition: color 0.4s ease; 115 | } 116 | 117 | .socials a:hover i { 118 | color: aqua; 119 | } 120 | 121 | .footer-bottom { 122 | background: var(--medium-darker); 123 | width: 100vw; 124 | padding: 3rem 2rem 4rem; 125 | text-align: center; 126 | } 127 | 128 | .footer-bottom p { 129 | float: left; 130 | font-size: 14px; 131 | word-spacing: 2px; 132 | } 133 | 134 | .footer-bottom p a { 135 | color: #ccc; 136 | font-size: 16px; 137 | text-decoration: none; 138 | } 139 | 140 | .footer-bottom span { 141 | text-transform: uppercase; 142 | } 143 | 144 | .footer-menu { 145 | float: right; 146 | } 147 | 148 | .footer-menu ul { 149 | display: flex; 150 | } 151 | 152 | .footer-menu ul li { 153 | padding-right: 10px; 154 | display: block; 155 | } 156 | 157 | .footer-menu ul li a { 158 | color: #cfd2d6; 159 | text-decoration: none; 160 | } 161 | 162 | .footer-menu ul li a:hover { 163 | color: #27bcda; 164 | } 165 | 166 | @media (max-width: 570px) { 167 | .footer-bottom p { 168 | float: none; 169 | } 170 | .footer-menu { 171 | float: none; 172 | } 173 | 174 | .footer-menu ul { 175 | display: flex; 176 | text-align: center; 177 | } 178 | } 179 | 180 | @media screen and (min-width: 570px) { 181 | .input-group .custom-search { 182 | background-color: #bdbdbd; 183 | transition: width 0.5s ease-in-out !important; 184 | width: 12.5rem !important; 185 | box-shadow: 0 0 5px #bdbdbd !important; 186 | border: none !important; 187 | } 188 | .input-group .custom-search:focus { 189 | width: 25rem !important; 190 | box-shadow: 0 0 6px #bdbdbd !important; 191 | } 192 | } 193 | 194 | .input-group .custom-search { 195 | background-color: #bdbdbd; 196 | transition: width 0.5s ease-in-out !important; 197 | box-shadow: 0 0 5px #bdbdbd !important; 198 | border: none !important; 199 | } 200 | .input-group .custom-search:focus { 201 | box-shadow: 0 0 6px #bdbdbd !important; 202 | } 203 | 204 | .input-group-text { 205 | background-color: #bdbdbd; 206 | color: #000; 207 | border: 0; 208 | border-radius: 0; 209 | box-shadow: 0 0 5px #bdbdbd !important; 210 | } 211 | 212 | .changelog { 213 | max-width: 50rem; 214 | margin: 4rem auto; 215 | padding: 2rem; 216 | overflow: auto; 217 | background-color: #87cefa; 218 | border-radius: 1rem; 219 | color: var(--darker); 220 | } 221 | 222 | .jumbotron { 223 | padding: 2rem 1rem; 224 | margin-bottom: 2rem; 225 | background-color: #87cefa !important; 226 | border-radius: 2rem !important; 227 | color: var(--darker); 228 | } 229 | 230 | .table { 231 | overflow-x: auto; 232 | } 233 | .cust-pri { 234 | transition: ease 0.5s !important; 235 | } 236 | .cust-pri:hover { 237 | background-image: linear-gradient(to bottom, #0077e6, #0063c9) !important; 238 | } 239 | 240 | [class^="col-sm-"] { 241 | margin-top: 20px; 242 | margin-bottom: 20px; 243 | } 244 | 245 | .dash-card * a.btn { 246 | white-space: normal !important; 247 | transition: ease 0.5s !important; 248 | margin: 0.2rem !important; 249 | } 250 | 251 | .dash-card { 252 | min-height: 100%; 253 | } 254 | 255 | .current-iol-license { 256 | margin: 1.5rem 0; 257 | } 258 | 259 | @media screen and (max-width: 50rem) { 260 | .changelog { 261 | max-width: 100%; 262 | margin: 1rem; 263 | } 264 | } 265 | 266 | @media (min-width: 576px) { 267 | .jumbotron { 268 | padding: 2rem 1rem; 269 | } 270 | } 271 | 272 | .dark-modal { 273 | background-color: var(--medium-darker) !important; 274 | } 275 | --------------------------------------------------------------------------------