Files
vmware-tools/vCert-6.1.1-20260401/lib/menu.py
T
2026-06-23 11:27:54 +08:00

439 lines
17 KiB
Python

# Copyright (c) 2024-2025 Broadcom. All Rights Reserved.
# The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
import glob
import os
import yaml
import logging
from lib.console import print_header, print_text, ColorKey, set_text_color, print_text_error
from lib.environment import Environment
from lib.exceptions import MenuExitException, OperationFailed
from lib.host_utils import get_config_file_path
from lib.input import MenuInput
from lib.operation import Operation
from lib.text_utils import translate_text
from lib.vmdir import get_identity_sources
logger = logging.getLogger(__name__)
class MenuItem(object):
"""
Class for holding menu item information
"""
def __init__(self, text, method, method_args=None, key=None, is_disabled=False,
is_hidden=False, is_default=False, use_label_as_key=False):
"""
Initialize MenuItem object
:param text: label for the menu item when displayed as part of menu
:param method: the method to be called if the menu item is executed
:param method_args: arguments to be supplied to method
:param key: specify menu key explicitly. If this key not specified,
the key will be generated using a sequenced number
:param is_disabled: show menu as disabled menu item
:param is_default: use this menu item if the input return empty key
:param use_label_as_key: use the label as the key for default input
"""
self.text = text
self.method = method
self.method_args = method_args
self.key = key
self.is_disabled = is_disabled
self.is_hidden = is_hidden
self.is_default = is_default
self.use_label_as_key = use_label_as_key
class Menu(object):
"""
Class for representing a single menu dialog, providing the method to
define and show menu list, the handling of accepted inputs and the
execution of menu item
"""
def __init__(self):
self.title = ''
self.sub_title = ''
self.items = []
self.input_text = None
self.run_once = False
def set_menu_options(self, title, sub_title=None, run_once=False, input_text=None):
"""
Set the menu option: menu title and the input text
"""
self.title = title
self.sub_title = sub_title
self.input_text = input_text
self.run_once = run_once
def add_menu_item(self, label, method=None, method_args=None, key=None, is_disabled=False,
is_hidden=False, is_default=False, use_label_as_key=False):
"""
Add menu item entry
:param label: label for the menu item when displayed as part of menu
:param method: method object to be called if the menu item is executed
:param method_args: arguments to be supplied to method
:param key: specify menu key explicitly. If this key not specified,
the key will be auto-generated using sequence number
:param is_disabled: show menu as disabled menu item
:param is_hidden: if True, hidden the menu entry
:param is_default: use this menu item if the input return empty key
:param use_label_as_key: use the label as the key for default input
"""
# validate that the new menu item entry doesn't have conflict with the existing ones
if key is not None:
key = key.upper()
for item in self.items:
if key is not None and key == item.key:
raise ValueError("Duplicating menu item key: {}".format(key))
if is_default and item.is_default:
raise ValueError('Duplicating default menu item')
if is_hidden and key is None:
raise ValueError('Hidden menu item requires explicit key value')
self.items.append(MenuItem(label, method, method_args, key, is_disabled, is_hidden,
is_default, use_label_as_key))
def get_all_menu_keys(self):
"""
Get all menu keys defined or generated for the current menu entries
:returns all menu keys
"""
user_keys = []
for item in self.items:
if item.key is not None:
user_keys.append(item.key)
keys = [str(i) for i in range(1, len(self.items) - len(user_keys) + 1)]
keys.extend(user_keys)
return keys
def get_all_menu_keys_string(self) -> str:
"""
Get all menu keys as a single string
The auto-generated keys will be simplified using range expression.
Example: When get_all_menu_keys() returns ['1', '2', '3', 'E'],
this method will return '1-3, E'
:return meny keys in a single-line string
"""
keys = []
for item in self.items:
if item.key is not None:
keys.append(item.key)
num_others = len(self.items) - len(keys)
if num_others == 1:
keys.insert(0, "1")
elif num_others > 1:
keys.insert(0, "{}-{}".format(1, num_others))
return ", ".join(keys)
def get_menu_item(self, key):
"""
Get menu item using key {key}
:param key: key to be used to search the menu item
:return: MenuItem object
"""
key = key.upper()
item_key = 1
for item in self.items:
if item.key is None:
if key == str(item_key):
return item
else:
item_key += 1
elif item.key == key:
return item
return None
def get_input(self):
"""
Get user input from console
:return: key entered by user
"""
keys = self.get_all_menu_keys()
# get default key
default_key = None
seq = 0
for item in self.items:
if not item.key:
seq += 1
if item.is_default:
default_key = item.key if item.key else str(seq)
default_key_text = item.text if item.use_label_as_key else default_key
Environment.get_environment().get_value('CURRENT_MENU')['__DEFAULT__'] = default_key
break
# compose default input text if required
input_text = self.input_text
if self.input_text is None:
if default_key is not None:
input_text = "Select an option [{key}]: ".format(key=default_key_text)
else:
input_text = 'Select an option: '
menu_input = MenuInput(translate_text(input_text), acceptable_inputs=keys, default_input=default_key)
return menu_input.get_input()
def show_menu(self):
"""
Show the menu list
"""
if self.title:
print()
print_header(self.title)
if self.sub_title:
print_text(self.sub_title)
seq = 1
for item in self.items:
if item.is_hidden:
continue
key = item.key
if not key:
key = str(seq)
seq += 1
if item.is_disabled:
set_text_color(ColorKey.YELLOW)
print_text("{:>2}. {}".format(key, translate_text(item.text)))
if item.is_disabled:
set_text_color(ColorKey.NORMAL)
def run(self):
"""
Execute the menu: show the menu list, get user input, and execute
the selected menu item
"""
Environment.get_environment().set_value('CURRENT_MENU', dict())
while True:
self.show_menu()
print()
try:
key = self.get_input()
item = self.get_menu_item(key)
except KeyboardInterrupt:
raise MenuExitException('KeyboardInterrupt exception')
logger.info('Running menu item: {}'.format(item))
if item is None:
raise RuntimeError('Menu item not found')
elif item.is_disabled:
print()
print_text_error('Operation is not available')
return
elif item.method == Menu.run_navigation_exit:
raise MenuExitException('Exit requested')
elif item.method == Menu.run_navigation_return:
return
try:
if item.method_args is not None:
item.method(**item.method_args)
else:
item.method()
except OperationFailed as e:
print()
print_text_error('Operation failed: {}'.format(str(e)))
except KeyboardInterrupt:
raise MenuExitException('KeyboardInterrupt exception')
if self.run_once:
return
@staticmethod
def load_menu_item_from_file(config_file):
"""
Load menu item definition from yaml config file
:param config_file: file to be loaded
:return: menu item type, label, and enable condition string
"""
config_file = get_config_file_path(config_file)
with open(config_file, 'r') as file:
item_config = yaml.safe_load(file)
label = item_config.get('label')
condition = item_config.get('condition')
if label is None:
label = item_config['title']
return item_config['type'], label, condition
@staticmethod
def load_menu_items_from_files(config_files):
"""
Load multiple menu item definitions from yaml config files, specified
using a file pattern (glob)
:param config_files: file pattern to be loaded.
:return: list of a tuple of menu item type, label, and condition
"""
items = []
env = Environment.get_environment()
config_files = config_files if os.path.isabs(config_files) \
else "{}/{}".format(env.get_value('SCRIPT_DIR'), config_files)
for config in sorted(glob.glob(config_files)):
with open(config, 'r') as file:
item_config = yaml.safe_load(file)
logger.info('Configuration from {} is: {}'.format(config, item_config))
label = item_config.get('label')
condition = item_config.get('condition')
if label is None:
label = item_config['title']
items.append((item_config['type'], label, config, condition))
return items
@staticmethod
def load_menu_from_config(config_file):
"""
Load a full menu context from config file
:param config_file: a yaml config file to be loaded
:return: Menu object created from the config file
"""
env = Environment.get_environment()
menu = Menu()
config_file = get_config_file_path(config_file)
with open(config_file, 'r') as file:
menu_config = yaml.safe_load(file)
title = menu_config['title']
sub_title = menu_config.get('sub_title', '')
run_once = menu_config.get('run_once', False)
input_text = None
if menu_config.get('input'):
input_text = menu_config['input']['text']
menu.set_menu_options(title, sub_title, run_once, input_text)
for item_config in menu_config['items']:
item_type = item_config['type']
item_label = item_config.get('label')
is_default = item_config.get('default') is True
condition = item_config.get('condition')
is_disabled = env.get_value(condition) is not True if condition else False
is_hidden = item_config.get('hidden') is True
use_label_as_key = item_config.get('use_label_as_key', False)
key = item_config.get('key')
if item_type == 'single':
submenu_config = item_config['config']
submenu_type, submenu_label, submenu_condition = Menu.load_menu_item_from_file(submenu_config)
if item_label is None:
item_label = submenu_label
if not is_disabled:
is_disabled = env.get_value(submenu_condition) is not True if submenu_condition else False
handler = Menu.run_menu_from_config if submenu_type == 'menu' else Menu.run_operation_from_config
menu.add_menu_item(item_label, handler, {'config': submenu_config}, key=key,
is_disabled=is_disabled, is_hidden=is_hidden, is_default=is_default)
elif item_type == 'multiple':
items = Menu.load_menu_items_from_files(item_config['config'])
for submenu_type, submenu_label, submenu_config, submenu_condition in items:
is_disabled = env.get_value(submenu_condition) is not True if submenu_condition else False
handler = Menu.run_menu_from_config \
if submenu_type == 'menu' else Menu.run_operation_from_config
menu.add_menu_item(submenu_label, handler, {'config': submenu_config},
is_disabled=is_disabled)
elif item_type == 'navigation:exit':
menu.add_menu_item(item_label, Menu.run_navigation_exit, key=key, is_disabled=is_disabled,
is_hidden=is_hidden, is_default=is_default)
elif item_type == 'navigation:return':
menu.add_menu_item(item_label, Menu.run_navigation_return, key=key, is_disabled=is_disabled,
is_hidden=is_hidden, is_default=is_default, use_label_as_key=use_label_as_key)
return menu
@staticmethod
def get_menu_label_and_condition_from_config(config):
"""
Obtain menu label and condition only from a menu config file
:param config: config file to be loaded
:return: tuple of menu item label and condition
"""
config = get_config_file_path(config)
with open(config, 'r') as file:
menu_config = yaml.safe_load(file)
label = menu_config.get('label')
if label is None:
label = menu_config['title']
condition = menu_config.get('condition')
return label, condition
@staticmethod
def run_menu_from_config(config):
"""
Method to be used by MenuItem that load another menu (submenu)
:param config: config to be load in the submenu
"""
menu = Menu.load_menu_from_config(config)
menu.run()
@staticmethod
def run_operation_from_config(config):
"""
Method to be used by MenuItem that execute operation
:param config: config to be loaded to run the operation
"""
operation = Operation.load_operation_from_config(config)
try:
operation.run()
except OperationFailed as e:
print()
print_text_error("Operation failed: {}".format(str(e)))
@staticmethod
def run_navigation_exit():
"""
Method to be used by MenuItem that cause application exit
"""
pass
@staticmethod
def run_navigation_return():
"""
Method to be used by MenuItem that cause return to parent menu
"""
pass
@staticmethod
def set_menu_conditions():
Menu.set_ldaps_condition()
Menu.set_machine_ssl_csr_condition()
Menu.set_backup_store_condition()
@staticmethod
def set_ldaps_condition():
env = Environment.get_environment()
identity_sources = get_identity_sources(use_machine_account=True)
value = True if identity_sources else False
env.set_value('HAS_LDAPS_IDENTITY_SOURCE', value)
@staticmethod
def set_machine_ssl_csr_condition():
# XXX Issue 43: Import lib.vecs in the function to break circular
# dependency with lib.vecs which imports lib.menu.
from lib.vecs import get_certificate_aliases
env = Environment.get_environment()
machine_ssl_aliases = get_certificate_aliases('MACHINE_SSL_CERT')
value = True if '__MACHINE_CSR' in machine_ssl_aliases else False
env.set_value('HAS_MACHINE_SSL_CSR', value)
@staticmethod
def set_backup_store_condition():
# XXX Issue 43: Import lib.vecs in the function to break circular
# dependency with lib.vecs which imports lib.menu.
from lib.vecs import get_store_list
env = Environment.get_environment()
backup_stores = ['BACKUP_STORE', 'BACKUP_STORE_H5C']
vecs_stores = get_store_list()
value = any(store in vecs_stores for store in backup_stores)
env.set_value('HAS_BACKUP_STORE', value)