Skip to content

Commit ef71226

Browse files
Merge pull request #4 from ColourblindGuy/releases
Releases
2 parents ecead2a + b600685 commit ef71226

6 files changed

Lines changed: 283 additions & 44 deletions

File tree

.github/workflows/deploy-rt.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
python-version: "3.11"
2020

2121
- name: Install dependencies
22-
run: pip install requests
22+
run: pip install requests paramiko
2323

2424
- name: Deploy RTEXE to target
2525
env:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# rt_deploy
2-
POC to automate the deploy of new versions of RTEXEs on RT Targets using Github runners (And source control for regressions)...
2+
POC to automate the deploy of new versions of RTEXEs on RT Targets using Github runners (And source control for regressions).........

releases/bin/startup.aliases

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[My Computer]
2+
My Computer="192.168.56.103"
3+
4+
[RT CompactRIO Target]
5+
RT CompactRIO Target="localhost"

releases/bin/startup.rtexe

5.65 KB
Binary file not shown.

scripts/deploy.py

Lines changed: 276 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,299 @@
1-
import ftplib
21
import os
32
import sys
43
import time
5-
import requests
4+
import paramiko
65
from pathlib import Path
6+
from datetime import datetime
7+
8+
# ---------------------------------------------------------------------------
9+
# Configuration
10+
# ---------------------------------------------------------------------------
711

812
RT_IP = os.environ["RT_TARGET_IP"]
913
RT_USER = os.environ["RT_FTP_USER"]
1014
RT_PASS = os.environ["RT_FTP_PASS"]
1115

12-
RTEXE_LOCAL = Path("releases/MyApp.rtexe") # adjust to your file path
13-
RTEXE_REMOTE = "/ni-rt/startup/MyApp.rtexe" # standard NI RT deploy path
14-
15-
def ftp_upload():
16-
print(f"Connecting to {RT_IP} via FTP...")
17-
with ftplib.FTP(RT_IP, RT_USER, RT_PASS) as ftp:
18-
ftp.set_pasv(True)
19-
with open(RTEXE_LOCAL, "rb") as f:
20-
ftp.storbinary(f"STOR {RTEXE_REMOTE}", f)
21-
print("Upload complete.")
22-
23-
def reboot_target():
24-
# NI Web-based Configuration and Monitoring (WBCM) REST API
25-
url = f"http://{RT_IP}/nisysapi/server"
26-
payload = {"Function": "Restart", "Params": {"objSelfURI": f"nisysapi://{RT_IP}"}}
27-
print("Sending reboot command...")
16+
BIN_LOCAL = Path("releases/bin")
17+
BIN_REMOTE = "/home/lvuser/natinst/bin"
18+
BACKUP_ROOT = "/home/lvuser/deploy_backups"
19+
20+
21+
# ---------------------------------------------------------------------------
22+
# Logging helper
23+
# ---------------------------------------------------------------------------
24+
25+
def log(msg):
26+
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}")
27+
28+
29+
# ---------------------------------------------------------------------------
30+
# SSH helpers
31+
# ---------------------------------------------------------------------------
32+
33+
def open_ssh():
34+
ssh = paramiko.SSHClient()
35+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
36+
ssh.connect(
37+
RT_IP,
38+
username=RT_USER,
39+
password=RT_PASS,
40+
look_for_keys=False,
41+
allow_agent=False,
42+
timeout=5
43+
)
44+
return ssh
45+
46+
47+
# ---------------------------------------------------------------------------
48+
# Remote directory helpers
49+
# ---------------------------------------------------------------------------
50+
51+
def ensure_remote_dir(sftp, path):
52+
"""Create remote directory tree if needed."""
53+
if path == "/" or path == "":
54+
return
55+
56+
parts = path.strip("/").split("/")
57+
current = ""
58+
for part in parts:
59+
current = f"/{part}" if current == "" else f"{current}/{part}"
60+
try:
61+
sftp.stat(current)
62+
except FileNotFoundError:
63+
log(f"Creating remote directory: {current}")
64+
sftp.mkdir(current)
65+
66+
67+
# ---------------------------------------------------------------------------
68+
# Recursive remote copy (remote -> remote)
69+
# ---------------------------------------------------------------------------
70+
71+
def recursive_remote_copy(sftp, src, dst):
72+
"""Copy a remote directory tree to another remote directory."""
73+
ensure_remote_dir(sftp, dst)
74+
75+
for attr in sftp.listdir_attr(src):
76+
src_item = f"{src}/{attr.filename}"
77+
dst_item = f"{dst}/{attr.filename}"
78+
79+
is_dir = bool(attr.st_mode & 0o40000)
80+
81+
if is_dir:
82+
recursive_remote_copy(sftp, src_item, dst_item)
83+
else:
84+
log(f"Backing up file: {src_item} -> {dst_item}")
85+
with sftp.open(src_item, "rb") as fsrc:
86+
data = fsrc.read()
87+
with sftp.open(dst_item, "wb") as fdst:
88+
fdst.write(data)
89+
90+
91+
# ---------------------------------------------------------------------------
92+
# Backup
93+
# ---------------------------------------------------------------------------
94+
95+
def backup_remote_bin():
96+
ssh = open_ssh()
97+
sftp = ssh.open_sftp()
98+
99+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
100+
backup_dir = f"{BACKUP_ROOT}/bin_{timestamp}"
101+
102+
log(f"Ensuring backup root exists: {BACKUP_ROOT}")
103+
ensure_remote_dir(sftp, BACKUP_ROOT)
104+
105+
log(f"Creating backup folder: {backup_dir}")
106+
ensure_remote_dir(sftp, backup_dir)
107+
108+
log("Starting recursive backup...")
109+
recursive_remote_copy(sftp, BIN_REMOTE, backup_dir)
110+
111+
sftp.close()
112+
ssh.close()
113+
114+
log(f"Backup completed: {backup_dir}")
115+
return backup_dir
116+
117+
118+
# ---------------------------------------------------------------------------
119+
# Recursive delete
120+
# ---------------------------------------------------------------------------
121+
122+
def clear_remote_folder(sftp, remote_path):
123+
"""Delete all contents of remote folder."""
124+
for attr in sftp.listdir_attr(remote_path):
125+
path = f"{remote_path}/{attr.filename}"
126+
is_dir = bool(attr.st_mode & 0o40000)
127+
128+
if is_dir:
129+
clear_remote_folder(sftp, path)
130+
log(f"Removing folder: {path}")
131+
sftp.rmdir(path)
132+
else:
133+
log(f"Removing file: {path}")
134+
sftp.remove(path)
135+
136+
137+
# ---------------------------------------------------------------------------
138+
# Upload local bin folder to remote
139+
# ---------------------------------------------------------------------------
140+
141+
def upload_directory_sftp(sftp, local_path, remote_path):
142+
ensure_remote_dir(sftp, remote_path)
143+
144+
for item in local_path.iterdir():
145+
dst = f"{remote_path}/{item.name}"
146+
if item.is_dir():
147+
upload_directory_sftp(sftp, item, dst)
148+
else:
149+
log(f"Uploading file: {item} -> {dst}")
150+
sftp.put(str(item), dst)
151+
152+
153+
# ---------------------------------------------------------------------------
154+
# Deploy bin folder (includes backup and rollback)
155+
# ---------------------------------------------------------------------------
156+
157+
def deploy_bin_folder():
158+
log(f"Deploying bin folder to {RT_IP}")
159+
160+
# 1. Backup
161+
backup_dir = backup_remote_bin()
162+
163+
# 2. Upload new files
164+
ssh = open_ssh()
165+
sftp = ssh.open_sftp()
166+
167+
try:
168+
log("Clearing remote bin folder...")
169+
clear_remote_folder(sftp, BIN_REMOTE)
170+
171+
log("Uploading new bin folder...")
172+
upload_directory_sftp(sftp, BIN_LOCAL, BIN_REMOTE)
173+
174+
sftp.close()
175+
ssh.close()
176+
log("Upload completed successfully.")
177+
178+
return backup_dir
179+
180+
except Exception as e:
181+
log(f"Upload failed: {e}")
182+
log("Starting rollback...")
183+
184+
rollback_from_backup(backup_dir)
185+
sys.exit(1)
186+
187+
188+
# ---------------------------------------------------------------------------
189+
# Rollback
190+
# ---------------------------------------------------------------------------
191+
192+
def recursive_restore_from_backup(sftp, src, dst):
193+
ensure_remote_dir(sftp, dst)
194+
195+
for attr in sftp.listdir_attr(src):
196+
src_item = f"{src}/{attr.filename}"
197+
dst_item = f"{dst}/{attr.filename}"
198+
is_dir = bool(attr.st_mode & 0o40000)
199+
200+
if is_dir:
201+
recursive_restore_from_backup(sftp, src_item, dst_item)
202+
else:
203+
log(f"Restoring file: {src_item} -> {dst_item}")
204+
with sftp.open(src_item, "rb") as fsrc:
205+
data = fsrc.read()
206+
with sftp.open(dst_item, "wb") as fdst:
207+
fdst.write(data)
208+
209+
210+
def rollback_from_backup(backup_dir):
211+
log("Rollback: restoring backup data")
212+
213+
ssh = open_ssh()
214+
sftp = ssh.open_sftp()
215+
216+
clear_remote_folder(sftp, BIN_REMOTE)
217+
recursive_restore_from_backup(sftp, backup_dir, BIN_REMOTE)
218+
219+
sftp.close()
220+
ssh.close()
221+
log("Rollback completed successfully.")
222+
223+
224+
# ---------------------------------------------------------------------------
225+
# Reboot + Wait Logic
226+
# ---------------------------------------------------------------------------
227+
228+
def reboot_target_via_ssh():
229+
log("Sending reboot command via SSH...")
230+
ssh = open_ssh()
28231
try:
29-
requests.post(url, json=payload, timeout=5)
30-
except requests.exceptions.ReadTimeout:
31-
pass # timeout is expected — target is rebooting
232+
ssh.exec_command("/sbin/reboot")
233+
log("Reboot command sent.")
234+
except Exception:
235+
log("Reboot disconnect occurred. This is normal.")
236+
finally:
237+
ssh.close()
238+
239+
240+
def wait_for_shutdown(timeout=30):
241+
log("Waiting for target to shut down...")
242+
deadline = time.time() + timeout
243+
244+
while time.time() < deadline:
245+
try:
246+
ssh = open_ssh()
247+
ssh.close()
248+
time.sleep(2)
249+
except Exception:
250+
log("Target is offline.")
251+
return True
32252

33-
def wait_for_target(timeout=90):
34-
print("Waiting for target to come back online...")
253+
log("Warning: target never appeared to go offline.")
254+
return False
255+
256+
257+
def wait_for_boot(timeout=90):
258+
log("Waiting for target to come online...")
35259
deadline = time.time() + timeout
260+
36261
while time.time() < deadline:
37262
try:
38-
ftplib.FTP(RT_IP, RT_USER, RT_PASS).quit()
39-
print("Target is back online.")
263+
ssh = open_ssh()
264+
ssh.close()
265+
log("Target is online again.")
40266
return True
41267
except Exception:
42268
time.sleep(5)
43-
print("ERROR: Target did not come back within timeout.")
44-
return False
45269

46-
def verify_version():
47-
# Read a version file you write from your RT app, or check a known file timestamp
48-
with ftplib.FTP(RT_IP, RT_USER, RT_PASS) as ftp:
49-
files = ftp.nlst("/ni-rt/startup/")
50-
if "MyApp.rtexe" in [f.split("/")[-1] for f in files]:
51-
print("Verification passed: RTEXE present on target.")
52-
return True
53-
print("Verification FAILED: RTEXE not found on target.")
270+
log("Error: target did not boot in time.")
54271
return False
55272

273+
274+
# ---------------------------------------------------------------------------
275+
# Main
276+
# ---------------------------------------------------------------------------
277+
56278
if __name__ == "__main__":
57-
ftp_upload()
58-
reboot_target()
59-
ok = wait_for_target()
60-
if not ok:
279+
log("=== Starting RT Deployment ===")
280+
281+
try:
282+
backup_dir = deploy_bin_folder()
283+
except Exception as e:
284+
log(f"Deployment failed before reboot: {e}")
61285
sys.exit(1)
62-
ok = verify_version()
63-
if not ok:
286+
287+
reboot_target_via_ssh()
288+
289+
if not wait_for_shutdown():
290+
log("Shutdown not confirmed. Rolling back.")
291+
rollback_from_backup(backup_dir)
64292
sys.exit(1)
65-
print("Deployment successful.")
293+
294+
if not wait_for_boot():
295+
log("Boot failure. Rolling back.")
296+
rollback_from_backup(backup_dir)
297+
sys.exit(1)
298+
299+
log("Deployment completed successfully.")

0 commit comments

Comments
 (0)