linux-image-manager 2.1.0__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.
lim/image/choosers.py ADDED
@@ -0,0 +1,111 @@
1
+ """Interactive selection of the distribution image to install."""
2
+
3
+ from lim import catalog, runner, ui
4
+ from lim.errors import LimError
5
+ from lim.image.plan import ImagePlan
6
+
7
+
8
+ def choose_arch(plan: ImagePlan) -> None:
9
+ version = ui.ask("Which Raspberry Pi will be used (e.g.: 1, 2, 3b, 3b+, 4...):")
10
+ entry = catalog.arch_rpi_images().get(version)
11
+ if entry is None:
12
+ raise LimError(f"Version {version} isn't supported.")
13
+ plan.raspberry_pi_version = version
14
+ plan.boot_size = "+500M"
15
+ plan.base_download_url = "http://os.archlinuxarm.org/os/"
16
+ plan.image_name = entry["image"]
17
+ plan.luks_memory_cost = entry["luks_memory_cost"]
18
+
19
+
20
+ def choose_manjaro(plan: ImagePlan) -> None:
21
+ plan.boot_size = "+500M"
22
+ flavour = ui.ask("Which version(e.g.:architect,gnome) should be used:")
23
+ if flavour == "architect":
24
+ plan.image_checksum = "6b1c2fce12f244c1e32212767a9d3af2cf8263b2"
25
+ plan.base_download_url = (
26
+ "https://osdn.net/frs/redir.php?m=dotsrc&f=%2Fstorage%2Fg%2Fm%2Fma"
27
+ "%2Fmanjaro%2Farchitect%2F20.0%2F"
28
+ )
29
+ plan.image_name = "manjaro-architect-20.0-200426-linux56.iso"
30
+ elif flavour == "gnome":
31
+ release = ui.ask("Which release(e.g.:20,21,raspberrypi) should be used:")
32
+ entry = catalog.manjaro_gnome_releases().get(release)
33
+ if entry is None:
34
+ raise LimError(f"Gnome release {release} isn't supported.")
35
+ plan.image_checksum = entry.get("checksum")
36
+ plan.base_download_url = entry["url"]
37
+ plan.image_name = entry["image"]
38
+ plan.luks_memory_cost = entry.get("luks_memory_cost")
39
+ plan.raspberry_pi_version = entry.get("raspberry_pi_version")
40
+ else:
41
+ raise LimError(f"Manjaro version {flavour} isn't supported.")
42
+
43
+
44
+ def choose_moode(plan: ImagePlan) -> None:
45
+ plan.boot_size = "+200M"
46
+ plan.image_checksum = "185cbc9a4994534bb7a4bc2744c78197"
47
+ plan.base_download_url = (
48
+ "https://github.com/moode-player/moode/releases/download/r651prod/"
49
+ )
50
+ plan.image_name = "moode-r651-iso.zip"
51
+
52
+
53
+ def choose_retropie(plan: ImagePlan) -> None:
54
+ plan.boot_size = "+500M"
55
+ version = ui.ask("Which version(e.g.:1,2,3,4) should be used:")
56
+ entry = catalog.retropie_images().get(version)
57
+ if entry is None:
58
+ raise LimError(f"Version {version} isn't supported.")
59
+ plan.raspberry_pi_version = version
60
+ plan.base_download_url = (
61
+ "https://github.com/RetroPie/RetroPie-Setup/releases/download/4.8/"
62
+ )
63
+ plan.image_checksum = entry["checksum"]
64
+ plan.image_name = entry["image"]
65
+
66
+
67
+ def choose_torbox(plan: ImagePlan) -> None:
68
+ plan.base_download_url = "https://www.torbox.ch/data/"
69
+ plan.image_name = "torbox-20220102-v050.gz"
70
+ plan.image_checksum = (
71
+ "0E1BA7FFD14AAAE5F0462C8293D95B62C3BF1D9E726E26977BD04772C55680D3"
72
+ )
73
+ plan.boot_size = "+200M"
74
+
75
+
76
+ def choose_android_x86(plan: ImagePlan) -> None:
77
+ plan.base_download_url = (
78
+ "https://www.fosshub.com/Android-x86.html?dwl=android-x86_64-9.0-r2.iso"
79
+ )
80
+ plan.image_name = "android-x86_64-9.0-r2.iso"
81
+ plan.image_checksum = (
82
+ "f7eb8fc56f29ad5432335dc054183acf086c539f3990f0b6e9ff58bd6df4604e"
83
+ )
84
+ plan.boot_size = "+500M"
85
+
86
+
87
+ DISTRIBUTION_CHOOSERS = {
88
+ "android-x86": choose_android_x86,
89
+ "torbox": choose_torbox,
90
+ "arch": choose_arch,
91
+ "manjaro": choose_manjaro,
92
+ "moode": choose_moode,
93
+ "retropie": choose_retropie,
94
+ }
95
+
96
+
97
+ def choose_linux_image(plan: ImagePlan) -> None:
98
+ distribution = ui.ask(
99
+ "Which distribution should be used [arch,moode,retropie,manjaro,torbox...]?"
100
+ )
101
+ chooser = DISTRIBUTION_CHOOSERS.get(distribution)
102
+ if chooser is None:
103
+ raise LimError(f"Distribution {distribution} isn't supported.")
104
+ plan.distribution = distribution
105
+ chooser(plan)
106
+
107
+
108
+ def choose_local_image(plan: ImagePlan) -> None:
109
+ ui.info("Available images:")
110
+ runner.run(["ls", "-l", str(plan.image_folder)], check=False)
111
+ plan.image_name = ui.ask("Which image would you like to use?")
lim/image/chroot.py ADDED
@@ -0,0 +1,23 @@
1
+ """Mount an image and open an interactive shell inside it."""
2
+
3
+ from lim import device as device_module
4
+ from lim import runner, ui
5
+ from lim.image.session import ImageSession
6
+
7
+
8
+ def run_chroot() -> None:
9
+ ui.info("Starting chroot...")
10
+ device = device_module.select_device()
11
+ session = ImageSession(device)
12
+ try:
13
+ session.make_working_folder()
14
+ session.make_mount_folders()
15
+ session.decrypt_root()
16
+ session.mount_partitions()
17
+ session.mount_chroot_binds()
18
+ session.copy_qemu()
19
+ session.copy_resolv_conf()
20
+ ui.info("Bash shell starts...")
21
+ runner.run(["chroot", str(session.root_mount_path), "/bin/bash"], sudo=True)
22
+ finally:
23
+ session.destructor()
@@ -0,0 +1,117 @@
1
+ """Remote-unlockable LUKS boot configuration (dropbear + mkinitcpio).
2
+
3
+ See https://gist.github.com/gea0/4fc2be0cb7a74d0e7cc4322aed710d38 and
4
+ https://gist.github.com/EnigmaCurry/2f9bed46073da8e38057fe78a61e7994
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ from lim import catalog, fsutil, packages, runner, ui
10
+ from lim.image.plan import ImagePlan
11
+ from lim.image.session import ImageSession, chroot_bash, install_packages
12
+
13
+ MKINITCPIO_HOOKS_PREFIX = (
14
+ "base udev autodetect microcode modconf kms keyboard keymap consolefont block"
15
+ )
16
+ MKINITCPIO_HOOKS_SUFFIX = "filesystems fsck"
17
+
18
+
19
+ def _configure_mkinitcpio(plan: ImagePlan, root: Path) -> None:
20
+ # Concerning mkinitcpio warnings, see
21
+ # https://gist.github.com/imrvelj/c65cd5ca7f5505a65e59204f5a3f7a6d
22
+ mkinitcpio_path = root / "etc/mkinitcpio.conf"
23
+ ui.info(f"Configuring {mkinitcpio_path}...")
24
+ additional_modules = catalog.mkinitcpio_modules_by_rpi().get(
25
+ plan.raspberry_pi_version
26
+ )
27
+ if additional_modules is None:
28
+ ui.warning(f"Version {plan.raspberry_pi_version} isn't supported.")
29
+ additional_modules = ""
30
+ modules = f"g_cdc usb_f_acm usb_f_ecm {additional_modules} g_ether".replace(" ", " ")
31
+ fsutil.replace_in_file("MODULES=()", f"MODULES=({modules})", mkinitcpio_path)
32
+ fsutil.replace_in_file(
33
+ "BINARIES=()", "BINARIES=(/usr/lib/libgcc_s.so.1)", mkinitcpio_path
34
+ )
35
+ fsutil.replace_in_file(
36
+ f"HOOKS=({MKINITCPIO_HOOKS_PREFIX} {MKINITCPIO_HOOKS_SUFFIX})",
37
+ f"HOOKS=({MKINITCPIO_HOOKS_PREFIX} sleep netconf dropbear encryptssh "
38
+ f"{MKINITCPIO_HOOKS_SUFFIX})",
39
+ mkinitcpio_path,
40
+ )
41
+ ui.info(f"Content of {mkinitcpio_path}:{mkinitcpio_path.read_text()}")
42
+ ui.info("Generating mkinitcpio...")
43
+ chroot_bash(root, "mkinitcpio -vP")
44
+
45
+
46
+ def _register_encrypted_root(plan: ImagePlan, session: ImageSession, root: Path) -> None:
47
+ fstab_path = root / "etc/fstab"
48
+ fstab_line = (
49
+ f"UUID={session.root_partition_uuid} / {plan.root_filesystem}"
50
+ " defaults,noatime 0 1"
51
+ )
52
+ ui.info(f"Configuring {fstab_path}...")
53
+ fsutil.ensure_line_in_file(fstab_line, fstab_path)
54
+ ui.info(f"Content of {fstab_path}:{fstab_path.read_text()}")
55
+
56
+ crypttab_path = root / "etc/crypttab"
57
+ crypttab_line = (
58
+ f"{session.root_mapper_name} UUID={session.root_partition_uuid} none luks"
59
+ )
60
+ ui.info(f"Configuring {crypttab_path}...")
61
+ fsutil.ensure_line_in_file(crypttab_line, crypttab_path)
62
+ ui.info(f"Content of {crypttab_path}:{crypttab_path.read_text()}")
63
+
64
+
65
+ def _configure_bootloader(plan: ImagePlan, session: ImageSession, root: Path) -> None:
66
+ cryptdevice = (
67
+ f"cryptdevice=UUID={session.root_partition_uuid}:{session.root_mapper_name} "
68
+ f"root={session.root_mapper_path}"
69
+ )
70
+ boot_txt_path = session.boot_mount_path / "boot.txt"
71
+ if boot_txt_path.is_file():
72
+ ui.info(f"Configuring {boot_txt_path}...")
73
+ hostname = (root / "etc/hostname").read_text().strip()
74
+ fsutil.replace_in_file(
75
+ "part uuid ${devtype} ${devnum}:2 uuid", "", boot_txt_path
76
+ )
77
+ fsutil.replace_in_file(
78
+ "setenv bootargs console=ttyS1,115200 console=tty0 root=PARTUUID=${uuid} "
79
+ 'rw rootwait smsc95xx.macaddr="${usbethaddr}"',
80
+ f"setenv bootargs console=ttyS1,115200 console=tty0 "
81
+ f"ip=::::{hostname}:eth0:dhcp {cryptdevice} rw rootwait "
82
+ # Concerning issues with network adapter names, see
83
+ # https://forum.iobroker.net/topic/40542/raspberry-pi4-kein-eth0-mehr/16
84
+ 'smsc95xx.macaddr="${usbethaddr}" net.ifnames=0 biosdevname=0',
85
+ boot_txt_path,
86
+ )
87
+ ui.info(f"Content of {boot_txt_path}:{boot_txt_path.read_text()}")
88
+ ui.info("Generating...")
89
+ chroot_bash(root, "cd /boot/ && ./mkscr || exit 1")
90
+ else:
91
+ cmdline_txt_path = session.boot_mount_path / "cmdline.txt"
92
+ ui.info(f"Configuring {cmdline_txt_path}...")
93
+ fsutil.replace_in_file(
94
+ "root=/dev/mmcblk0p2",
95
+ f"{cryptdevice} rootfstype={plan.root_filesystem}",
96
+ cmdline_txt_path,
97
+ )
98
+ ui.info(f"Content of {cmdline_txt_path}:{cmdline_txt_path.read_text()}")
99
+
100
+
101
+ def configure_encryption(
102
+ plan: ImagePlan, session: ImageSession, authorized_keys: Path
103
+ ) -> None:
104
+ root = session.root_mount_path
105
+ ui.info("Setup encryption...")
106
+ ui.info("Installing neccessary software...")
107
+ install_packages(
108
+ plan.distribution, root, " ".join(packages.get_packages("server/luks"))
109
+ )
110
+
111
+ dropbear_root_key_path = root / "etc/dropbear/root_key"
112
+ ui.info(f"Adding {authorized_keys} to dropbear...")
113
+ runner.run(["cp", "-v", str(authorized_keys), str(dropbear_root_key_path)], sudo=True)
114
+
115
+ _configure_mkinitcpio(plan, root)
116
+ _register_encrypted_root(plan, session, root)
117
+ _configure_bootloader(plan, session, root)
lim/image/plan.py ADDED
@@ -0,0 +1,31 @@
1
+ """The image setup plan collected from the user's answers."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+ DEFAULT_BOOT_SIZE = "+500M"
7
+
8
+
9
+ @dataclass
10
+ class ImagePlan:
11
+ operation_system: str = "linux"
12
+ distribution: str | None = None
13
+ base_download_url: str | None = None
14
+ image_name: str | None = None
15
+ image_checksum: str | None = None
16
+ boot_size: str = ""
17
+ luks_memory_cost: str | None = None
18
+ raspberry_pi_version: str | None = None
19
+ encrypt_system: bool = False
20
+ root_filesystem: str = ""
21
+ image_folder: Path = field(default_factory=Path)
22
+
23
+ @property
24
+ def download_url(self) -> str | None:
25
+ if self.base_download_url is None or self.image_name is None:
26
+ return None
27
+ return f"{self.base_download_url}{self.image_name}"
28
+
29
+ @property
30
+ def image_path(self) -> Path:
31
+ return self.image_folder / self.image_name
lim/image/raspberry.py ADDED
@@ -0,0 +1,236 @@
1
+ """Raspberry-Pi-specific configuration of a freshly transferred image."""
2
+
3
+ from pathlib import Path
4
+
5
+ from lim import device as device_module
6
+ from lim import fsutil, runner, ui
7
+ from lim.errors import LimError
8
+ from lim.image.encryption import configure_encryption
9
+ from lim.image.plan import ImagePlan
10
+ from lim.image.session import ImageSession, chroot_bash, install_packages
11
+
12
+ ADMINISTRATOR_USERNAME = "administrator"
13
+
14
+
15
+ def configure_sudoers(root_mount_path: Path, username: str) -> None:
16
+ sudo_config_dir = root_mount_path / "etc/sudoers.d"
17
+ sudo_config_file = sudo_config_dir / username
18
+ sudo_config_dir.mkdir(parents=True, exist_ok=True)
19
+ try:
20
+ sudo_config_file.write_text(f"{username} ALL=(ALL:ALL) ALL\n")
21
+ sudo_config_file.chmod(0o440)
22
+ except OSError as exc:
23
+ raise LimError(f"Failed to create sudoers file for {username}: {exc}") from exc
24
+
25
+
26
+ def configure_ssh_key(
27
+ public_key_path: str, target_ssh_folder: Path, authorized_keys: Path
28
+ ) -> None:
29
+ source = Path(public_key_path)
30
+ if not source.is_file():
31
+ raise LimError(
32
+ f'The ssh key "{public_key_path}" can\'t be copied to '
33
+ f'"{authorized_keys}" because it doesn\'t exist.'
34
+ )
35
+ ui.info("Copy ssh key to target...")
36
+ target_ssh_folder.mkdir(parents=True, exist_ok=True)
37
+ authorized_keys.write_text(source.read_text())
38
+ ui.info(f"{authorized_keys} contains the following: {authorized_keys.read_text()}")
39
+ ui.info("Set permissions with chmod...")
40
+ target_ssh_folder.chmod(0o700)
41
+ authorized_keys.chmod(0o600)
42
+
43
+
44
+ def _ensure_image_mounted(session: ImageSession) -> None:
45
+ ui.info("Start regular mounting procedure...")
46
+ if device_module.is_mounted(session.boot_partition_path):
47
+ ui.info(f"{session.boot_partition_path} is allready mounted...")
48
+ elif device_module.is_mounted(session.root_mapper_path):
49
+ ui.info(f"{session.root_mapper_path} is allready mounted...")
50
+ else:
51
+ session.decrypt_root()
52
+ session.mount_partitions()
53
+ if session.boot_partition_uuid is None:
54
+ # Partitions were mounted by an earlier run; fetch what mount_partitions
55
+ # would have provided so fstab/crypttab never see a None UUID.
56
+ session.read_partition_uuids()
57
+
58
+
59
+ def _seed_boot_uuid(session: ImageSession) -> None:
60
+ fstab_path = session.root_mount_path / "etc/fstab"
61
+ if "/dev/mmcblk0p1" not in fstab_path.read_text():
62
+ # PARTUUID=/LABEL= based images (e.g. RetroPie, Manjaro ARM) have
63
+ # nothing to seed — that is a valid state, not an error.
64
+ ui.info(f"{fstab_path} does not reference /dev/mmcblk0p1. Skipping UUID seeding.")
65
+ return
66
+ ui.info(f"Seeding UUID to {fstab_path} to avoid path conflicts...")
67
+ fsutil.replace_in_file(
68
+ "/dev/mmcblk0p1", f"UUID={session.boot_partition_uuid}", fstab_path
69
+ )
70
+ ui.info(f"Content of {fstab_path}:{fstab_path.read_text()}")
71
+
72
+
73
+ def _select_target_user(root: Path) -> tuple[str, str]:
74
+ """Return (default username, target username), renaming the home on request."""
75
+ target_home_path = root / "home"
76
+ home_entries = sorted(path.name for path in target_home_path.iterdir())
77
+ if not home_entries:
78
+ raise LimError(f"No home directory found under {target_home_path}.")
79
+ default_username = home_entries[0]
80
+ rename = default_username != ADMINISTRATOR_USERNAME and ui.confirm(
81
+ f"Should the {default_username} be renamed to {ADMINISTRATOR_USERNAME}? "
82
+ )
83
+ if not rename:
84
+ return default_username, default_username
85
+ ui.info(
86
+ f"Rename home directory from {target_home_path / default_username} "
87
+ f"to {target_home_path / ADMINISTRATOR_USERNAME}..."
88
+ )
89
+ runner.run(
90
+ [
91
+ "mv",
92
+ "-v",
93
+ str(target_home_path / default_username),
94
+ str(target_home_path / ADMINISTRATOR_USERNAME),
95
+ ],
96
+ sudo=True,
97
+ error_msg="Failed to rename home directory",
98
+ )
99
+ return default_username, ADMINISTRATOR_USERNAME
100
+
101
+
102
+ def _change_passwords(root: Path, target_username: str) -> None:
103
+ password = ui.ask(
104
+ f"Type in new password for user root and {target_username} (leave empty to skip): "
105
+ )
106
+ if not password:
107
+ ui.info("No password change requested, skipped password change...")
108
+ return
109
+ repeated = ui.ask(f'Repeat new password for "{target_username}": ')
110
+ if password != repeated:
111
+ raise LimError("Passwords didn't match.")
112
+ ui.info("Changing passwords on target system...")
113
+ chroot_bash(
114
+ root,
115
+ f"( echo '{password}'; echo '{password}' ) | passwd {target_username}\n"
116
+ f"( echo '{password}'; echo '{password}' ) | passwd\n",
117
+ error_msg="Failed to change password.",
118
+ )
119
+
120
+
121
+ def _configure_hostname(root: Path) -> None:
122
+ hostname_path = root / "etc/hostname"
123
+ hostname = ui.ask("Type in the hostname (leave empty to skip): ")
124
+ if hostname:
125
+ hostname_path.write_text(hostname + "\n")
126
+ else:
127
+ hostname = hostname_path.read_text().strip()
128
+ ui.info("No hostname change requested, skipped hostname change...")
129
+ ui.info(f"Used hostname is: {hostname}")
130
+
131
+
132
+ def _update_system(plan: ImagePlan, root: Path) -> None:
133
+ if not ui.confirm("Should the system be updated?"):
134
+ return
135
+ ui.info("Updating system...")
136
+ if plan.distribution in ("arch", "manjaro"):
137
+ chroot_bash(root, "pacman --noconfirm -Syyu")
138
+ elif plan.distribution in ("moode", "retropie"):
139
+ chroot_bash(root, "yes | apt update\nyes | apt upgrade\n")
140
+ else:
141
+ ui.warning(
142
+ f'System update for operation system "{plan.distribution}" '
143
+ "is not supported yet. Skipped."
144
+ )
145
+
146
+
147
+ def _run_retropie_procedures(session: ImageSession, public_key_path: str) -> None:
148
+ if public_key_path:
149
+ (session.boot_mount_path / "ssh").write_text("\n")
150
+ if not ui.confirm("Should the RetroFlag specific procedures be executed?"):
151
+ return
152
+ ui.info("Executing RetroFlag specific procedures...")
153
+ chroot_bash(
154
+ session.root_mount_path,
155
+ 'wget -O - "https://raw.githubusercontent.com/RetroFlag/'
156
+ 'retroflag-picase/master/install_gpi.sh" | bash\n',
157
+ )
158
+
159
+
160
+ def _configure_users_and_keys(session: ImageSession) -> tuple[str, Path]:
161
+ """Handle user rename, sudo rights and SSH key; return (public key path, authorized_keys)."""
162
+ root = session.root_mount_path
163
+ ui.info("Define target paths...")
164
+ default_username, target_username = _select_target_user(root)
165
+ renamed = default_username != target_username
166
+ target_user_ssh_folder = root / "home" / target_username / ".ssh"
167
+ target_authorized_keys = target_user_ssh_folder / "authorized_keys"
168
+
169
+ if ui.confirm(f"Should the {target_username} have sudo rights? "):
170
+ configure_sudoers(root, target_username)
171
+
172
+ public_key_path = ui.ask(
173
+ "Enter the path to the SSH key to be added to the image (default: none):"
174
+ )
175
+ if public_key_path:
176
+ configure_ssh_key(public_key_path, target_user_ssh_folder, target_authorized_keys)
177
+ else:
178
+ ui.info("Skipped SSH-key copying..")
179
+
180
+ ui.info("Start chroot procedures...")
181
+ session.mount_chroot_binds()
182
+ session.copy_qemu()
183
+ session.copy_resolv_conf()
184
+
185
+ chroot_user_home = f"/home/{target_username}/"
186
+ if renamed:
187
+ ui.info("Delete old user and create new user")
188
+ chroot_bash(
189
+ root,
190
+ f"userdel -r {default_username}\n"
191
+ f"useradd -m -d {chroot_user_home} -s /bin/bash {target_username}\n"
192
+ f"chown -R {target_username}:{target_username} {chroot_user_home}\n",
193
+ error_msg="Failed to delete old user and create new user",
194
+ )
195
+
196
+ if public_key_path:
197
+ ui.info("Chroot to set ownership...")
198
+ chroot_bash(
199
+ root,
200
+ f"chown -vR {target_username}:{target_username} {chroot_user_home}.ssh\n",
201
+ )
202
+
203
+ _change_passwords(root, target_username)
204
+ return public_key_path, target_authorized_keys
205
+
206
+
207
+ def configure_raspberry_image(plan: ImagePlan, session: ImageSession) -> None:
208
+ _ensure_image_mounted(session)
209
+ root = session.root_mount_path
210
+ _seed_boot_uuid(session)
211
+
212
+ public_key_path, target_authorized_keys = _configure_users_and_keys(session)
213
+
214
+ _configure_hostname(root)
215
+
216
+ if plan.distribution in ("arch", "manjaro"):
217
+ ui.info("Populating keys...")
218
+ chroot_bash(
219
+ root,
220
+ "yes | pacman-key --init\nyes | pacman-key --populate archlinuxarm\n",
221
+ )
222
+
223
+ _update_system(plan, root)
224
+
225
+ ui.info(f"Installing software for filesystem {plan.root_filesystem}...")
226
+ if plan.root_filesystem == "btrfs":
227
+ install_packages(plan.distribution, root, "btrfs-progs")
228
+ else:
229
+ ui.info("Skipped.")
230
+
231
+ if plan.encrypt_system:
232
+ configure_encryption(plan, session, target_authorized_keys)
233
+
234
+ ui.info("Running system specific procedures...")
235
+ if plan.distribution == "retropie":
236
+ _run_retropie_procedures(session, public_key_path)