Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 140d1d803f | |||
| ef2ea6eedd | |||
| b790be713a | |||
| 6809b34b9d | |||
| 65bd553b09 | |||
| 00fe5bf24c | |||
| 4b6cfeddad | |||
| 6b0d81d4e9 | |||
| a3e07033e7 | |||
| 5a3bb026d4 | |||
| 7aa752c3d3 | |||
| 6b412140d4 | |||
| 61157457b8 | |||
| 46cae312c8 | |||
| aa04718374 | |||
| 4c6495c4ee | |||
| 8f00ab9570 | |||
| 32dec36cd7 | |||
| f9b5c0d153 | |||
| b3dc60d950 | |||
| f8e93cedf2 | |||
| 074975650f | |||
| 7f654071d0 | |||
| 0711b55ad8 | |||
| 779d25def5 | |||
| bba6410bbd | |||
| edc5987293 | |||
| 2b6f0e41e6 | |||
| 444c2d3182 | |||
| a069cfe410 |
@ -2,7 +2,7 @@
|
||||
|
||||
## Demo
|
||||
|
||||
[autopve_demo.webm](https://github.com/natankeddem/autopve/assets/44515217/827bdd22-5311-43c1-9452-a56fa11998aa)
|
||||
https://github.com/natankeddem/autopve/assets/44515217/9439e2e2-a7bf-4677-aea8-0684318bea6c
|
||||
|
||||
## Information
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@ class Drawer(object):
|
||||
el.IButton(icon="add", on_click=self._display_answer_dialog)
|
||||
self._buttons["remove"] = el.IButton(icon="remove", on_click=lambda: self._modify_answer("remove"))
|
||||
self._buttons["edit"] = el.IButton(icon="edit", on_click=lambda: self._modify_answer("edit"))
|
||||
self._buttons["content_copy"] = el.IButton(icon="content_copy", on_click=lambda: self._modify_answer("content_copy"))
|
||||
ui.label(text="ANSWERS").classes("text-secondary")
|
||||
self._table = (
|
||||
ui.table(
|
||||
@ -83,7 +84,7 @@ class Drawer(object):
|
||||
self._table.add_rows({"name": name})
|
||||
self._table.visible = True
|
||||
|
||||
async def _display_answer_dialog(self, name=""):
|
||||
async def _display_answer_dialog(self, name="", copy=False):
|
||||
save = None
|
||||
|
||||
with ui.dialog() as answer_dialog, el.Card():
|
||||
@ -100,6 +101,8 @@ class Drawer(object):
|
||||
|
||||
def answer_check(value: str) -> Optional[bool]:
|
||||
spaceless = value.replace(" ", "")
|
||||
if len(spaceless) == 0:
|
||||
return False
|
||||
for invalid_value in all_answers:
|
||||
if invalid_value == spaceless:
|
||||
return False
|
||||
@ -108,25 +111,28 @@ class Drawer(object):
|
||||
def enter_submit(e: KeyEventArguments) -> None:
|
||||
if e.key == "Enter" and save_ea.no_errors is True:
|
||||
answer_dialog.submit("save")
|
||||
elif e.key == "Escape":
|
||||
answer_dialog.close()
|
||||
|
||||
answer_input = el.VInput(label="answer", value=" ", invalid_characters="""'`"$\\;&<>|(){}""", invalid_values=all_answers, check=answer_check, max_length=20)
|
||||
save_ea = el.ErrorAggregator(answer_input)
|
||||
el.DButton("SAVE", on_click=lambda: answer_dialog.submit("save")).bind_enabled_from(save_ea, "no_errors")
|
||||
ui.keyboard(on_key=enter_submit, ignore=[])
|
||||
answer_input.value = name
|
||||
answer_input.value = name
|
||||
|
||||
result = await answer_dialog
|
||||
if result == "save":
|
||||
answer = answer_input.value.strip()
|
||||
if len(answer) > 0 and name != "Default":
|
||||
storage.answer(answer)
|
||||
if name in storage.answers:
|
||||
storage.answers[answer] = storage.answer(name, copy=True)
|
||||
answer = answer_input.value.strip()
|
||||
if result == "save" and name != answer:
|
||||
if name in storage.answers:
|
||||
storage.answers[answer] = storage.answer(name, copy=True)
|
||||
if copy is False:
|
||||
del storage.answers[name]
|
||||
for row in self._table.rows:
|
||||
if name == row["name"]:
|
||||
self._table.remove_rows(row)
|
||||
self._add_answer_to_table(answer)
|
||||
for row in self._table.rows:
|
||||
if name == row["name"]:
|
||||
self._table.remove_rows(row)
|
||||
else:
|
||||
storage.answer(answer)
|
||||
self._add_answer_to_table(answer)
|
||||
|
||||
def _modify_answer(self, mode):
|
||||
self._hide_content()
|
||||
@ -152,17 +158,20 @@ class Drawer(object):
|
||||
|
||||
async def _selected(self, e):
|
||||
self._hide_content()
|
||||
if self._selection_mode == "edit":
|
||||
if len(e.selection) > 0 and e.selection[0]["name"] != "Default":
|
||||
await self._display_answer_dialog(name=e.selection[0]["name"])
|
||||
if self._selection_mode == "remove":
|
||||
if len(e.selection) > 0:
|
||||
for row in e.selection:
|
||||
if row["name"] != "Default":
|
||||
if row["name"] in storage.answers:
|
||||
del storage.answers[row["name"]]
|
||||
self._table.remove_rows(row)
|
||||
self._modify_answer(None)
|
||||
if len(e.selection) == 1:
|
||||
answer = e.selection[0]["name"]
|
||||
if self._selection_mode == "content_copy":
|
||||
await self._display_answer_dialog(name=answer, copy=True)
|
||||
self._modify_answer(None)
|
||||
elif answer == "Default":
|
||||
self._table._props["selected"] = []
|
||||
elif self._selection_mode == "edit":
|
||||
await self._display_answer_dialog(name=answer)
|
||||
self._modify_answer(None)
|
||||
elif self._selection_mode == "remove":
|
||||
if answer in storage.answers:
|
||||
del storage.answers[answer]
|
||||
self._table.remove_rows(e.selection[0])
|
||||
|
||||
async def _clicked(self, e):
|
||||
if "name" in e.args[1]:
|
||||
|
||||
@ -364,7 +364,7 @@ class FSelect(ui.select):
|
||||
clearable: bool = False,
|
||||
) -> None:
|
||||
super().__init__(options, label=label, value=value, on_change=on_change, with_input=with_input, new_value_mode=new_value_mode, multiple=multiple, clearable=clearable)
|
||||
self.tailwind.width("64")
|
||||
self.tailwind.width("[320px]")
|
||||
|
||||
|
||||
class JsonEditor(ui.json_editor):
|
||||
|
||||
@ -84,6 +84,15 @@ class History(Tab):
|
||||
el.JsonEditor(properties=properties)
|
||||
with ui.tab_panel(response_tab):
|
||||
response = e.args["data"]["response"]
|
||||
lines = response.splitlines()
|
||||
response_lines = []
|
||||
for line in lines:
|
||||
if line.strip().startswith("root_password"):
|
||||
response_lines.append('root_password = "SECRET"')
|
||||
else:
|
||||
response_lines.append(line)
|
||||
|
||||
response = "\n".join(response_lines)
|
||||
ui.code(response).tailwind.height("[320px]").width("[640px]")
|
||||
|
||||
with el.WRow() as row:
|
||||
|
||||
@ -14,103 +14,155 @@ class Setting(Tab):
|
||||
super().__init__(answer, type=type)
|
||||
|
||||
def _build(self):
|
||||
self.key_picker()
|
||||
self.keys_controls()
|
||||
|
||||
def key_picker(self):
|
||||
def keys_controls():
|
||||
with ui.column() as col:
|
||||
col.tailwind.width("[560px]").align_items("center")
|
||||
with ui.card() as card:
|
||||
card.tailwind.width("full")
|
||||
key_select = ui.select(list(self.keys.keys()), label="key", new_value_mode="add", with_input=True)
|
||||
key_select.tailwind.width("full")
|
||||
def keys_controls(self):
|
||||
with ui.column() as col:
|
||||
col.tailwind.width("[560px]").align_items("center")
|
||||
with ui.card() as card:
|
||||
card.tailwind.width("full")
|
||||
key_select = ui.select(list(self.keys.keys()), label="key", new_value_mode="add", with_input=True)
|
||||
key_select.tailwind.width("full")
|
||||
with ui.row() as row:
|
||||
row.tailwind.width("full").align_items("center").justify_content("between")
|
||||
with ui.row() as row:
|
||||
row.tailwind.width("full").align_items("center").justify_content("between")
|
||||
with ui.row() as row:
|
||||
row.tailwind.align_items("center")
|
||||
self.help = None
|
||||
key = el.FInput(label="key", on_change=lambda e: key_changed(e), read_only=True)
|
||||
key.bind_value_from(key_select)
|
||||
row.tailwind.align_items("center")
|
||||
self.help = None
|
||||
key = el.FInput(label="key", on_change=lambda e: self.key_changed(e.value), read_only=True)
|
||||
key.bind_value_from(key_select)
|
||||
with ui.button(icon="help"):
|
||||
self.help = ui.tooltip("NA")
|
||||
ui.button(icon="add", on_click=lambda key=key: self.add_key(key.value))
|
||||
ui.separator()
|
||||
self._scroll = ui.scroll_area()
|
||||
self._scroll.tailwind.width("full").height("[480px]")
|
||||
items = storage.answer(self.answer)
|
||||
if self.type is not None and self.type in items:
|
||||
for key, value in items[self.type].items():
|
||||
if isinstance(value, list):
|
||||
self.add_key(key, "[" + ",".join(str(v) for v in value) + "]")
|
||||
else:
|
||||
self.add_key(key, str(value))
|
||||
|
||||
def add_key(self, key: str, value: str = ""):
|
||||
if self.key_valid(key) is True:
|
||||
with self._scroll:
|
||||
with ui.row() as key_row:
|
||||
key_row.tailwind.width("full").align_items("center").justify_content("between")
|
||||
with ui.row() as row:
|
||||
row.tailwind.align_items("center")
|
||||
if key in self.keys and "options" in self.keys[key]:
|
||||
options = self.keys[key]["options"]
|
||||
if value != "" and value not in self.keys[key]["options"]:
|
||||
options.append(value)
|
||||
control = el.FSelect(
|
||||
label=key,
|
||||
options=options,
|
||||
with_input=True,
|
||||
new_value_mode="add-unique",
|
||||
on_change=lambda e, key=key: self.set_key(key, e.value) if e.value is not None else None,
|
||||
)
|
||||
else:
|
||||
control = el.FInput(
|
||||
label=key,
|
||||
password=True if key == "root_password" else False,
|
||||
password_toggle_button=False,
|
||||
on_change=lambda e, key=key: self.set_key(key, e.value),
|
||||
)
|
||||
self._elements[key] = {
|
||||
"control": control,
|
||||
"row": key_row,
|
||||
}
|
||||
if isinstance(control, el.FSelect):
|
||||
control.value = self.keys[key]["options"][0] if value == "" else value
|
||||
else:
|
||||
control.value = value
|
||||
if key in self.keys:
|
||||
with ui.button(icon="help"):
|
||||
self.help = ui.tooltip("NA")
|
||||
ui.button(icon="add", on_click=lambda key=key: add_key(key.value))
|
||||
ui.separator()
|
||||
self._scroll = ui.scroll_area()
|
||||
self._scroll.tailwind.width("full").height("[480px]")
|
||||
items = storage.answer(self.answer)
|
||||
if self.type is not None and self.type in items:
|
||||
for key, value in items[self.type].items():
|
||||
if isinstance(value, list):
|
||||
add_key(key, "[" + ",".join(str(v) for v in value) + "]")
|
||||
else:
|
||||
add_key(key, str(value))
|
||||
ui.tooltip(self.keys[key]["description"])
|
||||
ui.button(icon="remove", on_click=lambda _, key=key: self.remove_key(key))
|
||||
|
||||
def add_key(key: str, value: str = ""):
|
||||
if key is not None and key != "" and key not in self._elements.keys():
|
||||
with self._scroll:
|
||||
with ui.row() as key_row:
|
||||
key_row.tailwind.width("full").align_items("center").justify_content("between")
|
||||
with ui.row() as row:
|
||||
row.tailwind.align_items("center")
|
||||
self._elements[key] = {
|
||||
"control": el.FInput(
|
||||
key,
|
||||
password=True if key == "root_password" else False,
|
||||
autocomplete=self.keys[key]["options"] if key in self.keys and "options" in self.keys[key] else None,
|
||||
on_change=lambda e, key=key: set_key(key, e.value),
|
||||
),
|
||||
"row": key_row,
|
||||
}
|
||||
self._elements[key]["control"].value = value
|
||||
if key in self.keys:
|
||||
with ui.button(icon="help"):
|
||||
ui.tooltip(self.keys[key]["description"])
|
||||
ui.button(icon="remove", on_click=lambda _, key=key: remove_key(key))
|
||||
def key_valid(self, key: str) -> bool:
|
||||
if key is not None and key != "" and key not in self._elements.keys():
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_key(key):
|
||||
self._scroll.remove(self._elements[key]["row"])
|
||||
del self._elements[key]
|
||||
def remove_key(self, key: str):
|
||||
self._scroll.remove(self._elements[key]["row"])
|
||||
del self._elements[key]
|
||||
if key in storage.answer(self.answer)[self.type]:
|
||||
del storage.answer(self.answer)[self.type][key]
|
||||
|
||||
def set_key(key, value: str):
|
||||
v: Any = None
|
||||
if len(value) > 0:
|
||||
if key in self.keys and "type" in self.keys[key]:
|
||||
if self.keys[key]["type"] == "list":
|
||||
v = value[1:-1].split(",")
|
||||
elif self.keys[key]["type"] == "int":
|
||||
v = int(value)
|
||||
else:
|
||||
v = value
|
||||
def set_key(self, key: str, value: str):
|
||||
v: Any = ""
|
||||
if len(value) > 0:
|
||||
if key in self.keys and "type" in self.keys[key]:
|
||||
if self.keys[key]["type"] == "list" and len(value) > 2 and value.strip()[0] == "[" and value.strip()[-1] == "]":
|
||||
l = value.strip()[1:-1].replace('"', "").replace("'", "").split(",")
|
||||
v = [v.strip() for v in l]
|
||||
elif self.keys[key]["type"] == "int":
|
||||
v = int(value)
|
||||
else:
|
||||
if len(value) > 2 and value.strip()[0] == "[" and value.strip()[-1] == "]":
|
||||
v = value[1:-1].split(",")
|
||||
elif value.isnumeric():
|
||||
v = int(value)
|
||||
else:
|
||||
v = value
|
||||
if self.type not in storage.answer(self.answer):
|
||||
storage.answer(self.answer)[self.type] = {}
|
||||
storage.answer(self.answer)[self.type][key] = v
|
||||
|
||||
def key_changed(e):
|
||||
if self.help is not None:
|
||||
if e.value in self.keys:
|
||||
self.help.text = self.keys[e.value]["description"]
|
||||
v = value
|
||||
else:
|
||||
if len(value) > 2 and value.strip()[0] == "[" and value.strip()[-1] == "]":
|
||||
l = value.strip()[1:-1].replace('"', "").replace("'", "").split(",")
|
||||
v = [v.strip() for v in l]
|
||||
elif value.isnumeric():
|
||||
v = int(value)
|
||||
else:
|
||||
self.help.text = "NA"
|
||||
v = value
|
||||
if self.type not in storage.answer(self.answer):
|
||||
storage.answer(self.answer)[self.type] = {}
|
||||
storage.answer(self.answer)[self.type][key] = v
|
||||
|
||||
keys_controls()
|
||||
def key_changed(self, value: str):
|
||||
if self.help is not None:
|
||||
if value in self.keys:
|
||||
self.help.text = self.keys[value]["description"]
|
||||
else:
|
||||
self.help.text = "NA"
|
||||
|
||||
|
||||
class Global(Setting):
|
||||
def __init__(self, answer: str) -> None:
|
||||
keys = {
|
||||
"keyboard": {"description": "The keyboard layout with the following possible options"},
|
||||
"keyboard": {
|
||||
"description": "The keyboard layout with the following possible options",
|
||||
"options": [
|
||||
"de",
|
||||
"de-ch",
|
||||
"dk",
|
||||
"en-gb",
|
||||
"en-us",
|
||||
"es",
|
||||
"fi",
|
||||
"fr",
|
||||
"fr-be",
|
||||
"fr-ca",
|
||||
"fr-ch",
|
||||
"hu",
|
||||
"is",
|
||||
"it",
|
||||
"jp",
|
||||
"lt",
|
||||
"mk",
|
||||
"nl",
|
||||
"no",
|
||||
"pl",
|
||||
"pt",
|
||||
"pt-br",
|
||||
"se",
|
||||
"si",
|
||||
"tr",
|
||||
],
|
||||
},
|
||||
"country": {"description": "The country code in the two letter variant. For example, at, us or fr."},
|
||||
"fqdn": {"description": "The fully qualified domain name of the host. The domain part will be used as the search domain."},
|
||||
"mailto": {"description": "The default email address for the user root."},
|
||||
"timezone": {"description": "The timezone in tzdata format. For example, Europe/Vienna or America/New_York."},
|
||||
"root_password": {"description": "The password for the root user.", "type": "str"},
|
||||
"root-password-hashed": {"description": "The pre-hashed password for the root user, which will be written verbatim to /etc/passwd. May be used instead of root_password and can be generated using the mkpasswd tool, for example.", "type": "str"},
|
||||
"root_ssh_keys": {"description": "Optional. SSH public keys to add to the root users authorized_keys file after the installation."},
|
||||
"reboot_on_error": {
|
||||
"description": "If set to true, the installer will reboot automatically when an error is encountered. The default behavior is to wait to give the administrator a chance to investigate why the installation failed."
|
||||
@ -122,7 +174,7 @@ class Global(Setting):
|
||||
class Network(Setting):
|
||||
def __init__(self, answer: str) -> None:
|
||||
keys = {
|
||||
"source": {"description": "Where to source the static network configuration from. This can be from-dhcp or from-answer."},
|
||||
"source": {"description": "Where to source the static network configuration from. This can be from-dhcp or from-answer.", "options": ["from-dhcp", "from-answer"]},
|
||||
"cidr": {"description": "The IP address in CIDR notation. For example, 192.168.1.10/24."},
|
||||
"dns": {"description": "The IP address of the DNS server."},
|
||||
"gateway": {"description": "The IP address of the default gateway."},
|
||||
@ -145,21 +197,50 @@ class Disk(Setting):
|
||||
"description": "The RAID level that should be used. Options are raid0, raid1, raid10, raidz-1, raidz-2, or raidz-3.",
|
||||
"options": ["raid0", "raid1", "raid10", "raidz-1", "raidz-2", "raidz-3"],
|
||||
},
|
||||
"zfs.ashift": {"description": ""},
|
||||
"zfs.arc_max": {"description": ""},
|
||||
"zfs.checksum": {"description": ""},
|
||||
"zfs.compress": {"description": ""},
|
||||
"zfs.copies": {"description": ""},
|
||||
"zfs.hdsize": {"description": ""},
|
||||
"lvm.hdsize": {"description": ""},
|
||||
"lvm.swapsize": {"description": ""},
|
||||
"lvm.maxroot": {"description": ""},
|
||||
"lvm.maxvz": {"description": ""},
|
||||
"lvm.minfree": {"description": ""},
|
||||
"zfs.ashift": {
|
||||
"description": "Defines the ashift value for the created pool. The ashift needs to be set at least to the sector-size of the underlying disks (2 to the power of ashift is the sector-size), or any disk which might be put in the pool (for example the replacement of a defective disk)."
|
||||
},
|
||||
"zfs.arc_max": {
|
||||
"description": "Defines the maximum size the ARC can grow to and thus limits the amount of memory ZFS will use. See also the section on how to limit ZFS memory usage for more details."
|
||||
},
|
||||
"zfs.checksum": {"description": "Defines which checksumming algorithm should be used for rpool."},
|
||||
"zfs.compress": {"description": "Defines whether compression is enabled for rpool."},
|
||||
"zfs.copies": {
|
||||
"description": "Defines the copies parameter for rpool. Check the zfs(8) manpage for the semantics, and why this does not replace redundancy on disk-level."
|
||||
},
|
||||
"zfs.hdsize": {
|
||||
"description": "Defines the total hard disk size to be used. This is useful to save free space on the hard disk(s) for further partitioning (for example to create a swap-partition). hdsize is only honored for bootable disks, that is only the first disk or mirror for RAID0, RAID1 or RAID10, and all disks in RAID-Z[123]."
|
||||
},
|
||||
"lvm.hdsize": {
|
||||
"description": "Defines the total hard disk size to be used. This way you can reserve free space on the hard disk for further partitioning (for example for an additional PV and VG on the same hard disk that can be used for LVM storage)."
|
||||
},
|
||||
"lvm.swapsize": {
|
||||
"description": "Defines the size of the swap volume. The default is the size of the installed memory, minimum 4 GB and maximum 8 GB. The resulting value cannot be greater than hdsize/8."
|
||||
},
|
||||
"lvm.maxroot": {
|
||||
"description": "Defines the maximum size of the root volume, which stores the operation system. The maximum limit of the root volume size is hdsize/4."
|
||||
},
|
||||
"lvm.maxvz": {
|
||||
"description": "Defines the maximum size of the data volume. The actual size of the data volume is: datasize = hdsize - rootsize - swapsize - minfree Where datasize cannot be bigger than maxvz."
|
||||
},
|
||||
"lvm.minfree": {
|
||||
"description": "Defines the amount of free space that should be left in the LVM volume group pve. With more than 128GB storage available, the default is 16GB, otherwise hdsize/8 will be used."
|
||||
},
|
||||
"btrfs.raid": {
|
||||
"description": "",
|
||||
"description": "The RAID level that should be used. Options are raid0, raid1, and raid10",
|
||||
"options": ["raid0", "raid1", "raid10"],
|
||||
},
|
||||
"btrfs.hdsize": {"description": ""},
|
||||
}
|
||||
super().__init__(answer, type="disk-setup", keys=keys)
|
||||
|
||||
def key_valid(self, key: str) -> bool:
|
||||
if super().key_valid(key) is True:
|
||||
if "filter" in key and "disk_list" in self._elements.keys():
|
||||
el.Notification(f"Can not add {key} when disk_list is utilized!", type="negative", timeout=5)
|
||||
return False
|
||||
elif key == "disk_list" and any("filter" in k for k in self._elements.keys()):
|
||||
el.Notification("Can not add disk_list when a filter is utilized!", type="negative", timeout=5)
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -48,13 +48,11 @@ class System(Tab):
|
||||
with self.scroll:
|
||||
with ui.row() as row:
|
||||
row.tailwind.width("full").align_items("center").justify_content("between")
|
||||
with ui.row() as row:
|
||||
row.tailwind.align_items("center")
|
||||
self._elements[restriction] = {
|
||||
"control": el.FInput(value=restriction, read_only=True),
|
||||
"row": row,
|
||||
}
|
||||
self._elements[restriction]["control"].tailwind.width("[420px]")
|
||||
self._elements[restriction] = {
|
||||
"control": el.FInput(value=restriction, read_only=True),
|
||||
"row": row,
|
||||
}
|
||||
self._elements[restriction]["control"].tailwind.width("[420px]")
|
||||
ui.button(icon="remove", on_click=lambda _, r=restriction: remove_restriction(r))
|
||||
if self.type not in storage.answer(self.answer):
|
||||
storage.answer(self.answer)[self.type] = []
|
||||
|
||||
6
main.py
6
main.py
@ -102,6 +102,12 @@ async def post_answer(request: Request) -> PlainTextResponse:
|
||||
if "network" in default_data and "network" in answer_data:
|
||||
default_data["network"].update(answer_data["network"])
|
||||
if "disk-setup" in default_data and "disk-setup" in answer_data:
|
||||
if any("filter" in k for k in answer_data["disk-setup"]) and "disk_list" in default_data["disk-setup"]:
|
||||
del default_data["disk-setup"]["disk_list"]
|
||||
if "disk_list" in answer_data["disk-setup"]:
|
||||
for key in list(default_data["disk-setup"].keys()):
|
||||
if "filter" in key:
|
||||
del default_data["disk-setup"][key]
|
||||
default_data["disk-setup"].update(answer_data["disk-setup"])
|
||||
return response(answer, system_info, default_data)
|
||||
return response("Default", system_info, default_data)
|
||||
|
||||
Reference in New Issue
Block a user