machineconfig 8.51__py3-none-any.whl → 8.61__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. machineconfig/jobs/installer/python_scripts/sysabc.py +13 -34
  2. machineconfig/profile/mapper_dotfiles.toml +3 -3
  3. machineconfig/scripts/python/devops.py +1 -1
  4. machineconfig/scripts/python/devops_navigator.py +1 -1
  5. machineconfig/scripts/python/helper_env/path_manager_tui.py +1 -1
  6. machineconfig/scripts/python/helpers/helper_env/env_manager_tui.py +1 -1
  7. machineconfig/scripts/python/helpers/helper_env/path_manager_tui.py +1 -1
  8. machineconfig/scripts/python/helpers/helpers_croshell/croshell_impl.py +8 -4
  9. machineconfig/scripts/python/helpers/helpers_devops/cli_config.py +33 -1
  10. machineconfig/scripts/python/helpers/helpers_devops/cli_config_mount.py +77 -0
  11. machineconfig/scripts/python/helpers/helpers_devops/cli_data.py +4 -0
  12. machineconfig/scripts/python/helpers/helpers_devops/cli_nw.py +90 -6
  13. machineconfig/scripts/python/helpers/helpers_devops/cli_repos.py +3 -3
  14. machineconfig/scripts/python/helpers/helpers_devops/cli_self.py +41 -15
  15. machineconfig/scripts/python/helpers/helpers_devops/cli_share_temp.py +69 -0
  16. machineconfig/scripts/python/helpers/helpers_devops/cli_ssh.py +4 -4
  17. machineconfig/scripts/python/helpers/helpers_devops/mount_helpers/__init__.py +0 -0
  18. machineconfig/scripts/python/helpers/helpers_devops/mount_helpers/commands.py +25 -0
  19. machineconfig/scripts/python/helpers/helpers_devops/mount_helpers/device_entry.py +17 -0
  20. machineconfig/scripts/python/helpers/helpers_devops/mount_helpers/devices.py +17 -0
  21. machineconfig/scripts/python/helpers/helpers_devops/mount_helpers/linux.py +103 -0
  22. machineconfig/scripts/python/helpers/helpers_devops/mount_helpers/macos.py +100 -0
  23. machineconfig/scripts/python/helpers/helpers_devops/mount_helpers/selection.py +47 -0
  24. machineconfig/scripts/python/helpers/helpers_devops/mount_helpers/utils.py +28 -0
  25. machineconfig/scripts/python/helpers/helpers_devops/mount_helpers/windows.py +91 -0
  26. machineconfig/scripts/python/helpers/helpers_msearch/scripts_windows/fzfg.ps1 +1 -6
  27. machineconfig/scripts/python/helpers/helpers_network/ssh/__init__.py +0 -0
  28. machineconfig/scripts/python/helpers/helpers_network/ssh/ssh_add_key_windows.py +23 -0
  29. machineconfig/scripts/python/helpers/helpers_network/{ssh_add_ssh_key.py → ssh/ssh_add_ssh_key.py} +21 -27
  30. machineconfig/scripts/python/helpers/helpers_network/ssh/ssh_cloud_init.py +33 -0
  31. machineconfig/scripts/python/helpers/helpers_network/{ssh_debug_linux.py → ssh/ssh_debug_linux.py} +70 -51
  32. machineconfig/scripts/python/helpers/helpers_network/ssh/ssh_debug_linux_utils.py +35 -0
  33. machineconfig/scripts/python/helpers/helpers_network/{ssh_debug_windows.py → ssh/ssh_debug_windows.py} +12 -42
  34. machineconfig/scripts/python/helpers/helpers_network/ssh/ssh_debug_windows_utils.py +34 -0
  35. machineconfig/scripts/python/helpers/helpers_repos/cloud_repo_sync.py +2 -3
  36. machineconfig/scripts/python/helpers/{helpers_terminal/terminal_impl.py → helpers_sessions/attach_impl.py} +16 -25
  37. machineconfig/scripts/python/helpers/helpers_sessions/sessions_impl.py +57 -129
  38. machineconfig/scripts/python/helpers/helpers_sessions/utils.py +69 -0
  39. machineconfig/scripts/python/mcfg_entry.py +0 -7
  40. machineconfig/scripts/python/sessions.py +95 -14
  41. machineconfig/scripts/python/utils.py +3 -2
  42. machineconfig/settings/shells/bash/init.sh +0 -7
  43. machineconfig/settings/shells/pwsh/init.ps1 +2 -4
  44. machineconfig/settings/shells/wezterm/wezterm.lua +1 -0
  45. machineconfig/settings/shells/wt/settings.json +13 -19
  46. machineconfig/settings/shells/zsh/init.sh +0 -1
  47. machineconfig/settings/zellij/__init__.py +0 -0
  48. machineconfig/settings/zellij/config.kdl +0 -295
  49. machineconfig/settings/zellij/layouts/__init__.py +0 -0
  50. machineconfig/settings/zellij/layouts/st.kdl +0 -1
  51. machineconfig/settings/zellij/layouts/st2.kdl +6 -2
  52. machineconfig/setup_linux/web_shortcuts/interactive.sh +10 -10
  53. machineconfig/setup_windows/web_shortcuts/interactive.ps1 +10 -10
  54. machineconfig/utils/installer_utils/installer_cli.py +6 -2
  55. machineconfig/utils/installer_utils/installer_helper.py +50 -34
  56. machineconfig/utils/installer_utils/installer_locator_utils.py +3 -13
  57. machineconfig/utils/options_utils/tv_options.py +1 -1
  58. machineconfig/utils/procs.py +35 -27
  59. machineconfig/utils/schemas/layouts/layout_types.py +10 -0
  60. machineconfig/utils/source_of_truth.py +1 -0
  61. machineconfig/utils/ssh_utils/abc.py +1 -1
  62. {machineconfig-8.51.dist-info → machineconfig-8.61.dist-info}/METADATA +2 -3
  63. {machineconfig-8.51.dist-info → machineconfig-8.61.dist-info}/RECORD +68 -72
  64. {machineconfig-8.51.dist-info → machineconfig-8.61.dist-info}/entry_points.txt +0 -1
  65. machineconfig/jobs/scripts/bash_scripts/android.sh +0 -2
  66. machineconfig/jobs/scripts/bash_scripts/mount_drive +0 -128
  67. machineconfig/jobs/scripts/bash_scripts/mount_nfs +0 -49
  68. machineconfig/jobs/scripts/bash_scripts/mount_nw_drive +0 -61
  69. machineconfig/jobs/scripts/bash_scripts/mount_smb +0 -3
  70. machineconfig/jobs/scripts/bash_scripts/share_cloud.sh +0 -64
  71. machineconfig/jobs/scripts/bash_scripts/share_nfs +0 -49
  72. machineconfig/jobs/scripts/bash_scripts/start_docker +0 -23
  73. machineconfig/jobs/scripts/powershell_scripts/Restore-ThunderbirdProfile.ps1 +0 -92
  74. machineconfig/jobs/scripts/powershell_scripts/docker.ps1 +0 -7
  75. machineconfig/jobs/scripts/powershell_scripts/mount_nfs.ps1 +0 -42
  76. machineconfig/jobs/scripts/powershell_scripts/mount_nw.ps1 +0 -9
  77. machineconfig/jobs/scripts/powershell_scripts/mount_smb.ps1 +0 -2
  78. machineconfig/jobs/scripts/powershell_scripts/mount_ssh.ps1 +0 -13
  79. machineconfig/jobs/scripts/powershell_scripts/obs.ps1 +0 -4
  80. machineconfig/jobs/scripts/powershell_scripts/power_options.ps1 +0 -7
  81. machineconfig/jobs/scripts/powershell_scripts/share_cloud.cmd +0 -34
  82. machineconfig/jobs/scripts/powershell_scripts/share_smb.ps1 +0 -16
  83. machineconfig/scripts/python/helpers/helpers_network/mount_nfs.py +0 -85
  84. machineconfig/scripts/python/helpers/helpers_network/mount_nw_drive.py +0 -48
  85. machineconfig/scripts/python/helpers/helpers_network/mount_ssh.py +0 -64
  86. machineconfig/scripts/python/terminal.py +0 -58
  87. machineconfig/settings/zellij/config.orig.kdl +0 -295
  88. /machineconfig/{scripts/python/helpers/helpers_terminal → cluster/sessions_managers/wt_utils/examples}/__init__.py +0 -0
  89. /machineconfig/scripts/python/helpers/helpers_network/{ssh_add_identity.py → ssh/ssh_add_identity.py} +0 -0
  90. {machineconfig-8.51.dist-info → machineconfig-8.61.dist-info}/WHEEL +0 -0
  91. {machineconfig-8.51.dist-info → machineconfig-8.61.dist-info}/top_level.txt +0 -0
@@ -128,13 +128,13 @@ def add_ssh_key(
128
128
  github: Annotated[Optional[str], typer.Option(..., "--github", "-g", help="Fetch public keys from a GitHub username")] = None,
129
129
  ) -> None:
130
130
  """🔑 Add SSH public key to this machine so its accessible by owner of corresponding private key."""
131
- import machineconfig.scripts.python.helpers.helpers_network.ssh_add_ssh_key as helper
131
+ import machineconfig.scripts.python.helpers.helpers_network.ssh.ssh_add_ssh_key as helper
132
132
  helper.main(pub_path=path, pub_choose=choose, pub_val=value, from_github=github)
133
133
 
134
134
 
135
135
  def add_ssh_identity() -> None:
136
136
  """🗝️ Add SSH identity (private key) to this machine"""
137
- import machineconfig.scripts.python.helpers.helpers_network.ssh_add_identity as helper
137
+ import machineconfig.scripts.python.helpers.helpers_network.ssh.ssh_add_identity as helper
138
138
  helper.main()
139
139
 
140
140
 
@@ -142,10 +142,10 @@ def debug_ssh() -> None:
142
142
  """🐛 Debug SSH connection"""
143
143
  from platform import system
144
144
  if system() == "Linux" or system() == "Darwin":
145
- import machineconfig.scripts.python.helpers.helpers_network.ssh_debug_linux as helper
145
+ import machineconfig.scripts.python.helpers.helpers_network.ssh.ssh_debug_linux as helper
146
146
  helper.ssh_debug_linux()
147
147
  elif system() == "Windows":
148
- import machineconfig.scripts.python.helpers.helpers_network.ssh_debug_windows as helper
148
+ import machineconfig.scripts.python.helpers.helpers_network.ssh.ssh_debug_windows as helper
149
149
  helper.ssh_debug_windows()
150
150
  else:
151
151
  print(f"❌ Error: Platform {system()} is not supported.")
@@ -0,0 +1,25 @@
1
+ import subprocess
2
+
3
+
4
+ def run_command(command: list[str]) -> subprocess.CompletedProcess[str]:
5
+ result = subprocess.run(command, capture_output=True, text=True, check=False)
6
+ return result
7
+
8
+
9
+ def run_command_sudo(command: list[str]) -> subprocess.CompletedProcess[str]:
10
+ result = subprocess.run(["sudo", *command], capture_output=True, text=True, check=False)
11
+ return result
12
+
13
+
14
+ def run_powershell(command: str) -> subprocess.CompletedProcess[str]:
15
+ result = subprocess.run(["powershell", "-NoProfile", "-Command", command], capture_output=True, text=True, check=False)
16
+ return result
17
+
18
+
19
+ def ensure_ok(result: subprocess.CompletedProcess[str], context: str) -> str:
20
+ if result.returncode != 0:
21
+ stderr_value = result.stderr.strip()
22
+ stdout_value = result.stdout.strip()
23
+ error_text = stderr_value if stderr_value != "" else stdout_value
24
+ raise RuntimeError(f"{context} failed: {error_text}")
25
+ return result.stdout
@@ -0,0 +1,17 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class DeviceEntry:
6
+ platform_name: str
7
+ key: str
8
+ device_path: str
9
+ device_type: str | None
10
+ label: str | None
11
+ mount_point: str | None
12
+ fs_type: str | None
13
+ size: str | None
14
+ extra: str | None
15
+ disk_number: int | None
16
+ partition_number: int | None
17
+ drive_letter: str | None
@@ -0,0 +1,17 @@
1
+ import platform
2
+
3
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.device_entry import DeviceEntry
4
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.linux import list_linux_devices
5
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.macos import list_macos_devices
6
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.windows import list_windows_devices
7
+
8
+
9
+ def list_devices() -> list[DeviceEntry]:
10
+ platform_name = platform.system()
11
+ if platform_name == "Linux":
12
+ return list_linux_devices()
13
+ if platform_name == "Darwin":
14
+ return list_macos_devices()
15
+ if platform_name == "Windows":
16
+ return list_windows_devices()
17
+ return []
@@ -0,0 +1,103 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+
5
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.commands import ensure_ok, run_command, run_command_sudo
6
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.device_entry import DeviceEntry
7
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.selection import pick_device
8
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.utils import as_str
9
+
10
+
11
+ def _flatten_lsblk_devices(devices: list[dict[str, object]]) -> list[dict[str, object]]:
12
+ result: list[dict[str, object]] = []
13
+ stack = list(devices)
14
+ while stack:
15
+ item = stack.pop(0)
16
+ result.append(item)
17
+ children = item.get("children")
18
+ if isinstance(children, list):
19
+ for child in children:
20
+ if isinstance(child, dict):
21
+ stack.append(child)
22
+ return result
23
+
24
+
25
+ def list_linux_devices() -> list[DeviceEntry]:
26
+ result = run_command(["lsblk", "-J", "-o", "NAME,SIZE,TYPE,FSTYPE,LABEL,MOUNTPOINT,UUID,MODEL"])
27
+ text = ensure_ok(result, "lsblk")
28
+ data = json.loads(text)
29
+ raw_devices = data.get("blockdevices")
30
+ entries: list[DeviceEntry] = []
31
+ if isinstance(raw_devices, list):
32
+ for item in _flatten_lsblk_devices(raw_devices):
33
+ name_value = item.get("name")
34
+ if not isinstance(name_value, str):
35
+ continue
36
+ device_type = item.get("type")
37
+ if not isinstance(device_type, str):
38
+ continue
39
+ if device_type not in {"disk", "part"}:
40
+ continue
41
+ device_path = f"/dev/{name_value}"
42
+ label = as_str(item.get("label"))
43
+ mount_point = as_str(item.get("mountpoint"))
44
+ fs_type = as_str(item.get("fstype"))
45
+ size = as_str(item.get("size"))
46
+ model = as_str(item.get("model"))
47
+ entries.append(
48
+ DeviceEntry(
49
+ platform_name="Linux",
50
+ key=name_value,
51
+ device_path=device_path,
52
+ device_type=device_type,
53
+ label=label,
54
+ mount_point=mount_point,
55
+ fs_type=fs_type,
56
+ size=size,
57
+ extra=model,
58
+ disk_number=None,
59
+ partition_number=None,
60
+ drive_letter=None,
61
+ )
62
+ )
63
+ return entries
64
+
65
+
66
+ def mount_linux(entry: DeviceEntry, mount_point: str) -> None:
67
+ mount_path = Path(mount_point)
68
+ try:
69
+ mount_path.mkdir(parents=True, exist_ok=True)
70
+ except PermissionError:
71
+ mkdir_result = run_command_sudo(["mkdir", "-p", str(mount_path)])
72
+ ensure_ok(mkdir_result, "mkdir")
73
+ if os.geteuid() == 0:
74
+ result = run_command(["mount", entry.device_path, str(mount_path)])
75
+ else:
76
+ result = run_command_sudo(["mount", entry.device_path, str(mount_path)])
77
+ ensure_ok(result, "mount")
78
+
79
+
80
+ def _is_partition_of_disk(partition: DeviceEntry, disk: DeviceEntry) -> bool:
81
+ if partition.device_type != "part" or disk.device_type != "disk":
82
+ return False
83
+ if partition.key == disk.key:
84
+ return False
85
+ if not partition.key.startswith(disk.key):
86
+ return False
87
+ return True
88
+
89
+
90
+ def select_linux_partition(entries: list[DeviceEntry], entry: DeviceEntry) -> DeviceEntry:
91
+ if entry.device_type != "disk":
92
+ return entry
93
+ candidates = [device for device in entries if _is_partition_of_disk(device, entry)]
94
+ with_fs = [device for device in candidates if device.fs_type is not None and device.fs_type != ""]
95
+ if len(with_fs) == 1:
96
+ return with_fs[0]
97
+ if len(with_fs) > 1:
98
+ return pick_device(with_fs, header="Select partition to mount")
99
+ if len(candidates) == 1:
100
+ return candidates[0]
101
+ if len(candidates) > 1:
102
+ return pick_device(candidates, header="Select partition to mount")
103
+ raise RuntimeError("No mountable partitions found for selected disk")
@@ -0,0 +1,100 @@
1
+ import plistlib
2
+ from pathlib import Path
3
+
4
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.commands import ensure_ok, run_command
5
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.device_entry import DeviceEntry
6
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.utils import as_str, format_size_bytes
7
+
8
+
9
+ def _diskutil_info(identifier: str) -> dict[str, object]:
10
+ result = run_command(["diskutil", "info", "-plist", identifier])
11
+ text = ensure_ok(result, "diskutil info")
12
+ return plistlib.loads(text.encode("utf-8"))
13
+
14
+
15
+ def list_macos_devices() -> list[DeviceEntry]:
16
+ result = run_command(["diskutil", "list", "-plist"])
17
+ text = ensure_ok(result, "diskutil list")
18
+ data = plistlib.loads(text.encode("utf-8"))
19
+ entries: list[DeviceEntry] = []
20
+ all_disks = data.get("AllDisksAndPartitions")
21
+ if isinstance(all_disks, list):
22
+ for disk in all_disks:
23
+ if not isinstance(disk, dict):
24
+ continue
25
+ partitions = disk.get("Partitions")
26
+ if isinstance(partitions, list) and partitions:
27
+ for partition in partitions:
28
+ if not isinstance(partition, dict):
29
+ continue
30
+ identifier = partition.get("DeviceIdentifier")
31
+ if not isinstance(identifier, str):
32
+ continue
33
+ info = _diskutil_info(identifier)
34
+ mount_point = as_str(info.get("MountPoint"))
35
+ label = as_str(info.get("VolumeName"))
36
+ fs_type = as_str(info.get("FilesystemType"))
37
+ if fs_type is None:
38
+ fs_type = as_str(info.get("FileSystemType"))
39
+ size_value = info.get("TotalSize")
40
+ size = format_size_bytes(size_value) if isinstance(size_value, int) else None
41
+ media_name = as_str(info.get("MediaName"))
42
+ device_path = f"/dev/{identifier}"
43
+ entries.append(
44
+ DeviceEntry(
45
+ platform_name="Darwin",
46
+ key=identifier,
47
+ device_path=device_path,
48
+ device_type="part",
49
+ label=label,
50
+ mount_point=mount_point,
51
+ fs_type=fs_type,
52
+ size=size,
53
+ extra=media_name,
54
+ disk_number=None,
55
+ partition_number=None,
56
+ drive_letter=None,
57
+ )
58
+ )
59
+ else:
60
+ identifier = disk.get("DeviceIdentifier")
61
+ if not isinstance(identifier, str):
62
+ continue
63
+ info = _diskutil_info(identifier)
64
+ mount_point = as_str(info.get("MountPoint"))
65
+ label = as_str(info.get("VolumeName"))
66
+ fs_type = as_str(info.get("FilesystemType"))
67
+ if fs_type is None:
68
+ fs_type = as_str(info.get("FileSystemType"))
69
+ size_value = info.get("TotalSize")
70
+ size = format_size_bytes(size_value) if isinstance(size_value, int) else None
71
+ media_name = as_str(info.get("MediaName"))
72
+ device_path = f"/dev/{identifier}"
73
+ entries.append(
74
+ DeviceEntry(
75
+ platform_name="Darwin",
76
+ key=identifier,
77
+ device_path=device_path,
78
+ device_type="disk",
79
+ label=label,
80
+ mount_point=mount_point,
81
+ fs_type=fs_type,
82
+ size=size,
83
+ extra=media_name,
84
+ disk_number=None,
85
+ partition_number=None,
86
+ drive_letter=None,
87
+ )
88
+ )
89
+ return entries
90
+
91
+
92
+ def mount_macos(entry: DeviceEntry, mount_point: str) -> None:
93
+ if mount_point == "-":
94
+ result = run_command(["diskutil", "mount", entry.key])
95
+ ensure_ok(result, "diskutil mount")
96
+ return
97
+ mount_path = Path(mount_point)
98
+ mount_path.mkdir(parents=True, exist_ok=True)
99
+ result = run_command(["diskutil", "mount", "-mountPoint", str(mount_path), entry.key])
100
+ ensure_ok(result, "diskutil mount")
@@ -0,0 +1,47 @@
1
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.device_entry import DeviceEntry
2
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.utils import format_device
3
+
4
+
5
+ def pick_device(entries: list[DeviceEntry], header: str) -> DeviceEntry:
6
+ from machineconfig.utils.options import choose_from_options
7
+
8
+ options: list[str] = []
9
+ map_option: dict[str, DeviceEntry] = {}
10
+ for idx, entry in enumerate(entries):
11
+ option = f"{idx:02d} {format_device(entry)}"
12
+ options.append(option)
13
+ map_option[option] = entry
14
+ choice = choose_from_options(options=options, msg="Select a device", multi=False, header=header, tv=True)
15
+ selected = map_option.get(choice)
16
+ if selected is None:
17
+ raise RuntimeError("Selection not found")
18
+ return selected
19
+
20
+
21
+ def resolve_device(entries: list[DeviceEntry], query: str) -> DeviceEntry:
22
+ query_value = query.strip()
23
+ if query_value in {"?", ""}:
24
+ return pick_device(entries, header="Available devices")
25
+ exact_matches = [
26
+ entry
27
+ for entry in entries
28
+ if entry.device_path.lower() == query_value.lower()
29
+ or entry.key.lower() == query_value.lower()
30
+ or (entry.label is not None and entry.label.lower() == query_value.lower())
31
+ ]
32
+ if len(exact_matches) == 1:
33
+ return exact_matches[0]
34
+ if len(exact_matches) > 1:
35
+ return pick_device(exact_matches, header="Multiple matches")
36
+ partial_matches = [
37
+ entry
38
+ for entry in entries
39
+ if query_value.lower() in entry.device_path.lower()
40
+ or query_value.lower() in entry.key.lower()
41
+ or (entry.label is not None and query_value.lower() in entry.label.lower())
42
+ ]
43
+ if len(partial_matches) == 1:
44
+ return partial_matches[0]
45
+ if len(partial_matches) > 1:
46
+ return pick_device(partial_matches, header="Multiple matches")
47
+ return pick_device(entries, header="Available devices")
@@ -0,0 +1,28 @@
1
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.device_entry import DeviceEntry
2
+
3
+
4
+ def as_str(value: object) -> str | None:
5
+ if isinstance(value, str) and value != "":
6
+ return value
7
+ return None
8
+
9
+
10
+ def format_size_bytes(size_bytes: int) -> str:
11
+ units = ["B", "KB", "MB", "GB", "TB", "PB"]
12
+ value = float(size_bytes)
13
+ unit_index = 0
14
+ while value >= 1024 and unit_index < len(units) - 1:
15
+ value = value / 1024
16
+ unit_index += 1
17
+ if unit_index == 0:
18
+ return f"{int(value)} {units[unit_index]}"
19
+ return f"{value:.1f} {units[unit_index]}"
20
+
21
+
22
+ def format_device(entry: DeviceEntry) -> str:
23
+ label = entry.label if entry.label is not None and entry.label != "" else "-"
24
+ fs_type = entry.fs_type if entry.fs_type is not None and entry.fs_type != "" else "-"
25
+ size = entry.size if entry.size is not None and entry.size != "" else "-"
26
+ mount_point = entry.mount_point if entry.mount_point is not None and entry.mount_point != "" else "-"
27
+ extra = entry.extra if entry.extra is not None and entry.extra != "" else "-"
28
+ return f"{entry.key} | {entry.device_path} | {fs_type} | {size} | {mount_point} | {label} | {extra}"
@@ -0,0 +1,91 @@
1
+ import json
2
+ import re
3
+
4
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.commands import ensure_ok, run_powershell
5
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.device_entry import DeviceEntry
6
+ from machineconfig.scripts.python.helpers.helpers_devops.mount_helpers.utils import as_str, format_size_bytes
7
+
8
+
9
+ def _normalize_ps_json(raw_text: str) -> list[dict[str, object]]:
10
+ if raw_text.strip() == "":
11
+ return []
12
+ data = json.loads(raw_text)
13
+ if isinstance(data, list):
14
+ return [x for x in data if isinstance(x, dict)]
15
+ if isinstance(data, dict):
16
+ return [data]
17
+ return []
18
+
19
+
20
+ def list_windows_devices() -> list[DeviceEntry]:
21
+ partitions_result = run_powershell(
22
+ "Get-Partition | Select-Object DiskNumber,PartitionNumber,DriveLetter,Size,Type,Guid | ConvertTo-Json"
23
+ )
24
+ volumes_result = run_powershell(
25
+ "Get-Volume | Select-Object DriveLetter,FileSystemLabel,FileSystem,Size,SizeRemaining,DriveType,Path | ConvertTo-Json"
26
+ )
27
+ partitions_text = ensure_ok(partitions_result, "Get-Partition")
28
+ volumes_text = ensure_ok(volumes_result, "Get-Volume")
29
+ partitions = _normalize_ps_json(partitions_text)
30
+ volumes = _normalize_ps_json(volumes_text)
31
+ volume_map: dict[str, dict[str, object]] = {}
32
+ for volume in volumes:
33
+ drive_letter = as_str(volume.get("DriveLetter"))
34
+ if isinstance(drive_letter, str):
35
+ volume_map[drive_letter.upper()] = volume
36
+ entries: list[DeviceEntry] = []
37
+ for partition in partitions:
38
+ disk_number = partition.get("DiskNumber")
39
+ partition_number = partition.get("PartitionNumber")
40
+ drive_letter = as_str(partition.get("DriveLetter"))
41
+ volume = volume_map.get(drive_letter.upper()) if isinstance(drive_letter, str) else None
42
+ label = None
43
+ fs_type = None
44
+ mount_point = None
45
+ extra = as_str(partition.get("Type"))
46
+ if isinstance(volume, dict):
47
+ label = as_str(volume.get("FileSystemLabel"))
48
+ fs_type = as_str(volume.get("FileSystem"))
49
+ mount_point = as_str(volume.get("Path"))
50
+ if mount_point is None and isinstance(drive_letter, str) and drive_letter != "":
51
+ mount_point = f"{drive_letter}:\\"
52
+ size_value = partition.get("Size")
53
+ size = format_size_bytes(size_value) if isinstance(size_value, int) else None
54
+ key = f"Disk {disk_number} Part {partition_number}"
55
+ device_path = as_str(partition.get("Guid")) or key
56
+ entries.append(
57
+ DeviceEntry(
58
+ platform_name="Windows",
59
+ key=key,
60
+ device_path=device_path,
61
+ device_type="part",
62
+ label=label,
63
+ mount_point=mount_point,
64
+ fs_type=fs_type,
65
+ size=size,
66
+ extra=extra,
67
+ disk_number=disk_number if isinstance(disk_number, int) else None,
68
+ partition_number=partition_number if isinstance(partition_number, int) else None,
69
+ drive_letter=drive_letter.upper() if isinstance(drive_letter, str) and drive_letter != "" else None,
70
+ )
71
+ )
72
+ return entries
73
+
74
+
75
+ def _normalize_drive_letter(value: str) -> str:
76
+ match = re.search(r"([A-Za-z])", value)
77
+ if match is None:
78
+ raise RuntimeError("Invalid drive letter")
79
+ return match.group(1).upper()
80
+
81
+
82
+ def mount_windows(entry: DeviceEntry, mount_point: str) -> None:
83
+ if entry.disk_number is None or entry.partition_number is None:
84
+ raise RuntimeError("Partition details not available")
85
+ letter = _normalize_drive_letter(mount_point)
86
+ command = (
87
+ f"Get-Partition -DiskNumber {entry.disk_number} -PartitionNumber {entry.partition_number} | "
88
+ f"Set-Partition -NewDriveLetter {letter}"
89
+ )
90
+ result = run_powershell(command)
91
+ ensure_ok(result, "Set-Partition")
@@ -1,10 +1,5 @@
1
1
  #!/usr/bin/env pwsh
2
- [CmdletBinding()]
3
- param(
4
- [Parameter(ValueFromRemainingArguments = $true)]
5
- [string[]]$QueryTokens
6
- )
7
-
2
+ $QueryTokens = $args
8
3
  if ($null -eq $QueryTokens) {
9
4
  $QueryTokens = @()
10
5
  }
@@ -0,0 +1,23 @@
1
+ from pathlib import Path
2
+ import subprocess
3
+
4
+
5
+ def add_ssh_key_windows(path_to_key: Path) -> None:
6
+ sshd_dir = Path("C:/ProgramData/ssh")
7
+ admin_auth_keys = sshd_dir / "administrators_authorized_keys"
8
+ sshd_config = sshd_dir / "sshd_config"
9
+ key_content = path_to_key.read_text(encoding="utf-8").strip()
10
+ if admin_auth_keys.exists():
11
+ existing = admin_auth_keys.read_text(encoding="utf-8")
12
+ if not existing.endswith("\n"):
13
+ existing += "\n"
14
+ admin_auth_keys.write_text(existing + key_content + "\n", encoding="utf-8")
15
+ else:
16
+ admin_auth_keys.write_text(key_content + "\n", encoding="utf-8")
17
+ icacls_cmd = f'icacls "{admin_auth_keys}" /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"'
18
+ subprocess.run(icacls_cmd, shell=True, check=True)
19
+ if sshd_config.exists():
20
+ config_text = sshd_config.read_text(encoding="utf-8")
21
+ config_text = config_text.replace("#PubkeyAuthentication", "PubkeyAuthentication")
22
+ sshd_config.write_text(config_text, encoding="utf-8")
23
+ subprocess.run("Restart-Service sshd -Force", shell=True, check=True)
@@ -7,34 +7,12 @@ from rich.panel import Panel
7
7
  from rich import box
8
8
  from typing import Optional, Annotated
9
9
  import typer
10
- import subprocess
11
10
 
12
-
13
- console = Console()
11
+ from machineconfig.scripts.python.helpers.helpers_network.ssh.ssh_add_key_windows import add_ssh_key_windows
12
+ from machineconfig.scripts.python.helpers.helpers_network.ssh.ssh_cloud_init import check_cloud_init_overrides, generate_cloud_init_fix_script
14
13
 
15
14
 
16
- def _add_ssh_key_windows(path_to_key: Path) -> None:
17
- """Add SSH key on Windows using Python with proper UTF-8 encoding.
18
- This replaces the PowerShell script that was writing UTF-16LE encoded files which openssh server cannot read.
19
- """
20
- sshd_dir = Path("C:/ProgramData/ssh")
21
- admin_auth_keys = sshd_dir / "administrators_authorized_keys"
22
- sshd_config = sshd_dir / "sshd_config"
23
- key_content = path_to_key.read_text(encoding="utf-8").strip()
24
- if admin_auth_keys.exists():
25
- existing = admin_auth_keys.read_text(encoding="utf-8")
26
- if not existing.endswith("\n"):
27
- existing += "\n"
28
- admin_auth_keys.write_text(existing + key_content + "\n", encoding="utf-8")
29
- else:
30
- admin_auth_keys.write_text(key_content + "\n", encoding="utf-8")
31
- icacls_cmd = f'icacls "{admin_auth_keys}" /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"'
32
- subprocess.run(icacls_cmd, shell=True, check=True)
33
- if sshd_config.exists():
34
- config_text = sshd_config.read_text(encoding="utf-8")
35
- config_text = config_text.replace("#PubkeyAuthentication", "PubkeyAuthentication")
36
- sshd_config.write_text(config_text, encoding="utf-8")
37
- subprocess.run("Restart-Service sshd -Force", shell=True, check=True)
15
+ console = Console()
38
16
 
39
17
 
40
18
  def get_add_ssh_key_script(path_to_key: Path, verbose: bool = True) -> tuple[str, str]:
@@ -63,7 +41,7 @@ def get_add_ssh_key_script(path_to_key: Path, verbose: bool = True) -> tuple[str
63
41
  if os_name == "Linux" or os_name == "Darwin":
64
42
  program = f"cat {path_to_key} >> ~/.ssh/authorized_keys"
65
43
  elif os_name == "Windows":
66
- _add_ssh_key_windows(path_to_key)
44
+ add_ssh_key_windows(path_to_key)
67
45
  else:
68
46
  raise NotImplementedError
69
47
  else:
@@ -71,9 +49,25 @@ def get_add_ssh_key_script(path_to_key: Path, verbose: bool = True) -> tuple[str
71
49
  if os_name == "Linux" or os_name == "Darwin":
72
50
  program = f"cat {path_to_key} > ~/.ssh/authorized_keys"
73
51
  else:
74
- _add_ssh_key_windows(path_to_key)
52
+ add_ssh_key_windows(path_to_key)
75
53
 
76
54
  if os_name == "Linux" or os_name == "Darwin":
55
+ override_files, auth_overrides = check_cloud_init_overrides()
56
+ if override_files:
57
+ status_lines.append(f"\n⚠️ [yellow]Cloud-init override files detected:[/yellow]")
58
+ for of in override_files:
59
+ status_lines.append(f" • {of.name}")
60
+ blocking_overrides: list[str] = []
61
+ for key, (file_path, value) in auth_overrides.items():
62
+ if key == "PubkeyAuthentication" and value == "no":
63
+ blocking_overrides.append(f" ❌ {key}={value} in {file_path.name} - [red]blocks key auth![/red]")
64
+ elif key == "PasswordAuthentication" and value == "no":
65
+ blocking_overrides.append(f" ⚠️ {key}={value} in {file_path.name}")
66
+ if blocking_overrides:
67
+ status_lines.extend(blocking_overrides)
68
+ cloud_init_fix = generate_cloud_init_fix_script(auth_overrides)
69
+ if cloud_init_fix:
70
+ program += f"\n# === Fix cloud-init SSH overrides ===\n{cloud_init_fix}\n"
77
71
  program += """
78
72
  sudo chmod 700 ~/.ssh
79
73
  sudo chmod 644 ~/.ssh/authorized_keys
@@ -0,0 +1,33 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def check_cloud_init_overrides() -> tuple[list[Path], dict[str, tuple[Path, str]]]:
5
+ sshd_config_d = Path("/etc/ssh/sshd_config.d")
6
+ override_files: list[Path] = []
7
+ auth_overrides: dict[str, tuple[Path, str]] = {}
8
+ if not sshd_config_d.exists():
9
+ return override_files, auth_overrides
10
+ for conf_file in sorted(sshd_config_d.glob("*.conf")):
11
+ override_files.append(conf_file)
12
+ try:
13
+ conf_text = conf_file.read_text(encoding="utf-8")
14
+ for line in conf_text.split("\n"):
15
+ line_stripped = line.strip()
16
+ if line_stripped and not line_stripped.startswith("#"):
17
+ parts = line_stripped.split(None, 1)
18
+ if len(parts) >= 2:
19
+ key, value = parts[0], parts[1]
20
+ if key in ("PasswordAuthentication", "PubkeyAuthentication", "PermitRootLogin", "ChallengeResponseAuthentication", "KbdInteractiveAuthentication"):
21
+ auth_overrides[key] = (conf_file, value.lower())
22
+ except Exception:
23
+ pass
24
+ return override_files, auth_overrides
25
+
26
+
27
+ def generate_cloud_init_fix_script(auth_overrides: dict[str, tuple[Path, str]]) -> str:
28
+ fix_commands: list[str] = []
29
+ for key, (file_path, value) in auth_overrides.items():
30
+ if key in ("PasswordAuthentication", "PubkeyAuthentication") and value == "no":
31
+ fix_commands.append(f"# Fix {key} in {file_path.name}")
32
+ fix_commands.append(f"sudo sed -i 's/^{key}.*no/{key} yes/' {file_path}")
33
+ return "\n".join(fix_commands)