codexctl 0.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.
- codexctl/__init__.py +462 -0
- codexctl/__main__.py +4 -0
- codexctl/analysis.py +30 -0
- codexctl/device.py +640 -0
- codexctl/server.py +172 -0
- codexctl/sync.py +218 -0
- codexctl/updates.py +368 -0
- codexctl-0.1.0.dist-info/LICENSE +674 -0
- codexctl-0.1.0.dist-info/METADATA +105 -0
- codexctl-0.1.0.dist-info/RECORD +11 -0
- codexctl-0.1.0.dist-info/WHEEL +4 -0
codexctl/device.py
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import subprocess
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
import re
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from .server import startUpdate
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import paramiko
|
|
13
|
+
import psutil
|
|
14
|
+
except ImportError:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DeviceManager:
|
|
19
|
+
def __init__(
|
|
20
|
+
self, logger=None, remote=False, address=None, authentication=None
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Initializes the DeviceManager for codexctl
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
remote (bool, optional): Whether the device is remote. Defaults to False.
|
|
26
|
+
address (bool, optional): Known IP of remote device, if applicable. Defaults to None.
|
|
27
|
+
logger (logger, optional): Logger object for logging. Defaults to None.
|
|
28
|
+
Authentication (str, optional): Authentication method. Defaults to None.
|
|
29
|
+
"""
|
|
30
|
+
self.logger = logger
|
|
31
|
+
self.address = address
|
|
32
|
+
self.authentication = authentication
|
|
33
|
+
self.client = None
|
|
34
|
+
|
|
35
|
+
if self.logger is None:
|
|
36
|
+
self.logger = logging
|
|
37
|
+
|
|
38
|
+
if remote:
|
|
39
|
+
self.client = self.connect_to_device(
|
|
40
|
+
authentication=authentication, remote_address=address
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.client.authentication = authentication
|
|
44
|
+
self.client.address = address
|
|
45
|
+
|
|
46
|
+
ftp = self.client.open_sftp()
|
|
47
|
+
with ftp.file("/sys/devices/soc0/machine") as file:
|
|
48
|
+
machine_contents = file.read().decode("utf-8").strip("\n")
|
|
49
|
+
else:
|
|
50
|
+
with open("/sys/devices/soc0/machine") as file:
|
|
51
|
+
machine_contents = file.read().decode("utf-8").strip("\n")
|
|
52
|
+
|
|
53
|
+
if "reMarkable Ferrari" in machine_contents:
|
|
54
|
+
self.hardware = "ferrari"
|
|
55
|
+
elif "reMarkable 1" in machine_contents:
|
|
56
|
+
self.hardware = "reMarkable1"
|
|
57
|
+
else:
|
|
58
|
+
self.hardware = "reMarkable2"
|
|
59
|
+
|
|
60
|
+
def get_host_address(self) -> list[str] | list | None: # Interaction required
|
|
61
|
+
"""Gets the IP address of the host machine
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
str | None: IP address of the host machine, or None if not found
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
possible_ips = []
|
|
68
|
+
try:
|
|
69
|
+
for interface, snics in psutil.net_if_addrs().items():
|
|
70
|
+
self.logger.debug(f"New interface found: {interface}")
|
|
71
|
+
for snic in snics:
|
|
72
|
+
if snic.family == socket.AF_INET:
|
|
73
|
+
if snic.address.startswith("10.11.99"):
|
|
74
|
+
return snic.address
|
|
75
|
+
self.logger.debug(f"Adding new address: {snic.address}")
|
|
76
|
+
possible_ips.append(snic.address)
|
|
77
|
+
|
|
78
|
+
except Exception as error:
|
|
79
|
+
self.logger.error(f"Error automatically getting interfaces: {error}")
|
|
80
|
+
|
|
81
|
+
if possible_ips:
|
|
82
|
+
host_interfaces = "\n".join(possible_ips)
|
|
83
|
+
else:
|
|
84
|
+
host_interfaces = "Could not find any available interfaces."
|
|
85
|
+
|
|
86
|
+
print(f"\n{host_interfaces}")
|
|
87
|
+
while True:
|
|
88
|
+
host_address = input(
|
|
89
|
+
"\nPlease enter your host IP for the network the device is connected to: "
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if possible_ips and host_address not in host_interfaces.split("\n"):
|
|
93
|
+
print("Error: Invalid IP given")
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
if "n" in input("Are you sure? (Y/n): ").lower():
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
return host_address
|
|
102
|
+
|
|
103
|
+
def get_remarkable_address(self) -> str:
|
|
104
|
+
"""Gets the IP address of the remarkable device
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
str: IP address of the remarkable device
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
if self.check_is_address_reachable("10.11.99.1"):
|
|
111
|
+
return "10.11.99.1"
|
|
112
|
+
|
|
113
|
+
while True:
|
|
114
|
+
remote_ip = input("Please enter the IP of the remarkable device: ")
|
|
115
|
+
|
|
116
|
+
if self.check_is_address_reachable(remote_ip):
|
|
117
|
+
return remote_ip
|
|
118
|
+
|
|
119
|
+
print(f"Error: Device {remote_ip} is not reachable. Please try again.")
|
|
120
|
+
|
|
121
|
+
def check_is_address_reachable(self, remote_ip="10.11.99.1") -> bool:
|
|
122
|
+
"""Checks if the given IP address is reachable over SSH
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
remote_ip (str, optional): IP to check. Defaults to '10.11.99.1'.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
bool: True if reachable, False otherwise
|
|
129
|
+
"""
|
|
130
|
+
self.logger.debug(f"Checking if {remote_ip} is reachable")
|
|
131
|
+
try:
|
|
132
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
133
|
+
sock.settimeout(1)
|
|
134
|
+
|
|
135
|
+
sock.connect((remote_ip, 22))
|
|
136
|
+
sock.shutdown(2)
|
|
137
|
+
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
except Exception:
|
|
141
|
+
self.logger.debug(f"Device {remote_ip} is not reachable")
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
def connect_to_device(
|
|
145
|
+
self, remote_address=None, authentication=None
|
|
146
|
+
) -> paramiko.client.SSHClient:
|
|
147
|
+
"""Connects to the device using the given IP address
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
remote_address (str, optional): IP address of the device.
|
|
151
|
+
authentication (str, optional): Authentication credentials. Defaults to None.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
paramiko.client.SSHClient: SSH client object for the device.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
if remote_address is None:
|
|
158
|
+
remote_address = self.get_remarkable_address()
|
|
159
|
+
self.address = remote_address # For future reference
|
|
160
|
+
else:
|
|
161
|
+
if self.check_is_address_reachable(remote_address) is False:
|
|
162
|
+
raise SystemError(f"Error: Device {remote_address} is not reachable!")
|
|
163
|
+
|
|
164
|
+
client = paramiko.client.SSHClient()
|
|
165
|
+
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
166
|
+
|
|
167
|
+
if authentication:
|
|
168
|
+
self.logger.debug(f"Using authentication: {authentication}")
|
|
169
|
+
try:
|
|
170
|
+
if os.path.isfile(authentication):
|
|
171
|
+
self.logger.debug(
|
|
172
|
+
f"Attempting to connect to {remote_address} with key file {authentication}"
|
|
173
|
+
)
|
|
174
|
+
client.connect(
|
|
175
|
+
remote_address, username="root", key_filename=authentication
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
self.logger.debug(
|
|
179
|
+
f"Attempting to connect to {remote_address} with password {authentication}"
|
|
180
|
+
)
|
|
181
|
+
client.connect(
|
|
182
|
+
remote_address, username="root", password=authentication
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
except paramiko.ssh_exception.AuthenticationException:
|
|
186
|
+
print("Incorrect password or ssh path given in arguments!")
|
|
187
|
+
|
|
188
|
+
elif (
|
|
189
|
+
"n" in input("Would you like to use a password to connect? (Y/n): ").lower()
|
|
190
|
+
):
|
|
191
|
+
while True:
|
|
192
|
+
key_path = input("Enter path to SSH key: ")
|
|
193
|
+
|
|
194
|
+
if not os.path.isfile(key_path):
|
|
195
|
+
print("Invalid path given")
|
|
196
|
+
|
|
197
|
+
continue
|
|
198
|
+
try:
|
|
199
|
+
self.logger.debug(
|
|
200
|
+
f"Attempting to connect to {remote_address} with key file {key_path}"
|
|
201
|
+
)
|
|
202
|
+
client.connect(
|
|
203
|
+
remote_address, username="root", key_filename=key_path
|
|
204
|
+
)
|
|
205
|
+
except Exception:
|
|
206
|
+
print("Error while connecting to device: {error}")
|
|
207
|
+
|
|
208
|
+
continue
|
|
209
|
+
break
|
|
210
|
+
else:
|
|
211
|
+
while True:
|
|
212
|
+
password = input("Enter RM SSH password: ")
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
self.logger.debug(
|
|
216
|
+
f"Attempting to connect to {remote_address} with password {password}"
|
|
217
|
+
)
|
|
218
|
+
client.connect(remote_address, username="root", password=password)
|
|
219
|
+
except paramiko.ssh_exception.AuthenticationException:
|
|
220
|
+
print("Incorrect password given")
|
|
221
|
+
|
|
222
|
+
continue
|
|
223
|
+
break
|
|
224
|
+
|
|
225
|
+
print("Success: Connected to device")
|
|
226
|
+
|
|
227
|
+
return client
|
|
228
|
+
|
|
229
|
+
def get_device_status(self) -> tuple[str | None, str, str]:
|
|
230
|
+
"""Gets the status of the device
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
tuple: Beta status, previous version, and current version (in that order)
|
|
234
|
+
"""
|
|
235
|
+
old_update_engine = True
|
|
236
|
+
|
|
237
|
+
if self.client:
|
|
238
|
+
self.logger.debug("Connecting to FTP")
|
|
239
|
+
ftp = self.client.open_sftp()
|
|
240
|
+
self.logger.debug("Connected")
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
with ftp.file("/usr/share/remarkable/update.conf") as file:
|
|
244
|
+
xochitl_version = re.search(
|
|
245
|
+
"(?<=REMARKABLE_RELEASE_VERSION=).*",
|
|
246
|
+
file.read().decode("utf-8").strip("\n"),
|
|
247
|
+
).group()
|
|
248
|
+
except Exception:
|
|
249
|
+
with ftp.file("/etc/os-release") as file:
|
|
250
|
+
xochitl_version = (
|
|
251
|
+
re.search("(?<=IMG_VERSION=).*", file.read().decode("utf-8"))
|
|
252
|
+
.group()
|
|
253
|
+
.strip('"')
|
|
254
|
+
)
|
|
255
|
+
old_update_engine = False
|
|
256
|
+
|
|
257
|
+
with ftp.file("/etc/version") as file:
|
|
258
|
+
version_id = file.read().decode("utf-8").strip("\n")
|
|
259
|
+
|
|
260
|
+
with ftp.file("/home/root/.config/remarkable/xochitl.conf") as file:
|
|
261
|
+
beta_contents = file.read().decode("utf-8")
|
|
262
|
+
|
|
263
|
+
else:
|
|
264
|
+
if os.path.exists("/usr/share/remarkable/update.conf"):
|
|
265
|
+
with open("/usr/share/remarkable/update.conf") as file:
|
|
266
|
+
xochitl_version = re.search(
|
|
267
|
+
"(?<=REMARKABLE_RELEASE_VERSION=).*",
|
|
268
|
+
file.read().decode("utf-8").strip("\n"),
|
|
269
|
+
).group()
|
|
270
|
+
else:
|
|
271
|
+
with open("/etc/os-release") as file:
|
|
272
|
+
xochitl_version = (
|
|
273
|
+
re.search("(?<=IMG_VERSION=).*", file.read().decode("utf-8"))
|
|
274
|
+
.group()
|
|
275
|
+
.strip('"')
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
old_update_engine = False
|
|
279
|
+
if os.path.exists("/etc/version"):
|
|
280
|
+
with open("/etc/version") as file:
|
|
281
|
+
version_id = file.read().rstrip()
|
|
282
|
+
else:
|
|
283
|
+
version_id = ""
|
|
284
|
+
|
|
285
|
+
if os.path.exists("/home/root/.config/remarkable/xochitl.conf"):
|
|
286
|
+
with open("/home/root/.config/remarkable/xochitl.conf") as file:
|
|
287
|
+
beta_contents = file.read().rstrip()
|
|
288
|
+
else:
|
|
289
|
+
beta_contents = ""
|
|
290
|
+
|
|
291
|
+
beta_possible = re.search("(?<=GROUP=).*", beta_contents)
|
|
292
|
+
beta = "Release"
|
|
293
|
+
|
|
294
|
+
if beta_possible is not None:
|
|
295
|
+
beta = re.search("(?<=GROUP=).*", beta_contents).group()
|
|
296
|
+
|
|
297
|
+
return beta, old_update_engine, xochitl_version, version_id
|
|
298
|
+
|
|
299
|
+
def set_server_config(self, contents: str, server_host_name: str) -> str:
|
|
300
|
+
"""Converts the contents given to point to the given server IP and port
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
contents (str): Contents of the update.conf file
|
|
304
|
+
server_host_name (str): Hostname of the server
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
str: Converted contents
|
|
308
|
+
"""
|
|
309
|
+
data_attributes = contents.split("\n")
|
|
310
|
+
line = 0
|
|
311
|
+
|
|
312
|
+
self.logger.debug(f"Contents are:\n{contents}")
|
|
313
|
+
|
|
314
|
+
for i in range(0, len(data_attributes)):
|
|
315
|
+
if data_attributes[i].startswith("[General]"):
|
|
316
|
+
self.logger.debug("Found [General] line")
|
|
317
|
+
line = i + 1
|
|
318
|
+
if not data_attributes[i].startswith("SERVER="):
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
data_attributes[i] = f"#{data_attributes[i]}"
|
|
322
|
+
self.logger.debug(f"Using {data_attributes[i]}")
|
|
323
|
+
|
|
324
|
+
data_attributes.insert(line, f"SERVER={server_host_name}")
|
|
325
|
+
converted = "\n".join(data_attributes)
|
|
326
|
+
|
|
327
|
+
self.logger.debug(f"Converted contents are:\n{converted}")
|
|
328
|
+
|
|
329
|
+
return converted
|
|
330
|
+
|
|
331
|
+
def edit_update_conf(self, server_ip: str, server_port: str) -> bool:
|
|
332
|
+
"""Edits the update.conf file to point to the given server IP and port
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
server_ip (str): IP of update server
|
|
336
|
+
server_port (str): Port of update service
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
bool: True if successful, False otherwise
|
|
340
|
+
"""
|
|
341
|
+
server_host_name = f"http://{server_ip}:{server_port}"
|
|
342
|
+
self.logger.debug(f"Hostname is: {server_host_name}")
|
|
343
|
+
try:
|
|
344
|
+
if not self.client:
|
|
345
|
+
self.logger.debug("Detected running on local device")
|
|
346
|
+
with open(
|
|
347
|
+
"/usr/share/remarkable/update.conf", encoding="utf-8"
|
|
348
|
+
) as file:
|
|
349
|
+
modified_conf_version = self.set_server_config(
|
|
350
|
+
file.read(), server_host_name
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
with open("/usr/share/remarkable/update.conf", "w") as file:
|
|
354
|
+
file.write(modified_conf_version)
|
|
355
|
+
|
|
356
|
+
return True
|
|
357
|
+
|
|
358
|
+
self.logger.debug("Connecting to FTP")
|
|
359
|
+
ftp = self.client.open_sftp() # or ssh
|
|
360
|
+
self.logger.debug("Connected")
|
|
361
|
+
|
|
362
|
+
with ftp.file("/usr/share/remarkable/update.conf") as update_conf_file:
|
|
363
|
+
modified_conf_version = self.set_server_config(
|
|
364
|
+
update_conf_file.read().decode("utf-8"), server_host_name
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
with ftp.file("/usr/share/remarkable/update.conf", "w") as update_conf_file:
|
|
368
|
+
update_conf_file.write(modified_conf_version)
|
|
369
|
+
|
|
370
|
+
return True
|
|
371
|
+
except Exception as error:
|
|
372
|
+
self.logger.error(f"Error while editing update.conf: {error}")
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
def restore_previous_version(self) -> None:
|
|
376
|
+
"""Restores the previous version of the device"""
|
|
377
|
+
|
|
378
|
+
RESTORE_CODE = """/sbin/fw_setenv "upgrade_available" "1"
|
|
379
|
+
/sbin/fw_setenv "bootcount" "0"
|
|
380
|
+
|
|
381
|
+
OLDPART=$(/sbin/fw_printenv -n active_partition)
|
|
382
|
+
if [ $OLDPART == "2" ]; then
|
|
383
|
+
NEWPART="3"
|
|
384
|
+
else
|
|
385
|
+
NEWPART="2"
|
|
386
|
+
fi
|
|
387
|
+
echo "new: ${NEWPART}"
|
|
388
|
+
echo "fallback: ${OLDPART}"
|
|
389
|
+
|
|
390
|
+
/sbin/fw_setenv "fallback_partition" "${OLDPART}"
|
|
391
|
+
/sbin/fw_setenv "active_partition" "${NEWPART}\""""
|
|
392
|
+
|
|
393
|
+
if self.client:
|
|
394
|
+
self.logger.debug("Connecting to FTP")
|
|
395
|
+
ftp = self.client.open_sftp()
|
|
396
|
+
self.logger.debug("Connected")
|
|
397
|
+
|
|
398
|
+
with ftp.file("/usr/bin/restore.sh", "w") as file:
|
|
399
|
+
file.write(RESTORE_CODE)
|
|
400
|
+
|
|
401
|
+
self.logger.debug("Setting permissions and running restore.sh")
|
|
402
|
+
|
|
403
|
+
self.client.exec_command("chmod +x /usr/bin/restore.sh")
|
|
404
|
+
self.client.exec_command("bash /usr/bin/restore.sh")
|
|
405
|
+
else:
|
|
406
|
+
with open("/usr/bin/restore.sh", "w") as file:
|
|
407
|
+
file.write(RESTORE_CODE)
|
|
408
|
+
|
|
409
|
+
self.logger.debug("Setting permissions and running restore.sh")
|
|
410
|
+
|
|
411
|
+
os.system("chmod +x /usr/bin/restore.sh")
|
|
412
|
+
os.system("/usr/bin/restore.sh")
|
|
413
|
+
|
|
414
|
+
self.logger.debug("Restore script ran")
|
|
415
|
+
|
|
416
|
+
def install_sw_update(self, version_file: str) -> None:
|
|
417
|
+
"""
|
|
418
|
+
Installs new version from version file path, utilising swupdate
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
version_file (str): Path to img file
|
|
422
|
+
|
|
423
|
+
Raises:
|
|
424
|
+
SystemExit: If there was an error installing the update
|
|
425
|
+
|
|
426
|
+
"""
|
|
427
|
+
command = f'/usr/bin/swupdate -v -i VERSION_FILE -k /usr/share/swupdate/swupdate-payload-key-pub.pem -H "{self.hardware}:1.0" -e "stable,copy1"'
|
|
428
|
+
|
|
429
|
+
if self.client:
|
|
430
|
+
ftp_client = self.client.open_sftp()
|
|
431
|
+
|
|
432
|
+
print(f"Uploading {version_file} image")
|
|
433
|
+
|
|
434
|
+
out_location = f'/tmp/{os.path.basename(version_file)}.swu'
|
|
435
|
+
ftp_client.put(
|
|
436
|
+
version_file, out_location, callback=self.output_put_progress
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
print("\nDone! Running swupdate (PLEASE BE PATIENT, ~5 MINUTES)")
|
|
440
|
+
|
|
441
|
+
command = command.replace("VERSION_FILE", out_location)
|
|
442
|
+
|
|
443
|
+
for num in (1, 2):
|
|
444
|
+
command = command.replace(
|
|
445
|
+
"stable,copy1", f"stable,copy{num}"
|
|
446
|
+
) # terrible hack but it works
|
|
447
|
+
self.logger.debug(command)
|
|
448
|
+
_stdin, stdout, _stderr = self.client.exec_command(command)
|
|
449
|
+
|
|
450
|
+
self.logger.debug(f"Stdout of swupdate checking: {stdout.readlines()}")
|
|
451
|
+
|
|
452
|
+
exit_status = stdout.channel.recv_exit_status()
|
|
453
|
+
|
|
454
|
+
if exit_status != 0:
|
|
455
|
+
if "over our current root" in "".join(_stderr.readlines()):
|
|
456
|
+
continue
|
|
457
|
+
else:
|
|
458
|
+
print("".join(_stderr.readlines()))
|
|
459
|
+
raise SystemError("Update failed!")
|
|
460
|
+
|
|
461
|
+
print("Done! Now rebooting the device and disabling update service")
|
|
462
|
+
|
|
463
|
+
#### Now disable automatic updates
|
|
464
|
+
|
|
465
|
+
self.client.exec_command("sleep 1 && reboot") # Should be enough
|
|
466
|
+
self.client.close()
|
|
467
|
+
|
|
468
|
+
time.sleep(
|
|
469
|
+
2
|
|
470
|
+
) # Somehow the code runs faster than the time it takes for the device to reboot
|
|
471
|
+
|
|
472
|
+
print("Trying to connect to device")
|
|
473
|
+
|
|
474
|
+
while not self.check_is_address_reachable(self.address):
|
|
475
|
+
time.sleep(1)
|
|
476
|
+
|
|
477
|
+
self.client = self.connect_to_device(
|
|
478
|
+
remote_address=self.address, authentication=self.authentication
|
|
479
|
+
)
|
|
480
|
+
self.client.exec_command("systemctl stop swupdate memfaultd")
|
|
481
|
+
|
|
482
|
+
print(
|
|
483
|
+
"Update complete and update service disabled, restart device to enable it"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
else:
|
|
487
|
+
print("Running swupdate")
|
|
488
|
+
command = command.replace("VERSION_FILE", version_file)
|
|
489
|
+
|
|
490
|
+
for num in (1, 2):
|
|
491
|
+
command = command.replace(
|
|
492
|
+
"stable,copy1", f"stable,copy{num}"
|
|
493
|
+
) # terrible hack but it works
|
|
494
|
+
self.logger.debug(command)
|
|
495
|
+
|
|
496
|
+
with subprocess.Popen(
|
|
497
|
+
command,
|
|
498
|
+
text=True,
|
|
499
|
+
shell=True, # Being lazy...
|
|
500
|
+
stdout=subprocess.PIPE,
|
|
501
|
+
stderr=subprocess.PIPE,
|
|
502
|
+
env={"PATH": "/bin:/usr/bin:/sbin"},
|
|
503
|
+
) as process:
|
|
504
|
+
if process.wait() != 0:
|
|
505
|
+
if "installing over our current root" in "".join(
|
|
506
|
+
process.stderr.readlines()
|
|
507
|
+
):
|
|
508
|
+
continue
|
|
509
|
+
else:
|
|
510
|
+
print("".join(process.stderr.readlines()))
|
|
511
|
+
raise SystemError("Update failed")
|
|
512
|
+
|
|
513
|
+
self.logger.debug(
|
|
514
|
+
f'Stdout of update checking service is {"".join(process.stdout.readlines())}'
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
print("Update complete and device rebooting")
|
|
518
|
+
os.system("reboot")
|
|
519
|
+
|
|
520
|
+
def install_ohma_update(self, version_available: dict) -> None:
|
|
521
|
+
"""Installs version from update folder on the device
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
version_available (dict): Version available for installation from `get_available_version`
|
|
525
|
+
|
|
526
|
+
Raises:
|
|
527
|
+
SystemExit: If there was an error installing the update
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
server_host = self.get_host_address()
|
|
531
|
+
|
|
532
|
+
self.logger.debug("Editing config file")
|
|
533
|
+
|
|
534
|
+
if (
|
|
535
|
+
self.edit_update_conf(server_ip=server_host, server_port=8085) is False
|
|
536
|
+
): # We want a port that probably isn't being used
|
|
537
|
+
self.logger.error("Error while editing update.conf")
|
|
538
|
+
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
thread = threading.Thread(
|
|
542
|
+
target=startUpdate, args=(version_available, server_host), daemon=True
|
|
543
|
+
)
|
|
544
|
+
thread.start()
|
|
545
|
+
|
|
546
|
+
self.logger.debug("Thread started")
|
|
547
|
+
|
|
548
|
+
if self.client:
|
|
549
|
+
print("Checking if device can connect to this machine")
|
|
550
|
+
|
|
551
|
+
_stdin, stdout, _stderr = self.client.exec_command(
|
|
552
|
+
f"sleep 2 && echo | nc {server_host} 8085"
|
|
553
|
+
)
|
|
554
|
+
check = stdout.channel.recv_exit_status()
|
|
555
|
+
self.logger.debug(f"Stdout of nc checking: {stdout.readlines()}")
|
|
556
|
+
|
|
557
|
+
if check != 0:
|
|
558
|
+
raise SystemError(
|
|
559
|
+
"Device cannot connect to this machine! Is the firewall blocking connections?"
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
print("Starting update service on device")
|
|
563
|
+
|
|
564
|
+
self.client.exec_command("systemctl start update-engine")
|
|
565
|
+
|
|
566
|
+
_stdin, stdout, _stderr = self.client.exec_command(
|
|
567
|
+
"/usr/bin/update_engine_client -update"
|
|
568
|
+
)
|
|
569
|
+
exit_status = stdout.channel.recv_exit_status()
|
|
570
|
+
|
|
571
|
+
if exit_status != 0:
|
|
572
|
+
print("".join(_stderr.readlines()))
|
|
573
|
+
raise SystemError("There was an error updating :(")
|
|
574
|
+
|
|
575
|
+
self.logger.debug(
|
|
576
|
+
f'Stdout of update checking service is {"".join(_stderr.readlines())}'
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
#### Now disable automatic updates
|
|
580
|
+
|
|
581
|
+
print("Done! Now rebooting the device and disabling update service")
|
|
582
|
+
|
|
583
|
+
self.client.exec_command("sleep 1 && reboot") # Should be enough
|
|
584
|
+
self.client.close()
|
|
585
|
+
|
|
586
|
+
time.sleep(
|
|
587
|
+
2
|
|
588
|
+
) # Somehow the code runs faster than the time it takes for the device to reboot
|
|
589
|
+
|
|
590
|
+
print("Trying to connect to device")
|
|
591
|
+
|
|
592
|
+
while not self.check_is_address_reachable(address):
|
|
593
|
+
time.sleep(1)
|
|
594
|
+
|
|
595
|
+
self.client = self.connect_to_device(
|
|
596
|
+
remote_address=address, authentication=authentication
|
|
597
|
+
)
|
|
598
|
+
self.client.exec_command("systemctl stop update-engine")
|
|
599
|
+
|
|
600
|
+
print(
|
|
601
|
+
"Update complete and update service disabled. Restart device to enable it"
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
else:
|
|
605
|
+
print("Enabling update service")
|
|
606
|
+
|
|
607
|
+
subprocess.run(
|
|
608
|
+
["/bin/systemctl", "start", "update-engine"],
|
|
609
|
+
text=True,
|
|
610
|
+
check=True,
|
|
611
|
+
env={"PATH": "/bin:/usr/bin:/sbin"},
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
with subprocess.Popen(
|
|
615
|
+
["/usr/bin/update_engine_client", "-update"],
|
|
616
|
+
text=True,
|
|
617
|
+
stdout=subprocess.PIPE,
|
|
618
|
+
stderr=subprocess.PIPE,
|
|
619
|
+
env={"PATH": "/bin:/usr/bin:/sbin"},
|
|
620
|
+
) as process:
|
|
621
|
+
if process.wait() != 0:
|
|
622
|
+
print("".join(process.stderr.readlines()))
|
|
623
|
+
|
|
624
|
+
raise SystemError("There was an error updating :(")
|
|
625
|
+
|
|
626
|
+
self.logger.debug(
|
|
627
|
+
f'Stdout of update checking service is {"".join(process.stderr.readlines())}'
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
print("Update complete and device rebooting")
|
|
631
|
+
os.system("reboot")
|
|
632
|
+
|
|
633
|
+
@staticmethod
|
|
634
|
+
def output_put_progress(transferred: int, toBeTransferred: int) -> None:
|
|
635
|
+
"""Used for displaying progress for paramiko ftp.put function"""
|
|
636
|
+
|
|
637
|
+
print(
|
|
638
|
+
f"Transferring progress{int((transferred/toBeTransferred)*100)}%",
|
|
639
|
+
end="\r",
|
|
640
|
+
)
|