Rework user management (#1220)
* Rework users * Update user installation * Fix config serialization * Update * Update schemas and documentation * Update * Fix flake8 * Make users mypy compatible * Fix minor copy Co-authored-by: Daniel Girtler <girtler.daniel@gmail.com> Co-authored-by: Anton Hvornum <anton@hvornum.se>
This commit is contained in:
parent
353c05318c
commit
870da403e7
|
|
@ -15,4 +15,4 @@ jobs:
|
||||||
# one day this will be enabled
|
# one day this will be enabled
|
||||||
# run: mypy --strict --module archinstall || exit 0
|
# run: mypy --strict --module archinstall || exit 0
|
||||||
- name: run mypy
|
- name: run mypy
|
||||||
run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py
|
run: mypy --follow-imports=silent archinstall/lib/menu/selection_menu.py archinstall/lib/menu/global_menu.py archinstall/lib/models/network_configuration.py archinstall/lib/menu/list_manager.py archinstall/lib/user_interaction/network_conf.py archinstall/lib/models/users.py
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,8 @@ with archinstall.Installer('/mnt') as installation:
|
||||||
# In this case, we install a minimal profile that is empty
|
# In this case, we install a minimal profile that is empty
|
||||||
installation.install_profile('minimal')
|
installation.install_profile('minimal')
|
||||||
|
|
||||||
installation.user_create('devel', 'devel')
|
user = User('devel', 'devel', False)
|
||||||
|
installation.create_users(user)
|
||||||
installation.user_set_pw('root', 'airoot')
|
installation.user_set_pw('root', 'airoot')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
"""Arch Linux installer - guided, templates etc."""
|
"""Arch Linux installer - guided, templates etc."""
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
from .lib.disk import *
|
from .lib.disk import *
|
||||||
|
|
@ -13,6 +10,7 @@ from .lib.locale_helpers import *
|
||||||
from .lib.luks import *
|
from .lib.luks import *
|
||||||
from .lib.mirrors import *
|
from .lib.mirrors import *
|
||||||
from .lib.models.network_configuration import NetworkConfigurationHandler
|
from .lib.models.network_configuration import NetworkConfigurationHandler
|
||||||
|
from .lib.models.users import User
|
||||||
from .lib.networking import *
|
from .lib.networking import *
|
||||||
from .lib.output import *
|
from .lib.output import *
|
||||||
from .lib.models.dataclasses import (
|
from .lib.models.dataclasses import (
|
||||||
|
|
@ -211,6 +209,11 @@ def load_config():
|
||||||
handler = NetworkConfigurationHandler()
|
handler = NetworkConfigurationHandler()
|
||||||
handler.parse_arguments(arguments.get('nic'))
|
handler.parse_arguments(arguments.get('nic'))
|
||||||
arguments['nic'] = handler.configuration
|
arguments['nic'] = handler.configuration
|
||||||
|
if arguments.get('!users', None) is not None or arguments.get('!superusers', None) is not None:
|
||||||
|
users = arguments.get('!users', None)
|
||||||
|
superusers = arguments.get('!superusers', None)
|
||||||
|
arguments['!users'] = User.parse_arguments(users, superusers)
|
||||||
|
|
||||||
|
|
||||||
def post_process_arguments(arguments):
|
def post_process_arguments(arguments):
|
||||||
storage['arguments'] = arguments
|
storage['arguments'] = arguments
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ class ConfigurationOutput:
|
||||||
self._user_creds_file = "user_credentials.json"
|
self._user_creds_file = "user_credentials.json"
|
||||||
self._disk_layout_file = "user_disk_layout.json"
|
self._disk_layout_file = "user_disk_layout.json"
|
||||||
|
|
||||||
self._sensitive = ['!users', '!superusers', '!encryption-password']
|
self._sensitive = ['!users', '!encryption-password']
|
||||||
self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run']
|
self._ignore = ['abort', 'install', 'config', 'creds', 'dry_run']
|
||||||
|
|
||||||
self._process_config()
|
self._process_config()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from .profiles import Profile
|
||||||
from .disk.partition import get_mount_fs_type
|
from .disk.partition import get_mount_fs_type
|
||||||
from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError
|
from .exceptions import DiskError, ServiceException, RequirementError, HardwareIncompatibilityError, SysCallError
|
||||||
from .hsm import fido2_enroll
|
from .hsm import fido2_enroll
|
||||||
|
from .models.users import User
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
_: Any
|
_: Any
|
||||||
|
|
@ -1062,7 +1063,7 @@ class Installer:
|
||||||
self.log(f'Installing archinstall profile {profile}', level=logging.INFO)
|
self.log(f'Installing archinstall profile {profile}', level=logging.INFO)
|
||||||
return profile.install()
|
return profile.install()
|
||||||
|
|
||||||
def enable_sudo(self, entity: str, group :bool = False) -> bool:
|
def enable_sudo(self, entity: str, group :bool = False):
|
||||||
self.log(f'Enabling sudo permissions for {entity}.', level=logging.INFO)
|
self.log(f'Enabling sudo permissions for {entity}.', level=logging.INFO)
|
||||||
|
|
||||||
sudoers_dir = f"{self.target}/etc/sudoers.d"
|
sudoers_dir = f"{self.target}/etc/sudoers.d"
|
||||||
|
|
@ -1092,9 +1093,14 @@ class Installer:
|
||||||
# Guarantees sudoer conf file recommended perms
|
# Guarantees sudoer conf file recommended perms
|
||||||
os.chmod(pathlib.Path(rule_file_name), 0o440)
|
os.chmod(pathlib.Path(rule_file_name), 0o440)
|
||||||
|
|
||||||
return True
|
def create_users(self, users: Union[User, List[User]]):
|
||||||
|
if not isinstance(users, list):
|
||||||
|
users = [users]
|
||||||
|
|
||||||
def user_create(self, user :str, password :Optional[str] = None, groups :Optional[str] = None, sudo :bool = False) -> None:
|
for user in users:
|
||||||
|
self.user_create(user.username, user.password, user.groups, user.sudo)
|
||||||
|
|
||||||
|
def user_create(self, user :str, password :Optional[str] = None, groups :Optional[List[str]] = None, sudo :bool = False) -> None:
|
||||||
if groups is None:
|
if groups is None:
|
||||||
groups = []
|
groups = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ from ..user_interaction import ask_hostname
|
||||||
from ..user_interaction import ask_for_audio_selection
|
from ..user_interaction import ask_for_audio_selection
|
||||||
from ..user_interaction import ask_additional_packages_to_install
|
from ..user_interaction import ask_additional_packages_to_install
|
||||||
from ..user_interaction import ask_to_configure_network
|
from ..user_interaction import ask_to_configure_network
|
||||||
from ..user_interaction import ask_for_superuser_account
|
|
||||||
from ..user_interaction import ask_for_additional_users
|
from ..user_interaction import ask_for_additional_users
|
||||||
from ..user_interaction import select_language
|
from ..user_interaction import select_language
|
||||||
from ..user_interaction import select_mirror_regions
|
from ..user_interaction import select_mirror_regions
|
||||||
|
|
@ -33,7 +32,9 @@ from ..user_interaction import select_encrypted_partitions
|
||||||
from ..user_interaction import select_harddrives
|
from ..user_interaction import select_harddrives
|
||||||
from ..user_interaction import select_profile
|
from ..user_interaction import select_profile
|
||||||
from ..user_interaction import select_additional_repositories
|
from ..user_interaction import select_additional_repositories
|
||||||
|
from ..models.users import User
|
||||||
from ..user_interaction.partitioning_conf import current_partition_layout
|
from ..user_interaction.partitioning_conf import current_partition_layout
|
||||||
|
from ..output import FormattedOutput
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
_: Any
|
_: Any
|
||||||
|
|
@ -122,21 +123,13 @@ class GlobalMenu(GeneralMenu):
|
||||||
_('Root password'),
|
_('Root password'),
|
||||||
lambda preset:self._set_root_password(),
|
lambda preset:self._set_root_password(),
|
||||||
display_func=lambda x: secret(x) if x else 'None')
|
display_func=lambda x: secret(x) if x else 'None')
|
||||||
self._menu_options['!superusers'] = \
|
|
||||||
Selector(
|
|
||||||
_('Superuser account'),
|
|
||||||
lambda preset: self._create_superuser_account(),
|
|
||||||
default={},
|
|
||||||
exec_func=lambda n,v:self._users_resynch(),
|
|
||||||
dependencies_not=['!root-password'],
|
|
||||||
display_func=lambda x: self._display_superusers())
|
|
||||||
self._menu_options['!users'] = \
|
self._menu_options['!users'] = \
|
||||||
Selector(
|
Selector(
|
||||||
_('User account'),
|
_('User account'),
|
||||||
lambda x: self._create_user_account(),
|
lambda x: self._create_user_account(x),
|
||||||
default={},
|
default={},
|
||||||
exec_func=lambda n,v:self._users_resynch(),
|
display_func=lambda x: f'{len(x)} {_("User(s)")}' if len(x) > 0 else None,
|
||||||
display_func=lambda x: list(x.keys()) if x else '[]')
|
preview_func=self._prev_users)
|
||||||
self._menu_options['profile'] = \
|
self._menu_options['profile'] = \
|
||||||
Selector(
|
Selector(
|
||||||
_('Profile'),
|
_('Profile'),
|
||||||
|
|
@ -273,17 +266,28 @@ class GlobalMenu(GeneralMenu):
|
||||||
return text[:-1] # remove last new line
|
return text[:-1] # remove last new line
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _prev_users(self) -> Optional[str]:
|
||||||
|
selector = self._menu_options['!users']
|
||||||
|
if selector.has_selection():
|
||||||
|
users: List[User] = selector.current_selection
|
||||||
|
return FormattedOutput.as_table(users)
|
||||||
|
return None
|
||||||
|
|
||||||
def _missing_configs(self) -> List[str]:
|
def _missing_configs(self) -> List[str]:
|
||||||
def check(s):
|
def check(s):
|
||||||
return self._menu_options.get(s).has_selection()
|
return self._menu_options.get(s).has_selection()
|
||||||
|
|
||||||
|
def has_superuser() -> bool:
|
||||||
|
users = self._menu_options['!users'].current_selection
|
||||||
|
return any([u.sudo for u in users])
|
||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
if not check('bootloader'):
|
if not check('bootloader'):
|
||||||
missing += ['Bootloader']
|
missing += ['Bootloader']
|
||||||
if not check('hostname'):
|
if not check('hostname'):
|
||||||
missing += ['Hostname']
|
missing += ['Hostname']
|
||||||
if not check('!root-password') and not check('!superusers'):
|
if not check('!root-password') and not has_superuser():
|
||||||
missing += [str(_('Either root-password or at least 1 superuser must be specified'))]
|
missing += [str(_('Either root-password or at least 1 user with sudo privileges must be specified'))]
|
||||||
if not check('harddrives'):
|
if not check('harddrives'):
|
||||||
missing += ['Hard drives']
|
missing += ['Hard drives']
|
||||||
if check('harddrives'):
|
if check('harddrives'):
|
||||||
|
|
@ -380,23 +384,6 @@ class GlobalMenu(GeneralMenu):
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _create_superuser_account(self) -> Optional[Dict[str, Dict[str, str]]]:
|
def _create_user_account(self, defined_users: List[User]) -> List[User]:
|
||||||
superusers = ask_for_superuser_account(str(_('Manage superuser accounts: ')))
|
users = ask_for_additional_users(defined_users=defined_users)
|
||||||
return superusers if superusers else None
|
|
||||||
|
|
||||||
def _create_user_account(self) -> Dict[str, Dict[str, str | None]]:
|
|
||||||
users = ask_for_additional_users(str(_('Manage ordinary user accounts: ')))
|
|
||||||
return users
|
return users
|
||||||
|
|
||||||
def _display_superusers(self):
|
|
||||||
superusers = self._data_store.get('!superusers', {})
|
|
||||||
|
|
||||||
if self._menu_options.get('!root-password').has_selection():
|
|
||||||
return list(superusers.keys()) if superusers else '[]'
|
|
||||||
else:
|
|
||||||
return list(superusers.keys()) if superusers else ''
|
|
||||||
|
|
||||||
def _users_resynch(self) -> bool:
|
|
||||||
self.synch('!superusers')
|
|
||||||
self.synch('!users')
|
|
||||||
return False
|
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,12 @@ The contents in the base class of this methods serve for a very basic usage, and
|
||||||
```
|
```
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import copy
|
||||||
|
from os import system
|
||||||
|
from typing import Union, Any, TYPE_CHECKING, Dict, Optional
|
||||||
|
|
||||||
from .text_input import TextInput
|
from .text_input import TextInput
|
||||||
from .menu import Menu, MenuSelectionType
|
from .menu import Menu
|
||||||
from os import system
|
|
||||||
from copy import copy
|
|
||||||
from typing import Union, Any, TYPE_CHECKING, Dict, Optional
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
_: Any
|
_: Any
|
||||||
|
|
@ -144,14 +144,14 @@ class ListManager:
|
||||||
self.bottom_list = [self.confirm_action,self.cancel_action]
|
self.bottom_list = [self.confirm_action,self.cancel_action]
|
||||||
self.bottom_item = [self.cancel_action]
|
self.bottom_item = [self.cancel_action]
|
||||||
self.base_actions = base_actions if base_actions else [str(_('Add')),str(_('Copy')),str(_('Edit')),str(_('Delete'))]
|
self.base_actions = base_actions if base_actions else [str(_('Add')),str(_('Copy')),str(_('Edit')),str(_('Delete'))]
|
||||||
self.base_data = base_list
|
self._original_data = copy.deepcopy(base_list)
|
||||||
self._data = copy(base_list) # as refs, changes are immediate
|
self._data = copy.deepcopy(base_list) # as refs, changes are immediate
|
||||||
# default values for the null case
|
# default values for the null case
|
||||||
self.target: Optional[Any] = None
|
self.target: Optional[Any] = None
|
||||||
self.action = self._null_action
|
self.action = self._null_action
|
||||||
|
|
||||||
if len(self._data) == 0 and self._null_action:
|
if len(self._data) == 0 and self._null_action:
|
||||||
self.exec_action(self._data)
|
self._data = self.exec_action(self._data)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -175,12 +175,10 @@ class ListManager:
|
||||||
clear_screen=False,
|
clear_screen=False,
|
||||||
clear_menu_on_exit=False,
|
clear_menu_on_exit=False,
|
||||||
header=self.header,
|
header=self.header,
|
||||||
skip_empty_entries=True
|
skip_empty_entries=True,
|
||||||
|
skip=False
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
if target.type_ == MenuSelectionType.Esc:
|
|
||||||
return self.run()
|
|
||||||
|
|
||||||
if not target.value or target.value in self.bottom_list:
|
if not target.value or target.value in self.bottom_list:
|
||||||
self.action = target
|
self.action = target
|
||||||
break
|
break
|
||||||
|
|
@ -188,21 +186,23 @@ class ListManager:
|
||||||
if target.value and target.value in self._default_action:
|
if target.value and target.value in self._default_action:
|
||||||
self.action = target.value
|
self.action = target.value
|
||||||
self.target = None
|
self.target = None
|
||||||
self.exec_action(self._data)
|
self._data = self.exec_action(self._data)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(self._data,dict):
|
if isinstance(self._data,dict):
|
||||||
data_key = data_formatted[target.value]
|
data_key = data_formatted[target.value]
|
||||||
key = self._data[data_key]
|
key = self._data[data_key]
|
||||||
self.target = {data_key: key}
|
self.target = {data_key: key}
|
||||||
|
elif isinstance(self._data, list):
|
||||||
|
self.target = [d for d in self._data if d == data_formatted[target.value]][0]
|
||||||
else:
|
else:
|
||||||
self.target = self._data[data_formatted[target.value]]
|
self.target = self._data[data_formatted[target.value]]
|
||||||
|
|
||||||
# Possible enhacement. If run_actions returns false a message line indicating the failure
|
# Possible enhacement. If run_actions returns false a message line indicating the failure
|
||||||
self.run_actions(target.value)
|
self.run_actions(target.value)
|
||||||
|
|
||||||
if not target or target == self.cancel_action: # TODO dubious
|
if target.value == self.cancel_action: # TODO dubious
|
||||||
return self.base_data # return the original list
|
return self._original_data # return the original list
|
||||||
else:
|
else:
|
||||||
return self._data
|
return self._data
|
||||||
|
|
||||||
|
|
@ -221,10 +221,9 @@ class ListManager:
|
||||||
|
|
||||||
self.action = choice.value
|
self.action = choice.value
|
||||||
|
|
||||||
if not self.action or self.action == self.cancel_action:
|
if self.action and self.action != self.cancel_action:
|
||||||
return False
|
self._data = self.exec_action(self._data)
|
||||||
else:
|
|
||||||
return self.exec_action(self._data)
|
|
||||||
"""
|
"""
|
||||||
The following methods are expected to be overwritten by the user if the needs of the list are beyond the simple case
|
The following methods are expected to be overwritten by the user if the needs of the list are beyond the simple case
|
||||||
"""
|
"""
|
||||||
|
|
@ -293,3 +292,5 @@ class ListManager:
|
||||||
self._data[origkey] = value
|
self._data[origkey] = value
|
||||||
elif self.action == str(_('Delete')):
|
elif self.action == str(_('Delete')):
|
||||||
del self._data[origkey]
|
del self._data[origkey]
|
||||||
|
|
||||||
|
return self._data
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, List, Union, Any, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
_: Any
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class User:
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
sudo: bool
|
||||||
|
|
||||||
|
@property
|
||||||
|
def groups(self) -> List[str]:
|
||||||
|
# this property should be transferred into a class attr instead
|
||||||
|
# if it's every going to be used
|
||||||
|
return []
|
||||||
|
|
||||||
|
def json(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'username': self.username,
|
||||||
|
'!password': self.password,
|
||||||
|
'sudo': self.sudo
|
||||||
|
}
|
||||||
|
|
||||||
|
def display(self) -> str:
|
||||||
|
password = '*' * len(self.password)
|
||||||
|
return f'{_("Username")}: {self.username:16} {_("Password")}: {password:16} sudo: {str(self.sudo)}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse(cls, config_users: List[Dict[str, Any]]) -> List['User']:
|
||||||
|
users = []
|
||||||
|
|
||||||
|
for entry in config_users:
|
||||||
|
username = entry.get('username', None)
|
||||||
|
password = entry.get('!password', '')
|
||||||
|
sudo = entry.get('sudo', False)
|
||||||
|
|
||||||
|
if username is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
user = User(username, password, sudo)
|
||||||
|
users.append(user)
|
||||||
|
|
||||||
|
return users
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_backwards_compatible(cls, config_users: Dict, sudo: bool) -> List['User']:
|
||||||
|
if len(config_users.keys()) > 0:
|
||||||
|
username = list(config_users.keys())[0]
|
||||||
|
password = config_users[username]['!password']
|
||||||
|
|
||||||
|
if password:
|
||||||
|
return [User(username, password, sudo)]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_arguments(
|
||||||
|
cls,
|
||||||
|
config_users: Union[List[Dict[str, str]], Dict[str, str]],
|
||||||
|
config_superusers: Union[List[Dict[str, str]], Dict[str, str]]
|
||||||
|
) -> List['User']:
|
||||||
|
users = []
|
||||||
|
|
||||||
|
# backwards compability
|
||||||
|
if isinstance(config_users, dict):
|
||||||
|
users += cls._parse_backwards_compatible(config_users, False)
|
||||||
|
else:
|
||||||
|
users += cls._parse(config_users)
|
||||||
|
|
||||||
|
# backwards compability
|
||||||
|
if isinstance(config_superusers, dict):
|
||||||
|
users += cls._parse_backwards_compatible(config_superusers, True)
|
||||||
|
|
||||||
|
return users
|
||||||
|
|
@ -2,11 +2,47 @@ import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union, List, Any
|
||||||
|
|
||||||
from .storage import storage
|
from .storage import storage
|
||||||
|
|
||||||
|
|
||||||
|
class FormattedOutput:
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def values(cls, o: Any) -> Dict[str, Any]:
|
||||||
|
if hasattr(o, 'json'):
|
||||||
|
return o.json()
|
||||||
|
else:
|
||||||
|
return o.__dict__
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def as_table(cls, obj: List[Any]) -> str:
|
||||||
|
column_width: Dict[str, int] = {}
|
||||||
|
for o in obj:
|
||||||
|
for k, v in cls.values(o).items():
|
||||||
|
column_width.setdefault(k, 0)
|
||||||
|
column_width[k] = max([column_width[k], len(str(v)), len(k)])
|
||||||
|
|
||||||
|
output = ''
|
||||||
|
for key, width in column_width.items():
|
||||||
|
key = key.replace('!', '')
|
||||||
|
output += key.ljust(width) + ' | '
|
||||||
|
|
||||||
|
output = output[:-3] + '\n'
|
||||||
|
output += '-' * len(output) + '\n'
|
||||||
|
|
||||||
|
for o in obj:
|
||||||
|
for k, v in cls.values(o).items():
|
||||||
|
if '!' in k:
|
||||||
|
v = '*' * len(str(v))
|
||||||
|
output += str(v).ljust(column_width[k]) + ' | '
|
||||||
|
output = output[:-3]
|
||||||
|
output += '\n'
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
class Journald:
|
class Journald:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def log(message :str, level :int = logging.DEBUG) -> None:
|
def log(message :str, level :int = logging.DEBUG) -> None:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from .save_conf import save_config
|
from .save_conf import save_config
|
||||||
from .manage_users_conf import ask_for_superuser_account, ask_for_additional_users
|
from .manage_users_conf import ask_for_additional_users
|
||||||
from .backwards_compatible_conf import generic_select, generic_multi_select
|
from .backwards_compatible_conf import generic_select, generic_multi_select
|
||||||
from .locale_conf import select_locale_lang, select_locale_enc
|
from .locale_conf import select_locale_lang, select_locale_enc
|
||||||
from .system_conf import select_kernel, select_harddrives, select_driver, ask_for_bootloader, ask_for_swap
|
from .system_conf import select_kernel, select_harddrives, select_driver, ask_for_bootloader, ask_for_swap
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, TYPE_CHECKING, List
|
from typing import Any, Dict, TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
|
from .utils import get_password
|
||||||
from ..menu import Menu
|
from ..menu import Menu
|
||||||
from ..menu.list_manager import ListManager
|
from ..menu.list_manager import ListManager
|
||||||
from ..output import log
|
from ..models.users import User
|
||||||
from ..storage import storage
|
|
||||||
from .utils import get_password
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
_: Any
|
_: Any
|
||||||
|
|
@ -19,7 +17,7 @@ class UserList(ListManager):
|
||||||
subclass of ListManager for the managing of user accounts
|
subclass of ListManager for the managing of user accounts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, prompt: str, lusers: dict, sudo: bool = None):
|
def __init__(self, prompt: str, lusers: List[User]):
|
||||||
"""
|
"""
|
||||||
param: prompt
|
param: prompt
|
||||||
type: str
|
type: str
|
||||||
|
|
@ -27,140 +25,83 @@ class UserList(ListManager):
|
||||||
type: Dict
|
type: Dict
|
||||||
param: sudo. boolean to determine if we handle superusers or users. If None handles both types
|
param: sudo. boolean to determine if we handle superusers or users. If None handles both types
|
||||||
"""
|
"""
|
||||||
self.sudo = sudo
|
self._actions = [
|
||||||
self.actions = [
|
|
||||||
str(_('Add a user')),
|
str(_('Add a user')),
|
||||||
str(_('Change password')),
|
str(_('Change password')),
|
||||||
str(_('Promote/Demote user')),
|
str(_('Promote/Demote user')),
|
||||||
str(_('Delete User'))
|
str(_('Delete User'))
|
||||||
]
|
]
|
||||||
super().__init__(prompt, lusers, self.actions, self.actions[0])
|
super().__init__(prompt, lusers, self._actions, self._actions[0])
|
||||||
|
|
||||||
def reformat(self, data: List) -> Dict:
|
def reformat(self, data: List[User]) -> Dict[str, User]:
|
||||||
def format_element(elem :str):
|
return {e.display(): e for e in data}
|
||||||
# secret gives away the length of the password
|
|
||||||
if data[elem].get('!password'):
|
|
||||||
pwd = '*' * 16
|
|
||||||
else:
|
|
||||||
pwd = ''
|
|
||||||
if data[elem].get('sudoer'):
|
|
||||||
super_user = 'Superuser'
|
|
||||||
else:
|
|
||||||
super_user = ' '
|
|
||||||
return f"{elem:16}: password {pwd:16} {super_user}"
|
|
||||||
|
|
||||||
return {format_element(e): e for e in data}
|
|
||||||
|
|
||||||
def action_list(self):
|
def action_list(self):
|
||||||
if self.target:
|
active_user = self.target if self.target else None
|
||||||
active_user = list(self.target.keys())[0]
|
|
||||||
else:
|
|
||||||
active_user = None
|
|
||||||
sudoer = self.target[active_user].get('sudoer', False)
|
|
||||||
if self.sudo is None:
|
|
||||||
return self.actions
|
|
||||||
if self.sudo and sudoer:
|
|
||||||
return self.actions
|
|
||||||
elif self.sudo and not sudoer:
|
|
||||||
return [self.actions[2]]
|
|
||||||
elif not self.sudo and sudoer:
|
|
||||||
return [self.actions[2]]
|
|
||||||
else:
|
|
||||||
return self.actions
|
|
||||||
|
|
||||||
def exec_action(self, data: Any):
|
if active_user is None:
|
||||||
|
return [self._actions[0]]
|
||||||
|
else:
|
||||||
|
return self._actions[1:]
|
||||||
|
|
||||||
|
def exec_action(self, data: List[User]) -> List[User]:
|
||||||
if self.target:
|
if self.target:
|
||||||
active_user = list(self.target.keys())[0]
|
active_user = self.target
|
||||||
else:
|
else:
|
||||||
active_user = None
|
active_user = None
|
||||||
|
|
||||||
if self.action == self.actions[0]: # add
|
if self.action == self._actions[0]: # add
|
||||||
new_user = self.add_user()
|
new_user = self._add_user()
|
||||||
# no unicity check, if exists will be replaced
|
if new_user is not None:
|
||||||
data.update(new_user)
|
# in case a user with the same username as an existing user
|
||||||
elif self.action == self.actions[1]: # change password
|
# was created we'll replace the existing one
|
||||||
data[active_user]['!password'] = get_password(
|
data = [d for d in data if d.username != new_user.username]
|
||||||
prompt=str(_('Password for user "{}": ').format(active_user)))
|
data += [new_user]
|
||||||
elif self.action == self.actions[2]: # promote/demote
|
elif self.action == self._actions[1]: # change password
|
||||||
data[active_user]['sudoer'] = not data[active_user]['sudoer']
|
prompt = str(_('Password for user "{}": ').format(active_user.username))
|
||||||
elif self.action == self.actions[3]: # delete
|
new_password = get_password(prompt=prompt)
|
||||||
del data[active_user]
|
if new_password:
|
||||||
|
user = next(filter(lambda x: x == active_user, data), 1)
|
||||||
|
user.password = new_password
|
||||||
|
elif self.action == self._actions[2]: # promote/demote
|
||||||
|
user = next(filter(lambda x: x == active_user, data), 1)
|
||||||
|
user.sudo = False if user.sudo else True
|
||||||
|
elif self.action == self._actions[3]: # delete
|
||||||
|
data = [d for d in data if d != active_user]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def _check_for_correct_username(self, username: str) -> bool:
|
def _check_for_correct_username(self, username: str) -> bool:
|
||||||
if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
|
if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
|
||||||
return True
|
return True
|
||||||
log("The username you entered is invalid. Try again", level=logging.WARNING, fg='red')
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def add_user(self):
|
def _add_user(self) -> Optional[User]:
|
||||||
print(_('\nDefine a new user\n'))
|
print(_('\nDefine a new user\n'))
|
||||||
prompt = str(_("User Name : "))
|
prompt = str(_('Enter username (leave blank to skip): '))
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
userid = input(prompt).strip(' ')
|
username = input(prompt).strip(' ')
|
||||||
if not userid:
|
if not username:
|
||||||
return {} # end
|
return None
|
||||||
if not self._check_for_correct_username(userid):
|
if not self._check_for_correct_username(username):
|
||||||
pass
|
prompt = str(_("The username you entered is invalid. Try again")) + '\n' + prompt
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
if self.sudo:
|
|
||||||
sudoer = True
|
password = get_password(prompt=str(_('Password for user "{}": ').format(username)))
|
||||||
elif self.sudo is not None and not self.sudo:
|
|
||||||
sudoer = False
|
choice = Menu(
|
||||||
else:
|
str(_('Should "{}" be a superuser (sudo)?')).format(username), Menu.yes_no(),
|
||||||
sudoer = False
|
|
||||||
sudo_choice = Menu(str(_('Should {} be a superuser (sudoer)?')).format(userid), Menu.yes_no(),
|
|
||||||
skip=False,
|
skip=False,
|
||||||
preset_values=Menu.yes() if sudoer else Menu.no(),
|
default_option=Menu.no()
|
||||||
default_option=Menu.no()).run()
|
).run()
|
||||||
sudoer = True if sudo_choice == Menu.yes() else False
|
|
||||||
|
|
||||||
password = get_password(prompt=str(_('Password for user "{}": ').format(userid)))
|
sudo = True if choice.value == Menu.yes() else False
|
||||||
|
return User(username, password, sudo)
|
||||||
return {userid: {"!password": password, "sudoer": sudoer}}
|
|
||||||
|
|
||||||
|
|
||||||
def manage_users(prompt: str, sudo: bool) -> tuple[dict, dict]:
|
def ask_for_additional_users(prompt: str = '', defined_users: List[User] = []) -> List[User]:
|
||||||
# TODO Filtering and some kind of simpler code
|
prompt = prompt if prompt else _('Enter username (leave blank to skip): ')
|
||||||
lusers = {}
|
users = UserList(prompt, defined_users).run()
|
||||||
if storage['arguments'].get('!superusers', {}):
|
|
||||||
lusers.update({
|
|
||||||
uid: {
|
|
||||||
'!password': storage['arguments']['!superusers'][uid].get('!password'),
|
|
||||||
'sudoer': True
|
|
||||||
}
|
|
||||||
for uid in storage['arguments'].get('!superusers', {})
|
|
||||||
})
|
|
||||||
if storage['arguments'].get('!users', {}):
|
|
||||||
lusers.update({
|
|
||||||
uid: {
|
|
||||||
'!password': storage['arguments']['!users'][uid].get('!password'),
|
|
||||||
'sudoer': False
|
|
||||||
}
|
|
||||||
for uid in storage['arguments'].get('!users', {})
|
|
||||||
})
|
|
||||||
# processing
|
|
||||||
lusers = UserList(prompt, lusers, sudo).run()
|
|
||||||
# return data
|
|
||||||
superusers = {
|
|
||||||
uid: {
|
|
||||||
'!password': lusers[uid].get('!password')
|
|
||||||
}
|
|
||||||
for uid in lusers if lusers[uid].get('sudoer', False)
|
|
||||||
}
|
|
||||||
users = {uid: {'!password': lusers[uid].get('!password')} for uid in lusers if not lusers[uid].get('sudoer', False)}
|
|
||||||
storage['arguments']['!superusers'] = superusers
|
|
||||||
storage['arguments']['!users'] = users
|
|
||||||
return superusers, users
|
|
||||||
|
|
||||||
|
|
||||||
def ask_for_superuser_account(prompt: str) -> Dict[str, Dict[str, str]]:
|
|
||||||
prompt = prompt if prompt else str(_('Define users with sudo privilege, by username: '))
|
|
||||||
superusers, dummy = manage_users(prompt, sudo=True)
|
|
||||||
return superusers
|
|
||||||
|
|
||||||
|
|
||||||
def ask_for_additional_users(prompt: str = '') -> Dict[str, Dict[str, str | None]]:
|
|
||||||
prompt = prompt if prompt else _('Any additional users to install (leave blank for no users): ')
|
|
||||||
dummy, users = manage_users(prompt, sudo=False)
|
|
||||||
return users
|
return users
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ class ManualNetworkConfig(ListManager):
|
||||||
elif self.action == self._action_delete:
|
elif self.action == self._action_delete:
|
||||||
del data[iface_name]
|
del data[iface_name]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def _select_iface(self, existing_ifaces: List[str]) -> Optional[Any]:
|
def _select_iface(self, existing_ifaces: List[str]) -> Optional[Any]:
|
||||||
all_ifaces = list_interfaces().values()
|
all_ifaces = list_interfaces().values()
|
||||||
available = set(all_ifaces) - set(existing_ifaces)
|
available = set(all_ifaces) - set(existing_ifaces)
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ class SubvolumeList(ListManager):
|
||||||
|
|
||||||
data.update(self.target)
|
data.update(self.target)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class SubvolumeMenu(GeneralMenu):
|
class SubvolumeMenu(GeneralMenu):
|
||||||
def __init__(self,parameters,action=None):
|
def __init__(self,parameters,action=None):
|
||||||
|
|
|
||||||
|
|
@ -163,21 +163,20 @@ Options for ``--creds``
|
||||||
"!root-password" : "SecretSanta2022"
|
"!root-password" : "SecretSanta2022"
|
||||||
}
|
}
|
||||||
|
|
||||||
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
|
+----------------------+--------------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
|
||||||
| Key | Values | Description | Required |
|
| Key | Values | Description | Required |
|
||||||
| | | | |
|
+======================+========================================================+======================================================================================+===============================================+
|
||||||
+======================+=====================================================+======================================================================================+===============================================+
|
|
||||||
| !encryption-password | any | Password to encrypt disk, not encrypted if password not provided | No |
|
| !encryption-password | any | Password to encrypt disk, not encrypted if password not provided | No |
|
||||||
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
|
+----------------------+--------------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
|
||||||
| !root-password | any | The root account password | No |
|
| !root-password | any | The root account password | No |
|
||||||
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
|
+----------------------+--------------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
|
||||||
| !superusers | { "<username>": { "!password": "<password>"}, ..} | List of superuser credentials, see configuration for reference | Yes[1] |
|
| !users | { "username": "<USERNAME>" | List of regular user credentials, see configuration for reference | No |
|
||||||
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
|
| | "!password": "<PASSWORD>", | | |
|
||||||
| !users | { "<username>": { "!password": "<password>"}, ..} | List of regular user credentials, see configuration for reference | No |
|
| | "sudo": false|true} | | |
|
||||||
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
|
+----------------------+--------------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
[1] ``!superusers`` is optional only if ``!root-password`` was set. ``!superusers`` will be enforced otherwise and the minimum amount of superusers required will be set to 1.
|
[1] ``!users`` is optional only if ``!root-password`` was set. ``!users`` will be enforced otherwise and the minimum amount of users with sudo privileges required will be set to 1.
|
||||||
|
|
||||||
Options for ``--disk_layouts``
|
Options for ``--disk_layouts``
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
{
|
{
|
||||||
"!root-password": "<root password>",
|
"!root-password": "<root password>",
|
||||||
"!users": {
|
"!users": [
|
||||||
"username": {"!password": "<user password>"}
|
{
|
||||||
|
"username": "<USERNAME>",
|
||||||
|
"!password": "<PASSWORD>",
|
||||||
|
"sudo": false
|
||||||
},
|
},
|
||||||
"!superusers": {
|
{
|
||||||
"admin": {"!password": "<admin password>"}
|
"username": "<SUDO_USERNAME>",
|
||||||
|
"!password": "<PASSWORD>",
|
||||||
|
"sudo": true
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +72,6 @@ def ask_user_questions():
|
||||||
# Ask for a root password (optional, but triggers requirement for super-user if skipped)
|
# Ask for a root password (optional, but triggers requirement for super-user if skipped)
|
||||||
global_menu.enable('!root-password')
|
global_menu.enable('!root-password')
|
||||||
|
|
||||||
global_menu.enable('!superusers')
|
|
||||||
global_menu.enable('!users')
|
global_menu.enable('!users')
|
||||||
|
|
||||||
# Ask for archinstall-specific profiles (such as desktop environments etc)
|
# Ask for archinstall-specific profiles (such as desktop environments etc)
|
||||||
|
|
@ -220,13 +219,8 @@ def perform_installation(mountpoint):
|
||||||
if archinstall.arguments.get('profile', None):
|
if archinstall.arguments.get('profile', None):
|
||||||
installation.install_profile(archinstall.arguments.get('profile', None))
|
installation.install_profile(archinstall.arguments.get('profile', None))
|
||||||
|
|
||||||
if archinstall.arguments.get('!users',{}):
|
if users := archinstall.arguments.get('!users', None):
|
||||||
for user, user_info in archinstall.arguments.get('!users', {}).items():
|
installation.create_users(users)
|
||||||
installation.user_create(user, user_info["!password"], sudo=False)
|
|
||||||
|
|
||||||
if archinstall.arguments.get('!superusers',{}):
|
|
||||||
for superuser, user_info in archinstall.arguments.get('!superusers', {}).items():
|
|
||||||
installation.user_create(superuser, user_info["!password"], sudo=True)
|
|
||||||
|
|
||||||
if timezone := archinstall.arguments.get('timezone', None):
|
if timezone := archinstall.arguments.get('timezone', None):
|
||||||
installation.set_timezone(timezone)
|
installation.set_timezone(timezone)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import archinstall
|
import archinstall
|
||||||
|
|
||||||
# Select a harddrive and a disk password
|
# Select a harddrive and a disk password
|
||||||
|
from archinstall import User
|
||||||
|
|
||||||
archinstall.log("Minimal only supports:")
|
archinstall.log("Minimal only supports:")
|
||||||
archinstall.log(" * Being installed to a single disk")
|
archinstall.log(" * Being installed to a single disk")
|
||||||
|
|
||||||
|
|
@ -28,8 +30,8 @@ def install_on(mountpoint):
|
||||||
installation.add_additional_packages(['nano', 'wget', 'git'])
|
installation.add_additional_packages(['nano', 'wget', 'git'])
|
||||||
installation.install_profile('minimal')
|
installation.install_profile('minimal')
|
||||||
|
|
||||||
installation.user_create('devel', 'devel')
|
user = User('devel', 'devel', False)
|
||||||
installation.user_set_pw('root', 'airoot')
|
installation.create_users(user)
|
||||||
|
|
||||||
# Once this is done, we output some useful information to the user
|
# Once this is done, we output some useful information to the user
|
||||||
# And the installation is complete.
|
# And the installation is complete.
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ class MyMenu(archinstall.GlobalMenu):
|
||||||
if self._execution_mode in ('full','lineal'):
|
if self._execution_mode in ('full','lineal'):
|
||||||
options_list = ['keyboard-layout', 'mirror-region', 'harddrives', 'disk_layouts',
|
options_list = ['keyboard-layout', 'mirror-region', 'harddrives', 'disk_layouts',
|
||||||
'!encryption-password','swap', 'bootloader', 'hostname', '!root-password',
|
'!encryption-password','swap', 'bootloader', 'hostname', '!root-password',
|
||||||
'!superusers', '!users', 'profile', 'audio', 'kernels', 'packages','additional-repositories','nic',
|
'!users', 'profile', 'audio', 'kernels', 'packages','additional-repositories','nic',
|
||||||
'timezone', 'ntp']
|
'timezone', 'ntp']
|
||||||
if archinstall.arguments.get('advanced',False):
|
if archinstall.arguments.get('advanced',False):
|
||||||
options_list.extend(['sys-language','sys-encoding'])
|
options_list.extend(['sys-language','sys-encoding'])
|
||||||
|
|
@ -229,7 +229,7 @@ class MyMenu(archinstall.GlobalMenu):
|
||||||
mandatory_list = ['harddrives']
|
mandatory_list = ['harddrives']
|
||||||
elif self._execution_mode == 'only_os':
|
elif self._execution_mode == 'only_os':
|
||||||
options_list = ['keyboard-layout', 'mirror-region','bootloader', 'hostname',
|
options_list = ['keyboard-layout', 'mirror-region','bootloader', 'hostname',
|
||||||
'!root-password', '!superusers', '!users', 'profile', 'audio', 'kernels',
|
'!root-password', '!users', 'profile', 'audio', 'kernels',
|
||||||
'packages', 'additional-repositories', 'nic', 'timezone', 'ntp']
|
'packages', 'additional-repositories', 'nic', 'timezone', 'ntp']
|
||||||
mandatory_list = ['hostname']
|
mandatory_list = ['hostname']
|
||||||
if archinstall.arguments.get('advanced',False):
|
if archinstall.arguments.get('advanced',False):
|
||||||
|
|
@ -262,8 +262,12 @@ class MyMenu(archinstall.GlobalMenu):
|
||||||
def check(s):
|
def check(s):
|
||||||
return self.option(s).has_selection()
|
return self.option(s).has_selection()
|
||||||
|
|
||||||
|
def has_superuser() -> bool:
|
||||||
|
users = self._menu_options['!users'].current_selection
|
||||||
|
return any([u.sudo for u in users])
|
||||||
|
|
||||||
_, missing = self.mandatory_overview()
|
_, missing = self.mandatory_overview()
|
||||||
if mode in ('full','only_os') and (not check('!root-password') and not check('!superusers')):
|
if mode in ('full','only_os') and (not check('!root-password') and not has_superuser()):
|
||||||
missing += 1
|
missing += 1
|
||||||
if mode in ('full', 'only_hd') and check('harddrives'):
|
if mode in ('full', 'only_hd') and check('harddrives'):
|
||||||
if not self.option('harddrives').is_empty() and not check('disk_layouts'):
|
if not self.option('harddrives').is_empty() and not check('disk_layouts'):
|
||||||
|
|
@ -420,13 +424,8 @@ def os_setup(installation):
|
||||||
if archinstall.arguments.get('profile', None):
|
if archinstall.arguments.get('profile', None):
|
||||||
installation.install_profile(archinstall.arguments.get('profile', None))
|
installation.install_profile(archinstall.arguments.get('profile', None))
|
||||||
|
|
||||||
if archinstall.arguments.get('!users',{}):
|
if users := archinstall.arguments.get('!users', None):
|
||||||
for user, user_info in archinstall.arguments.get('!users', {}).items():
|
installation.create_users(users)
|
||||||
installation.user_create(user, user_info["!password"], sudo=False)
|
|
||||||
|
|
||||||
if archinstall.arguments.get('!superusers',{}):
|
|
||||||
for superuser, user_info in archinstall.arguments.get('!superusers', {}).items():
|
|
||||||
installation.user_create(superuser, user_info["!password"], sudo=True)
|
|
||||||
|
|
||||||
if timezone := archinstall.arguments.get('timezone', None):
|
if timezone := archinstall.arguments.get('timezone', None):
|
||||||
installation.set_timezone(timezone)
|
installation.set_timezone(timezone)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ with archinstall.Filesystem(harddrive) as fs:
|
||||||
installation.add_additional_packages(__packages__)
|
installation.add_additional_packages(__packages__)
|
||||||
installation.install_profile('awesome')
|
installation.install_profile('awesome')
|
||||||
|
|
||||||
installation.user_create('devel', 'devel')
|
user = User('devel', 'devel', False)
|
||||||
|
installation.create_users(user)
|
||||||
installation.user_set_pw('root', 'toor')
|
installation.user_set_pw('root', 'toor')
|
||||||
|
|
||||||
print(f'Submitting {archinstall.__version__}: success')
|
print(f'Submitting {archinstall.__version__}: success')
|
||||||
|
|
|
||||||
10
schema.json
10
schema.json
|
|
@ -167,7 +167,15 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"required": [
|
"required": [
|
||||||
"!superusers"
|
"!users": {
|
||||||
|
"description": "User account",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"username": "string",
|
||||||
|
"!password": "string",
|
||||||
|
"sudo": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue