siliconcompiler 0.28.9__py3-none-any.whl → 0.29.1__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.
- siliconcompiler/_metadata.py +1 -1
- siliconcompiler/apps/__init__.py +26 -0
- siliconcompiler/apps/sc_remote.py +15 -14
- siliconcompiler/apps/sc_show.py +5 -5
- siliconcompiler/apps/utils/replay.py +194 -0
- siliconcompiler/checklists/__init__.py +12 -0
- siliconcompiler/core.py +89 -22
- siliconcompiler/flows/__init__.py +34 -0
- siliconcompiler/flows/_common.py +11 -13
- siliconcompiler/flows/asicflow.py +83 -42
- siliconcompiler/flows/showflow.py +1 -1
- siliconcompiler/libs/__init__.py +5 -0
- siliconcompiler/optimizer/__init__.py +199 -0
- siliconcompiler/optimizer/vizier.py +259 -0
- siliconcompiler/pdks/__init__.py +5 -0
- siliconcompiler/remote/__init__.py +11 -0
- siliconcompiler/remote/client.py +753 -815
- siliconcompiler/report/report.py +2 -0
- siliconcompiler/report/summary_table.py +1 -1
- siliconcompiler/scheduler/__init__.py +118 -58
- siliconcompiler/scheduler/send_messages.py +1 -1
- siliconcompiler/schema/schema_cfg.py +16 -4
- siliconcompiler/schema/schema_obj.py +29 -10
- siliconcompiler/schema/utils.py +2 -0
- siliconcompiler/sphinx_ext/__init__.py +85 -0
- siliconcompiler/sphinx_ext/dynamicgen.py +19 -34
- siliconcompiler/sphinx_ext/schemagen.py +3 -2
- siliconcompiler/targets/__init__.py +26 -0
- siliconcompiler/targets/gf180_demo.py +3 -3
- siliconcompiler/templates/replay/replay.py.j2 +62 -0
- siliconcompiler/templates/replay/requirements.txt +7 -0
- siliconcompiler/templates/replay/setup.sh +130 -0
- siliconcompiler/tools/__init__.py +60 -0
- siliconcompiler/tools/_common/__init__.py +15 -1
- siliconcompiler/tools/_common/asic.py +17 -9
- siliconcompiler/tools/builtin/concatenate.py +1 -1
- siliconcompiler/tools/ghdl/ghdl.py +1 -2
- siliconcompiler/tools/klayout/convert_drc_db.py +1 -1
- siliconcompiler/tools/klayout/drc.py +1 -1
- siliconcompiler/tools/klayout/export.py +8 -1
- siliconcompiler/tools/klayout/klayout.py +2 -2
- siliconcompiler/tools/klayout/klayout_convert_drc_db.py +2 -2
- siliconcompiler/tools/klayout/klayout_export.py +7 -5
- siliconcompiler/tools/klayout/klayout_operations.py +4 -3
- siliconcompiler/tools/klayout/klayout_show.py +3 -2
- siliconcompiler/tools/klayout/klayout_utils.py +1 -1
- siliconcompiler/tools/klayout/operations.py +8 -0
- siliconcompiler/tools/klayout/screenshot.py +6 -1
- siliconcompiler/tools/klayout/show.py +8 -1
- siliconcompiler/tools/magic/magic.py +1 -1
- siliconcompiler/tools/openroad/__init__.py +103 -0
- siliconcompiler/tools/openroad/{openroad.py → _apr.py} +415 -423
- siliconcompiler/tools/openroad/antenna_repair.py +78 -0
- siliconcompiler/tools/openroad/clock_tree_synthesis.py +64 -0
- siliconcompiler/tools/openroad/detailed_placement.py +59 -0
- siliconcompiler/tools/openroad/detailed_route.py +62 -0
- siliconcompiler/tools/openroad/endcap_tapcell_insertion.py +52 -0
- siliconcompiler/tools/openroad/fillercell_insertion.py +58 -0
- siliconcompiler/tools/openroad/{dfm.py → fillmetal_insertion.py} +35 -19
- siliconcompiler/tools/openroad/global_placement.py +58 -0
- siliconcompiler/tools/openroad/global_route.py +63 -0
- siliconcompiler/tools/openroad/init_floorplan.py +103 -0
- siliconcompiler/tools/openroad/macro_placement.py +65 -0
- siliconcompiler/tools/openroad/metrics.py +23 -8
- siliconcompiler/tools/openroad/pin_placement.py +56 -0
- siliconcompiler/tools/openroad/power_grid.py +65 -0
- siliconcompiler/tools/openroad/rcx_bench.py +7 -4
- siliconcompiler/tools/openroad/rcx_extract.py +2 -1
- siliconcompiler/tools/openroad/rdlroute.py +4 -4
- siliconcompiler/tools/openroad/repair_design.py +59 -0
- siliconcompiler/tools/openroad/repair_timing.py +63 -0
- siliconcompiler/tools/openroad/screenshot.py +9 -20
- siliconcompiler/tools/openroad/scripts/apr/postamble.tcl +44 -0
- siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +95 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_antenna_repair.tcl +51 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_clock_tree_synthesis.tcl +66 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_detailed_placement.tcl +41 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_detailed_route.tcl +71 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_endcap_tapcell_insertion.tcl +55 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_fillercell_insertion.tcl +27 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_fillmetal_insertion.tcl +36 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_global_placement.tcl +26 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_global_route.tcl +61 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +333 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_macro_placement.tcl +123 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_metrics.tcl +22 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_pin_placement.tcl +41 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_power_grid.tcl +60 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_repair_design.tcl +68 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_repair_timing.tcl +83 -0
- siliconcompiler/tools/openroad/scripts/apr/sc_write_data.tcl +125 -0
- siliconcompiler/tools/openroad/scripts/common/debugging.tcl +28 -0
- siliconcompiler/tools/openroad/scripts/common/procs.tcl +727 -0
- siliconcompiler/tools/openroad/scripts/common/read_input_files.tcl +59 -0
- siliconcompiler/tools/openroad/scripts/common/read_liberty.tcl +20 -0
- siliconcompiler/tools/openroad/scripts/common/read_timing_constraints.tcl +16 -0
- siliconcompiler/tools/openroad/scripts/common/reports.tcl +180 -0
- siliconcompiler/tools/openroad/scripts/common/screenshot.tcl +18 -0
- siliconcompiler/tools/openroad/scripts/common/write_images.tcl +395 -0
- siliconcompiler/tools/openroad/scripts/{sc_rcx_bench.tcl → rcx/sc_rcx_bench.tcl} +5 -5
- siliconcompiler/tools/openroad/scripts/{sc_rcx_extract.tcl → rcx/sc_rcx_extract.tcl} +0 -0
- siliconcompiler/tools/openroad/scripts/sc_rcx.tcl +5 -16
- siliconcompiler/tools/openroad/scripts/sc_rdlroute.tcl +51 -51
- siliconcompiler/tools/openroad/scripts/sc_show.tcl +110 -0
- siliconcompiler/tools/openroad/show.py +28 -23
- siliconcompiler/tools/openroad/{export.py → write_data.py} +31 -26
- siliconcompiler/tools/opensta/__init__.py +2 -2
- siliconcompiler/tools/opensta/check_library.py +27 -0
- siliconcompiler/tools/opensta/scripts/sc_check_library.tcl +255 -0
- siliconcompiler/tools/opensta/scripts/sc_timing.tcl +1 -1
- siliconcompiler/tools/sv2v/sv2v.py +1 -2
- siliconcompiler/tools/verilator/verilator.py +6 -7
- siliconcompiler/tools/vivado/vivado.py +1 -1
- siliconcompiler/tools/yosys/__init__.py +149 -0
- siliconcompiler/tools/yosys/lec.py +22 -9
- siliconcompiler/tools/yosys/sc_lec.tcl +94 -49
- siliconcompiler/tools/yosys/sc_syn.tcl +1 -0
- siliconcompiler/tools/yosys/screenshot.py +2 -2
- siliconcompiler/tools/yosys/syn_asic.py +105 -74
- siliconcompiler/tools/yosys/syn_asic.tcl +58 -12
- siliconcompiler/tools/yosys/syn_fpga.py +2 -3
- siliconcompiler/tools/yosys/syn_fpga.tcl +26 -19
- siliconcompiler/toolscripts/_tools.json +5 -5
- siliconcompiler/utils/__init__.py +7 -3
- {siliconcompiler-0.28.9.dist-info → siliconcompiler-0.29.1.dist-info}/METADATA +22 -17
- {siliconcompiler-0.28.9.dist-info → siliconcompiler-0.29.1.dist-info}/RECORD +131 -114
- {siliconcompiler-0.28.9.dist-info → siliconcompiler-0.29.1.dist-info}/WHEEL +1 -1
- {siliconcompiler-0.28.9.dist-info → siliconcompiler-0.29.1.dist-info}/entry_points.txt +13 -0
- siliconcompiler/libs/asap7sc7p5t.py +0 -8
- siliconcompiler/libs/gf180mcu.py +0 -8
- siliconcompiler/libs/interposer.py +0 -8
- siliconcompiler/libs/nangate45.py +0 -8
- siliconcompiler/libs/sg13g2_stdcell.py +0 -8
- siliconcompiler/libs/sky130hd.py +0 -8
- siliconcompiler/libs/sky130io.py +0 -8
- siliconcompiler/pdks/asap7.py +0 -8
- siliconcompiler/pdks/freepdk45.py +0 -8
- siliconcompiler/pdks/gf180.py +0 -8
- siliconcompiler/pdks/ihp130.py +0 -8
- siliconcompiler/pdks/interposer.py +0 -8
- siliconcompiler/pdks/skywater130.py +0 -8
- siliconcompiler/tools/openroad/cts.py +0 -45
- siliconcompiler/tools/openroad/floorplan.py +0 -75
- siliconcompiler/tools/openroad/physyn.py +0 -27
- siliconcompiler/tools/openroad/place.py +0 -41
- siliconcompiler/tools/openroad/route.py +0 -45
- siliconcompiler/tools/openroad/scripts/__init__.py +0 -0
- siliconcompiler/tools/openroad/scripts/sc_apr.tcl +0 -514
- siliconcompiler/tools/openroad/scripts/sc_cts.tcl +0 -68
- siliconcompiler/tools/openroad/scripts/sc_dfm.tcl +0 -22
- siliconcompiler/tools/openroad/scripts/sc_export.tcl +0 -100
- siliconcompiler/tools/openroad/scripts/sc_floorplan.tcl +0 -456
- siliconcompiler/tools/openroad/scripts/sc_metrics.tcl +0 -1
- siliconcompiler/tools/openroad/scripts/sc_physyn.tcl +0 -6
- siliconcompiler/tools/openroad/scripts/sc_place.tcl +0 -84
- siliconcompiler/tools/openroad/scripts/sc_procs.tcl +0 -494
- siliconcompiler/tools/openroad/scripts/sc_report.tcl +0 -189
- siliconcompiler/tools/openroad/scripts/sc_route.tcl +0 -143
- siliconcompiler/tools/openroad/scripts/sc_screenshot.tcl +0 -18
- siliconcompiler/tools/openroad/scripts/sc_write_images.tcl +0 -393
- siliconcompiler/tools/yosys/yosys.py +0 -148
- /siliconcompiler/tools/openroad/scripts/{sc_write.tcl → common/write_data.tcl} +0 -0
- {siliconcompiler-0.28.9.dist-info → siliconcompiler-0.29.1.dist-info}/LICENSE +0 -0
- {siliconcompiler-0.28.9.dist-info → siliconcompiler-0.29.1.dist-info}/top_level.txt +0 -0
siliconcompiler/remote/client.py
CHANGED
|
@@ -19,16 +19,27 @@ from siliconcompiler.remote import JobStatus
|
|
|
19
19
|
# Step name to use while logging
|
|
20
20
|
remote_step_name = 'remote'
|
|
21
21
|
|
|
22
|
-
# Client / server timeout
|
|
23
|
-
__timeout = 10
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
class Client():
|
|
24
|
+
# Step name to use while logging
|
|
25
|
+
STEP_NAME = "remote"
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
def __init__(self, chip, default_server=default_server):
|
|
28
|
+
self.__chip = chip
|
|
29
|
+
self.__logger = self.__chip.logger.getChild('remote-client')
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
self.__default_server = default_server
|
|
32
|
+
|
|
33
|
+
# Used when reporting node information during run
|
|
34
|
+
self.__maxlinelength = 70
|
|
35
|
+
|
|
36
|
+
self.__init_config()
|
|
37
|
+
self.__init_baseurl()
|
|
38
|
+
|
|
39
|
+
# Client / server timeout
|
|
40
|
+
self.__timeout = 10
|
|
41
|
+
self.__max_timeouts = 10
|
|
42
|
+
self.__tos_str = '''Please review the SiliconCompiler cloud's terms of service:
|
|
32
43
|
|
|
33
44
|
https://www.siliconcompiler.com/terms
|
|
34
45
|
|
|
@@ -36,161 +47,269 @@ In particular, please ensure that you have the right to distribute any IP
|
|
|
36
47
|
which is contained in designs that you upload to the service. This public
|
|
37
48
|
service, provided by SiliconCompiler, is not intended to process proprietary IP.
|
|
38
49
|
'''
|
|
50
|
+
# Runtime
|
|
51
|
+
self.__download_pool = None
|
|
52
|
+
self.__check_interval = None
|
|
53
|
+
self.__node_information = None
|
|
54
|
+
|
|
55
|
+
def __get_remote_config_file(self, fail=True):
|
|
56
|
+
if self.__chip.get('option', 'credentials'):
|
|
57
|
+
# Use the provided remote credentials file.
|
|
58
|
+
cfg_file = os.path.abspath(self.__chip.get('option', 'credentials'))
|
|
59
|
+
|
|
60
|
+
if fail and not os.path.isfile(cfg_file) and \
|
|
61
|
+
getattr(self, '_error_on_missing_file', True):
|
|
62
|
+
# Check if it's a file since its been requested by the user
|
|
63
|
+
raise SiliconCompilerError(
|
|
64
|
+
f'Unable to find the credentials file: {cfg_file}',
|
|
65
|
+
chip=self.__chip)
|
|
66
|
+
else:
|
|
67
|
+
# Use the default config file path.
|
|
68
|
+
cfg_file = utils.default_credentials_file()
|
|
39
69
|
|
|
70
|
+
return cfg_file
|
|
40
71
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'''Helper method to get the root URL for API calls, given a Chip object.
|
|
44
|
-
'''
|
|
72
|
+
def __init_config(self):
|
|
73
|
+
cfg_file = self.__get_remote_config_file()
|
|
45
74
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
remote_cfg = {}
|
|
76
|
+
cfg_dir = os.path.dirname(cfg_file)
|
|
77
|
+
if os.path.isdir(cfg_dir) and os.path.isfile(cfg_file):
|
|
78
|
+
self.__logger.info(f'Using credentials: {cfg_file}')
|
|
79
|
+
with open(cfg_file, 'r') as cfgf:
|
|
80
|
+
remote_cfg = json.loads(cfgf.read())
|
|
81
|
+
else:
|
|
82
|
+
if getattr(self, '_print_server_warning', True):
|
|
83
|
+
self.__logger.warning('Could not find remote server configuration: '
|
|
84
|
+
f'defaulting to {self.__default_server}')
|
|
85
|
+
remote_cfg = {
|
|
86
|
+
"address": self.__default_server,
|
|
87
|
+
"directory_whitelist": []
|
|
88
|
+
}
|
|
89
|
+
if 'address' not in remote_cfg:
|
|
90
|
+
raise SiliconCompilerError(
|
|
91
|
+
'Improperly formatted remote server configuration - '
|
|
92
|
+
'please run "sc-remote -configure" and enter your server address and '
|
|
93
|
+
'credentials.', chip=self.__chip)
|
|
58
94
|
|
|
95
|
+
self.__config = remote_cfg
|
|
59
96
|
|
|
60
|
-
def
|
|
61
|
-
|
|
97
|
+
def __init_baseurl(self):
|
|
98
|
+
remote_host = self.__config['address']
|
|
99
|
+
if 'port' in self.__config:
|
|
100
|
+
remote_port = self.__config['port']
|
|
101
|
+
else:
|
|
102
|
+
remote_port = 443
|
|
103
|
+
remote_host += f':{remote_port}'
|
|
104
|
+
if remote_host.startswith('http'):
|
|
105
|
+
remote_protocol = ''
|
|
106
|
+
else:
|
|
107
|
+
remote_protocol = 'https://' if remote_port == 443 else 'http://'
|
|
108
|
+
self.__url = remote_protocol + remote_host
|
|
109
|
+
|
|
110
|
+
def __get_url(self, action):
|
|
111
|
+
return urllib.parse.urljoin(self.__url, action)
|
|
112
|
+
|
|
113
|
+
def remote_manifest(self):
|
|
114
|
+
return f'{self.__chip.getworkdir()}/sc_remote.pkg.json'
|
|
115
|
+
|
|
116
|
+
def print_configuration(self):
|
|
117
|
+
self.__logger.info(f'Server: {self.__url}')
|
|
118
|
+
if 'username' in self.__config:
|
|
119
|
+
self.__logger.info(f'Username: {self.__config["username"]}')
|
|
120
|
+
if 'directory_whitelist' in self.__config and self.__config['directory_whitelist']:
|
|
121
|
+
self.__logger.info('Directory whitelist:')
|
|
122
|
+
for path in sorted(self.__config['directory_whitelist']):
|
|
123
|
+
self.__logger.info(f' {path}')
|
|
124
|
+
|
|
125
|
+
def __get_post_params(self, include_job_name=False, include_job_id=False):
|
|
126
|
+
'''
|
|
127
|
+
Helper function to build the params for the post request
|
|
128
|
+
'''
|
|
129
|
+
# Use authentication if necessary.
|
|
130
|
+
post_params = {}
|
|
131
|
+
|
|
132
|
+
if include_job_id and self.__chip.get('record', 'remoteid'):
|
|
133
|
+
post_params['job_hash'] = self.__chip.get('record', 'remoteid')
|
|
134
|
+
|
|
135
|
+
if include_job_name and self.__chip.get('option', 'jobname'):
|
|
136
|
+
post_params['job_id'] = self.__chip.get('option', 'jobname')
|
|
137
|
+
|
|
138
|
+
# Forward authentication information
|
|
139
|
+
if ('username' in self.__config) and ('password' in self.__config) and \
|
|
140
|
+
(self.__config['username']) and (self.__config['password']):
|
|
141
|
+
post_params['username'] = self.__config['username']
|
|
142
|
+
post_params['key'] = self.__config['password']
|
|
143
|
+
|
|
144
|
+
return post_params
|
|
145
|
+
|
|
146
|
+
###################################
|
|
147
|
+
def __post(self, action, post_action, success_action, error_action=None):
|
|
148
|
+
'''
|
|
149
|
+
Helper function to handle the post request
|
|
150
|
+
'''
|
|
151
|
+
redirect_url = self.__get_url(action)
|
|
152
|
+
|
|
153
|
+
timeouts = 0
|
|
154
|
+
while redirect_url:
|
|
155
|
+
try:
|
|
156
|
+
resp = post_action(redirect_url)
|
|
157
|
+
except requests.Timeout:
|
|
158
|
+
timeouts += 1
|
|
159
|
+
if timeouts > self.__max_timeouts:
|
|
160
|
+
raise SiliconCompilerError('Server communications timed out', chip=self.__chip)
|
|
161
|
+
time.sleep(self.__timeout)
|
|
162
|
+
continue
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise SiliconCompilerError(f'Server communications error: {e}', chip=self.__chip)
|
|
165
|
+
|
|
166
|
+
code = resp.status_code
|
|
167
|
+
if 200 <= code and code < 300:
|
|
168
|
+
return success_action(resp)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
msg_json = resp.json()
|
|
172
|
+
if 'message' in msg_json:
|
|
173
|
+
msg = msg_json['message']
|
|
174
|
+
else:
|
|
175
|
+
msg = resp.text
|
|
176
|
+
except requests.JSONDecodeError:
|
|
177
|
+
msg = resp.text
|
|
62
178
|
|
|
179
|
+
if 300 <= code and code < 400:
|
|
180
|
+
if 'Location' in resp.headers:
|
|
181
|
+
redirect_url = resp.headers['Location']
|
|
182
|
+
continue
|
|
63
183
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
184
|
+
if error_action:
|
|
185
|
+
return error_action(code, msg)
|
|
186
|
+
else:
|
|
187
|
+
raise SiliconCompilerError(f'Server responded with {code}: {msg}', chip=self.__chip)
|
|
188
|
+
|
|
189
|
+
def cancel_job(self):
|
|
190
|
+
'''
|
|
191
|
+
Helper method to request that the server cancel an ongoing job.
|
|
192
|
+
'''
|
|
193
|
+
|
|
194
|
+
def post_action(url):
|
|
195
|
+
return requests.post(
|
|
196
|
+
url,
|
|
197
|
+
data=json.dumps(self.__get_post_params(
|
|
198
|
+
include_job_id=True
|
|
199
|
+
)),
|
|
200
|
+
timeout=self.__timeout)
|
|
201
|
+
|
|
202
|
+
def success_action(resp):
|
|
203
|
+
return json.loads(resp.text)
|
|
204
|
+
|
|
205
|
+
return self.__post('/cancel_job/', post_action, success_action)
|
|
206
|
+
|
|
207
|
+
###################################
|
|
208
|
+
def delete_job(self):
|
|
209
|
+
'''
|
|
210
|
+
Helper method to delete a job from shared remote storage.
|
|
211
|
+
'''
|
|
212
|
+
|
|
213
|
+
def post_action(url):
|
|
214
|
+
return requests.post(
|
|
215
|
+
url,
|
|
216
|
+
data=json.dumps(self.__get_post_params(
|
|
217
|
+
include_job_id=True
|
|
218
|
+
)),
|
|
219
|
+
timeout=self.__timeout)
|
|
220
|
+
|
|
221
|
+
def success_action(resp):
|
|
222
|
+
return resp.text
|
|
223
|
+
|
|
224
|
+
return self.__post('/delete_job/', post_action, success_action)
|
|
225
|
+
|
|
226
|
+
def check_job_status(self):
|
|
227
|
+
# Make the request and print its response.
|
|
228
|
+
def post_action(url):
|
|
229
|
+
return requests.post(
|
|
230
|
+
url,
|
|
231
|
+
data=json.dumps(self.__get_post_params(include_job_id=True, include_job_name=True)),
|
|
232
|
+
timeout=self.__timeout)
|
|
233
|
+
|
|
234
|
+
def error_action(code, msg):
|
|
235
|
+
return {
|
|
236
|
+
'busy': True,
|
|
237
|
+
'message': ''
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
def success_action(resp):
|
|
241
|
+
json_response = json.loads(resp.text)
|
|
242
|
+
|
|
243
|
+
if json_response['status'] != JobStatus.RUNNING:
|
|
244
|
+
if json_response['status'] == JobStatus.REJECTED:
|
|
245
|
+
self.__logger.error(f'Job was rejected: {json_response["message"]}')
|
|
246
|
+
elif json_response['status'] != JobStatus.UNKNOWN:
|
|
247
|
+
self.__logger.info(f'Job status: {json_response["status"]}')
|
|
248
|
+
info = {
|
|
249
|
+
'busy': json_response['status'] == JobStatus.RUNNING,
|
|
250
|
+
'message': None
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if isinstance(json_response['message'], str):
|
|
254
|
+
info['message'] = json_response['message']
|
|
255
|
+
else:
|
|
256
|
+
info['message'] = json.dumps(json_response['message'])
|
|
257
|
+
return info
|
|
258
|
+
|
|
259
|
+
info = self.__post(
|
|
260
|
+
'/check_progress/',
|
|
261
|
+
post_action,
|
|
262
|
+
success_action,
|
|
263
|
+
error_action=error_action)
|
|
264
|
+
|
|
265
|
+
if not info:
|
|
266
|
+
info = {
|
|
267
|
+
'busy': True,
|
|
268
|
+
'message': ''
|
|
269
|
+
}
|
|
270
|
+
return info
|
|
70
271
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
272
|
+
def __log_node_status(self, status, nodes):
|
|
273
|
+
'''
|
|
274
|
+
Helper method to log truncated information about flowgraph nodes
|
|
275
|
+
with a given status, on a single line.
|
|
276
|
+
Used to print info about all statuses besides NodeStatus.RUNNING.
|
|
277
|
+
'''
|
|
278
|
+
|
|
279
|
+
num_nodes = len(nodes)
|
|
280
|
+
if num_nodes > 0:
|
|
281
|
+
line_len = 0
|
|
282
|
+
nodes_log = f' {status.title()} ({num_nodes}): '
|
|
283
|
+
log_nodes = []
|
|
284
|
+
for node, _ in nodes:
|
|
285
|
+
node_len = len(node)
|
|
286
|
+
|
|
287
|
+
if node_len + line_len + 2 < self.__maxlinelength:
|
|
288
|
+
log_nodes.append(node)
|
|
289
|
+
line_len += node_len + 2
|
|
290
|
+
else:
|
|
291
|
+
if len(log_nodes) == num_nodes - 1:
|
|
292
|
+
log_nodes.append(node)
|
|
293
|
+
else:
|
|
294
|
+
log_nodes.append('...')
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
nodes_log += ', '.join(log_nodes)
|
|
298
|
+
self.__logger.info(nodes_log)
|
|
299
|
+
|
|
300
|
+
def _report_job_status(self, info):
|
|
301
|
+
if not info['busy']:
|
|
302
|
+
# Job is not running
|
|
303
|
+
return [], False
|
|
87
304
|
|
|
88
305
|
try:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
except requests.JSONDecodeError:
|
|
95
|
-
msg = resp.text
|
|
306
|
+
# Decode response JSON, if possible.
|
|
307
|
+
job_info = json.loads(info['message'])
|
|
308
|
+
except json.JSONDecodeError as e:
|
|
309
|
+
self.__logger.warning(f"Job is still running: {e}")
|
|
310
|
+
return [], True
|
|
96
311
|
|
|
97
|
-
|
|
98
|
-
if 'Location' in resp.headers:
|
|
99
|
-
redirect_url = resp.headers['Location']
|
|
100
|
-
continue
|
|
101
|
-
|
|
102
|
-
if error_action:
|
|
103
|
-
return error_action(code, msg)
|
|
104
|
-
else:
|
|
105
|
-
raise SiliconCompilerError(f'Server responded with {code}: {msg}', chip=chip)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
###################################
|
|
109
|
-
def __build_post_params(chip, verbose, job_name=None, job_hash=None):
|
|
110
|
-
'''
|
|
111
|
-
Helper function to build the params for the post request
|
|
112
|
-
'''
|
|
113
|
-
# Use authentication if necessary.
|
|
114
|
-
post_params = {}
|
|
115
|
-
|
|
116
|
-
if job_hash:
|
|
117
|
-
post_params['job_hash'] = job_hash
|
|
118
|
-
|
|
119
|
-
if job_name:
|
|
120
|
-
post_params['job_id'] = job_name
|
|
121
|
-
|
|
122
|
-
rcfg = get_remote_config(chip, verbose)
|
|
123
|
-
|
|
124
|
-
if ('username' in rcfg) and ('password' in rcfg) and \
|
|
125
|
-
(rcfg['username']) and (rcfg['password']):
|
|
126
|
-
post_params['username'] = rcfg['username']
|
|
127
|
-
post_params['key'] = rcfg['password']
|
|
128
|
-
|
|
129
|
-
return post_params
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
###################################
|
|
133
|
-
def _remote_preprocess(chip):
|
|
134
|
-
'''
|
|
135
|
-
Helper method to run a local import stage for remote jobs.
|
|
136
|
-
'''
|
|
137
|
-
|
|
138
|
-
# Ensure packages with python sources are copied
|
|
139
|
-
for key in chip.allkeys():
|
|
140
|
-
key_type = chip.get(*key, field='type')
|
|
141
|
-
|
|
142
|
-
if 'dir' in key_type or 'file' in key_type:
|
|
143
|
-
for _, step, index in chip.schema._getvals(*key, return_defvalue=False):
|
|
144
|
-
packages = chip.get(*key, field='package', step=step, index=index)
|
|
145
|
-
force_copy = False
|
|
146
|
-
for package in packages:
|
|
147
|
-
if not package:
|
|
148
|
-
continue
|
|
149
|
-
if package.startswith('python://'):
|
|
150
|
-
force_copy = True
|
|
151
|
-
if force_copy:
|
|
152
|
-
chip.set(*key, True, field='copy', step=step, index=index)
|
|
153
|
-
|
|
154
|
-
# Collect inputs into a collection directory only for remote runs, since
|
|
155
|
-
# we need to send inputs up to the server.
|
|
156
|
-
cfg = get_remote_config(chip, False)
|
|
157
|
-
chip.collect(whitelist=cfg.setdefault('directory_whitelist', []))
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
###################################
|
|
161
|
-
def _log_truncated_stats(chip, status, nodes_with_status, nodes_to_print):
|
|
162
|
-
'''
|
|
163
|
-
Helper method to log truncated information about flowgraph nodes
|
|
164
|
-
with a given status, on a single line.
|
|
165
|
-
Used to print info about all statuses besides NodeStatus.RUNNING.
|
|
166
|
-
'''
|
|
167
|
-
|
|
168
|
-
num_nodes = len(nodes_with_status)
|
|
169
|
-
if num_nodes > 0:
|
|
170
|
-
nodes_log = f' {status.title()} ({num_nodes}): '
|
|
171
|
-
log_nodes = []
|
|
172
|
-
for i in range(min(nodes_to_print, num_nodes)):
|
|
173
|
-
log_nodes.append(nodes_with_status[i][0])
|
|
174
|
-
if num_nodes > nodes_to_print:
|
|
175
|
-
log_nodes.append('...')
|
|
176
|
-
nodes_log += ', '.join(log_nodes)
|
|
177
|
-
chip.logger.info(nodes_log)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
###################################
|
|
181
|
-
def _process_progress_info(chip, progress_info, recorded_nodes, all_nodes, nodes_to_print=3):
|
|
182
|
-
'''
|
|
183
|
-
Helper method to log information about a remote run's progress,
|
|
184
|
-
based on information returned from a 'check_progress/' call.
|
|
185
|
-
'''
|
|
186
|
-
|
|
187
|
-
completed = []
|
|
188
|
-
try:
|
|
189
|
-
# Decode response JSON, if possible.
|
|
190
|
-
job_info = json.loads(progress_info['message'])
|
|
191
|
-
|
|
192
|
-
# Sort and store info about the job's progress.
|
|
193
|
-
chip.logger.info("Job is still running. Status:")
|
|
312
|
+
completed = []
|
|
194
313
|
|
|
195
314
|
nodes_to_log = {}
|
|
196
315
|
for node, node_info in job_info.items():
|
|
@@ -201,722 +320,541 @@ def _process_progress_info(chip, progress_info, recorded_nodes, all_nodes, nodes
|
|
|
201
320
|
# collect completed
|
|
202
321
|
completed.append(node)
|
|
203
322
|
|
|
204
|
-
if node in
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
323
|
+
if self.__node_information and node in self.__node_information:
|
|
324
|
+
self.__chip.set('record', 'status', status,
|
|
325
|
+
step=self.__node_information[node]["step"],
|
|
326
|
+
index=self.__node_information[node]["index"])
|
|
208
327
|
|
|
209
328
|
nodes_to_log = {key: nodes_to_log[key] for key in sorted(nodes_to_log.keys())}
|
|
210
329
|
|
|
211
330
|
# Log information about the job's progress.
|
|
212
|
-
|
|
213
|
-
|
|
331
|
+
self.__logger.info("Job is still running. Status:")
|
|
332
|
+
|
|
214
333
|
for stat, nodes in nodes_to_log.items():
|
|
215
334
|
if SCNodeStatus.is_done(stat):
|
|
216
|
-
|
|
335
|
+
self.__log_node_status(stat, nodes)
|
|
217
336
|
|
|
218
337
|
# Running / in-progress flowgraph nodes should all be printed:
|
|
219
338
|
for stat, nodes in nodes_to_log.items():
|
|
220
339
|
if SCNodeStatus.is_running(stat):
|
|
221
|
-
|
|
340
|
+
self.__logger.info(f' {stat.title()} ({len(nodes)}):')
|
|
222
341
|
for node, node_info in nodes:
|
|
223
342
|
running_log = f" {node}"
|
|
224
343
|
if 'elapsed_time' in node_info:
|
|
225
344
|
running_log += f" ({node_info['elapsed_time']})"
|
|
226
|
-
|
|
345
|
+
self.__logger.info(running_log)
|
|
227
346
|
|
|
228
347
|
# Queued and pending flowgraph nodes:
|
|
229
348
|
for stat, nodes in nodes_to_log.items():
|
|
230
349
|
if SCNodeStatus.is_waiting(stat):
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
raise
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
350
|
+
self.__log_node_status(stat, nodes)
|
|
351
|
+
|
|
352
|
+
return completed, True
|
|
353
|
+
|
|
354
|
+
def __check(self):
|
|
355
|
+
def post_action(url):
|
|
356
|
+
return requests.post(
|
|
357
|
+
url,
|
|
358
|
+
data=json.dumps(self.__get_post_params()),
|
|
359
|
+
timeout=self.__timeout)
|
|
360
|
+
|
|
361
|
+
def success_action(resp):
|
|
362
|
+
return resp.json()
|
|
363
|
+
|
|
364
|
+
response_info = self.__post('/check_server/', post_action, success_action)
|
|
365
|
+
if not response_info:
|
|
366
|
+
raise ValueError('Server response is not valid.')
|
|
367
|
+
|
|
368
|
+
return response_info
|
|
369
|
+
|
|
370
|
+
def check(self):
|
|
371
|
+
'''
|
|
372
|
+
Helper method to call check_server on server
|
|
373
|
+
'''
|
|
374
|
+
|
|
375
|
+
# Make the request and print its response.
|
|
376
|
+
response_info = self.__check()
|
|
377
|
+
|
|
378
|
+
# Print status value.
|
|
379
|
+
server_status = response_info['status']
|
|
380
|
+
self.__logger.info(f'Server status: {server_status}')
|
|
381
|
+
if server_status != 'ready':
|
|
382
|
+
self.__logger.warning(' Status is not "ready", server cannot accept new jobs.')
|
|
383
|
+
|
|
384
|
+
# Print server-side version info.
|
|
385
|
+
version_info = response_info['versions']
|
|
386
|
+
version_suffix = ' version'
|
|
387
|
+
max_name_string_len = max([len(s) for s in version_info.keys()]) + len(version_suffix)
|
|
388
|
+
self.__logger.info('Server software versions:')
|
|
389
|
+
for name, version in version_info.items():
|
|
390
|
+
print_name = f'{name}{version_suffix}'
|
|
391
|
+
self.__logger.info(f' {print_name: <{max_name_string_len}}: {version}')
|
|
392
|
+
|
|
393
|
+
# Print user info if applicable.
|
|
394
|
+
if 'user_info' in response_info:
|
|
395
|
+
user_info = response_info['user_info']
|
|
396
|
+
if ('compute_time' not in user_info) or \
|
|
397
|
+
('bandwidth_kb' not in user_info):
|
|
398
|
+
self.__logger.info('Error fetching user information from the remote server.')
|
|
399
|
+
raise ValueError(f'Server response is not valid or missing fields: {user_info}')
|
|
400
|
+
|
|
401
|
+
if 'username' in self.__config:
|
|
402
|
+
# Print the user's account info, and return.
|
|
403
|
+
self.__logger.info(f'User {self.__config["username"]}:')
|
|
404
|
+
|
|
405
|
+
time_remaining = user_info["compute_time"] / 60.0
|
|
406
|
+
bandwidth_remaining = user_info["bandwidth_kb"]
|
|
407
|
+
self.__logger.info(f' Remaining compute time: {(time_remaining):.2f} minutes')
|
|
408
|
+
self.__logger.info(f' Remaining results bandwidth: {bandwidth_remaining} KiB')
|
|
409
|
+
|
|
410
|
+
self.__print_tos(response_info)
|
|
411
|
+
|
|
412
|
+
# Return the response info in case the caller wants to inspect it.
|
|
413
|
+
return response_info
|
|
414
|
+
|
|
415
|
+
def __print_tos(self, response_info):
|
|
416
|
+
# Print terms-of-service message, if the server provides one.
|
|
417
|
+
if 'terms' in response_info and response_info['terms']:
|
|
418
|
+
self.__logger.info('Terms of Service info for this server:')
|
|
419
|
+
for line in response_info['terms'].splitlines():
|
|
420
|
+
if line:
|
|
421
|
+
self.__logger.info(line)
|
|
422
|
+
|
|
423
|
+
def run(self):
|
|
424
|
+
'''
|
|
425
|
+
Dispatch the Chip to a remote server for processing.
|
|
426
|
+
'''
|
|
427
|
+
should_resume = not self.__chip.get('option', 'clean')
|
|
428
|
+
remote_resume = should_resume and self.__chip.get('record', 'remoteid')
|
|
429
|
+
|
|
430
|
+
# Pre-process: Run an starting nodes locally, and upload the
|
|
431
|
+
# in-progress build directory to the remote server.
|
|
432
|
+
# Data is encrypted if user / key were specified.
|
|
433
|
+
# run remote process
|
|
434
|
+
if self.__chip.get('arg', 'step'):
|
|
435
|
+
raise SiliconCompilerError('Cannot pass [arg,step] parameter into remote flow.',
|
|
436
|
+
chip=self.__chip)
|
|
437
|
+
if self.__chip.get('arg', 'index'):
|
|
438
|
+
raise SiliconCompilerError('Cannot pass [arg,index] parameter into remote flow.',
|
|
439
|
+
chip=self.__chip)
|
|
440
|
+
|
|
441
|
+
# Only run the pre-process step if the job doesn't already have a remote ID.
|
|
442
|
+
if not remote_resume:
|
|
443
|
+
self.__run_preprocess()
|
|
444
|
+
|
|
445
|
+
# Run the job on the remote server, and wait for it to finish.
|
|
446
|
+
# Set logger to indicate remote run
|
|
447
|
+
self.__chip._init_logger(step=self.STEP_NAME, index=None, in_run=True)
|
|
448
|
+
|
|
449
|
+
# Ask the remote server to start processing the requested step.
|
|
450
|
+
self.__request_run()
|
|
451
|
+
|
|
452
|
+
# Run the main 'check_progress' loop to monitor job status until it finishes.
|
|
453
|
+
self._run_loop()
|
|
454
|
+
|
|
455
|
+
# Restore logger
|
|
456
|
+
self.__chip._init_logger(in_run=True)
|
|
457
|
+
|
|
458
|
+
def __request_run(self):
|
|
459
|
+
'''
|
|
460
|
+
Helper method to make a web request to start a job stage.
|
|
461
|
+
'''
|
|
462
|
+
|
|
463
|
+
remote_status = self.__check()
|
|
464
|
+
|
|
465
|
+
if remote_status['status'] != 'ready':
|
|
466
|
+
raise SiliconCompilerError('Remote server is not available.', chip=self.__chip)
|
|
467
|
+
|
|
468
|
+
self.__print_tos(remote_status)
|
|
469
|
+
|
|
470
|
+
remote_resume = not self.__chip.get('option', 'clean') and \
|
|
471
|
+
self.__chip.get('record', 'remoteid')
|
|
472
|
+
# Only package and upload the entry steps if starting a new job.
|
|
473
|
+
if not remote_resume:
|
|
474
|
+
upload_file = tempfile.TemporaryFile(prefix='sc', suffix='remote.tar.gz')
|
|
475
|
+
with tarfile.open(fileobj=upload_file, mode='w:gz') as tar:
|
|
476
|
+
tar.add(self.__chip.getworkdir(), arcname='')
|
|
477
|
+
# Flush file to ensure everything is written
|
|
478
|
+
upload_file.flush()
|
|
479
|
+
|
|
480
|
+
if 'pre_upload' in remote_status:
|
|
481
|
+
self.__logger.info(remote_status['pre_upload']['message'])
|
|
482
|
+
time.sleep(remote_status['pre_upload']['delay'])
|
|
483
|
+
|
|
484
|
+
# Make the actual request, streaming the bulk data as a multipart file.
|
|
485
|
+
# Redirected POST requests are translated to GETs. This is actually
|
|
486
|
+
# part of the HTTP spec, so we need to manually follow the trail.
|
|
487
|
+
post_params = {
|
|
488
|
+
'chip_cfg': self.__chip.schema.cfg,
|
|
489
|
+
'params': self.__get_post_params(include_job_id=True)
|
|
280
490
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
'
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
except KeyboardInterrupt:
|
|
351
|
-
manifest_path = get_remote_manifest(chip)
|
|
352
|
-
reconnect_cmd = f'sc-remote -cfg {manifest_path} -reconnect'
|
|
353
|
-
cancel_cmd = f'sc-remote -cfg {manifest_path} -cancel'
|
|
354
|
-
chip.logger.info('Disconnecting from remote job')
|
|
355
|
-
chip.logger.info(f'To reconnect to this job use: {reconnect_cmd}')
|
|
356
|
-
chip.logger.info(f'To cancel this job use: {cancel_cmd}')
|
|
357
|
-
raise SiliconCompilerError('Job canceled by user keyboard interrupt')
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
###################################
|
|
361
|
-
def __remote_run_loop(chip, check_interval):
|
|
362
|
-
# Check the job's progress periodically until it finishes.
|
|
363
|
-
is_busy = True
|
|
364
|
-
all_nodes = {}
|
|
365
|
-
completed = []
|
|
366
|
-
result_procs = []
|
|
367
|
-
|
|
368
|
-
recorded = []
|
|
369
|
-
|
|
370
|
-
for step, index in nodes_to_execute(chip):
|
|
371
|
-
if SCNodeStatus.is_done(chip.get('record', 'status', step=step, index=index)):
|
|
372
|
-
continue
|
|
373
|
-
all_nodes[f'{step}{index}'] = (step, index)
|
|
374
|
-
|
|
375
|
-
def import_manifests():
|
|
491
|
+
|
|
492
|
+
post_files = {'params': json.dumps(post_params)}
|
|
493
|
+
if not remote_resume:
|
|
494
|
+
post_files['import'] = upload_file
|
|
495
|
+
upload_file.seek(0)
|
|
496
|
+
|
|
497
|
+
def post_action(url):
|
|
498
|
+
return requests.post(
|
|
499
|
+
url,
|
|
500
|
+
files=post_files,
|
|
501
|
+
timeout=self.__timeout)
|
|
502
|
+
|
|
503
|
+
def success_action(resp):
|
|
504
|
+
return resp.json()
|
|
505
|
+
|
|
506
|
+
resp = self.__post('/remote_run/', post_action, success_action)
|
|
507
|
+
if not remote_resume:
|
|
508
|
+
upload_file.close()
|
|
509
|
+
|
|
510
|
+
if 'message' in resp and resp['message']:
|
|
511
|
+
self.__logger.info(resp['message'])
|
|
512
|
+
self.__chip.set('record', 'remoteid', resp['job_hash'])
|
|
513
|
+
|
|
514
|
+
self.__chip.write_manifest(self.remote_manifest())
|
|
515
|
+
|
|
516
|
+
self.__logger.info(f"Your job's reference ID is: {resp['job_hash']}")
|
|
517
|
+
|
|
518
|
+
self.__check_interval = remote_status['progress_interval']
|
|
519
|
+
|
|
520
|
+
def __run_preprocess(self):
|
|
521
|
+
'''
|
|
522
|
+
Helper method to run a local import stage for remote jobs.
|
|
523
|
+
'''
|
|
524
|
+
|
|
525
|
+
# Ensure packages with python sources are copied
|
|
526
|
+
for key in self.__chip.allkeys():
|
|
527
|
+
key_type = self.__chip.get(*key, field='type')
|
|
528
|
+
|
|
529
|
+
if 'dir' in key_type or 'file' in key_type:
|
|
530
|
+
for _, step, index in self.__chip.schema._getvals(*key, return_defvalue=False):
|
|
531
|
+
packages = self.__chip.get(*key, field='package', step=step, index=index)
|
|
532
|
+
force_copy = False
|
|
533
|
+
for package in packages:
|
|
534
|
+
if not package:
|
|
535
|
+
continue
|
|
536
|
+
if package.startswith('python://'):
|
|
537
|
+
force_copy = True
|
|
538
|
+
if force_copy:
|
|
539
|
+
self.__chip.set(*key, True, field='copy', step=step, index=index)
|
|
540
|
+
|
|
541
|
+
# Collect inputs into a collection directory only for remote runs, since
|
|
542
|
+
# we need to send inputs up to the server.
|
|
543
|
+
self.__chip.collect(whitelist=self.__config.setdefault('directory_whitelist', []))
|
|
544
|
+
|
|
545
|
+
def _run_loop(self):
|
|
546
|
+
# Wrapper to allow for capturing of Ctrl+C
|
|
547
|
+
try:
|
|
548
|
+
self.__run_loop()
|
|
549
|
+
self._finalize_loop()
|
|
550
|
+
except KeyboardInterrupt:
|
|
551
|
+
manifest_path = self.remote_manifest()
|
|
552
|
+
reconnect_cmd = f'sc-remote -cfg {manifest_path} -reconnect'
|
|
553
|
+
cancel_cmd = f'sc-remote -cfg {manifest_path} -cancel'
|
|
554
|
+
self.__logger.info('Disconnecting from remote job')
|
|
555
|
+
self.__logger.info(f'To reconnect to this job use: {reconnect_cmd}')
|
|
556
|
+
self.__logger.info(f'To cancel this job use: {cancel_cmd}')
|
|
557
|
+
raise SiliconCompilerError('Job canceled by user keyboard interrupt')
|
|
558
|
+
|
|
559
|
+
def __import_run_manifests(self):
|
|
376
560
|
changed = False
|
|
377
|
-
for
|
|
378
|
-
if
|
|
561
|
+
for _, node_info in self.__node_information.items():
|
|
562
|
+
if node_info["imported"]:
|
|
379
563
|
continue
|
|
380
564
|
|
|
381
|
-
manifest = os.path.join(
|
|
382
|
-
|
|
383
|
-
|
|
565
|
+
manifest = os.path.join(
|
|
566
|
+
self.__chip.getworkdir(step=node_info["step"], index=node_info["index"]),
|
|
567
|
+
'outputs',
|
|
568
|
+
f'{self.__chip.design}.pkg.json')
|
|
384
569
|
if os.path.exists(manifest):
|
|
385
570
|
try:
|
|
386
|
-
|
|
387
|
-
|
|
571
|
+
self.__chip.schema.read_journal(manifest)
|
|
572
|
+
node_info["imported"] = True
|
|
388
573
|
changed = True
|
|
389
574
|
except: # noqa E722
|
|
390
575
|
# Import may fail if file is still getting written
|
|
391
576
|
pass
|
|
392
577
|
return changed
|
|
393
578
|
|
|
394
|
-
def
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
579
|
+
def __ensure_run_loop_information(self):
|
|
580
|
+
self.__chip._init_logger(step=self.STEP_NAME, index='0', in_run=True)
|
|
581
|
+
if not self.__download_pool:
|
|
582
|
+
self.__download_pool = multiprocessing.Pool()
|
|
583
|
+
|
|
584
|
+
if self.__check_interval is None:
|
|
585
|
+
check_info = self.__check()
|
|
586
|
+
self.__check_interval = check_info['progress_interval']
|
|
587
|
+
|
|
588
|
+
self.__node_information = {}
|
|
589
|
+
for step, index in nodes_to_execute(self.__chip):
|
|
590
|
+
done = SCNodeStatus.is_done(self.__chip.get('record', 'status', step=step, index=index))
|
|
591
|
+
node_info = {
|
|
592
|
+
"step": step,
|
|
593
|
+
"index": index,
|
|
594
|
+
"imported": done,
|
|
595
|
+
"fetched": done
|
|
596
|
+
}
|
|
597
|
+
self.__node_information[f'{step}{index}'] = node_info
|
|
598
|
+
|
|
599
|
+
def __run_loop(self):
|
|
600
|
+
self.__ensure_run_loop_information()
|
|
601
|
+
|
|
602
|
+
# Check the job's progress periodically until it finishes.
|
|
603
|
+
running = True
|
|
604
|
+
|
|
605
|
+
while running:
|
|
606
|
+
time.sleep(self.__check_interval)
|
|
607
|
+
self.__import_run_manifests()
|
|
608
|
+
|
|
609
|
+
# Check progress
|
|
610
|
+
job_info = self.check_job_status()
|
|
611
|
+
completed, running = self._report_job_status(job_info)
|
|
612
|
+
|
|
613
|
+
if self.__chip._dash:
|
|
614
|
+
# Update dashboard if active
|
|
615
|
+
self.__chip._dash.update_manifest()
|
|
616
|
+
|
|
617
|
+
nodes_to_fetch = []
|
|
618
|
+
for node in completed:
|
|
619
|
+
if not self.__node_information[node]["fetched"]:
|
|
620
|
+
nodes_to_fetch.append(node)
|
|
621
|
+
|
|
622
|
+
if nodes_to_fetch:
|
|
623
|
+
self.__logger.info(' Fetching completed results:')
|
|
624
|
+
for node in nodes_to_fetch:
|
|
625
|
+
self.__schedule_fetch_result(node)
|
|
626
|
+
|
|
627
|
+
# Done: try to fetch any node results which still haven't been retrieved.
|
|
628
|
+
self.__logger.info('Remote job completed! Retrieving final results...')
|
|
629
|
+
for node, node_info in self.__node_information.items():
|
|
630
|
+
if not node_info["fetched"]:
|
|
631
|
+
self.__schedule_fetch_result(node)
|
|
632
|
+
self.__schedule_fetch_result(node)
|
|
633
|
+
|
|
634
|
+
self._finalize_loop()
|
|
635
|
+
|
|
636
|
+
# Un-set the 'remote' option to avoid from/to-based summary/show errors
|
|
637
|
+
self.__chip.unset('option', 'remote')
|
|
638
|
+
|
|
639
|
+
if self.__chip._dash:
|
|
640
|
+
self.__chip._dash.update_manifest()
|
|
641
|
+
|
|
642
|
+
def _finalize_loop(self):
|
|
643
|
+
if self.__download_pool:
|
|
644
|
+
self.__download_pool.close()
|
|
645
|
+
self.__download_pool.join()
|
|
646
|
+
self.__download_pool = None
|
|
647
|
+
|
|
648
|
+
self.__import_run_manifests()
|
|
649
|
+
|
|
650
|
+
def __schedule_fetch_result(self, node):
|
|
651
|
+
self.__node_information[node]["fetched"] = True
|
|
652
|
+
self.__download_pool.apply_async(Client._fetch_result, (self, node))
|
|
399
653
|
if node is None:
|
|
400
654
|
node = 'final result'
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
timeout=__timeout)
|
|
508
|
-
|
|
509
|
-
def success_action(resp):
|
|
510
|
-
return resp.json()
|
|
511
|
-
|
|
512
|
-
resp = __post(chip, '/remote_run/', post_action, success_action)
|
|
513
|
-
if not remote_resume:
|
|
514
|
-
upload_file.close()
|
|
515
|
-
|
|
516
|
-
if 'message' in resp and resp['message']:
|
|
517
|
-
chip.logger.info(resp['message'])
|
|
518
|
-
chip.set('record', 'remoteid', resp['job_hash'])
|
|
519
|
-
|
|
520
|
-
manifest = get_remote_manifest(chip)
|
|
521
|
-
chip.write_manifest(manifest)
|
|
522
|
-
|
|
523
|
-
chip.logger.info(f"Your job's reference ID is: {resp['job_hash']}")
|
|
524
|
-
|
|
525
|
-
return remote_status['progress_interval']
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
###################################
|
|
529
|
-
def is_job_busy(chip):
|
|
530
|
-
'''
|
|
531
|
-
Helper method to make an async request asking the remote server
|
|
532
|
-
whether a job is busy, or ready to accept a new step.
|
|
533
|
-
Returns True if the job is busy, False if not.
|
|
534
|
-
'''
|
|
535
|
-
|
|
536
|
-
# Make the request and print its response.
|
|
537
|
-
def post_action(url):
|
|
538
|
-
params = __build_post_params(chip,
|
|
539
|
-
False,
|
|
540
|
-
job_hash=chip.get('record', 'remoteid'),
|
|
541
|
-
job_name=chip.get('option', 'jobname'))
|
|
542
|
-
return requests.post(url,
|
|
543
|
-
data=json.dumps(params),
|
|
544
|
-
timeout=__timeout)
|
|
545
|
-
|
|
546
|
-
def error_action(code, msg):
|
|
547
|
-
return {
|
|
548
|
-
'busy': True,
|
|
549
|
-
'message': ''
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
def success_action(resp):
|
|
553
|
-
json_response = json.loads(resp.text)
|
|
554
|
-
|
|
555
|
-
if json_response['status'] != JobStatus.RUNNING:
|
|
556
|
-
if json_response['status'] == JobStatus.REJECTED:
|
|
557
|
-
chip.logger.error(f'Job was rejected: {json_response["message"]}')
|
|
558
|
-
elif json_response['status'] != JobStatus.UNKNOWN:
|
|
559
|
-
chip.logger.info(f'Job status: {json_response["status"]}')
|
|
560
|
-
info = {
|
|
561
|
-
'busy': json_response['status'] == JobStatus.RUNNING,
|
|
562
|
-
'message': None
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if isinstance(json_response['message'], str):
|
|
566
|
-
info['message'] = json_response['message']
|
|
655
|
+
self.__logger.info(f' {node}')
|
|
656
|
+
|
|
657
|
+
def _fetch_result(self, node):
|
|
658
|
+
'''
|
|
659
|
+
Helper method to fetch and open job results from a remote compute cluster.
|
|
660
|
+
Optional 'node' argument fetches results for only the specified
|
|
661
|
+
flowgraph node (e.g. "floorplan0")
|
|
662
|
+
'''
|
|
663
|
+
|
|
664
|
+
# Collect local values.
|
|
665
|
+
job_hash = self.__chip.get('record', 'remoteid')
|
|
666
|
+
local_dir = self.__chip.get('option', 'builddir')
|
|
667
|
+
|
|
668
|
+
# Set default results archive path if necessary, and fetch it.
|
|
669
|
+
with tempfile.TemporaryDirectory(prefix=f'sc_{job_hash}_', suffix=f'_{node}') as tmpdir:
|
|
670
|
+
results_path = os.path.join(tmpdir, 'result.tar.gz')
|
|
671
|
+
|
|
672
|
+
with open(results_path, 'wb') as rd:
|
|
673
|
+
# Fetch results archive.
|
|
674
|
+
def post_action(url):
|
|
675
|
+
post_params = self.__get_post_params()
|
|
676
|
+
if node:
|
|
677
|
+
post_params['node'] = node
|
|
678
|
+
return requests.post(
|
|
679
|
+
url,
|
|
680
|
+
data=json.dumps(post_params),
|
|
681
|
+
stream=True,
|
|
682
|
+
timeout=self.__timeout)
|
|
683
|
+
|
|
684
|
+
def success_action(resp):
|
|
685
|
+
shutil.copyfileobj(resp.raw, rd)
|
|
686
|
+
return 0
|
|
687
|
+
|
|
688
|
+
def error_action(code, msg):
|
|
689
|
+
# Results are fetched in parallel, and a failure in one node
|
|
690
|
+
# does not necessarily mean that the whole job failed.
|
|
691
|
+
if node:
|
|
692
|
+
self.__logger.warning(f'Could not fetch results for node: {node}')
|
|
693
|
+
else:
|
|
694
|
+
self.__logger.warning('Could not fetch results for final results.')
|
|
695
|
+
return 404
|
|
696
|
+
|
|
697
|
+
results_code = self.__post(
|
|
698
|
+
f'/get_results/{job_hash}.tar.gz',
|
|
699
|
+
post_action,
|
|
700
|
+
success_action,
|
|
701
|
+
error_action=error_action
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
# Note: the server should eventually delete the results as they age out (~8h),
|
|
705
|
+
# but this will give us a brief period to look at failed results.
|
|
706
|
+
if results_code:
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
# Unzip the results.
|
|
710
|
+
# Unauthenticated jobs get a gzip archive, authenticated jobs get nested archives.
|
|
711
|
+
# So we need to extract and delete those.
|
|
712
|
+
# Archive contents: server-side build directory. Format:
|
|
713
|
+
# [job_hash]/[design]/[job_name]/[step]/[index]/...
|
|
714
|
+
try:
|
|
715
|
+
with tarfile.open(results_path, 'r:gz') as tar:
|
|
716
|
+
tar.extractall(path=tmpdir)
|
|
717
|
+
except tarfile.TarError as e:
|
|
718
|
+
self.__logger.error(f'Failed to extract data from {results_path}: {e}')
|
|
719
|
+
return
|
|
720
|
+
|
|
721
|
+
work_dir = os.path.join(tmpdir, job_hash)
|
|
722
|
+
if os.path.exists(work_dir):
|
|
723
|
+
shutil.copytree(work_dir, local_dir, dirs_exist_ok=True)
|
|
724
|
+
else:
|
|
725
|
+
self.__logger.error(f'Empty file returned from remote for: {node}')
|
|
726
|
+
return
|
|
727
|
+
|
|
728
|
+
def configure_server(self, server=None, username=None, password=None):
|
|
729
|
+
|
|
730
|
+
def confirm_dialog(message):
|
|
731
|
+
confirmed = False
|
|
732
|
+
while not confirmed:
|
|
733
|
+
oin = input(f'{message} y/N: ')
|
|
734
|
+
if (not oin) or (oin == 'n') or (oin == 'N'):
|
|
735
|
+
return False
|
|
736
|
+
elif (oin == 'y') or (oin == 'Y'):
|
|
737
|
+
return True
|
|
738
|
+
return False
|
|
739
|
+
|
|
740
|
+
default_server_name = urllib.parse.urlparse(self.__default_server).hostname
|
|
741
|
+
|
|
742
|
+
# Find the config file/directory path.
|
|
743
|
+
cfg_file = self.__get_remote_config_file()
|
|
744
|
+
cfg_dir = os.path.dirname(cfg_file)
|
|
745
|
+
|
|
746
|
+
# Create directory if it doesn't exist.
|
|
747
|
+
if cfg_dir and not os.path.isdir(cfg_dir):
|
|
748
|
+
os.makedirs(cfg_dir, exist_ok=True)
|
|
749
|
+
|
|
750
|
+
# If an existing config file exists, prompt the user to overwrite it.
|
|
751
|
+
if os.path.isfile(cfg_file):
|
|
752
|
+
if not confirm_dialog('Overwrite existing remote configuration?'):
|
|
753
|
+
return
|
|
754
|
+
|
|
755
|
+
self.__config = {}
|
|
756
|
+
|
|
757
|
+
# If a command-line argument is passed in, use that as a public server address.
|
|
758
|
+
if server:
|
|
759
|
+
srv_addr = server
|
|
760
|
+
self.__logger.info(f'Creating remote configuration file for server: {srv_addr}')
|
|
567
761
|
else:
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
'''
|
|
588
|
-
Helper method to request that the server cancel an ongoing job.
|
|
589
|
-
'''
|
|
590
|
-
|
|
591
|
-
def post_action(url):
|
|
592
|
-
return requests.post(url,
|
|
593
|
-
data=json.dumps(__build_post_params(
|
|
594
|
-
chip,
|
|
595
|
-
False,
|
|
596
|
-
job_hash=chip.get('record', 'remoteid'))),
|
|
597
|
-
timeout=__timeout)
|
|
598
|
-
|
|
599
|
-
def success_action(resp):
|
|
600
|
-
return json.loads(resp.text)
|
|
601
|
-
|
|
602
|
-
return __post(chip, '/cancel_job/', post_action, success_action)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
###################################
|
|
606
|
-
def delete_job(chip):
|
|
607
|
-
'''
|
|
608
|
-
Helper method to delete a job from shared remote storage.
|
|
609
|
-
'''
|
|
610
|
-
|
|
611
|
-
def post_action(url):
|
|
612
|
-
return requests.post(url,
|
|
613
|
-
data=json.dumps(__build_post_params(
|
|
614
|
-
chip,
|
|
615
|
-
False,
|
|
616
|
-
job_hash=chip.get('record', 'remoteid'))),
|
|
617
|
-
timeout=__timeout)
|
|
618
|
-
|
|
619
|
-
def success_action(resp):
|
|
620
|
-
return resp.text
|
|
621
|
-
|
|
622
|
-
return __post(chip, '/delete_job/', post_action, success_action)
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
###################################
|
|
626
|
-
def fetch_results_request(chip, node, results_fd):
|
|
627
|
-
'''
|
|
628
|
-
Helper method to fetch job results from a remote compute cluster.
|
|
629
|
-
Optional 'node' argument fetches results for only the specified
|
|
630
|
-
flowgraph node (e.g. "floorplan0")
|
|
631
|
-
|
|
632
|
-
Returns:
|
|
633
|
-
* 0 if no error was encountered.
|
|
634
|
-
* [response code] if the results could not be retrieved.
|
|
635
|
-
'''
|
|
636
|
-
|
|
637
|
-
# Set the request URL.
|
|
638
|
-
job_hash = chip.get('record', 'remoteid')
|
|
639
|
-
|
|
640
|
-
# Fetch results archive.
|
|
641
|
-
def post_action(url):
|
|
642
|
-
post_params = __build_post_params(chip, False)
|
|
643
|
-
if node:
|
|
644
|
-
post_params['node'] = node
|
|
645
|
-
return requests.post(url,
|
|
646
|
-
data=json.dumps(post_params),
|
|
647
|
-
stream=True,
|
|
648
|
-
timeout=__timeout)
|
|
649
|
-
|
|
650
|
-
def success_action(resp):
|
|
651
|
-
shutil.copyfileobj(resp.raw, results_fd)
|
|
652
|
-
return 0
|
|
653
|
-
|
|
654
|
-
def error_action(code, msg):
|
|
655
|
-
# Results are fetched in parallel, and a failure in one node
|
|
656
|
-
# does not necessarily mean that the whole job failed.
|
|
657
|
-
if node:
|
|
658
|
-
chip.logger.warning(f'Could not fetch results for node: {node}')
|
|
762
|
+
# If no arguments were passed in, interactively request credentials from the user.
|
|
763
|
+
srv_addr = input('Remote server address (leave blank to use default server):\n')
|
|
764
|
+
srv_addr = srv_addr.replace(" ", "")
|
|
765
|
+
|
|
766
|
+
if not srv_addr:
|
|
767
|
+
srv_addr = self.__default_server
|
|
768
|
+
self.__logger.info(f'Using {srv_addr} as server')
|
|
769
|
+
|
|
770
|
+
server = urllib.parse.urlparse(srv_addr)
|
|
771
|
+
has_scheme = True
|
|
772
|
+
if not server.hostname:
|
|
773
|
+
# fake add a scheme to the url
|
|
774
|
+
has_scheme = False
|
|
775
|
+
server = urllib.parse.urlparse('https://' + srv_addr)
|
|
776
|
+
if not server.hostname:
|
|
777
|
+
raise ValueError(f'Invalid address provided: {srv_addr}')
|
|
778
|
+
|
|
779
|
+
if has_scheme:
|
|
780
|
+
self.__config['address'] = f'{server.scheme}://{server.hostname}'
|
|
659
781
|
else:
|
|
660
|
-
|
|
661
|
-
return 404
|
|
662
|
-
|
|
663
|
-
return __post(chip,
|
|
664
|
-
f'/get_results/{job_hash}.tar.gz',
|
|
665
|
-
post_action,
|
|
666
|
-
success_action,
|
|
667
|
-
error_action=error_action)
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
###################################
|
|
671
|
-
def fetch_results(chip, node):
|
|
672
|
-
'''
|
|
673
|
-
Helper method to fetch and open job results from a remote compute cluster.
|
|
674
|
-
Optional 'node' argument fetches results for only the specified
|
|
675
|
-
flowgraph node (e.g. "floorplan0")
|
|
676
|
-
'''
|
|
677
|
-
|
|
678
|
-
# Collect local values.
|
|
679
|
-
job_hash = chip.get('record', 'remoteid')
|
|
680
|
-
local_dir = chip.get('option', 'builddir')
|
|
681
|
-
|
|
682
|
-
# Set default results archive path if necessary, and fetch it.
|
|
683
|
-
with tempfile.TemporaryDirectory(prefix=f'sc_{job_hash}_', suffix=f'_{node}') as tmpdir:
|
|
684
|
-
results_path = os.path.join(tmpdir, 'result.tar.gz')
|
|
685
|
-
|
|
686
|
-
with open(results_path, 'wb') as rd:
|
|
687
|
-
results_code = fetch_results_request(chip, node, rd)
|
|
688
|
-
|
|
689
|
-
# Note: the server should eventually delete the results as they age out (~8h), but this will
|
|
690
|
-
# give us a brief period to look at failed results.
|
|
691
|
-
if results_code:
|
|
692
|
-
return
|
|
693
|
-
|
|
694
|
-
# Unzip the results.
|
|
695
|
-
# Unauthenticated jobs get a gzip archive, authenticated jobs get nested archives.
|
|
696
|
-
# So we need to extract and delete those.
|
|
697
|
-
# Archive contents: server-side build directory. Format:
|
|
698
|
-
# [job_hash]/[design]/[job_name]/[step]/[index]/...
|
|
699
|
-
try:
|
|
700
|
-
with tarfile.open(results_path, 'r:gz') as tar:
|
|
701
|
-
tar.extractall(path=tmpdir)
|
|
702
|
-
except tarfile.TarError as e:
|
|
703
|
-
chip.logger.error(f'Failed to extract data from {results_path}: {e}')
|
|
704
|
-
return
|
|
782
|
+
self.__config['address'] = server.hostname
|
|
705
783
|
|
|
706
|
-
|
|
707
|
-
if
|
|
708
|
-
shutil.copytree(work_dir, local_dir, dirs_exist_ok=True)
|
|
709
|
-
else:
|
|
710
|
-
chip.logger.error(f'Empty file returned from remote for: {node}')
|
|
784
|
+
public_server = default_server_name in srv_addr
|
|
785
|
+
if public_server and not confirm_dialog(self.__tos_str):
|
|
711
786
|
return
|
|
712
787
|
|
|
788
|
+
if server.port is not None:
|
|
789
|
+
self.__config['port'] = server.port
|
|
713
790
|
|
|
714
|
-
|
|
715
|
-
# Make the request and print its response.
|
|
716
|
-
rcfg = __build_post_params(chip, True)
|
|
717
|
-
|
|
718
|
-
def post_action(url):
|
|
719
|
-
return requests.post(url,
|
|
720
|
-
data=json.dumps(rcfg),
|
|
721
|
-
timeout=__timeout)
|
|
722
|
-
|
|
723
|
-
def success_action(resp):
|
|
724
|
-
return resp.json()
|
|
725
|
-
|
|
726
|
-
response_info = __post(chip, '/check_server/', post_action, success_action)
|
|
727
|
-
if not response_info:
|
|
728
|
-
raise ValueError('Server response is not valid.')
|
|
729
|
-
|
|
730
|
-
return response_info
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
###################################
|
|
734
|
-
def __print_tos(chip, response_info):
|
|
735
|
-
# Print terms-of-service message, if the server provides one.
|
|
736
|
-
if 'terms' in response_info and response_info['terms']:
|
|
737
|
-
chip.logger.info('Terms of Service info for this server:')
|
|
738
|
-
for line in response_info['terms'].splitlines():
|
|
739
|
-
if line:
|
|
740
|
-
chip.logger.info(line)
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
###################################
|
|
744
|
-
def remote_ping(chip):
|
|
745
|
-
'''
|
|
746
|
-
Helper method to call check_server on server
|
|
747
|
-
'''
|
|
748
|
-
|
|
749
|
-
# Make the request and print its response.
|
|
750
|
-
response_info = _remote_ping(chip)
|
|
751
|
-
|
|
752
|
-
# Print status value.
|
|
753
|
-
server_status = response_info['status']
|
|
754
|
-
chip.logger.info(f'Server status: {server_status}')
|
|
755
|
-
if server_status != 'ready':
|
|
756
|
-
chip.logger.warning(' Status is not "ready", server cannot accept new jobs.')
|
|
757
|
-
|
|
758
|
-
# Print server-side version info.
|
|
759
|
-
version_info = response_info['versions']
|
|
760
|
-
version_suffix = ' version'
|
|
761
|
-
max_name_string_len = max([len(s) for s in version_info.keys()]) + len(version_suffix)
|
|
762
|
-
chip.logger.info('Server software versions:')
|
|
763
|
-
for name, version in version_info.items():
|
|
764
|
-
print_name = f'{name}{version_suffix}'
|
|
765
|
-
chip.logger.info(f' {print_name: <{max_name_string_len}}: {version}')
|
|
766
|
-
|
|
767
|
-
# Print user info if applicable.
|
|
768
|
-
if 'user_info' in response_info:
|
|
769
|
-
user_info = response_info['user_info']
|
|
770
|
-
if ('compute_time' not in user_info) or \
|
|
771
|
-
('bandwidth_kb' not in user_info):
|
|
772
|
-
chip.logger.info('Error fetching user information from the remote server.')
|
|
773
|
-
raise ValueError(f'Server response is not valid or missing fields: {user_info}')
|
|
774
|
-
|
|
775
|
-
remote_cfg = get_remote_config(chip, False)
|
|
776
|
-
if 'username' in remote_cfg:
|
|
777
|
-
# Print the user's account info, and return.
|
|
778
|
-
chip.logger.info(f'User {remote_cfg["username"]}:')
|
|
779
|
-
|
|
780
|
-
time_remaining = user_info["compute_time"] / 60.0
|
|
781
|
-
bandwidth_remaining = user_info["bandwidth_kb"]
|
|
782
|
-
chip.logger.info(f' Remaining compute time: {(time_remaining):.2f} minutes')
|
|
783
|
-
chip.logger.info(f' Remaining results bandwidth: {bandwidth_remaining} KiB')
|
|
784
|
-
|
|
785
|
-
__print_tos(chip, response_info)
|
|
786
|
-
|
|
787
|
-
# Return the response info in case the caller wants to inspect it.
|
|
788
|
-
return response_info
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
def configure_server(chip, server=None, port=None, username=None, password=None):
|
|
792
|
-
|
|
793
|
-
def confirm_dialog(message):
|
|
794
|
-
confirmed = False
|
|
795
|
-
while not confirmed:
|
|
796
|
-
oin = input(f'{message} y/N: ')
|
|
797
|
-
if (not oin) or (oin == 'n') or (oin == 'N'):
|
|
798
|
-
return False
|
|
799
|
-
elif (oin == 'y') or (oin == 'Y'):
|
|
800
|
-
return True
|
|
801
|
-
return False
|
|
802
|
-
|
|
803
|
-
default_server_name = urllib.parse.urlparse(default_server).hostname
|
|
804
|
-
|
|
805
|
-
# Find the config file/directory path.
|
|
806
|
-
cfg_file = get_remote_config_file(chip, False)
|
|
807
|
-
cfg_dir = os.path.dirname(cfg_file)
|
|
808
|
-
|
|
809
|
-
# Create directory if it doesn't exist.
|
|
810
|
-
if cfg_dir and not os.path.isdir(cfg_dir):
|
|
811
|
-
os.makedirs(cfg_dir, exist_ok=True)
|
|
812
|
-
|
|
813
|
-
# If an existing config file exists, prompt the user to overwrite it.
|
|
814
|
-
if os.path.isfile(cfg_file):
|
|
815
|
-
if not confirm_dialog('Overwrite existing remote configuration?'):
|
|
816
|
-
return
|
|
817
|
-
|
|
818
|
-
config = {}
|
|
819
|
-
|
|
820
|
-
# If a command-line argument is passed in, use that as a public server address.
|
|
821
|
-
if server:
|
|
822
|
-
srv_addr = server
|
|
823
|
-
chip.logger.info(f'Creating remote configuration file for server: {srv_addr}')
|
|
824
|
-
else:
|
|
825
|
-
# If no arguments were passed in, interactively request credentials from the user.
|
|
826
|
-
srv_addr = input('Remote server address (leave blank to use default server):\n')
|
|
827
|
-
srv_addr = srv_addr.replace(" ", "")
|
|
828
|
-
|
|
829
|
-
if not srv_addr:
|
|
830
|
-
srv_addr = default_server
|
|
831
|
-
chip.logger.info(f'Using {srv_addr} as server')
|
|
832
|
-
|
|
833
|
-
server = urllib.parse.urlparse(srv_addr)
|
|
834
|
-
has_scheme = True
|
|
835
|
-
if not server.hostname:
|
|
836
|
-
# fake add a scheme to the url
|
|
837
|
-
has_scheme = False
|
|
838
|
-
server = urllib.parse.urlparse('https://' + srv_addr)
|
|
839
|
-
if not server.hostname:
|
|
840
|
-
raise ValueError(f'Invalid address provided: {srv_addr}')
|
|
841
|
-
|
|
842
|
-
if has_scheme:
|
|
843
|
-
config['address'] = f'{server.scheme}://{server.hostname}'
|
|
844
|
-
else:
|
|
845
|
-
config['address'] = server.hostname
|
|
846
|
-
|
|
847
|
-
public_server = default_server_name in srv_addr
|
|
848
|
-
if public_server and not confirm_dialog(__tos_str):
|
|
849
|
-
return
|
|
850
|
-
|
|
851
|
-
if server.port is not None:
|
|
852
|
-
config['port'] = server.port
|
|
853
|
-
|
|
854
|
-
if not public_server:
|
|
855
|
-
if username is None:
|
|
856
|
-
username = server.username
|
|
791
|
+
if not public_server:
|
|
857
792
|
if username is None:
|
|
858
|
-
username =
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
793
|
+
username = server.username
|
|
794
|
+
if username is None:
|
|
795
|
+
username = input('Remote username (leave blank for no username):\n')
|
|
796
|
+
username = username.replace(" ", "")
|
|
862
797
|
if password is None:
|
|
863
|
-
password =
|
|
864
|
-
|
|
798
|
+
password = server.password
|
|
799
|
+
if password is None:
|
|
800
|
+
password = input('Remote password (leave blank for no password):\n')
|
|
801
|
+
password = password.replace(" ", "")
|
|
802
|
+
|
|
803
|
+
if username:
|
|
804
|
+
self.__config['username'] = username
|
|
805
|
+
if password:
|
|
806
|
+
self.__config['password'] = password
|
|
807
|
+
|
|
808
|
+
self.__config['directory_whitelist'] = []
|
|
865
809
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
config['password'] = password
|
|
810
|
+
# Save the values to the target config file in JSON format.
|
|
811
|
+
with open(cfg_file, 'w') as f:
|
|
812
|
+
f.write(json.dumps(self.__config, indent=4))
|
|
870
813
|
|
|
871
|
-
|
|
814
|
+
# Let the user know that we finished successfully.
|
|
815
|
+
self.__logger.info(f'Remote configuration saved to: {cfg_file}')
|
|
872
816
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
f.write(json.dumps(config, indent=4))
|
|
817
|
+
def configure_whitelist(self, add=None, remove=None):
|
|
818
|
+
cfg_file = self.__get_remote_config_file()
|
|
876
819
|
|
|
877
|
-
|
|
878
|
-
chip.logger.info(f'Remote configuration saved to: {cfg_file}')
|
|
820
|
+
self.__logger.info(f'Updating credentials: {cfg_file}')
|
|
879
821
|
|
|
822
|
+
if 'directory_whitelist' not in self.__config:
|
|
823
|
+
self.__config['directory_whitelist'] = []
|
|
880
824
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
825
|
+
if add:
|
|
826
|
+
for path in add:
|
|
827
|
+
path = os.path.abspath(path)
|
|
828
|
+
self.__logger.info(f'Adding {path}')
|
|
829
|
+
self.__config['directory_whitelist'].append(path)
|
|
886
830
|
|
|
887
|
-
|
|
888
|
-
|
|
831
|
+
if remove:
|
|
832
|
+
for path in remove:
|
|
833
|
+
path = os.path.abspath(path)
|
|
834
|
+
if path in self.__config['directory_whitelist']:
|
|
835
|
+
self.__logger.info(f'Removing {path}')
|
|
836
|
+
self.__config['directory_whitelist'].remove(path)
|
|
889
837
|
|
|
890
|
-
|
|
891
|
-
|
|
838
|
+
# Cleanup
|
|
839
|
+
self.__config['directory_whitelist'] = list(set(self.__config['directory_whitelist']))
|
|
892
840
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
chip.logger.info(f'Adding {path}')
|
|
897
|
-
cfg['directory_whitelist'].append(path)
|
|
841
|
+
# Save the values to the target config file in JSON format.
|
|
842
|
+
with open(cfg_file, 'w') as f:
|
|
843
|
+
f.write(json.dumps(self.__config, indent=4))
|
|
898
844
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
chip.logger.info(f'Removing {path}')
|
|
904
|
-
cfg['directory_whitelist'].remove(path)
|
|
845
|
+
#######################################
|
|
846
|
+
def __getstate__(self):
|
|
847
|
+
# Called when generating a serial stream of the object
|
|
848
|
+
attributes = self.__dict__.copy()
|
|
905
849
|
|
|
906
|
-
|
|
850
|
+
attributes['_Client__download_pool'] = None
|
|
907
851
|
|
|
908
|
-
|
|
909
|
-
with open(cfg_file, 'w') as f:
|
|
910
|
-
f.write(json.dumps(cfg, indent=4))
|
|
852
|
+
return attributes
|
|
911
853
|
|
|
912
854
|
|
|
913
|
-
|
|
914
|
-
|
|
855
|
+
class ConfigureClient(Client):
|
|
856
|
+
def __init__(self, chip):
|
|
857
|
+
self._print_server_warning = False
|
|
858
|
+
self._error_on_missing_file = False
|
|
915
859
|
|
|
916
|
-
|
|
917
|
-
if 'username' in cfg:
|
|
918
|
-
chip.logger.info(f'Username: {cfg["username"]}')
|
|
919
|
-
if 'directory_whitelist' in cfg and cfg['directory_whitelist']:
|
|
920
|
-
chip.logger.info('Directory whitelist:')
|
|
921
|
-
for path in sorted(cfg['directory_whitelist']):
|
|
922
|
-
chip.logger.info(f' {path}')
|
|
860
|
+
super().__init__(chip)
|