• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SwissDataScienceCenter / renku-python / 9058668052

13 May 2024 07:05AM UTC coverage: 77.713% (-8.4%) from 86.115%
9058668052

Pull #3727

github

web-flow
Merge 128d38387 into 050ed61bf
Pull Request #3727: fix: don't fail session launch when gitlab couldn't be reached

15 of 29 new or added lines in 3 files covered. (51.72%)

2594 existing lines in 125 files now uncovered.

23893 of 30745 relevant lines covered (77.71%)

3.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

40.53
/renku/core/session/renkulab.py
1
#  Copyright Swiss Data Science Center (SDSC). A partnership between
2
#  École Polytechnique Fédérale de Lausanne (EPFL) and
3
#  Eidgenössische Technische Hochschule Zürich (ETHZ).
4
#
5
# Licensed under the Apache License, Version 2.0 (the "License");
6
# you may not use this file except in compliance with the License.
7
# You may obtain a copy of the License at
8
#
9
#     http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
# See the License for the specific language governing permissions and
15
# limitations under the License.
16
"""Docker based interactive session provider."""
7✔
17

18
import pty
7✔
19
import urllib
7✔
20
import webbrowser
7✔
21
from datetime import datetime
7✔
22
from pathlib import Path
7✔
23
from time import monotonic, sleep
7✔
24
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
7✔
25

26
from renku.command.command_builder.command import inject
7✔
27
from renku.core import errors
7✔
28
from renku.core.config import get_value
7✔
29
from renku.core.constant import ProviderPriority
7✔
30
from renku.core.interface.storage_service_gateway import IStorageService
7✔
31
from renku.core.login import read_renku_token
7✔
32
from renku.core.plugin import hookimpl
7✔
33
from renku.core.session.utils import get_renku_project_name, get_renku_url
7✔
34
from renku.core.util import communication, requests
7✔
35
from renku.core.util.git import get_remote
7✔
36
from renku.core.util.jwt import is_token_expired
7✔
37
from renku.core.util.ssh import SystemSSHConfig
7✔
38
from renku.domain_model.project_context import project_context
7✔
39
from renku.domain_model.session import IHibernatingSessionProvider, Session, SessionStopStatus
7✔
40

41
if TYPE_CHECKING:
7✔
42
    from renku.core.dataset.providers.models import ProviderParameter
×
43

44

45
class RenkulabSessionProvider(IHibernatingSessionProvider):
7✔
46
    """A session provider that uses the notebook service API to launch sessions."""
47

48
    DEFAULT_TIMEOUT_SECONDS = 300
7✔
49
    # NOTE: Give the renkulab provider the lowest priority so that it's checked last
50
    priority: ProviderPriority = ProviderPriority.LOWEST
7✔
51

52
    def __init__(self):
7✔
53
        self.__renku_url: Optional[str] = None
7✔
54
        self.__notebooks_url: Optional[str] = None
7✔
55
        self._force_build: bool = False
7✔
56

57
    def _renku_url(self) -> str:
7✔
58
        """Get the URL of the renku instance."""
59
        if not self.__renku_url:
×
60
            renku_url = get_renku_url()
×
61
            if not renku_url:
×
62
                raise errors.RenkulabSessionGetUrlError()
×
63
            self.__renku_url = renku_url
×
64
        return self.__renku_url
×
65

66
    def _notebooks_url(self) -> str:
7✔
67
        """Get the url of the notebooks API."""
68
        if not self.__notebooks_url:
1✔
69
            url = urllib.parse.urljoin(self._renku_url(), "api/notebooks")
1✔
70
            self.__notebooks_url = url
1✔
71
        return self.__notebooks_url
1✔
72

73
    def _get_token(self) -> str:
7✔
74
        """Get the JWT token used to authenticate against Renku."""
75
        token = read_renku_token(endpoint=self._renku_url())
×
76
        if token is None:
×
77
            raise errors.AuthenticationError("Please run the renku login command to authenticate with Renku.")
×
78
        elif is_token_expired(token):
×
79
            raise errors.AuthenticationError(
×
80
                "Authentication token is expired: Please run the renku login command to authenticate with Renku."
81
            )
82
        return token
×
83

84
    def _auth_header(self) -> Dict[str, str]:
7✔
85
        """Get the authentication header with the JWT token or cookie needed to authenticate with Renku."""
86
        return {"Authorization": f"Bearer {self._get_token()}"}
×
87

88
    @staticmethod
7✔
89
    def _get_renku_project_name_parts():
7✔
90
        repository = project_context.repository
1✔
91
        if project_context.remote.name and project_context.remote.owner:
1✔
92
            if get_remote(repository, name="renku-backup-origin") and project_context.remote.owner.startswith("repos/"):
×
93
                owner = project_context.remote.owner.replace("repos/", "", 1)
×
94
            else:
95
                owner = project_context.remote.owner
×
96
            return {
×
97
                "namespace": owner,
98
                "project": project_context.remote.name,
99
            }
100
        else:
101
            # INFO: In this case the owner/name split is not available. The project name is then
102
            # derived from the combined name of the remote and has to be split up in the two parts.
103
            parts = get_renku_project_name().split("/")
1✔
104
            return {
1✔
105
                "namespace": "/".join(parts[:-1]),
106
                "project": parts[:-1],
107
            }
108

109
    def _wait_for_session_status(
7✔
110
        self,
111
        name: Optional[str],
112
        status: str,
113
    ):
114
        if not name:
×
115
            return
×
116
        start = monotonic()
×
117
        while monotonic() - start < self.DEFAULT_TIMEOUT_SECONDS:
×
118
            res = self._send_renku_request(
×
119
                "get",
120
                f"{self._notebooks_url()}/servers/{name}",
121
                headers=self._auth_header(),
122
            )
123
            if res.status_code == 404 and status == "stopping":
×
124
                return
×
125
            if res.status_code in [200, 204] and status != "stopping":
×
126
                if res.json().get("status", {}).get("state") == status:
×
127
                    return
×
128
            sleep(5)
×
129
        raise errors.RenkulabSessionError(f"Waiting for the session {name} to reach status {status} timed out.")
×
130

131
    def _wait_for_image(
7✔
132
        self,
133
        image_name: str,
134
        config: Optional[Dict[str, Any]],
135
    ):
136
        """Check if an image exists, and if it does not wait for it to appear.
137

138
        Timeout after a specific period of time.
139
        """
140
        start = monotonic()
×
141
        while monotonic() - start < self.DEFAULT_TIMEOUT_SECONDS:
×
142
            if self.find_image(image_name, config):
×
143
                return
×
144
            sleep(5)
×
145
        raise errors.RenkulabSessionError(
×
146
            f"Waiting for the image {image_name} to be built timed out."
147
            "Are you sure that the image was successfully built? This could be the result "
148
            "of problems with your Dockerfile."
149
        )
150

151
    def pre_start_checks(self, ssh: bool = False, **kwargs):
7✔
152
        """Check if the state of the repository is as expected before starting a session."""
153
        from renku.core.session.session import ssh_setup
1✔
154

155
        repository = project_context.repository
1✔
156

157
        if repository.is_dirty():
1✔
158
            communication.confirm(
×
159
                "You have new uncommitted or untracked changes to your repository. "
160
                "Renku can automatically commit these changes so that it builds "
161
                "the correct environment for your session. Do you wish to proceed?",
162
                abort=True,
163
            )
164
            repository.add(all=True)
×
165
            repository.commit("Automated commit by Renku CLI.")
×
166

167
        if ssh:
1✔
168
            system_config = SystemSSHConfig()
1✔
169

170
            if not system_config.is_configured:
1✔
171
                if communication.confirm(
×
172
                    "Your system is not set up for SSH connections to Renkulab. Would you like to set it up?"
173
                ):
174
                    ssh_setup()
×
175
                    self._force_build = True
×
176
                else:
177
                    raise errors.RenkulabSessionError(
×
178
                        "Can't run ssh session without setting up Renku SSH support. Run without '--ssh' or "
179
                        "run 'renku session ssh-setup'."
180
                    )
181

182
            if system_config.setup_session_keys():
1✔
183
                self._force_build = True
1✔
184

185
    def _cleanup_ssh_connection_configs(
7✔
186
        self, project_name: str, running_sessions: Optional[List[Session]] = None
187
    ) -> None:
188
        """Cleanup leftover SSH connections that aren't valid anymore.
189

190
        Args:
191
            project_name(str): Name of the project.
192
            running_sessions(List[Session], optional): List of running sessions to check against, otherwise will be
193
                gotten from the server.
194
        """
195
        if not running_sessions:
×
196
            running_sessions = self.session_list(project_name="", ssh_garbage_collection=False)
×
197

198
        system_config = SystemSSHConfig()
×
199

200
        name = self._project_name_from_full_project_name(project_name)
×
201

202
        session_config_paths = [system_config.session_config_path(name, s.id) for s in running_sessions]
×
203

204
        for path in system_config.renku_ssh_root.glob(f"00-{name}*.conf"):
×
205
            if path not in session_config_paths:
×
206
                path.unlink()
×
207

208
    @staticmethod
7✔
209
    def _remote_head_hexsha():
7✔
210
        remote = get_remote(repository=project_context.repository)
×
211

212
        if remote is None:
×
213
            raise errors.GitRemoteNotFoundError()
×
214

215
        return remote.head
×
216

217
    def _send_renku_request(self, verb: str, *args, **kwargs):
7✔
218
        response = getattr(requests, verb)(*args, **kwargs)
×
219
        if response.status_code == 401:
×
220
            # NOTE: Check if logged in to KC but not the Renku UI
221
            token = read_renku_token(endpoint=self._renku_url())
×
222
            if token and not is_token_expired(token):
×
223
                raise errors.AuthenticationError(
×
224
                    f"Please log in the Renku UI at {self._renku_url()} to complete authentication with Renku"
225
                )
226
            raise errors.AuthenticationError(
×
227
                "Please run the renku login command to authenticate with Renku or to refresh your expired credentials."
228
            )
229
        return response
×
230

231
    @staticmethod
7✔
232
    def _project_name_from_full_project_name(project_name: str) -> str:
7✔
233
        """Get just project name of project name if in owner/name form."""
234
        if "/" not in project_name:
1✔
235
            return project_name
1✔
236
        return project_name.rsplit("/", 1)[1]
×
237

238
    @property
7✔
239
    def name(self) -> str:
7✔
240
        """Return session provider's name."""
241
        return "renkulab"
7✔
242

243
    def is_remote_provider(self) -> bool:
7✔
244
        """Return True for remote providers (i.e. not local Docker)."""
245
        return True
1✔
246

247
    def build_image(self, image_descriptor: Path, image_name: str, config: Optional[Dict[str, Any]]):
7✔
248
        """Builds the container image."""
249
        if self.find_image(image_name, config=config):
×
250
            return
×
251
        repository = project_context.repository
×
252
        if repository.head.commit.hexsha != self._remote_head_hexsha():
×
253
            repository.push()
×
254
        self._wait_for_image(image_name=image_name, config=config)
×
255

256
    def find_image(self, image_name: str, config: Optional[Dict[str, Any]]) -> bool:
7✔
257
        """Find the given container image."""
258
        return (
×
259
            self._send_renku_request(
260
                "get",
261
                f"{self._notebooks_url()}/images",
262
                headers=self._auth_header(),
263
                params={"image_url": image_name},
264
            ).status_code
265
            == 200
266
        )
267

268
    def get_cloudstorage(self):
7✔
269
        """Get cloudstorage configured for the project."""
270
        storage_service = cast(IStorageService, inject.instance(IStorageService))
1✔
271

272
        try:
1✔
273
            project_id = storage_service.project_id
1✔
NEW
274
        except errors.ProjectNotFound:
×
NEW
275
            project_id = None
×
276

277
        if project_id is None:
1✔
NEW
278
            communication.warn("Skipping cloud storage mounting as project couldn't be loaded from gitlab.")
×
NEW
279
            return []
×
280

281
        storages = storage_service.list(project_id)
1✔
282

283
        if not storages:
1✔
284
            return []
×
285

286
        storages_to_mount = []
1✔
287
        for storage, private_fields in storages:
1✔
288
            if not communication.confirm(f"Do you want to mount storage '{storage.name}'({storage.storage_type})?"):
1✔
289
                continue
1✔
290
            if storage.private:
×
291
                # check for credentials for user
292
                private_field_names = [f["name"] for f in private_fields]
×
293
                for name, value in storage.configuration.items():
×
294
                    if name not in private_field_names:
×
295
                        continue
×
296
                    field = next(f for f in private_fields if f["name"] == name)
×
297

298
                    secret = communication.prompt(f"{field['help']}\nPlease provide a value for secret '{name}'")
×
299
                    storage.configuration[name] = secret
×
300

NEW
301
            storages_to_mount.append(
×
302
                {
303
                    "storage_id": storage.storage_id,
304
                    "configuration": storage.configuration,
305
                }
306
            )
307

308
        return storages_to_mount
1✔
309

310
    @hookimpl
7✔
311
    def session_provider(self) -> IHibernatingSessionProvider:
7✔
312
        """Supported session provider.
313

314
        Returns:
315
            a reference to ``self``.
316
        """
317
        return self
7✔
318

319
    def get_start_parameters(self) -> List["ProviderParameter"]:
7✔
320
        """Returns parameters that can be set for session start."""
321
        from renku.core.dataset.providers.models import ProviderParameter
7✔
322

323
        return [
7✔
324
            ProviderParameter("ssh", help="Enable ssh connections to the session.", is_flag=True),
325
        ]
326

327
    def get_open_parameters(self) -> List["ProviderParameter"]:
7✔
328
        """Returns parameters that can be set for session open."""
329
        from renku.core.dataset.providers.models import ProviderParameter
7✔
330

331
        return [
7✔
332
            ProviderParameter("ssh", help="Open a remote terminal through SSH.", is_flag=True),
333
        ]
334

335
    def session_list(self, project_name: str, ssh_garbage_collection: bool = True) -> List[Session]:
7✔
336
        """Lists all the sessions currently running by the given session provider.
337

338
        Returns:
339
            list: a list of sessions.
340
        """
341
        sessions_res = self._send_renku_request(
×
342
            "get",
343
            f"{self._notebooks_url()}/servers",
344
            headers=self._auth_header(),
345
            params=self._get_renku_project_name_parts(),
346
        )
347
        if sessions_res.status_code == 200:
×
348
            sessions = [
×
349
                Session(
350
                    id=session["name"],
351
                    status=session.get("status", {}).get("state", "unknown"),
352
                    url=self.session_url(session["name"]),
353
                    start_time=datetime.fromisoformat(session.get("started")),
354
                    commit=session.get("annotations", {}).get("renku.io/commit-sha"),
355
                    branch=session.get("annotations", {}).get("renku.io/branch"),
356
                    provider="renkulab",
357
                    ssh_enabled=get_value("renku", "ssh_supported") == "true"
358
                    or project_context.project.template_metadata.ssh_supported,
359
                )
360
                for session in sessions_res.json().get("servers", {}).values()
361
            ]
362
            if ssh_garbage_collection:
×
363
                self._cleanup_ssh_connection_configs(project_name, running_sessions=sessions)
×
364
            return sessions
×
365
        return []
×
366

367
    def session_start(
7✔
368
        self,
369
        image_name: str,
370
        project_name: str,
371
        config: Optional[Dict[str, Any]],
372
        cpu_request: Optional[float] = None,
373
        mem_request: Optional[str] = None,
374
        disk_request: Optional[str] = None,
375
        gpu_request: Optional[str] = None,
376
        ssh: bool = False,
377
        **kwargs,
378
    ) -> Tuple[str, str]:
379
        """Creates an interactive session.
380

381
        Returns:
382
            Tuple[str, str]: Provider message and a possible warning message.
383
        """
384
        ssh_supported = (
1✔
385
            get_value("renku", "ssh_supported") == "true" or project_context.project.template_metadata.ssh_supported
386
        )
387
        if ssh and not ssh_supported:
1✔
388
            raise errors.RenkulabSessionError(
1✔
389
                "Cannot start session with SSH support because this project doesn't support SSH."
390
            )
391

392
        repository = project_context.repository
1✔
393

394
        session_commit = repository.head.commit.hexsha
1✔
395
        if repository.head.commit.hexsha != self._remote_head_hexsha():
1✔
396
            # INFO: The user is registered, the image is pinned or already available
397
            # but the local repository is not fully in sync with the remote
398
            communication.confirm(
×
399
                "You have unpushed commits that will not be present in your session. "
400
                "Renku can automatically push these commits so that they are present "
401
                "in the session you are launching. Do you wish to proceed?",
402
                abort=True,
403
            )
404
            repository.push()
×
405
        if ssh:
1✔
406
            self._cleanup_ssh_connection_configs(project_name)
1✔
407

408
        server_options: Dict[str, Union[str, float]] = {}
1✔
409
        if cpu_request:
1✔
410
            server_options["cpu_request"] = cpu_request
×
411
        if mem_request:
1✔
412
            server_options["mem_request"] = mem_request
×
413
        if gpu_request:
1✔
414
            server_options["gpu_request"] = int(gpu_request)
×
415
        if disk_request:
1✔
416
            server_options["disk_request"] = disk_request
×
417
        payload = {
1✔
418
            "image": image_name,
419
            "commit_sha": session_commit,
420
            "serverOptions": server_options,
421
            "branch": repository.active_branch.name if repository.active_branch else "master",
422
            "cloudstorage": self.get_cloudstorage(),
423
            **self._get_renku_project_name_parts(),
424
        }
425
        res = self._send_renku_request(
1✔
426
            "post",
427
            f"{self._notebooks_url()}/servers",
428
            headers=self._auth_header(),
429
            json=payload,
430
        )
431
        if res.status_code in [200, 201]:
1✔
432
            session_name = res.json()["name"]
1✔
433
            self._wait_for_session_status(session_name, "running")
1✔
434
            if ssh:
1✔
435
                name = self._project_name_from_full_project_name(project_name)
1✔
436
                connection = SystemSSHConfig().setup_session_config(name, session_name)
1✔
437
                return (
1✔
438
                    f"Session {session_name} successfully started, use 'renku session open --ssh {session_name}'"
439
                    f" or 'ssh {connection}' to connect to it",
440
                    "",
441
                )
442
            return (
×
443
                f"Session {session_name} successfully started, use 'renku session open {session_name}'"
444
                " to connect to it",
445
                "",
446
            )
447
        raise errors.RenkulabSessionError("Cannot start session via the notebook service because " + res.text)
×
448

449
    def session_stop(self, project_name: str, session_name: Optional[str], stop_all: bool) -> SessionStopStatus:
7✔
450
        """Stops all sessions (for the given project) or a specific interactive session."""
451
        responses = []
×
452
        sessions = self.session_list(project_name=project_name)
×
453
        n_sessions = len(sessions)
×
454

455
        if n_sessions == 0:
×
456
            return SessionStopStatus.NO_ACTIVE_SESSION
×
457

458
        if stop_all:
×
459
            for session in sessions:
×
460
                responses.append(
×
461
                    self._send_renku_request(
462
                        "delete",
463
                        f"{self._notebooks_url()}/servers/{session.id}",
464
                        headers=self._auth_header(),
465
                    )
466
                )
467
                self._wait_for_session_status(session.id, "stopping")
×
468
        elif session_name:
×
469
            responses.append(
×
470
                self._send_renku_request(
471
                    "delete",
472
                    f"{self._notebooks_url()}/servers/{session_name}",
473
                    headers=self._auth_header(),
474
                )
475
            )
476
            self._wait_for_session_status(session_name, "stopping")
×
477
        elif n_sessions == 1:
×
478
            responses.append(
×
479
                self._send_renku_request(
480
                    "delete",
481
                    f"{self._notebooks_url()}/servers/{sessions[0].id}",
482
                    headers=self._auth_header(),
483
                )
484
            )
485
            self._wait_for_session_status(sessions[0].id, "stopping")
×
486
        else:
487
            return SessionStopStatus.NAME_NEEDED
×
488

489
        self._cleanup_ssh_connection_configs(project_name)
×
490

491
        n_successfully_stopped = len([r for r in responses if r.status_code == 204])
×
492

493
        return SessionStopStatus.SUCCESSFUL if n_successfully_stopped == n_sessions else SessionStopStatus.FAILED
×
494

495
    def session_open(
7✔
496
        self,
497
        project_name: str,
498
        session_name: Optional[str],
499
        ssh: bool = False,
500
        **kwargs,
501
    ) -> bool:
502
        """Open a given interactive session.
503

504
        Args:
505
            project_name(str): Renku project name.
506
            session_name(Optional[str]): The unique id of the interactive session.
507
            ssh(bool): Whether to open an SSH connection or a normal browser interface.
508
        """
509
        sessions = self.session_list(project_name="")
×
510
        system_config = SystemSSHConfig()
×
511
        name = self._project_name_from_full_project_name(project_name)
×
512
        ssh_prefix = f"{system_config.renku_host}-{name}-"
×
513

514
        if not session_name:
×
515
            if len(sessions) == 1:
×
516
                session_name = sessions[0].id
×
517
            else:
518
                return False
×
519

520
        if session_name.startswith(ssh_prefix):
×
521
            # NOTE: User passed in ssh connection name instead of session id by accident
522
            session_name = session_name.replace(ssh_prefix, "", 1)
×
523

524
        if not any(s.id == session_name for s in sessions):
×
525
            return False
×
526

527
        if ssh:
×
528
            ssh_setup = True
×
529
            if not system_config.is_configured:
×
530
                raise errors.RenkulabSessionError(
×
531
                    "SSH not set up for session. Run without '--ssh' or "
532
                    "run 'renku session ssh-setup' and start the session again."
533
                )
534
            elif not system_config.session_config_path(name, session_name).exists():
×
535
                # NOTE: Session wasn't launched from CLI
536
                if system_config.is_session_configured(session_name):
×
537
                    raise errors.RenkulabSessionError(
×
538
                        "Session wasn't started using 'renku session start --ssh ...' "
539
                        "and is not configured for SSH access by you."
540
                    )
541
                communication.info(f"Setting up SSH connection config for session {session_name}")
×
542
                system_config.setup_session_config(name, session_name)
×
543
                ssh_setup = False
×
544

545
            exit_code = pty.spawn(["ssh", session_name])
×
546

547
            if exit_code > 0 and not ssh_setup:
×
548
                # NOTE: We tried to connect to SSH even though it wasn't started from CLI
549
                # This failed, so we'll remove the temporary connection information.
550
                if system_config.session_config_path(project_name, session_name).exists():
×
551
                    system_config.session_config_path(project_name, session_name).unlink()
×
552
        else:
553
            url = self.session_url(session_name)
×
554

555
            webbrowser.open(url)
×
556
        return True
×
557

558
    def session_url(self, session_name: str) -> str:
7✔
559
        """Get the URL of the interactive session."""
560
        project_name_parts = self._get_renku_project_name_parts()
×
561
        session_url_parts = [
×
562
            "projects",
563
            project_name_parts["namespace"],
564
            project_name_parts["project"],
565
            "sessions/show",
566
            session_name,
567
        ]
568
        return urllib.parse.urljoin(self._renku_url(), "/".join(session_url_parts))
×
569

570
    def force_build_image(self, **kwargs) -> bool:
7✔
571
        """Whether we should force build the image directly or check for an existing image first."""
572
        return self._force_build
1✔
573

574
    def session_pause(self, project_name: str, session_name: Optional[str], **_) -> SessionStopStatus:
7✔
575
        """Pause all sessions (for the given project) or a specific interactive session."""
576

577
        def pause(session_name: str):
×
578
            result = self._send_renku_request(
×
579
                "patch",
580
                f"{self._notebooks_url()}/servers/{session_name}",
581
                headers=self._auth_header(),
582
                json={"state": "hibernated"},
583
            )
584

585
            self._wait_for_session_status(session_name, "hibernated")
×
586

587
            return result
×
588

589
        sessions = self.session_list(project_name=project_name)
×
590
        n_sessions = len(sessions)
×
591

592
        if n_sessions == 0:
×
593
            return SessionStopStatus.NO_ACTIVE_SESSION
×
594

595
        if session_name:
×
596
            response = pause(session_name)
×
597
        elif n_sessions == 1:
×
598
            response = pause(sessions[0].name)
×
599
        else:
600
            return SessionStopStatus.NAME_NEEDED
×
601

602
        return SessionStopStatus.SUCCESSFUL if response.status_code == 204 else SessionStopStatus.FAILED
×
603

604
    def session_resume(self, project_name: str, session_name: Optional[str], **kwargs) -> bool:
7✔
605
        """Resume a paused session.
606

607
        Args:
608
            project_name(str): Renku project name.
609
            session_name(Optional[str]): The unique id of the interactive session.
610
        """
611
        sessions = self.session_list(project_name="")
×
612
        system_config = SystemSSHConfig()
×
613
        name = self._project_name_from_full_project_name(project_name)
×
614
        ssh_prefix = f"{system_config.renku_host}-{name}-"
×
615

616
        if not session_name:
×
617
            if len(sessions) == 1:
×
618
                session_name = sessions[0].name
×
619
            else:
620
                return False
×
621
        else:
622
            if session_name.startswith(ssh_prefix):
×
623
                # NOTE: User passed in ssh connection name instead of session id by accident
624
                session_name = session_name.replace(ssh_prefix, "", 1)
×
625

626
            if not any(s.name == session_name for s in sessions):
×
627
                return False
×
628

629
        self._send_renku_request(
×
630
            "patch",
631
            f"{self._notebooks_url()}/servers/{session_name}",
632
            headers=self._auth_header(),
633
            json={"state": "running"},
634
        )
635

636
        self._wait_for_session_status(session_name, "running")
×
637

638
        return True
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc