20 Commits

Author SHA1 Message Date
8a45c99efb Merge branch 'main' of https://github.com/OVERLORD7F/SVMU 2025-09-25 16:13:15 +03:00
9592382966 fixed #30 2025-09-25 16:12:52 +03:00
754db8ff41 Update README.md 2025-09-25 13:32:16 +03:00
d6642c62f4 Fix #29 2025-09-25 12:59:53 +03:00
dfff6c74e6 changed ico 2025-09-25 12:36:29 +03:00
22b746e7a8 SVMU exe logos 2025-09-25 11:55:27 +03:00
0df2fccc62 Update README.md
finally, first picture in readme!
2025-09-24 16:54:17 +03:00
OVERLORD7F
abf36d8661 Merge branch 'main' of https://github.com/OVERLORD7F/SVMU 2025-09-24 16:51:05 +03:00
OVERLORD7F
8141afb769 main menu pic 2025-09-24 16:50:46 +03:00
bdf81db7e6 Update README.md
Updated readme to reflect changes in 0.4-dev
2025-09-24 16:25:39 +03:00
43d0cd06b6 Update README.md
Updated readme to reflect changes in 0.4-dev
2025-09-24 16:23:13 +03:00
OVERLORD7F
cf3e71add4 Updated readme to reflect changes in 0.4-dev 2025-09-24 16:15:28 +03:00
OVERLORD7F
050d501900 Merge branch 'main' of https://github.com/OVERLORD7F/SVMU 2025-09-24 15:55:37 +03:00
OVERLORD7F
29af815ad3 0.4-dev
Added config profiles #23 (Massive changes in caonfig_data_import)
Removed example config #24
Fixed #25
Added current node display #26
Tested ISO auto-mount: Currently attached iso will be swapped (if any present)
Unified promts styling & other menu improvements
2025-09-24 15:47:32 +03:00
OVERLORD7F
18f13d94ef - Promts no longer case sensetive
- hopefully removed circular import and warnings with get_iso_name / get_vm_name
2025-09-18 15:59:44 +03:00
OVERLORD7F
6c272e46ab Implemented #22 2025-09-17 16:46:04 +03:00
OVERLORD7F
2210746bec and this one 2025-09-15 17:45:24 +03:00
OVERLORD7F
0f6e17a73c - option to auto-attach ISO to VMs #20
- added splash screen (ASCII ART) option to skip #21
- Implemented disk options as variables from config #19
- Related changes in config creation / editing menus
2025-09-15 17:44:55 +03:00
OVERLORD7F
1a92e86704 - added random splash screen on startup (testing)
- branding changes in menus
- ver. now stored in variable
2025-09-11 16:27:26 +03:00
OVERLORD7F
162a38be6d ver 6.5.7 tested - works! 2025-09-04 16:30:58 +03:00
10 changed files with 662 additions and 155 deletions

View File

@@ -1,11 +1,14 @@
# SpaceVM_VM_Utility
# SpaceVM Utility (SVMU)
Utility to manage Virtual Machines in SpaceVM.
Written in python, uses [SpaceVM API](https://spacevm.ru/docs/6.5/api/) to collect and manage existing Virtual Machines in your SpaceVM cluster.
![Utility Main Menu](https://github.com/OVERLORD7F/SVMU/blob/main/assets/images/svmu-main-menu.png)
>[!NOTE]
>_For now, this utility is focused on managing virtual disks_<br>
>_Works with SpaceVM 6.5.5+_
>_This utility is focused on managing virtual disks_<br>
>_Works with SpaceVM 6.5.5 / 6.5.6 / 6.5.7_ <br>
> [:file_folder:_Repo Mirror Available Here_:clipboard:](https://gt.7fproject.com/OVERLORD/SVMU)
# Requirements
- Fully setup SpaceVM cluster with VMs
@@ -15,15 +18,16 @@ Written in python, uses [SpaceVM API](https://spacevm.ru/docs/6.5/api/) to colle
> Utility is only tested on Windows 10
- For Windows 10 - [New Microsoft Terminal](https://github.com/microsoft/terminal) is highly recommended (correct colors, menus, etc)
# Utility usage
Clone repository or use compiled .exe from [Releases Tab](https://github.com/OVERLORD7F/SpaceVM_VM_Utility/releases)
# Utility usage options:
+ Clone repository, run `main.py` using python
+ Use precompiled .exe from [Releases Tab](https://github.com/OVERLORD7F/SpaceVM_VM_Utility/releases)
Fill in the config file as stated below.
## Config / Profile File
Directory _./profiles_ contains all configured profiles with necessary data for utility.
## Config File (SpaceVM_Utility.conf)
_SpaceVM_Utility.conf_ contains all necessary data for utility and has to be placed in the same directory as Utility itself.
You can create config and change specific options within the Utility.
This directory will be placed in the same directory as Utility itself.
>[!TIP]
>_You can create profiles and change specific options within the Utility._
```
[General]
#Master Controller IP of your cluster
@@ -35,12 +39,37 @@ controller_ip = 10.20.30.44
# do not specify JWT tag with your key!
api_key =
#skip start up splash screen (ASCII art)
skip_startup_splash = no
#loads this profile on utility startup by default
#only one profile could be loaded by default
load_by_default = false
[Data_Pool]
#Data pool which will be used for utility operations
#(Targeted storage for new vDisks)
data_pool_uuid =
[VM_Options]
#Select interface which will be used in virtual disk creation.
#Available options: virtio / ide / scsi / sata
disk_interface = virtio
#Select allocation type for virtual disks
#Available options: none / falloc / full / metadata
preallocation = falloc
#Specify uuid of iso you wish to automatically mount to Virtual Machines during operations (Courses)
#This step is skipped if "none" provided
iso_uuid = none
[Courses-Space-VM]
#Set vDisk size for "Prepare VMs for Courses" option
disk1 =
disk2 =
disk3 =
[VM_List]
#Selected VMs which will be used for utility operations
#How to find UUID:
@@ -48,10 +77,4 @@ data_pool_uuid =
#Use https://spacevm.ru/docs/latest/cli/space/vm/info/ or copy UUID from web panel
uuid_1 =
uuid_2 =
[Courses-Space-VM]
#Set vDisk size for "Prepare VMs for Courses" option
disk1 =
disk2 =
disk3 =
```

BIN
assets/images/SVMU.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/images/SVMU.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -12,66 +12,59 @@ console = Console()
def config_menu(base_url, api_key, config_relative_path):
cls()
config_menu_options="[gold bold][0] [grey53 italic]Show example configuration\n[/]\
\n[gold bold][1] [grey53 italic]Show current configuration\n[/] \
\n[gold bold][2] [grey53 italic]Setup new config file[/]\n \
\n[gold bold][3] [grey53 italic]Change selected data pool[/]\n\
\n[gold bold][4] [grey53 italic]Change selected VMs[/]\
config_menu_options="[gold bold][1] [grey53 italic]Show current profile configuration\n[/]\
\n[gold bold][2] [grey53 italic]Setup new profile[/]\n \
\n[gold bold][3] [grey53 italic]Switch profile[/]\n\
\n[gold bold][4] [grey53 italic]Delete profile[/]\n\
\n[gold bold][5] [grey53 italic]Set default profile[/]\n\
\n[gold bold][6] [grey53 italic]Change selected data pool[/]\n\
\n[gold bold][7] [grey53 italic]Change selected VMs[/]\n\
\n[gold bold][8] [grey53 italic]Change ISO UUID (auto-mount)[/]\n\
\n[gold bold][9] [grey53 italic]Skip start-up splash[/]\n\
\n\n[green_yellow bold]ENTER - return to Main Menu[/]"
config_menu_options=Align.center(config_menu_options, vertical="middle")
console = Console()
console.print(Panel(config_menu_options, title="[gold bold]SpaceVM Utility - Utility Configuration" , border_style="magenta" , width=150 , padding = 2))
sub_choice=str(input("\n>>> "))
if sub_choice == "0":
config_show_example()
sub_choice = console.input("[bold yellow]\n>>> [/]")
needs_reload = False
if sub_choice == "1":
config_show(config_relative_path)
config_menu(base_url, api_key, config_relative_path)
return config_menu(base_url, api_key, config_relative_path)
if sub_choice == "2":
config_edit(config_relative_path)
new_path = create_new_profile()
if new_path:
return new_path
if sub_choice == "3":
change_data_pool(base_url, api_key, config_relative_path)
new_profile_path = switch_profile()
if new_profile_path: # If we got a new profile path
return new_profile_path # Return it to main.py
if sub_choice == "4":
change_vm_uuids(config_relative_path)
delete_profile(config_relative_path)
def config_show_example():
conf_example= """
[General]
#Master Controller IP of your cluster
#Has to be accessible for a machine, which will be executing this Utility
controller_ip = 10.20.30.44
if sub_choice == "5":
set_default_profile()
#Integration API Key. how to get your key:
# ( https://spacevm.ru/docs/latest/base/operator_guide/security/users/#_14 )
# do not specify JWT tag with your key!
api_key =
if sub_choice == "6":
change_data_pool(base_url, api_key, config_relative_path)
needs_reload = True
if sub_choice == "7":
change_vm_uuids(config_relative_path, base_url, api_key)
needs_reload = True
[Data_Pool]
#Data pool which will be used for utility operations
#(Targeted storage for new vDisks)
data_pool_uuid =
if sub_choice == "8":
change_iso_uuid(config_relative_path)
needs_reload = True
[VM_List]
#Selected VMs which will be used for utility operations
#How to find UUID:
#List all available VMs in Utility Main Menu (Option 6)
#Use https://spacevm.ru/docs/latest/cli/space/vm/info/ or copy UUID from web panel
uuid_1 =
uuid_2 =
[Courses-Space-VM]
#Set vDisk size for "Prepare VMs for Courses™" option
disk1 = 10
disk2 = 20
disk3 = 20
"""
cls()
console.rule(title = "Example config file", align="center", style="yellow")
console.print(conf_example)
console.rule(style="yellow")
Prompt.ask("[green_yellow bold]ENTER - return to Utility Configuration.. :right_arrow_curving_down:")
if sub_choice == "9":
change_startup_option(config_relative_path)
needs_reload = True
return needs_reload
def config_show(config_relative_path):
cls()
@@ -85,6 +78,7 @@ def config_import(config_relative_path):
config = configparser.ConfigParser()
config.read(config_relative_path)
skip_startup_splash = config.get('General', 'skip_startup_splash')
base_url = config.get('General', 'controller_ip')
api_key = "jwt " + config.get('General', 'api_key') #That was realy obvious DACOM >:C
data_pool_uuid = config.get('Data_Pool', 'data_pool_uuid')
@@ -94,6 +88,27 @@ def config_import(config_relative_path):
for key, value in config['VM_List'].items():
vm_list.append(value)
#importing VM_Options
if config.has_section('VM_Options'):
iso_uuid = config.get('VM_Options' , 'iso_uuid')
disk_interface = config.get('VM_Options' , 'disk_interface')
preallocation = config.get('VM_Options' , 'preallocation')
iso_name=get_iso_name(base_url, api_key, iso_uuid)
else:
console.print("[bold yellow]Applying default values to Virtual Machine Options")
iso_uuid = "none"
disk_interface = "virtio"
preallocation = "falloc"
iso_name= "none"
config = configparser.ConfigParser() #writing default values to config
config["VM_Options"] = {
"disk_interface": "virtio",
"preallocation": "falloc",
"iso_uuid": "none",
}
with open(config_relative_path, "a") as configfile: # appending to existing config file
config.write(configfile)
#importing disk sizes for SpaceVM courses
if config.has_section('Courses-Space-VM'):
disk1_size = config.get('Courses-Space-VM', 'disk1')
@@ -117,12 +132,34 @@ def config_import(config_relative_path):
vm_names=[]
for x in vm_list:
vm_names.append(get_vm_name(base_url, api_key, x))
return base_url, api_key, data_pool_uuid, data_pool_name, vm_list, vm_names, disk1_size, disk2_size, disk3_size
return skip_startup_splash, base_url, api_key, data_pool_uuid, data_pool_name, vm_list, vm_names, disk1_size, disk2_size, disk3_size, disk_interface, preallocation, iso_uuid, iso_name
def change_startup_option(config_relative_path):
cls()
# Ask the user once and normalize the answer
new_value = Prompt.ask("[yellow bold]Skip start-up splash ?[/]", choices=["Y", "N"], default="N", case_sensitive=False)
if new_value.lower() == "y":
startup_option = "yes"
else:
startup_option = "no"
config = configparser.ConfigParser()
config.read(config_relative_path)
# Ensure General section exists so we can write the option
if not config.has_section('General'):
config.add_section('General')
config.set('General', 'skip_startup_splash', startup_option)
with open(config_relative_path, 'w') as config_file:
config.write(config_file)
console.print(f"[green bold]Option set to: {new_value}")
def change_data_pool(base_url, api_key, config_relative_path): #change selected data pool in config
cls()
show_data_pools(base_url, api_key)
new_data_pool_uuid = input("Type NEW Data Pool UUID: ")
new_data_pool_uuid = console.input("[bold yellow]Type NEW Data Pool UUID: [/]")
config = configparser.ConfigParser()
config.read(config_relative_path)
if config.has_section('Data_Pool'):
@@ -133,8 +170,20 @@ def change_data_pool(base_url, api_key, config_relative_path): #change selected
print("No 'Data_Pool' section in config file..")
config_show(config_relative_path)
def change_iso_uuid(config_relative_path):
cls()
new_iso_uuid = console.input("[bold yellow]Type ISO UUID: [/]")
config = configparser.ConfigParser()
config.read(config_relative_path)
if config.has_section('VM_Options'):
config.set('VM_Options', 'iso_uuid', new_iso_uuid)
with open(config_relative_path, 'w') as config_file:
config.write(config_file)
else:
print("No 'VM_Options' section in config file..")
config_show(config_relative_path)
def change_vm_uuids(config_relative_path): #change selected VM uuids in config
def change_vm_uuids(config_relative_path, base_url, api_key): #change selected VM uuids in config
config = configparser.ConfigParser()
config.read(config_relative_path)
# Remove old VM_List section if it exists, then add a fresh one
@@ -142,12 +191,17 @@ def change_vm_uuids(config_relative_path): #change selected VM uuids in config
config.remove_section('VM_List')
config.add_section('VM_List')
cls()
console.print("[yellow bold]Type new VM UUIDs one by one (input ENTER to stop):")
console.print("[yellow bold]Type new VM UUIDs one by one [red bold](ENTER to stop)[/] ")
x = 0
while True:
vm_input = input(">> ")
vm_input = console.input("[bold yellow]>> [/]" )
if not vm_input:
break
# validate only the entered VM UUID via get_vm_name
vm_name = get_vm_name(base_url, "jwt " + api_key, vm_input)
if not vm_name:
console.print("[red bold]Invalid VM UUID (not found)")
continue
x += 1
config.set('VM_List', f'uuid_{x}', vm_input)
@@ -159,52 +213,12 @@ def change_vm_uuids(config_relative_path): #change selected VM uuids in config
config_show(config_relative_path)
def config_edit(config_relative_path):
read_input = input("Create new config file? (Y / N): ")
menu_choice = str(read_input)
if menu_choice == "Y" or menu_choice == "y":
base_url = input("Type SpaceVM Controller IP: ")
while check_ping(base_url) != True:
base_url = console.input("[bold red]No response.\nCheck and type SpaceVM Controller IP again: [/]")
api_key = input("Type your API Key: ")
while check_api_key(base_url, "jwt " + api_key) != 200:
api_key = console.input("[bold red]Check and type SpaceVM Controller API Key again: [/]")
show_data_pools(base_url, "jwt " + api_key)
data_pool_uuid = input("Type Data Pool UUID you wish to use: ")
config = configparser.ConfigParser()
config["General"] = {
"controller_ip": base_url,
"api_key": api_key,
}
config["Data_Pool"] = {"data_pool_uuid": data_pool_uuid}
with open(config_relative_path, "w") as configfile:
config.write(configfile)
print("Type VM UUIDs one by one (input ENTER to stop)")
with open(config_relative_path, "a") as file:
file.write("[VM_List]\n") #manually writing section for VMs
vm_input = []
x = 0
while vm_input != "":
vm_input = input(">> ")
if vm_input:
x += 1
file.write(f"uuid_{x} = {vm_input}\n")
console.print("[green bold]VM UUIDs have been written in config :pencil:")
console.print("[green bold]Configuration completed ! :white_check_mark:")
Prompt.ask("[green_yellow bold]Press ENTER to proceed.. :right_arrow_curving_down:")
cls()
def check_config(config_relative_path):
if os.path.exists(config_relative_path) and os.path.getsize(config_relative_path) > 0: #check if config exists and not empty
pass #do nothing
else:
console.print("[yellow bold italic]Config file was not found or empty.. ")
config_edit(config_relative_path)
"""Check if config exists and is valid"""
# Only check if the file is empty and needs to be removed
if os.path.exists(config_relative_path) and os.path.getsize(config_relative_path) == 0:
console.print("[red bold]Config file is empty!")
os.remove(config_relative_path) # Remove empty file
def cls():
os.system('cls' if os.name=='nt' else 'clear')
@@ -219,4 +233,229 @@ def check_ping(base_url):
if status == 0:
return True
else:
return False
return False
def create_profiles_dir():
"""Create profiles directory if it doesn't exist"""
profiles_dir = os.path.join(os.getcwd(), 'profiles')
if not os.path.exists(profiles_dir):
os.makedirs(profiles_dir)
return profiles_dir
def get_available_profiles():
"""Get list of available profile names"""
profiles_dir = create_profiles_dir()
profiles = []
for filename in os.listdir(profiles_dir):
if filename.endswith('.conf'):
profiles.append(filename[:-5]) # Remove .conf extension
return profiles
def create_new_profile():
"""Create a new profile configuration"""
cls()
profiles_dir = create_profiles_dir()
profile_name = Prompt.ask("[yellow bold]Enter new profile name")
profile_path = os.path.join(profiles_dir, f"{profile_name}.conf")
if os.path.exists(profile_path):
console.print("[red bold]Profile already exists!")
return
base_url = console.input("[bold yellow]Type SpaceVM Controller IP: [/]")
while check_ping(base_url) != True:
base_url = console.input("[bold red]No response.\nCheck and type SpaceVM Controller IP again: [/]")
api_key = console.input("[bold yellow]Type your API Key: [/]")
while check_api_key(base_url, "jwt " + api_key) != 200:
api_key = console.input("[bold red]Check and type SpaceVM Controller API Key again: [/]")
show_data_pools(base_url, "jwt " + api_key)
data_pool_uuid = console.input("[bold yellow]Type Data Pool UUID you wish to use: [/]")
config = configparser.ConfigParser()
config["General"] = {
"controller_ip": base_url,
"api_key": api_key,
"skip_startup_splash": "no",
}
config["Data_Pool"] = {"data_pool_uuid": data_pool_uuid}
with open(profile_path, "w") as configfile:
config.write(configfile)
console.print("[yellow bold]Type VM UUIDs one by one (input ENTER to stop):")
with open(profile_path, "a") as file:
file.write("[VM_List]\n")
x = 0
while True:
vm_input = console.input("[bold yellow]>> [/]")
if not vm_input:
break
# validate only the vm_input by fetching VM name
vm_name = get_vm_name(base_url, "jwt " + api_key, vm_input)
if not vm_name:
console.print("[red bold]Invalid VM UUID (not found)")
continue
x += 1
file.write(f"uuid_{x} = {vm_input}\n")
console.print("[green bold]Configuration completed! :white_check_mark:")
# Prompt to switch to the newly created profile now
switch_now = Prompt.ask("[yellow bold]Switch to new profile now?", choices=["Y", "N"] , default="Y", case_sensitive=False)
if switch_now.lower() == "y" or switch_now.lower() == "Y":
# Also ask if user wants to set this profile as default
set_default = Prompt.ask("[yellow bold]Set new profile as default?", choices=["Y", "N"], default="N", case_sensitive=False)
if set_default.lower() == "y" or set_default.lower() == "Y":
# Use existing set_default_profile logic: mark this profile as default
profiles_dir = create_profiles_dir()
profiles = get_available_profiles()
for p in profiles:
p_path = os.path.join(profiles_dir, f"{p}.conf")
cfg = configparser.ConfigParser()
cfg.read(p_path)
if cfg.has_section('General'):
cfg.set('General', 'load_by_default', 'false')
with open(p_path, 'w') as f:
cfg.write(f)
# set new profile as default
cfg = configparser.ConfigParser()
cfg.read(profile_path)
if not cfg.has_section('General'):
cfg.add_section('General')
cfg.set('General', 'load_by_default', 'true')
with open(profile_path, 'w') as f:
cfg.write(f)
return profile_path
def switch_profile():
"""Switch to a different profile."""
cls()
profiles = get_available_profiles()
if not profiles:
console.print("[red bold]No profiles found!")
return
console.print("[yellow bold]Available profiles:")
for i, profile in enumerate(profiles, 1):
console.print(f"[grey53]{i}. {profile}")
choice = Prompt.ask("[yellow bold]Select profile number: ", choices=[str(i) for i in range(1, len(profiles) + 1)])
selected_profile = profiles[int(choice) - 1]
# Get the path for the selected profile
selected_profile_path = os.path.join(create_profiles_dir(), f"{selected_profile}.conf")
if not os.path.exists(selected_profile_path):
console.print(f"[red bold]Profile '{selected_profile}' does not exist!")
return
# Return the new profile path to update in main.py
console.print(f"[green bold]Switched to profile: {selected_profile}")
return selected_profile_path # Return the new path instead of True
def delete_profile(current_profile_path):
"""Delete an existing profile"""
cls()
profiles = get_available_profiles()
if not profiles:
console.print("[red bold]No profiles found!")
return
console.print("[yellow bold]Available profiles:")
for i, profile in enumerate(profiles, 1):
console.print(f"[grey53]{i}. {profile}")
choice = Prompt.ask("[yellow bold]Select profile to delete", choices=[str(i) for i in range(1, len(profiles)+1)])
selected_profile = profiles[int(choice)-1]
selected_profile_path = os.path.join(create_profiles_dir(), f"{selected_profile}.conf")
if os.path.normpath(selected_profile_path) == os.path.normpath(current_profile_path):
console.print("[red bold]Cannot delete the currently active profile!")
Prompt.ask("[green_yellow bold]Press ENTER to return.. :right_arrow_curving_down:")
return
confirm = Prompt.ask(f"[red bold]Are you sure you want to delete {selected_profile}?", choices=["Y", "N"] , default="Y" , case_sensitive=False)
if confirm.upper() == "Y":
os.remove(os.path.join(os.getcwd(), 'profiles', f"{selected_profile}.conf"))
console.print(f"[green bold]Profile {selected_profile} deleted!")
def set_default_profile():
"""Set the selected profile as the default profile."""
cls()
profiles = get_available_profiles()
if not profiles:
console.print("[red bold]No profiles found!")
return
console.print("[yellow bold]Available profiles:")
for i, profile in enumerate(profiles, 1):
console.print(f"[grey53]{i}. {profile}")
choice = Prompt.ask("[yellow bold]Select profile number to set as default", choices=[str(i) for i in range(1, len(profiles) + 1)])
selected_profile = profiles[int(choice) - 1]
# Set all other profiles' default flag to false
profiles_dir = create_profiles_dir()
for profile in profiles:
profile_path = os.path.join(profiles_dir, f"{profile}.conf")
config = configparser.ConfigParser()
config.read(profile_path)
if config.has_section('General'):
config.set('General', 'load_by_default', 'false')
with open(profile_path, 'w') as f:
config.write(f)
# Set the selected profile as default
selected_profile_path = os.path.join(profiles_dir, f"{selected_profile}.conf")
config = configparser.ConfigParser()
config.read(selected_profile_path)
if not config.has_section('General'):
config.add_section('General')
config.set('General', 'load_by_default', 'true')
with open(selected_profile_path, 'w') as f:
config.write(f)
console.print(f"[green bold]Profile '{selected_profile}' set as default!")
def get_default_config_path():
"""Retrieve the path of the default profile configuration or prompt the user to select one."""
profiles_dir = create_profiles_dir()
profiles = get_available_profiles()
for profile in profiles:
profile_path = os.path.join(profiles_dir, f"{profile}.conf")
config = configparser.ConfigParser()
config.read(profile_path)
if config.has_section('General') and config.has_option('General', 'load_by_default'):
if config.getboolean('General', 'load_by_default'):
return profile_path
# If no default profile is found, prompt the user to select one
if profiles:
console.print("[yellow bold]No default profile found. Please select a profile to load:")
for i, profile in enumerate(profiles, 1):
console.print(f"[grey53]{i}. {profile}")
choice = Prompt.ask("[yellow bold]Select profile number", choices=[str(i) for i in range(1, len(profiles) + 1)])
selected_profile = profiles[int(choice) - 1]
return os.path.join(profiles_dir, f"{selected_profile}.conf")
console.print("[red bold]No profiles available. Please create a new profile.")
create_new_profile()
# Refresh the profiles list and directly return the newly created profile
profiles = get_available_profiles()
if profiles:
new_profile_path = os.path.join(profiles_dir, f"{profiles[-1]}.conf")
console.print(f"[green bold]Loaded newly created profile: {profiles[-1]}")
return new_profile_path
raise RuntimeError("Failed to create or load a profile.")
# config_relative_path will be set when passed from main.py

View File

@@ -1,10 +1,11 @@
import os
import os, sys
import requests
from domain_api import *
from rich.prompt import Prompt
from rich.console import Console , Align
from rich.panel import Panel
def disk_edit_mode(base_url , api_key , data_pool_uuid , vm_uuids, disk1_size, disk2_size, disk3_size):
def disk_edit_mode(base_url , api_key , data_pool_uuid , vm_uuids, disk1_size, disk2_size, disk3_size, disk_interface, preallocation, iso_uuid):
os.system('cls' if os.name=='nt' else 'clear')
diks_edit_menu_options="[gold bold][1] [grey53 italic]Delete vDisk by UUID\n[/grey53 italic] \
\n[gold bold][2] [grey53 italic]Delete ALL vDisks on selected Virtual Machine[/grey53 italic]\n \
@@ -14,15 +15,15 @@ def disk_edit_mode(base_url , api_key , data_pool_uuid , vm_uuids, disk1_size, d
diks_edit_menu_options = Align.center(diks_edit_menu_options, vertical="middle")
console = Console()
console.print(Panel(diks_edit_menu_options, title="[bold red]Disk Edit Mode" , border_style="magenta" , width=150 , padding = 2))
sub_choice=str(input("\n>>> "))
sub_choice = console.input("[bold yellow]\n>>> [/]")
if sub_choice == "1":
read_input=input("Input vDisk uuid to delete: ")
read_input = console.input("[bold yellow]Input vDisk uuid to delete: [/]" )
vdisk_uuid=str(read_input)
delete_disk(base_url , api_key , vdisk_uuid)
if sub_choice == "2":
print(vm_uuids)
select_uuids=int(input("Select VM to delete disks from. \n Type VM uuid index number (from list above) to select: ")) - 1
select_uuids = int(console.input("[bold yellow]Select VM to delete disks from. \n Type VM uuid index number (from list above) to select: [/]")) - 1
vm_check_power(base_url , api_key , vm_uuids[select_uuids]) #power on check
domain_all_content = get_domain_all_content(base_url , api_key , vm_uuids[select_uuids])
disk_uuids = get_disk_uuids(base_url , api_key , domain_all_content)
@@ -31,11 +32,11 @@ def disk_edit_mode(base_url , api_key , data_pool_uuid , vm_uuids, disk1_size, d
console.print("[bold red]All attached vDisks has been deleted!")
if sub_choice == "3":
vdisk_size=str(input("Enter disk size (GB): "))
vdisk_size = str(console.input("[bold yellow]Enter disk size (GB): [/]"))
print(vm_uuids)
select_uuids=int(input("Select VM to attach new disk. \n Type VM uuid index number (from list above) to select: ")) - 1
select_uuids = int(console.input("[bold yellow]Select VM to attach new disk. \n Type VM uuid index number (from list above) to select: [/]")) - 1
print(f"{vm_uuids[select_uuids]} - {data_pool_uuid} - {vdisk_size} ")
create_and_attach_disk(base_url , api_key , vm_uuids[select_uuids] , data_pool_uuid , vdisk_size , "falloc")
create_and_attach_disk(base_url , api_key , vm_uuids[select_uuids] , data_pool_uuid , vdisk_size , disk_interface, preallocation)
if sub_choice == "4":
os.system('cls' if os.name=='nt' else 'clear')
@@ -51,17 +52,25 @@ def disk_edit_mode(base_url , api_key , data_pool_uuid , vm_uuids, disk1_size, d
if domain_info:
disk_uuids = get_disk_uuids(base_url , api_key , domain_all_content)
for y in disk_uuids:
delete_disk(base_url , api_key , y)
if not delete_disk(base_url , api_key , y): #if delete_disk returns False - aborting
console.print("[bold red] Aborting further operations.")
sys.exit(1)
for z in vm_uuids: # only for creating disks
domain_uuid = z.strip('\n')
vm_name = get_vm_name(base_url, api_key, domain_uuid)
console.print(f"\n[bold underline yellow]Creating and attaching disk to[/] [bright_cyan]{vm_name}:")
console.print(f"\n[bold underline yellow]Creating and attaching disks to[/] [bright_cyan]{vm_name}:")
domain_info = get_domain_info(base_url , api_key , domain_uuid)
domain_all_content = get_domain_all_content(base_url , api_key , domain_uuid)
#domain_all_content = get_domain_all_content(base_url , api_key , domain_uuid)
if domain_info:
create_and_attach_disk(base_url , api_key , domain_uuid , data_pool_uuid, disk1_size, "falloc")
create_and_attach_disk(base_url , api_key , domain_uuid , data_pool_uuid, disk2_size, "falloc")
create_and_attach_disk(base_url , api_key , domain_uuid , data_pool_uuid, disk3_size, "falloc")
#iso_uuid="b95241c1-6134-4263-9da5-013459612eeb"
create_and_attach_disk(base_url , api_key , domain_uuid , data_pool_uuid, disk1_size, disk_interface, preallocation)
create_and_attach_disk(base_url , api_key , domain_uuid , data_pool_uuid, disk2_size, disk_interface, preallocation)
create_and_attach_disk(base_url , api_key , domain_uuid , data_pool_uuid, disk3_size, disk_interface, preallocation)
if iso_uuid == 'none':
console.print("[grey53 italic]iso_uuid was not specified. Skipping ISO auto-mount..[/]")
else:
attach_iso(base_url, api_key, domain_uuid, iso_uuid)
console.print("[bold green]\nDone. Happy virtualization :thumbs_up::thumbs_up:")
Prompt.ask("[green_yellow bold]ENTER - return to Main Menu.. :right_arrow_curving_down:")
os.system('cls' if os.name=='nt' else 'clear')

View File

@@ -1,9 +1,9 @@
# functions for working with domain-api
import requests
import requests, json
import secrets #for generating unique names
import os
import configparser
from config_data_import import *
#from config_data_import import * hopefully removes circular import and warnings with get_iso_name / get_vm_name
from rich.console import Console , Align
from rich.columns import Columns
from rich.panel import Panel
@@ -76,7 +76,13 @@ def delete_disk(base_url , api_key , vdisk_uuid):
console.print(f"[grey53 italic]{vdisk_uuid}[/] :wastebasket:")
return True
else:
print(f"ERROR deleting disk {vdisk_uuid} :\n {response.status_code} - {response.text}")
#print(f"ERROR deleting disk {vdisk_uuid} :\n {response.status_code} - {response.text}")
error_data = json.loads(response.text)
#decoding unicode response-string
error_detail = error_data.get("errors", [{}])[0].get("detail", "")
decoded_error = error_detail.encode('utf-8').decode('utf-8')
console.print(f"[bold red]ERROR {response.status_code} deleting disk {vdisk_uuid} :cross_mark:")
console.print(f"[bold red]{decoded_error}")
return False
@@ -108,7 +114,26 @@ def get_disk_info(domain_all_content):
console.print(Columns(disk_info_renderables))
def get_cdrom_uuid(domain_all_content):
if 'cdroms' in domain_all_content:
cdrom_ids = [item['id'] for item in domain_all_content['cdroms']]
#print("CD-ROM UUIDs:")
for cdrom_id in cdrom_ids:
#print(cdrom_id)
return (cdrom_id)
else:
console.print("[bold yellow]No 'cdroms' field in recieved data. \nProbably VM does not have any CD-ROMs?")
def get_iso_name(base_url, api_key, iso_uuid):
url = f"http://{base_url}//api/iso/{iso_uuid}/"
response = requests.get(url, headers={'Authorization': api_key})
if response.status_code == 200:
iso_name = response.json()
return (f"{iso_name['filename']}")
def get_vm_name(base_url, api_key, vm_uuids):
console = Console()
url = f"http://{base_url}//api/domains/{vm_uuids}/"
response = requests.get(url, headers={'Authorization': api_key})
if response.status_code == 200:
@@ -116,12 +141,39 @@ def get_vm_name(base_url, api_key, vm_uuids):
return (f"{vm_name['verbose_name']}")
def get_vm_node_name(domain_all_content):
"""Return the node verbose_name from domain_all_content if present, else None.
Expected domain_all_content format:
"node": {"id": "<uuid>", "verbose_name": "string"}
"""
try:
if isinstance(domain_all_content, dict):
node = domain_all_content.get('node')
if isinstance(node, dict):
return node.get('verbose_name')
except Exception:
# Defensive: any unexpected structure returns None
pass
return None
def vm_info(base_url, api_key, vm_uuids):
domain_info = get_domain_info(base_url, api_key, vm_uuids)
domain_all_content = get_domain_all_content(base_url, api_key, vm_uuids)
if domain_info:
console = Console()
vm_info_lines = f"[bold]Power State:[/] [bold red]{power_state[domain_info['user_power_state']]}[/bold red] \n[bold]vDisks:[/] {domain_info['vdisks_count']}"
# Get the node verbose name from domain_all_content (if present)
vm_node = get_vm_node_name(domain_all_content)
# fallback display value
vm_node = vm_node if vm_node else "Unknown"
vm_info_lines = (
f"[bold]Power State:[/] [bold red]{power_state[domain_info['user_power_state']]}[/bold red]"
f" \n[bold]Node:[/] [bold bright_cyan]{vm_node}[/]"
f" \n[bold]vDisks:[/] {domain_info.get('vdisks_count', 'N/A')}"
)
vm_info_renderable = Panel(vm_info_lines, title=f"[bold magenta]{domain_info['verbose_name']}" , expand=False , border_style="yellow")
vm_info_renderable=Align.center(vm_info_renderable, vertical="middle")
print("\n")
@@ -129,9 +181,10 @@ def vm_info(base_url, api_key, vm_uuids):
console.print(vm_info_renderable)
console.rule(title = "[bold yellow]vDisks Info" , style="grey53" , align="center")
get_disk_info(domain_all_content)
#console.rule(title = "[bold yellow]CD-ROM UUIDs" , style="grey53" , align="center")
#print(get_cdrom_uuid(domain_all_content))
console.rule(style="yellow")
def vm_info_short(base_url, api_key):
url = f"http://{base_url}/api/domains/"
response = requests.get(url, headers={'Authorization': api_key})
@@ -157,7 +210,7 @@ def vm_info_short(base_url, api_key):
os.system('cls' if os.name=='nt' else 'clear')
def create_and_attach_disk(base_url , api_key , vm_id, data_pool_uuid, vdisk_size, preallocation):
def create_and_attach_disk(base_url , api_key , vm_id, data_pool_uuid, vdisk_size, disk_interface, preallocation):
domain_name=get_domain_info(base_url , api_key , vm_id)
disk_name=domain_name["verbose_name"] + "_" + secrets.token_hex(5) #generates unique hex id. this method can generate ~million unique ids
url = f"http://{base_url}/api/domains/{vm_id}/create-attach-vdisk/"
@@ -170,7 +223,7 @@ def create_and_attach_disk(base_url , api_key , vm_id, data_pool_uuid, vdisk_siz
"preallocation": preallocation,
"size": vdisk_size,
"datapool": data_pool_uuid,
"target_bus": "virtio",
"target_bus": disk_interface, #"virtio"
}
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}")) as progress:
task = progress.add_task("Creating and attaching vDisk...", total=None)
@@ -200,7 +253,7 @@ def select_vm_by_tags(base_url, api_key, config_relative_path):
url = f"http://{base_url}/api/domains/"
response = requests.get(url, headers={'Authorization': api_key})
if response.status_code == 200:
verbose_name_input = input("Specify tag: ")
verbose_name_input = console.input("[bold yellow]Specify tag: [/]")
output_renderables = []
vm_info_short = response.json()
y= vm_info_short
@@ -218,8 +271,8 @@ def select_vm_by_tags(base_url, api_key, config_relative_path):
console.rule(style="grey53")
if vm_id_list: # promt to write found VM UUIDs to config
write_to_config = Confirm.ask("[bold yellow]Write these VM UUIDs to config file?")
if write_to_config:
write_to_config = Prompt.ask("[bold yellow]Write these VM UUIDs to config file?" , choices=["Y" , "N"], default="Y", case_sensitive=False)
if write_to_config == "y" or write_to_config == "Y":
config = configparser.ConfigParser()
config.read(config_relative_path)
# Remove old VM_List section if it exists, then add a fresh one
@@ -236,6 +289,32 @@ def select_vm_by_tags(base_url, api_key, config_relative_path):
os.system('cls' if os.name=='nt' else 'clear')
return(vm_id_list)
def attach_iso(base_url, api_key, vm_id, iso_uuid):
url = f"http://{base_url}/api/domains/{vm_id}/attach-iso/"
domain_all_content = get_domain_all_content(base_url, api_key, vm_id)
cdrom_uuid=get_cdrom_uuid(domain_all_content)
headers={
"Authorization" : api_key,
"Content-Type" : "application/json",
}
payload= {
"iso": iso_uuid,
"cdrom": cdrom_uuid
}
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}")) as progress:
task = progress.add_task("Attaching selected ISO to VM...", total=None)
response = requests.post(url , headers=headers, json=payload)
progress.remove_task(task)
if response.status_code == 200:
iso_name = get_iso_name(base_url, api_key, iso_uuid)
console.print(f"[grey53 italic]ISO {iso_name} attached[/] :white_check_mark:")
return True
else:
console.print(f"[bold yellow]ERROR {response.status_code} attaching ISO \nProbably VM does not have any CD-ROMs?")
return False
def vm_menu(base_url, api_key, vm_uuids, config_relative_path):
os.system('cls' if os.name=='nt' else 'clear')
config_menu_options="[gold bold][1] [grey53 italic]Show VM info \n (for selected VMs in config)[/grey53 italic]\n \
@@ -245,7 +324,7 @@ def vm_menu(base_url, api_key, vm_uuids, config_relative_path):
config_menu_options=Align.center(config_menu_options, vertical="middle")
console = Console()
console.print(Panel(config_menu_options, title="[gold bold]Show VM info" , border_style="magenta" , width=150 , padding = 2))
sub_choice=str(input("\n>>> "))
sub_choice = console.input("[bold yellow]\n>>> [/]")
if sub_choice == "1":
os.system('cls' if os.name=='nt' else 'clear')
for x in vm_uuids:

55
main.py
View File

@@ -1,4 +1,5 @@
import os
from splash_screen import *
from config_data_import import *
from cluster_api import *
from domain_api import *
@@ -6,40 +7,60 @@ from data_pools_api import *
from disk_edit_mode import *
from rich.panel import Panel
from rich.console import Console , Align
SVMU_ver="0.4-dev"
config_relative_path = os.path.join(os.getcwd() , 'SpaceVM_Utility.conf') #config in the same directory with main.py
# Initialize console and clear screen
console = Console()
os.system('cls' if os.name=='nt' else 'clear')
print("Reading config from:", os.path.abspath(config_relative_path))
if not os.path.exists(config_relative_path):
print(f"Config file not found: {config_relative_path}")
# Initialize config path and ensure it points to a valid profile
config_relative_path = get_default_config_path()
# Get initial configuration and show startup screen
skip_startup_splash = get_skip_startup_splash(config_relative_path)
show_startup_logo(skip_startup_splash, SVMU_ver) #shows startup splash (ASCII art)
menu_choice=0
console = Console()
os.system('cls' if os.name=='nt' else 'clear')
while(menu_choice != ""): #main menu loop
check_config(config_relative_path)
base_url, api_key, data_pool_uuid, data_pool_name, vm_uuids, vm_names, disk1_size, disk2_size, disk3_size = config_import(config_relative_path) #importing API-KEY / IP / DATA POOL UUID / VM-UUIDs from config
menu_options=f"[gold bold][1] [grey53 italic]Manage utility config\n[/grey53 italic] \
profile_name = os.path.basename(os.path.splitext(config_relative_path)[0]) #converting full path to pretty file name
skip_startup_splash, base_url, api_key, data_pool_uuid, data_pool_name, vm_uuids, vm_names, disk1_size, disk2_size, disk3_size, disk_interface, preallocation, iso_uuid, iso_name = config_import(config_relative_path) #importing API-KEY / IP / DATA POOL UUID / VM-UUIDs from config
vm_pretty_names = ', '.join(vm_names)
menu_options=f"[gold bold][1] [grey53 italic]Utiliy Configuration / Profiles\n[/grey53 italic] \
\n[gold bold][2] [grey53 italic]Enter disk edit mode[/grey53 italic]\n \
\n[gold bold][3] [grey53 italic]Show breif cluster overview[/grey53 italic]\n \
\n[gold bold][4] [grey53 italic]Enter VM menu[/grey53 italic]\n \
\n[gold bold][5] [grey53 italic]Show data pools[/grey53 italic]\n \
\n\n[green_yellow bold]ENTER - exit Utility[/]\n\n \
[underline bold grey53]Currently imported config:[/]\n \
[bold grey53]Connected to Controller: [bright_yellow]{base_url}[/]\n Selected Data Pool: [bright_yellow]{data_pool_name}[/]\n Selected VMs:\n [bright_yellow]{vm_names}"
[underline bold grey53]Current profile:[/] [bold green]{profile_name}[/]\n \
[bold grey53]Connected to Controller: [bright_yellow]{base_url}[/]\n Selected Data Pool: [bright_yellow]{data_pool_name}[/]\n Selected VMs:\n [bright_yellow]{vm_pretty_names}[/]\n Auto-mount ISO: [bright_cyan]{iso_name}[/]"
menu_options=Align.center(menu_options, vertical="middle")
menu_subtitle = "[blue bold][link=https://github.com/OVERLORD7F/SpaceVM_VM_Utility]:wrench: Project_GitHub[/link] [yellow]| [magenta bold][link=https://spacevm.ru/docs/]:books: SpaceVM_Docs[/link] [yellow]| [red bold][link=https://comptek.ru]:briefcase: Comptek[/link]"
console.print(Panel(menu_options, title="[bold magenta]SpaceVM Utility - Main Menu" , subtitle = menu_subtitle, subtitle_align="right" , style="yellow" , width=150 , padding = 2))
menu_choice=str(input("\n>>> "))
menu_subtitle = "[blue bold][link=https://github.com/OVERLORD7F/SVMU]:wrench: Project_GitHub[/link] [yellow]| [magenta bold][link=https://spacevm.ru/docs/]:books: SpaceVM_Docs[/link] [yellow]| [red bold][link=https://comptek.ru]:briefcase: Comptek[/link]"
console.print(Panel(menu_options, title=f"[bold magenta]SpaceVM Utility {SVMU_ver} - Main Menu" , subtitle = menu_subtitle, subtitle_align="right" , style="yellow" , width=150 , padding = 2))
menu_choice = console.input("[bold yellow]\n>>> [/]")
if menu_choice == "1":
config_menu(base_url, api_key, config_relative_path)
result = config_menu(base_url, api_key, config_relative_path)
# Check if we got a new profile path
if isinstance(result, str):
# Update config path and reload
config_relative_path = result
os.system('cls' if os.name=='nt' else 'clear')
continue
# Check if we need to reload config
elif isinstance(result, bool) and result:
# Clear screen and continue to reload config
os.system('cls' if os.name=='nt' else 'clear')
continue
if menu_choice == "2":
disk_edit_mode(base_url , api_key , data_pool_uuid , vm_uuids, disk1_size, disk2_size, disk3_size)
disk_edit_mode(base_url , api_key , data_pool_uuid , vm_uuids, disk1_size, disk2_size, disk3_size, disk_interface, preallocation, iso_uuid)
os.system('cls' if os.name=='nt' else 'clear')
if menu_choice == "3":
cluster_info(base_url , api_key)
os.system('cls' if os.name=='nt' else 'clear')
if menu_choice == "4":
vm_menu(base_url , api_key, vm_uuids, config_relative_path)
os.system('cls' if os.name=='nt' else 'clear')
if menu_choice == "5":
show_data_pools(base_url , api_key)
os.system('cls' if os.name=='nt' else 'clear') #clears screen before looping back to main menu
os.system('cls' if os.name=='nt' else 'clear')
console.print("[red bold]Exiting Utility ")

76
splash-screens.txt Normal file
View File

@@ -0,0 +1,76 @@
================================================================================================================================================================
.dMMMb dMMMMb .aMMMb .aMMMb dMMMMMP dMP dMP dMMMMMMMMb dMP dMP dMMMMMMP dMP dMP dMP dMMMMMMP dMP dMP
dMP" VP dMP.dMP dMP"dMP dMP"VMP dMP dMP dMP dMP"dMP"dMP dMP dMP dMP amr dMP amr dMP dMP.dMP
VMMMb dMMMMP" dMMMMMP dMP dMMMP dMP dMP dMP dMP dMP dMP dMP dMP dMP dMP dMP dMP VMMMMP
dP .dMP dMP dMP dMP dMP.aMP dMP YMvAP" dMP dMP dMP dMP.aMP dMP dMP dMP dMP dMP dA .dMP
VMMMP" dMP dMP dMP VMMMP" dMMMMMP VP" dMP dMP dMP VMMMP" dMP dMP dMMMMMP dMP dMP VMMMP"
================================================================================================================================================================
.d8888b. 888 888 888b d888 888 888 888 d8b 888 d8b 888
d88P Y88b 888 888 8888b d8888 888 888 888 Y8P 888 Y8P 888
Y88b. 888 888 88888b.d88888 888 888 888 888 888
"Y888b. 88888b. 8888b. .d8888b .d88b. Y88b d88P 888Y88888P888 888 888 888888 888 888 888 888888 888 888
"Y88b. 888 "88b "88b d88P" d8P Y8b Y88b d88P 888 Y888P 888 888 888 888 888 888 888 888 888 888
"888 888 888 .d888888 888 88888888 Y88o88P 888 Y8P 888 888 888 888 888 888 888 888 888 888
Y88b d88P 888 d88P 888 888 Y88b. Y8b. Y888P 888 " 888 Y88b. .d88P Y88b. 888 888 888 Y88b. Y88b 888
"Y8888P" 88888P" "Y888888 "Y8888P "Y8888 Y8P 888 888 "Y88888P" "Y888 888 888 888 "Y888 "Y88888
888 888
888 Y8b d88P
888 "Y88P"
================================================================================================================================================================
oooooooo8 ooooo oooo oooo oooo ooooo oooo o8 o88 o888 o88 o8
888 ooooooooo ooooooo ooooooo ooooooooo8 888 88 8888o 888 888 88 o888oo oooo 888 oooo o888oo oooo oooo
888oooooo 888 888 ooooo888 888 888 888oooooo8 888 88 88 888o8 88 888 88 888 888 888 888 888 888 888
888 888 888 888 888 888 888 88888 88 888 88 888 88 888 888 888 888 888 888 888
o88oooo888 888ooo88 88ooo88 8o 88ooo888 88oooo888 888 o88o 8 o88o 888oo88 888o o888o o888o o888o 888o 8888
o888 o8o888
================================================================================================================================================================
______ __ __ __ __ __ __ __ __ __ __ __
/ \ | \ | \| \ / \ | \ | \ | \ | \| \| \ | \
| $$$$$$\ ______ ______ _______ ______ | $$ | $$| $$\ / $$ | $$ | $$ _| $$_ \$$| $$ \$$ _| $$_ __ __
| $$___\$$ / \ | \ / \ / \| $$ | $$| $$$\ / $$$ | $$ | $$| $$ \ | \| $$| \| $$ \ | \ | \
\$$ \ | $$$$$$\ \$$$$$$\| $$$$$$$| $$$$$$\\$$\ / $$| $$$$\ $$$$ | $$ | $$ \$$$$$$ | $$| $$| $$ \$$$$$$ | $$ | $$
_\$$$$$$\| $$ | $$ / $$| $$ | $$ $$ \$$\ $$ | $$\$$ $$ $$ | $$ | $$ | $$ __ | $$| $$| $$ | $$ __ | $$ | $$
| \__| $$| $$__/ $$| $$$$$$$| $$_____ | $$$$$$$$ \$$ $$ | $$ \$$$| $$ | $$__/ $$ | $$| \| $$| $$| $$ | $$| \| $$__/ $$
\$$ $$| $$ $$ \$$ $$ \$$ \ \$$ \ \$$$ | $$ \$ | $$ \$$ $$ \$$ $$| $$| $$| $$ \$$ $$ \$$ $$
\$$$$$$ | $$$$$$$ \$$$$$$$ \$$$$$$$ \$$$$$$$ \$ \$$ \$$ \$$$$$$ \$$$$ \$$ \$$ \$$ \$$$$ _\$$$$$$$
| $$ | \__| $$
| $$ \$$ $$
\$$ \$$$$$$
================================================================================================================================================================
______ ______ ______ ______ ______ __ __ __ __ __ __ ______ __ __ __ ______ __ __
/\ ___\ /\ == \ /\ __ \ /\ ___\ /\ ___\ /\ \ / / /\ "-./ \ /\ \/\ \ /\__ _\ /\ \ /\ \ /\ \ /\__ _\ /\ \_\ \
\ \___ \ \ \ _-/ \ \ __ \ \ \ \____ \ \ __\ \ \ \'/ \ \ \-./\ \ \ \ \_\ \ \/_/\ \/ \ \ \ \ \ \____ \ \ \ \/_/\ \/ \ \____ \
\/\_____\ \ \_\ \ \_\ \_\ \ \_____\ \ \_____\ \ \__| \ \_\ \ \_\ \ \_____\ \ \_\ \ \_\ \ \_____\ \ \_\ \ \_\ \/\_____\
\/_____/ \/_/ \/_/\/_/ \/_____/ \/_____/ \/_/ \/_/ \/_/ \/_____/ \/_/ \/_/ \/_____/ \/_/ \/_/ \/_____/
================================================================================================================================================================
███████╗██████╗ █████╗ ██████╗███████╗██╗ ██╗███╗ ███╗ ██╗ ██╗████████╗██╗██╗ ██╗████████╗██╗ ██╗
██╔════╝██╔══██╗██╔══██╗██╔════╝██╔════╝██║ ██║████╗ ████║ ██║ ██║╚══██╔══╝██║██║ ██║╚══██╔══╝╚██╗ ██╔╝
███████╗██████╔╝███████║██║ █████╗ ██║ ██║██╔████╔██║ ██║ ██║ ██║ ██║██║ ██║ ██║ ╚████╔╝
╚════██║██╔═══╝ ██╔══██║██║ ██╔══╝ ╚██╗ ██╔╝██║╚██╔╝██║ ██║ ██║ ██║ ██║██║ ██║ ██║ ╚██╔╝
███████║██║ ██║ ██║╚██████╗███████╗ ╚████╔╝ ██║ ╚═╝ ██║ ╚██████╔╝ ██║ ██║███████╗██║ ██║ ██║
╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝ ╚═══╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝
================================================================================================================================================================
#### ##### # # # ##### #### # # # # ##### # # ##### # # ##### ####
# # # # # # # # # # ## ## # # # # ## # # # ## # # #
# # # ##### #### #### #### # # # # # # # # # # # # # # ####
# # # # # # # # # # # # # ## # # # ## # # # #
#### # # # # # ##### #### # # # # # # # # # # # # #
================================================================================================================================================================
____
.--.--. ,' , `. ___ ,--, ___
/ / '. ,-.----. ,---. ,-+-,.' _ | ,--, ,--.'|_ ,--, ,--.'| ,--, ,--.'|_
| : /`. / \ / \ /__./| ,-+-. ; , || ,'_ /| | | :,' ,--.'| | | : ,--.'| | | :,'
; | |--` | : | ,---.; ; | ,--.'|' | ;| .--. | | : : : ' : | |, : : ' | |, : : ' :
| : ;_ | | .\ : ,--.--. ,---. ,---./___/ \ | || | ,', | ': ,'_ /| : . |.;__,' / `--'_ | ' | `--'_ .;__,' / .--,
\ \ `. . : |: | / \ / \ / \ ; \ ' || | / | | || | ' | | . .| | | ,' ,'| ' | | ,' ,'| | | | /_ ./|
`----. \| | \ :.--. .-. | / / ' / / \ \ \: |' | : | : |, | | ' | | |:__,'| : ' | | | | : ' | | :__,'| : , ' , ' :
__ \ \ || : . | \__\/: . .. ' / . ' / |; \ ' .; . | ; |--' : | | : ' ; ' : |__ | | : ' : |__ | | : ' : |__/___/ \: |
/ /`--' /: |`-' ," .--.; |' ; :__ ' ; /| \ \ '| : | | , | ; ' | | ' | | '.'|' : |__ | | '.'|' : |__ | | '.'|. \ ' |
'--'. / : : : / / ,. |' | '.'|' | / | \ ` ;| : ' |/ : | : ; ; | ; : ;| | '.'|; : ;| | '.'|; : ; \ ; :
`--'---' | | : ; : .' \ : :| : | : \ |; | |`-' ' : `--' \ | , / ; : ;| , / ; : ;| , / \ \ ;
`---'.| | , .-./\ \ / \ \ / '---" | ;/ : , .-./ ---`-' | , / ---`-' | , / ---`-' : \ \
`---` `--`---' `----' `----' '---' `--`----' ---`-' ---`-' \ ' ;
`--`

60
splash_screen.py Normal file
View File

@@ -0,0 +1,60 @@
import os
import random
import time
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
console = Console()
def get_skip_startup_splash(config_path):
"""Read only skip_startup_splash from config file."""
try:
with open(config_path, "r", encoding="utf-8") as f:
for line in f:
if line.strip().startswith("skip_startup_splash"):
# Example: skip_startup_splash = yes
return line.split("=")[-1].strip().lower()
except Exception:
pass
return "no" # Default if not found
def show_startup_logo(skip_startup_splash, SVMU_ver):
if skip_startup_splash == "yes":
return
splash_file = os.path.join(os.path.dirname(__file__), "splash-screens.txt")
if not os.path.exists(splash_file):
console.print("[bold red]Splash screens file not found![/bold red]")
return
with open(splash_file, "r", encoding="utf-8") as f:
content = f.read()
# Split ASCII art blocks by lines of '=' (at least 160 in a row)
blocks = [block.strip() for block in content.split("\n" + "="*160) if block.strip()]
if not blocks:
console.print("[bold red]No splash screens found![/bold red]")
return
art = random.choice(blocks)
# Animated spinner while showing splash
with Progress(
SpinnerColumn(),
TextColumn("[bold cyan]Loading SpaceVM Utility...[/bold cyan]"),
transient=True,
console=console,
) as progress:
task = progress.add_task("startup", total=None)
# Simulate loading
import time
time.sleep(1.5)
progress.remove_task(task)
# Show the ASCII art in a pretty panel
console.print(Panel.fit(art, subtitle=f"[bold magenta]{SVMU_ver}[/bold magenta]", subtitle_align="right", border_style="grey53" , width=200 , padding = 2))
time.sleep(2) #pause for 2 sec
os.system('cls' if os.name=='nt' else 'clear') #clears screen before returning
# show_startup_logo()