Merge lp:~townsend/libertine/use-lxc-manager into lp:libertine
- use-lxc-manager
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Larry Price |
Approved revision: | 309 |
Merged at revision: | 310 |
Proposed branch: | lp:~townsend/libertine/use-lxc-manager |
Merge into: | lp:libertine |
Diff against target: |
385 lines (+155/-97) 5 files modified
python/libertine/Libertine.py (+2/-3) python/libertine/LxcContainer.py (+76/-56) python/libertine/utils.py (+17/-0) tools/libertine-lxc-manager (+59/-22) tools/update-puritine-containers (+1/-16) |
To merge this branch: | bzr merge lp:~townsend/libertine/use-lxc-manager |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Larry Price | Approve | ||
Libertine CI Bot | continuous-integration | Approve | |
Review via email: mp+307864@code.launchpad.net |
Commit message
Use libertine-
Description of the change
If a user is logged into a U8 session, we want to be careful about running l-c-m operations, especially if the user has an app running in the container.
Some example scenarios to try:
1. Run an app in a container. Ensure l-c-m operations won't run.
2. Run an l-c-m operation and then try to start an app. Ensure the app fails to start and some meaningful message is put in the upstart log for the app.
3. Run l-c-m in an ssh session and not logged in to the U8 desktop. Ensure l-c-m will still work when the libertine-
4. Run l-c-m in an ssh session while logged into the U8 session. Ensure l-c-m will work by automatically discovering the session DBus address and run through libertine-
Known issue (not necessarily relevant to this MP):
1. If libertine-
Libertine CI Bot (libertine-ci-bot) wrote : | # |
- 308. By Christopher Townsend
-
Only bind-mount user dirs when launching an app.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:308
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Larry Price (larryprice) wrote : | # |
Couple of optional inlines, and now I'm off to actually test it...
- 309. By Christopher Townsend
-
Changes based on review.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:309
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Larry Price (larryprice) wrote : | # |
This works really well, lgtm.
Preview Diff
1 | === modified file 'python/libertine/Libertine.py' |
2 | --- python/libertine/Libertine.py 2016-09-30 18:55:22 +0000 |
3 | +++ python/libertine/Libertine.py 2016-10-07 15:43:27 +0000 |
4 | @@ -265,9 +265,8 @@ |
5 | """ |
6 | def __init__(self, container): |
7 | super().__init__() |
8 | - if not container.is_running(): |
9 | - container.start_container() |
10 | - self.callback(lambda: container.stop_container()) |
11 | + container.start_container() |
12 | + self.callback(lambda: container.stop_container()) |
13 | |
14 | |
15 | class LibertineContainer(object): |
16 | |
17 | === modified file 'python/libertine/LxcContainer.py' |
18 | --- python/libertine/LxcContainer.py 2016-09-20 13:58:52 +0000 |
19 | +++ python/libertine/LxcContainer.py 2016-10-07 15:43:27 +0000 |
20 | @@ -56,6 +56,14 @@ |
21 | return os.path.join(home_path, '.config', 'lxc') |
22 | |
23 | |
24 | +def get_lxc_manager_dbus_name(): |
25 | + return LIBERTINE_LXC_MANAGER_NAME |
26 | + |
27 | + |
28 | +def get_lxc_manager_dbus_path(): |
29 | + return LIBERTINE_LXC_MANAGER_PATH |
30 | + |
31 | + |
32 | def lxc_container(container_id): |
33 | config_path = utils.get_libertine_containers_dir_path() |
34 | if not os.path.exists(config_path): |
35 | @@ -66,6 +74,28 @@ |
36 | return container |
37 | |
38 | |
39 | +def lxc_start(container, lxc_log_file): |
40 | + container.append_config_item("lxc.logfile", lxc_log_file) |
41 | + container.append_config_item("lxc.logpriority", "3") |
42 | + |
43 | + if not container.start(): |
44 | + return (False, "Container failed to start") |
45 | + |
46 | + if not container.wait("RUNNING", 10): |
47 | + return (False, "Container failed to enter the RUNNING state") |
48 | + |
49 | + if not container.get_ips(timeout=30): |
50 | + lxc_stop(container) |
51 | + return (False, "Not able to connect to the network.") |
52 | + |
53 | + return (True, "") |
54 | + |
55 | + |
56 | +def lxc_stop(container): |
57 | + if container.running: |
58 | + container.stop() |
59 | + |
60 | + |
61 | def get_host_timezone(): |
62 | with open(os.path.join('/', 'etc', 'timezone'), 'r') as fd: |
63 | host_timezone = fd.read().strip('\n') |
64 | @@ -107,6 +137,17 @@ |
65 | super().__init__(container_id) |
66 | self.container_type = "lxc" |
67 | self.container = lxc_container(container_id) |
68 | + self._set_lxc_log() |
69 | + self.lxc_manager_interface = None |
70 | + |
71 | + utils.set_session_dbus_env_var() |
72 | + |
73 | + try: |
74 | + bus = dbus.SessionBus() |
75 | + lxc_mgr_service = bus.get_object(get_lxc_manager_dbus_name(), get_lxc_manager_dbus_path()) |
76 | + self.lxc_manager_interface = dbus.Interface(lxc_mgr_service, get_lxc_manager_dbus_name()) |
77 | + except dbus.exceptions.DBusException: |
78 | + pass |
79 | |
80 | def is_running(self): |
81 | return self.container.running |
82 | @@ -122,40 +163,26 @@ |
83 | else: |
84 | return True |
85 | |
86 | - def _dynamic_bind_mounts(self): |
87 | - for user_dir in utils.get_common_xdg_user_directories(): |
88 | - xdg_user_dir_entry = ( |
89 | - "%s %s/%s none bind,create=dir,optional" |
90 | - % (user_dir[0], home_path.strip('/'), user_dir[1]) |
91 | - ) |
92 | - self.container.append_config_item("lxc.mount.entry", xdg_user_dir_entry) |
93 | - |
94 | - def start_container(self, launchable = False): |
95 | - if not self.container.defined: |
96 | - raise RuntimeError("Container %s is not valid" % self.container_id) |
97 | - |
98 | - if not self.container.running: |
99 | - self._set_lxc_log() |
100 | - self._dynamic_bind_mounts() |
101 | - if not self.container.start(): |
102 | - self._dump_lxc_log() |
103 | - raise RuntimeError("Container failed to start") |
104 | - if not self.container.wait("RUNNING", 10): |
105 | - self._dump_lxc_log() |
106 | - raise RuntimeError("Container failed to enter the RUNNING state") |
107 | - |
108 | - if not self.container.get_ips(timeout=30): |
109 | - self.stop_container() |
110 | - raise RuntimeError("Not able to connect to the network.") |
111 | - |
112 | - if not launchable: |
113 | - if self.run_in_container("mountpoint -q /tmp/.X11-unix") == 0: |
114 | - self.run_in_container("umount /tmp/.X11-unix") |
115 | - if self.run_in_container("mountpoint -q /usr/lib/locale") == 0: |
116 | - self.run_in_container("umount -l /usr/lib/locale") |
117 | + def start_container(self): |
118 | + if self.lxc_manager_interface: |
119 | + (result, error) = self.lxc_manager_interface.operation_start(self.container_id, self.lxc_log_file) |
120 | + else: |
121 | + (result, error) = lxc_start(self.container, self.lxc_log_file) |
122 | + |
123 | + if not result: |
124 | + self._dump_lxc_log() |
125 | + raise RuntimeError(error) |
126 | + |
127 | + if self.run_in_container("mountpoint -q /tmp/.X11-unix") == 0: |
128 | + self.run_in_container("umount /tmp/.X11-unix") |
129 | + if self.run_in_container("mountpoint -q /usr/lib/locale") == 0: |
130 | + self.run_in_container("umount -l /usr/lib/locale") |
131 | |
132 | def stop_container(self): |
133 | - self.container.stop() |
134 | + if self.lxc_manager_interface: |
135 | + self.lxc_manager_interface.operation_stop(self.container_id) |
136 | + else: |
137 | + lxc_stop(self.container) |
138 | |
139 | def run_in_container(self, command_string): |
140 | cmd_args = shlex.split(command_string) |
141 | @@ -283,21 +310,15 @@ |
142 | self.container.save_config() |
143 | |
144 | def launch_application(self, app_exec_line): |
145 | - bus = dbus.SessionBus() |
146 | - lxc_mgr_service = bus.get_object(LIBERTINE_LXC_MANAGER_NAME, LIBERTINE_LXC_MANAGER_PATH) |
147 | - lxc_manager_interface = dbus.Interface(lxc_mgr_service, LIBERTINE_LXC_MANAGER_NAME) |
148 | - |
149 | - if lxc_manager_interface.app_start(self.container_id): |
150 | - if not self.container.wait("RUNNING", 10): |
151 | - print("Container failed to enter the RUNNING state") |
152 | - return |
153 | - |
154 | - if not self.container.get_ips(timeout=30): |
155 | - print("Not able to connect to the network.") |
156 | - return |
157 | - |
158 | - else: |
159 | - print("Failure detected from libertine-lxc-manager") |
160 | + if self.lxc_manager_interface == None: |
161 | + print("No interface to libertine-lxc-manager. Failing application launch.") |
162 | + return |
163 | + |
164 | + (result, error) = self.lxc_manager_interface.app_start(self.container_id, self.lxc_log_file) |
165 | + |
166 | + if not result: |
167 | + self._dump_lxc_log() |
168 | + print("%s" % error) |
169 | return |
170 | |
171 | window_manager = self.container.attach(lxc.attach_run_command, |
172 | @@ -314,17 +335,16 @@ |
173 | utils.terminate_window_manager(psutil.Process(window_manager)) |
174 | |
175 | # Tell libertine-lxc-manager that the app has stopped. |
176 | - lxc_manager_interface.app_stop(self.container_id) |
177 | + self.lxc_manager_interface.app_stop(self.container_id) |
178 | |
179 | def _set_lxc_log(self): |
180 | self.lxc_log_file = os.path.join(tempfile.mkdtemp(), 'lxc-start.log') |
181 | - self.container.append_config_item("lxc.logfile", self.lxc_log_file) |
182 | - self.container.append_config_item("lxc.logpriority", "3") |
183 | |
184 | def _dump_lxc_log(self): |
185 | - try: |
186 | - with open(self.lxc_log_file, 'r') as fd: |
187 | - for line in fd: |
188 | - print(line.lstrip()) |
189 | - except Exception as ex: |
190 | - print("Could not open LXC log file: %s" % ex, file=sys.stderr) |
191 | + if os.path.exists(self.lxc_log_file): |
192 | + try: |
193 | + with open(self.lxc_log_file, 'r') as fd: |
194 | + for line in fd: |
195 | + print(line.lstrip()) |
196 | + except Exception as ex: |
197 | + print("Could not open LXC log file: %s" % ex, file=sys.stderr) |
198 | |
199 | === modified file 'python/libertine/utils.py' |
200 | --- python/libertine/utils.py 2016-09-06 12:36:47 +0000 |
201 | +++ python/libertine/utils.py 2016-10-07 15:43:27 +0000 |
202 | @@ -173,3 +173,20 @@ |
203 | (scopes_object_path, invalidate_signal, libertine_scope_id)) |
204 | |
205 | subprocess.Popen(shlex.split(gdbus_cmd), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
206 | + |
207 | + |
208 | +def set_session_dbus_env_var(): |
209 | + if not 'DBUS_SESSION_BUS_ADDRESS' in os.environ: |
210 | + dbus_session_path = os.path.join('/', 'run', 'user', str(os.getuid()), 'dbus-session') |
211 | + |
212 | + if os.path.exists(dbus_session_path): |
213 | + with open(dbus_session_path, 'r') as fd: |
214 | + dbus_session_str = fd.read() |
215 | + |
216 | + os.environ['DBUS_SESSION_BUS_ADDRESS'] = dbus_session_str.partition('DBUS_SESSION_BUS_ADDRESS=')[2].rstrip('\n') |
217 | + |
218 | + return True |
219 | + else: |
220 | + return False |
221 | + |
222 | + return True |
223 | |
224 | === modified file 'tools/libertine-lxc-manager' |
225 | --- tools/libertine-lxc-manager 2016-09-06 12:36:47 +0000 |
226 | +++ tools/libertine-lxc-manager 2016-10-07 15:43:27 +0000 |
227 | @@ -30,8 +30,10 @@ |
228 | from gi.repository import GLib |
229 | |
230 | |
231 | -LIBERTINE_LXC_MANAGER_NAME = "com.canonical.libertine.LxcManager" |
232 | -LIBERTINE_LXC_MANAGER_PATH = "/LxcManager" |
233 | +home_path = os.environ['HOME'] |
234 | + |
235 | +LIBERTINE_LXC_MANAGER_NAME = libertine.LxcContainer.get_lxc_manager_dbus_name() |
236 | +LIBERTINE_LXC_MANAGER_PATH = libertine.LxcContainer.get_lxc_manager_dbus_path() |
237 | |
238 | |
239 | class Service(dbus.service.Object): |
240 | @@ -39,6 +41,7 @@ |
241 | def __init__(self): |
242 | self.is_pulse_setup = False |
243 | self.app_counter = Counter() |
244 | + self.operation_running = {} |
245 | |
246 | DBusGMainLoop(set_as_default=True) |
247 | try: |
248 | @@ -51,15 +54,18 @@ |
249 | super().__init__(bus_name, LIBERTINE_LXC_MANAGER_PATH) |
250 | |
251 | @dbus.service.method(LIBERTINE_LXC_MANAGER_NAME, |
252 | - in_signature='s', |
253 | - out_signature='b') |
254 | - def app_start(self, container_id): |
255 | - started = self._launch_lxc_container(container_id) |
256 | + in_signature='ss', |
257 | + out_signature='(bs)') |
258 | + def app_start(self, container_id, lxc_logfile): |
259 | + if container_id in self.operation_running and self.operation_running[container_id]: |
260 | + return (False, "Libertine container operation already running: cannot launch application.") |
261 | + |
262 | + (started, reason) = self._launch_lxc_container(container_id, lxc_logfile) |
263 | |
264 | if started: |
265 | self.app_counter[container_id] += 1 |
266 | |
267 | - return started |
268 | + return (started, reason) |
269 | |
270 | @dbus.service.method(LIBERTINE_LXC_MANAGER_NAME, |
271 | in_signature='s') |
272 | @@ -70,6 +76,26 @@ |
273 | self._stop_lxc_container(container_id) |
274 | del self.app_counter[container_id] |
275 | |
276 | + @dbus.service.method(LIBERTINE_LXC_MANAGER_NAME, |
277 | + in_signature='ss', |
278 | + out_signature='(bs)') |
279 | + def operation_start(self, container_id, lxc_log_file): |
280 | + if self.app_counter[container_id] != 0: |
281 | + return (False, "Application already running in container: cannot run operation.") |
282 | + |
283 | + (started, reason) = self._launch_lxc_container(container_id, lxc_log_file, launchable=False) |
284 | + |
285 | + if started: |
286 | + self.operation_running[container_id] = True |
287 | + |
288 | + return (started, reason) |
289 | + |
290 | + @dbus.service.method(LIBERTINE_LXC_MANAGER_NAME, |
291 | + in_signature='s') |
292 | + def operation_stop(self, container_id): |
293 | + self._stop_lxc_container(container_id) |
294 | + del self.operation_running[container_id] |
295 | + |
296 | def _setup_pulse(self): |
297 | pulse_socket_path = os.path.join(libertine.utils.get_libertine_runtime_dir(), 'pulse_socket') |
298 | |
299 | @@ -87,25 +113,36 @@ |
300 | |
301 | self.is_pulse_setup = True |
302 | |
303 | - def _launch_lxc_container(self, container_id): |
304 | - container = libertine.LxcContainer.LibertineLXC(container_id) |
305 | - |
306 | - if not self.is_pulse_setup: |
307 | + def _launch_lxc_container(self, container_id, lxc_log_file, launchable=True): |
308 | + container = libertine.LxcContainer.lxc_container(container_id) |
309 | + |
310 | + if not container.defined: |
311 | + return (False, "Container {} is not valid".format(container_id)) |
312 | + |
313 | + if launchable and not self.is_pulse_setup: |
314 | self._setup_pulse() |
315 | |
316 | - if not container.is_running(): |
317 | - try: |
318 | - container.start_container(True) |
319 | - except RuntimeError as e: |
320 | - print("%s" % e) |
321 | - return False |
322 | - |
323 | - return True |
324 | + if not container.running: |
325 | + if launchable: |
326 | + self._dynamic_bind_mounts(container) |
327 | + |
328 | + return libertine.LxcContainer.lxc_start(container, lxc_log_file) |
329 | + |
330 | + return (True, "") |
331 | |
332 | def _stop_lxc_container(self, container_id): |
333 | - container = libertine.LxcContainer.LibertineLXC(container_id) |
334 | - if container.is_running(): |
335 | - container.stop_container() |
336 | + container = libertine.LxcContainer.lxc_container(container_id) |
337 | + |
338 | + libertine.LxcContainer.lxc_stop(container) |
339 | + |
340 | + |
341 | + def _dynamic_bind_mounts(self, container): |
342 | + for user_dir in libertine.utils.get_common_xdg_user_directories(): |
343 | + xdg_user_dir_entry = ( |
344 | + "%s %s/%s none bind,create=dir,optional" |
345 | + % (user_dir[0], home_path.strip('/'), user_dir[1]) |
346 | + ) |
347 | + container.append_config_item("lxc.mount.entry", xdg_user_dir_entry) |
348 | |
349 | |
350 | def sigterm(self): |
351 | |
352 | === modified file 'tools/update-puritine-containers' |
353 | --- tools/update-puritine-containers 2016-09-06 12:36:47 +0000 |
354 | +++ tools/update-puritine-containers 2016-10-07 15:43:27 +0000 |
355 | @@ -29,21 +29,6 @@ |
356 | puritine_symlink_farm_file = os.path.join(puritine_hook_dir, 'PuritineSymlinkFarm.json') |
357 | puritine_click_config_file = os.path.join('libertine-config', 'libertine', 'ContainersConfig.json') |
358 | |
359 | -def set_session_dbus_env_var(): |
360 | - if not 'DBUS_SESSION_BUS_ADDRESS' in os.environ: |
361 | - dbus_session_path = os.path.join('/', 'run', 'user', str(os.getuid()), 'dbus-session') |
362 | - |
363 | - if os.path.exists(dbus_session_path): |
364 | - with open(dbus_session_path, 'r') as fd: |
365 | - dbus_session_str = fd.read() |
366 | - |
367 | - os.environ['DBUS_SESSION_BUS_ADDRESS'] = dbus_session_str.partition('DBUS_SESSION_BUS_ADDRESS=')[2].rstrip('\n') |
368 | - |
369 | - return True |
370 | - else: |
371 | - return False |
372 | - |
373 | - return True |
374 | |
375 | def symlink_farm_entries_count(): |
376 | if (puritine_symlink_farm_list and |
377 | @@ -168,7 +153,7 @@ |
378 | |
379 | if __name__ == '__main__': |
380 | puritine_symlink_farm_list = {} |
381 | - update_libertine_scope = set_session_dbus_env_var() |
382 | + update_libertine_scope = libertine.utils.set_session_dbus_env_var() |
383 | |
384 | if not os.path.exists(puritine_hook_dir): |
385 | os.makedirs(puritine_hook_dir) |
PASSED: Continuous integration, rev:307 /jenkins. canonical. com/libertine/ job/lp- libertine- ci/150/ /jenkins. canonical. com/libertine/ job/build/ 382 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=amd64, release= vivid+overlay, testname= default/ 303 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=amd64, release= xenial+ overlay, testname= default/ 303 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=amd64, release= yakkety, testname= default/ 303 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=i386, release= vivid+overlay, testname= default/ 303 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=i386, release= xenial+ overlay, testname= default/ 303 /jenkins. canonical. com/libertine/ job/test- 0-autopkgtest/ label=i386, release= yakkety, testname= default/ 303 /jenkins. canonical. com/libertine/ job/lp- generic- update- mp/291/ console /jenkins. canonical. com/libertine/ job/build- 0-fetch/ 384 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= vivid+overlay/ 366 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= xenial+ overlay/ 366 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= yakkety/ 366 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= vivid+overlay/ 365 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= vivid+overlay/ 365/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= xenial+ overlay/ 365 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= xenial+ overlay/ 365/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= yakkety/ 365 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= yakkety/ 365/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= vivid+overlay/ 365 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= vivid+overlay/ 365/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= xenial+ overlay/ 365 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= xenial+ overlay/ 365/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= yakkety/ 365 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= yakkety/ 365/artifact/ output/ *zip*/output. zip
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild: /jenkins. canonical. com/libertine/ job/lp- libertine- ci/150/ rebuild
https:/