index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
-rw-r--r-- | archinstall/__init__.py | 147 | ||||
-rw-r--r-- | archinstall/lib/general.py | 28 | ||||
-rw-r--r-- | examples/guided.py | 15 |
diff --git a/archinstall/__init__.py b/archinstall/__init__.py index b0c938ad..865e9844 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -28,64 +28,149 @@ __version__ = "2.3.1.dev0" storage['__version__'] = __version__ -def initialize_arguments(): - config = {} +def define_arguments(): + """ + Define which explicit arguments do we allow. + Refer to https://docs.python.org/3/library/argparse.html for documentation and + https://docs.python.org/3/howto/argparse.html for a tutorial + Remember that the property/entry name python assigns to the parameters is the first string defined as argument and + dashes inside it '-' are changed to '_' + """ parser.add_argument("--config", nargs="?", help="JSON configuration file or URL") parser.add_argument("--creds", nargs="?", help="JSON credentials configuration file") + parser.add_argument("--disk_layouts","--disk_layout","--disk-layouts","--disk-layout",nargs="?", + help="JSON disk layout file") parser.add_argument("--silent", action="store_true", help="WARNING: Disables all prompts for input and confirmation. If no configuration is provided, this is ignored") - parser.add_argument("--dry-run", action="store_true", + parser.add_argument("--dry-run","--dry_run",action="store_true", help="Generates a configuration file and then exits instead of performing an installation") parser.add_argument("--script", default="guided", nargs="?", help="Script to run for installation", type=str) + parser.add_argument("--mount-point","--mount_point",nargs="?",type=str,help="Define an alternate mount point for installation") + parser.add_argument("--debug",action="store_true",help="Adds debug info into the log") + parser.add_argument("--plugin",nargs="?",type=str) + +def parse_unspecified_argument_list(unknowns :list, multiple :bool = False, error :bool = False) -> dict: + """We accept arguments not defined to the parser. (arguments "ad hoc"). + Internally argparse return to us a list of words so we have to parse its contents, manually. + We accept following individual syntax for each argument + --argument value + --argument=value + --argument = value + --argument (boolean as default) + the optional paramters to the function alter a bit its behaviour: + * multiple allows multivalued arguments, each value separated by whitespace. They're returned as a list + * error. If set any non correctly specified argument-value pair to raise an exception. Else, simply notifies the existence of a problem and continues processing. + + To a certain extent, multiple and error are incompatible. In fact, the only error this routine can catch, as of now, is the event + argument value value ... + which isn't am error if multiple is specified + """ + tmp_list = unknowns[:] # wastes a few bytes, but avoids any collateral effect of the destructive nature of the pop method() + config = {} + key = None + last_key = None + while tmp_list: + element = tmp_list.pop(0) # retreive an element of the list + if element.startswith('--'): # is an argument ? + if '=' in element: # uses the arg=value syntax ? + key, value = [x.strip() for x in element[2:].split('=', 1)] + config[key] = value + last_key = key # for multiple handling + key = None # we have the kwy value pair we need + else: + key = element[2:] + config[key] = True # every argument starts its lifecycle as boolean + else: + if element == '=': + continue + if key: + config[key] = element + last_key = key # multiple + key = None + else: + if multiple and last_key: + if isinstance(config[last_key],str): + config[last_key] = [config[last_key],element] + else: + config[last_key].append(element) + elif error: + raise ValueError(f"Entry {element} is not related to any argument") + else: + print(f" We ignore the entry {element} as it isn't related to any argument") + return config + +def get_arguments(): + """ The handling of parameters from the command line + Is done on following steps: + 0) we create a dict to store the arguments and their values + 1) preprocess. + We take those arguments which use Json files, and read them into the argument dict. So each first level entry becomes a argument un it's own right + 2) Load. + We convert the predefined argument list directly into the dict vía the vars() función. Non specified arguments are loaded with value None or false if they are booleans (action="store_true"). + The name is chosen according to argparse conventions. See above (the first text is used as argument name, but underscore substitutes dash) + We then load all the undefined arguments. In this case the names are taken as written. + Important. This way explicit command line arguments take precedence over configuración files. + 3) Amend + Change whatever is needed on the configuration dictionary (it could be done in post_process_arguments but this ougth to be left to changes anywhere else in the code, not in the arguments dictionary + """ + config = {} args, unknowns = parser.parse_known_args() + # preprocess the json files. + # TODO Expand the url access to the other JSON file arguments ? if args.config is not None: try: # First, let's check if this is a URL scheme instead of a filename parsed_url = urllib.parse.urlparse(args.config) if not parsed_url.scheme: # The Profile was not a direct match on a remote URL, it must be a local file. - with open(args.config) as file: - config = json.load(file) + if not json_stream_to_structure('--config',args.config,config): + exit(1) else: # Attempt to load the configuration from the URL. with urllib.request.urlopen(urllib.request.Request(args.config, headers={'User-Agent': 'ArchInstall'})) as response: - config = json.loads(response.read()) + config.update(json.loads(response.read())) except Exception as e: raise ValueError(f"Could not load --config because: {e}") if args.creds is not None: - with open(args.creds) as file: - config.update(json.load(file)) - - # Installation can't be silent if config is not passed + if not json_stream_to_structure('--creds',args.creds,config): + exit(1) + # load the parameters. first the known, then the unknowns + config.update(vars(args)) + config.update(parse_unspecified_argument_list(unknowns)) + # amend the parameters (check internal consistency) + # Installation can't be silent if config is not passed + if args.config is not None : config["silent"] = args.silent + else: + config["silent"] = False - for arg in unknowns: - if '--' == arg[:2]: - if '=' in arg: - key, val = [x.strip() for x in arg[2:].split('=', 1)] - else: - key, val = arg[2:], True - config[key] = val - - config["script"] = args.script + # avoiding a compatibility issue + if 'dry-run' in config: + del config['dry-run'] + return config - if args.dry_run is not None: - config["dry-run"] = args.dry_run +def post_process_arguments(arguments): + storage['arguments'] = arguments + if arguments.get('mount_point'): + storage['MOUNT_POINT'] = arguments['mount_point'] - return config + if arguments.get('debug',False): + log(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!", fg="red", level=logging.WARNING) + from .lib.plugins import plugins, load_plugin # This initiates the plugin loading ceremony + if arguments.get('plugin', None): + load_plugin(arguments['plugin']) -arguments = initialize_arguments() -storage['arguments'] = arguments -if arguments.get('debug'): - log(f"Warning: --debug mode will write certain credentials to {storage['LOG_PATH']}/{storage['LOG_FILE']}!", fg="red", level=logging.WARNING) -if arguments.get('mount-point'): - storage['MOUNT_POINT'] = arguments['mount-point'] + if arguments.get('disk_layouts', None) is not None: + if 'disk_layouts' not in storage: + storage['disk_layouts'] = {} + if not json_stream_to_structure('--disk_layouts',arguments['disk_layouts'],storage['disk_layouts']): + exit(1) -from .lib.plugins import plugins, load_plugin # This initiates the plugin loading ceremony -if arguments.get('plugin', None): - load_plugin(arguments['plugin']) +define_arguments() +arguments = get_arguments() +post_process_arguments(arguments) # TODO: Learn the dark arts of argparse... (I summon thee dark spawn of cPython) diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 7c8f8ea3..cc50e80a 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -481,3 +481,31 @@ def run_custom_user_commands(commands, installation): execution_output = SysCommand(f"arch-chroot {installation.target} bash /var/tmp/user-command.{index}.sh") log(execution_output) os.unlink(f"{installation.target}/var/tmp/user-command.{index}.sh") + +def json_stream_to_structure(id : str, stream :str, target :dict) -> bool : + """ Function to load a stream (file (as name) or valid JSON string into an existing dictionary + Returns true if it could be done + Return false if operation could not be executed + +id is just a parameter to get meaningful, but not so long messages + """ + from pathlib import Path + if Path(stream).exists(): + try: + with open(Path(stream)) as fh: + target.update(json.load(fh)) + except Exception as e: + log(f"{id} = {stream} does not contain a valid JSON format: {e}",level=logging.ERROR) + return False + else: + log(f"{id} = {stream} does not exists in the filesystem. Trying as JSON stream",level=logging.DEBUG) + # NOTE: failure of this check doesn't make stream 'real' invalid JSON, just it first level entry is not an object (i.e. dict), so it is not a format we handle. + if stream.strip().startswith('{') and stream.strip().endswith('}'): + try: + target.update(json.loads(stream)) + except Exception as e: + log(f" {id} Contains an invalid JSON format : {e}",level=logging.ERROR) + return False + else: + log(f" {id} is neither a file nor is a JSON string:",level=logging.ERROR) + return False + return True diff --git a/examples/guided.py b/examples/guided.py index c394e596..6db06c7e 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -1,7 +1,6 @@ import json import logging import os -import pathlib import time import archinstall @@ -50,18 +49,6 @@ def load_config(): archinstall.storage['gfx_driver_packages'] = archinstall.AVAILABLE_GFX_DRIVERS.get(archinstall.arguments.get('gfx_driver', None), None) if archinstall.arguments.get('servers', None) is not None: archinstall.storage['_selected_servers'] = archinstall.arguments.get('servers', None) - if archinstall.arguments.get('disk_layouts', None) is not None: - if (dl_path := pathlib.Path(archinstall.arguments['disk_layouts'])).exists() and str(dl_path).endswith('.json'): - try: - with open(dl_path) as fh: - archinstall.storage['disk_layouts'] = json.load(fh) - except Exception as e: - raise ValueError(f"--disk_layouts does not contain a valid JSON format: {e}") - else: - try: - archinstall.storage['disk_layouts'] = json.loads(archinstall.arguments['disk_layouts']) - except: - raise ValueError("--disk_layouts=<json> needs either a JSON file or a JSON string given with a valid disk layout.") def ask_user_questions(): @@ -212,7 +199,7 @@ def perform_filesystem_operations(): disk_layout_file.write(user_disk_layout) print() - if archinstall.arguments.get('dry-run'): + if archinstall.arguments.get('dry_run'): exit(0) if not archinstall.arguments.get('silent'): |