1- import ftplib
21import os
32import sys
43import time
5- import requests
4+ import paramiko
65from pathlib import Path
6+ from datetime import datetime
7+
8+ # ---------------------------------------------------------------------------
9+ # Configuration
10+ # ---------------------------------------------------------------------------
711
812RT_IP = os .environ ["RT_TARGET_IP" ]
913RT_USER = os .environ ["RT_FTP_USER" ]
1014RT_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+
56278if __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