dstack 0.19.4rc3__py3-none-any.whl → 0.19.6rc1__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.
- dstack/_internal/cli/commands/attach.py +22 -20
- dstack/_internal/cli/commands/offer.py +116 -0
- dstack/_internal/cli/main.py +2 -0
- dstack/_internal/cli/services/configurators/base.py +1 -2
- dstack/_internal/cli/services/configurators/fleet.py +43 -20
- dstack/_internal/cli/services/configurators/run.py +3 -3
- dstack/_internal/cli/utils/run.py +43 -38
- dstack/_internal/core/backends/aws/auth.py +1 -2
- dstack/_internal/core/backends/aws/compute.py +24 -9
- dstack/_internal/core/backends/aws/configurator.py +2 -3
- dstack/_internal/core/backends/aws/resources.py +10 -0
- dstack/_internal/core/backends/azure/auth.py +1 -2
- dstack/_internal/core/backends/azure/compute.py +15 -5
- dstack/_internal/core/backends/azure/configurator.py +4 -5
- dstack/_internal/core/backends/azure/resources.py +14 -0
- dstack/_internal/core/backends/base/compute.py +99 -31
- dstack/_internal/core/backends/gcp/auth.py +1 -2
- dstack/_internal/core/backends/gcp/compute.py +58 -14
- dstack/_internal/core/backends/gcp/configurator.py +2 -3
- dstack/_internal/core/backends/gcp/features/tcpx.py +31 -0
- dstack/_internal/core/backends/gcp/resources.py +10 -0
- dstack/_internal/core/backends/nebius/compute.py +6 -2
- dstack/_internal/core/backends/nebius/configurator.py +4 -10
- dstack/_internal/core/backends/nebius/models.py +14 -1
- dstack/_internal/core/backends/nebius/resources.py +91 -10
- dstack/_internal/core/backends/oci/auth.py +1 -2
- dstack/_internal/core/backends/oci/configurator.py +1 -2
- dstack/_internal/core/backends/runpod/compute.py +1 -1
- dstack/_internal/core/errors.py +4 -0
- dstack/_internal/core/models/common.py +2 -14
- dstack/_internal/core/models/configurations.py +24 -2
- dstack/_internal/core/models/envs.py +2 -2
- dstack/_internal/core/models/fleets.py +34 -3
- dstack/_internal/core/models/gateways.py +18 -4
- dstack/_internal/core/models/instances.py +2 -1
- dstack/_internal/core/models/profiles.py +12 -0
- dstack/_internal/core/models/runs.py +6 -0
- dstack/_internal/core/models/secrets.py +1 -1
- dstack/_internal/core/models/volumes.py +17 -1
- dstack/_internal/proxy/gateway/resources/nginx/service.jinja2 +3 -3
- dstack/_internal/proxy/gateway/services/nginx.py +0 -1
- dstack/_internal/proxy/gateway/services/registry.py +0 -1
- dstack/_internal/server/background/tasks/process_instances.py +12 -9
- dstack/_internal/server/background/tasks/process_running_jobs.py +66 -15
- dstack/_internal/server/routers/fleets.py +22 -0
- dstack/_internal/server/routers/runs.py +1 -0
- dstack/_internal/server/schemas/fleets.py +12 -2
- dstack/_internal/server/schemas/runner.py +6 -0
- dstack/_internal/server/schemas/runs.py +3 -0
- dstack/_internal/server/services/docker.py +1 -2
- dstack/_internal/server/services/fleets.py +30 -12
- dstack/_internal/server/services/gateways/__init__.py +1 -0
- dstack/_internal/server/services/instances.py +3 -1
- dstack/_internal/server/services/jobs/__init__.py +1 -2
- dstack/_internal/server/services/jobs/configurators/base.py +17 -8
- dstack/_internal/server/services/locking.py +16 -1
- dstack/_internal/server/services/projects.py +1 -2
- dstack/_internal/server/services/proxy/repo.py +1 -2
- dstack/_internal/server/services/runner/client.py +3 -0
- dstack/_internal/server/services/runs.py +19 -16
- dstack/_internal/server/services/services/__init__.py +1 -2
- dstack/_internal/server/services/volumes.py +29 -2
- dstack/_internal/server/statics/00a6e1fb461ed2929fb9.png +0 -0
- dstack/_internal/server/statics/0cae4d9f0a36034984a7.png +0 -0
- dstack/_internal/server/statics/391de232cc0e30cae513.png +0 -0
- dstack/_internal/server/statics/4e0eead8c1a73689ef9d.svg +1 -0
- dstack/_internal/server/statics/544afa2f63428c2235b0.png +0 -0
- dstack/_internal/server/statics/54a4f50f74c6b9381530.svg +7 -0
- dstack/_internal/server/statics/68dd1360a7d2611e0132.svg +4 -0
- dstack/_internal/server/statics/69544b4c81973b54a66f.png +0 -0
- dstack/_internal/server/statics/77a8b02b17af19e39266.png +0 -0
- dstack/_internal/server/statics/83a93a8871c219104367.svg +9 -0
- dstack/_internal/server/statics/8f28bb8e9999e5e6a48b.svg +4 -0
- dstack/_internal/server/statics/9124086961ab8c366bc4.svg +9 -0
- dstack/_internal/server/statics/9a9ebaeb54b025dbac0a.svg +5 -0
- dstack/_internal/server/statics/a3428392dc534f3b15c4.svg +7 -0
- dstack/_internal/server/statics/ae22625574d69361f72c.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-144x144.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-192x192.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-256x256.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-36x36.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-384x384.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-48x48.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-512x512.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-72x72.png +0 -0
- dstack/_internal/server/statics/assets/android-chrome-96x96.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-1024x1024.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-114x114.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-120x120.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-144x144.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-152x152.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-167x167.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-180x180.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-57x57.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-60x60.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-72x72.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-76x76.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon-precomposed.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-icon.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1125x2436.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1136x640.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1170x2532.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1179x2556.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1242x2208.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1242x2688.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1284x2778.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1290x2796.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1334x750.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1488x2266.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1536x2048.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1620x2160.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1640x2160.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1668x2224.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1668x2388.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-1792x828.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2048x1536.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2048x2732.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2160x1620.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2160x1640.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2208x1242.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2224x1668.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2266x1488.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2388x1668.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2436x1125.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2532x1170.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2556x1179.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2688x1242.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2732x2048.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2778x1284.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-2796x1290.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-640x1136.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-750x1334.png +0 -0
- dstack/_internal/server/statics/assets/apple-touch-startup-image-828x1792.png +0 -0
- dstack/_internal/server/statics/assets/browserconfig.xml +12 -0
- dstack/_internal/server/statics/assets/favicon-16x16.png +0 -0
- dstack/_internal/server/statics/assets/favicon-32x32.png +0 -0
- dstack/_internal/server/statics/assets/favicon-48x48.png +0 -0
- dstack/_internal/server/statics/assets/favicon.ico +0 -0
- dstack/_internal/server/statics/assets/manifest.webmanifest +67 -0
- dstack/_internal/server/statics/assets/mstile-144x144.png +0 -0
- dstack/_internal/server/statics/assets/mstile-150x150.png +0 -0
- dstack/_internal/server/statics/assets/mstile-310x150.png +0 -0
- dstack/_internal/server/statics/assets/mstile-310x310.png +0 -0
- dstack/_internal/server/statics/assets/mstile-70x70.png +0 -0
- dstack/_internal/server/statics/assets/yandex-browser-50x50.png +0 -0
- dstack/_internal/server/statics/assets/yandex-browser-manifest.json +9 -0
- dstack/_internal/server/statics/b7ae68f44193474fc578.png +0 -0
- dstack/_internal/server/statics/d2f008c75b2b5b191f3f.png +0 -0
- dstack/_internal/server/statics/d44c33e1b92e05c379fd.png +0 -0
- dstack/_internal/server/statics/dd43ff0552815179d7ab.png +0 -0
- dstack/_internal/server/statics/dd4e7166c0b9aac197d7.png +0 -0
- dstack/_internal/server/statics/e30b27916930d43d2271.png +0 -0
- dstack/_internal/server/statics/e467d7d60aae81ab198b.svg +6 -0
- dstack/_internal/server/statics/eb9b344b73818fe2b71a.png +0 -0
- dstack/_internal/server/statics/f517dd626eb964120de0.png +0 -0
- dstack/_internal/server/statics/f958aecddee5d8e3222c.png +0 -0
- dstack/_internal/server/statics/index.html +3 -0
- dstack/_internal/server/statics/main-8f9c66f404e9c7e7e020.css +3 -0
- dstack/_internal/server/statics/main-b4f65323f5df007e1664.js +136480 -0
- dstack/_internal/server/statics/main-b4f65323f5df007e1664.js.map +1 -0
- dstack/_internal/server/statics/manifest.json +16 -0
- dstack/_internal/server/statics/robots.txt +3 -0
- dstack/_internal/server/statics/static/media/entraID.d65d1f3e9486a8e56d24fc07b3230885.svg +9 -0
- dstack/_internal/server/statics/static/media/github.1f7102513534c83a9d8d735d2b8c12a2.svg +3 -0
- dstack/_internal/server/statics/static/media/logo.f602feeb138844eda97c8cb641461448.svg +124 -0
- dstack/_internal/server/statics/static/media/okta.12f178e6873a1100965f2a4dbd18fcec.svg +2 -0
- dstack/_internal/server/statics/static/media/theme.3994c817bb7dda191c1c9640dee0bf42.svg +3 -0
- dstack/_internal/server/testing/common.py +10 -0
- dstack/_internal/utils/tags.py +42 -0
- dstack/api/server/__init__.py +3 -1
- dstack/api/server/_fleets.py +52 -9
- dstack/api/server/_gateways.py +17 -2
- dstack/api/server/_runs.py +34 -11
- dstack/api/server/_volumes.py +2 -3
- dstack/version.py +1 -1
- {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/METADATA +2 -2
- {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/RECORD +180 -76
- dstack-0.19.4rc3.data/data/dstack/_internal/proxy/gateway/resources/nginx/00-log-format.conf +0 -1
- dstack-0.19.4rc3.data/data/dstack/_internal/proxy/gateway/resources/nginx/entrypoint.jinja2 +0 -27
- dstack-0.19.4rc3.data/data/dstack/_internal/proxy/gateway/resources/nginx/service.jinja2 +0 -88
- {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/WHEEL +0 -0
- {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/entry_points.txt +0 -0
- {dstack-0.19.4rc3.dist-info → dstack-0.19.6rc1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -69,6 +69,7 @@ class AttachCommand(APIBaseCommand):
|
|
|
69
69
|
run = self.api.runs.get(args.run_name)
|
|
70
70
|
if run is None:
|
|
71
71
|
raise CLIError(f"Run {args.run_name} not found")
|
|
72
|
+
exit_code = 0
|
|
72
73
|
try:
|
|
73
74
|
attached = run.attach(
|
|
74
75
|
ssh_identity_file=args.ssh_identity_file,
|
|
@@ -90,35 +91,36 @@ class AttachCommand(APIBaseCommand):
|
|
|
90
91
|
replica_num=args.replica,
|
|
91
92
|
job_num=args.job,
|
|
92
93
|
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
pass
|
|
94
|
+
for log in logs:
|
|
95
|
+
sys.stdout.buffer.write(log)
|
|
96
|
+
sys.stdout.buffer.flush()
|
|
97
|
+
_print_finished_message_when_available(run)
|
|
98
|
+
exit_code = get_run_exit_code(run)
|
|
99
99
|
else:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
pass
|
|
100
|
+
while True:
|
|
101
|
+
time.sleep(10)
|
|
102
|
+
except KeyboardInterrupt:
|
|
103
|
+
console.print("\nDetached")
|
|
105
104
|
finally:
|
|
106
105
|
run.detach()
|
|
107
106
|
# TODO: Handle run resubmissions similar to dstack apply
|
|
107
|
+
exit(exit_code)
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
|
|
110
|
+
def _print_finished_message_when_available(run: Run) -> None:
|
|
111
|
+
# After reading the logs, the run may not be marked as finished immediately.
|
|
112
|
+
# Give the run some time to transition to a finished state before exiting.
|
|
113
|
+
for _ in range(30):
|
|
114
|
+
run.refresh()
|
|
115
|
+
if run.status.is_finished():
|
|
116
|
+
print_finished_message(run)
|
|
117
|
+
break
|
|
118
|
+
time.sleep(1)
|
|
119
|
+
else:
|
|
117
120
|
console.print(
|
|
118
121
|
"[error]Lost run connection. Timed out waiting for run final status."
|
|
119
122
|
" Check `dstack ps` to see if it's done or failed."
|
|
120
123
|
)
|
|
121
|
-
exit(1)
|
|
122
124
|
|
|
123
125
|
|
|
124
126
|
_IGNORED_PORTS = [DSTACK_RUNNER_HTTP_PORT]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import contextlib
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from dstack._internal.cli.commands import APIBaseCommand
|
|
7
|
+
from dstack._internal.cli.services.configurators.run import (
|
|
8
|
+
BaseRunConfigurator,
|
|
9
|
+
)
|
|
10
|
+
from dstack._internal.cli.utils.common import console
|
|
11
|
+
from dstack._internal.cli.utils.run import print_run_plan
|
|
12
|
+
from dstack._internal.core.models.configurations import (
|
|
13
|
+
ApplyConfigurationType,
|
|
14
|
+
TaskConfiguration,
|
|
15
|
+
)
|
|
16
|
+
from dstack._internal.core.models.runs import RunSpec
|
|
17
|
+
from dstack.api.utils import load_profile
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OfferConfigurator(BaseRunConfigurator):
|
|
21
|
+
# TODO: The command currently uses `BaseRunConfigurator` to register arguments.
|
|
22
|
+
# This includes --env, --retry-policy, and other arguments that are unnecessary for this command.
|
|
23
|
+
# Eventually, we should introduce a base `OfferConfigurator` that doesn't include those arguments—
|
|
24
|
+
# `BaseRunConfigurator` will inherit from `OfferConfigurator`.
|
|
25
|
+
#
|
|
26
|
+
# Additionally, it should have its own type: `ApplyConfigurationType.OFFER`.
|
|
27
|
+
TYPE = ApplyConfigurationType.TASK
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def register_args(
|
|
31
|
+
cls,
|
|
32
|
+
parser: argparse.ArgumentParser,
|
|
33
|
+
):
|
|
34
|
+
super().register_args(parser, default_max_offers=50)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# TODO: Support aggregated offers
|
|
38
|
+
# TODO: Add tests
|
|
39
|
+
class OfferCommand(APIBaseCommand):
|
|
40
|
+
NAME = "offer"
|
|
41
|
+
DESCRIPTION = "List offers"
|
|
42
|
+
|
|
43
|
+
def _register(self):
|
|
44
|
+
super()._register()
|
|
45
|
+
self._parser.add_argument(
|
|
46
|
+
"--format",
|
|
47
|
+
choices=["plain", "json"],
|
|
48
|
+
default="plain",
|
|
49
|
+
help="Output format (default: plain)",
|
|
50
|
+
)
|
|
51
|
+
self._parser.add_argument(
|
|
52
|
+
"--json",
|
|
53
|
+
action="store_const",
|
|
54
|
+
const="json",
|
|
55
|
+
dest="format",
|
|
56
|
+
help="Output in JSON format (equivalent to --format json)",
|
|
57
|
+
)
|
|
58
|
+
OfferConfigurator.register_args(self._parser)
|
|
59
|
+
|
|
60
|
+
def _command(self, args: argparse.Namespace):
|
|
61
|
+
super()._command(args)
|
|
62
|
+
conf = TaskConfiguration(commands=[":"])
|
|
63
|
+
|
|
64
|
+
configurator = OfferConfigurator(api_client=self.api)
|
|
65
|
+
configurator.apply_args(conf, args, [])
|
|
66
|
+
profile = load_profile(Path.cwd(), profile_name=args.profile)
|
|
67
|
+
|
|
68
|
+
run_spec = RunSpec(
|
|
69
|
+
configuration=conf,
|
|
70
|
+
ssh_key_pub="(dummy)",
|
|
71
|
+
profile=profile,
|
|
72
|
+
)
|
|
73
|
+
if args.format == "plain":
|
|
74
|
+
status = console.status("Getting offers...")
|
|
75
|
+
else:
|
|
76
|
+
status = contextlib.nullcontext()
|
|
77
|
+
with status:
|
|
78
|
+
run_plan = self.api.client.runs.get_plan(
|
|
79
|
+
self.api.project,
|
|
80
|
+
run_spec,
|
|
81
|
+
max_offers=args.max_offers,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
job_plan = run_plan.job_plans[0]
|
|
85
|
+
|
|
86
|
+
if args.format == "json":
|
|
87
|
+
output = {
|
|
88
|
+
"project": run_plan.project_name,
|
|
89
|
+
"user": run_plan.user,
|
|
90
|
+
"resources": job_plan.job_spec.requirements.resources.dict(),
|
|
91
|
+
"max_price": (job_plan.job_spec.requirements.max_price),
|
|
92
|
+
"spot": run_spec.configuration.spot_policy,
|
|
93
|
+
"reservation": run_plan.run_spec.configuration.reservation,
|
|
94
|
+
"offers": [],
|
|
95
|
+
"total_offers": job_plan.total_offers,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for offer in job_plan.offers:
|
|
99
|
+
output["offers"].append(
|
|
100
|
+
{
|
|
101
|
+
"backend": (
|
|
102
|
+
"ssh" if offer.backend.value == "remote" else offer.backend.value
|
|
103
|
+
),
|
|
104
|
+
"region": offer.region,
|
|
105
|
+
"instance_type": offer.instance.name,
|
|
106
|
+
"resources": offer.instance.resources.dict(),
|
|
107
|
+
"spot": offer.instance.resources.spot,
|
|
108
|
+
"price": float(offer.price),
|
|
109
|
+
"availability": offer.availability.value,
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
print(json.dumps(output, indent=2))
|
|
114
|
+
return
|
|
115
|
+
else:
|
|
116
|
+
print_run_plan(run_plan, include_run_properties=False)
|
dstack/_internal/cli/main.py
CHANGED
|
@@ -14,6 +14,7 @@ from dstack._internal.cli.commands.gateway import GatewayCommand
|
|
|
14
14
|
from dstack._internal.cli.commands.init import InitCommand
|
|
15
15
|
from dstack._internal.cli.commands.logs import LogsCommand
|
|
16
16
|
from dstack._internal.cli.commands.metrics import MetricsCommand
|
|
17
|
+
from dstack._internal.cli.commands.offer import OfferCommand
|
|
17
18
|
from dstack._internal.cli.commands.ps import PsCommand
|
|
18
19
|
from dstack._internal.cli.commands.server import ServerCommand
|
|
19
20
|
from dstack._internal.cli.commands.stats import StatsCommand
|
|
@@ -65,6 +66,7 @@ def main():
|
|
|
65
66
|
FleetCommand.register(subparsers)
|
|
66
67
|
GatewayCommand.register(subparsers)
|
|
67
68
|
InitCommand.register(subparsers)
|
|
69
|
+
OfferCommand.register(subparsers)
|
|
68
70
|
LogsCommand.register(subparsers)
|
|
69
71
|
MetricsCommand.register(subparsers)
|
|
70
72
|
PsCommand.register(subparsers)
|
|
@@ -5,7 +5,6 @@ from typing import List, Optional, Union, cast
|
|
|
5
5
|
|
|
6
6
|
from dstack._internal.cli.services.args import env_var
|
|
7
7
|
from dstack._internal.core.errors import ConfigurationError
|
|
8
|
-
from dstack._internal.core.models.common import is_core_model_instance
|
|
9
8
|
from dstack._internal.core.models.configurations import (
|
|
10
9
|
AnyApplyConfiguration,
|
|
11
10
|
ApplyConfigurationType,
|
|
@@ -100,7 +99,7 @@ class ApplyEnvVarsConfiguratorMixin:
|
|
|
100
99
|
for k, v in cast(List[EnvVarTuple], configurator_args.env_vars):
|
|
101
100
|
env[k] = v
|
|
102
101
|
for k, v in env.items():
|
|
103
|
-
if
|
|
102
|
+
if isinstance(v, EnvSentinel):
|
|
104
103
|
try:
|
|
105
104
|
env[k] = v.from_env(os.environ)
|
|
106
105
|
except ValueError as e:
|
|
@@ -17,7 +17,13 @@ from dstack._internal.cli.utils.common import (
|
|
|
17
17
|
)
|
|
18
18
|
from dstack._internal.cli.utils.fleet import get_fleets_table
|
|
19
19
|
from dstack._internal.cli.utils.rich import MultiItemStatus
|
|
20
|
-
from dstack._internal.core.errors import
|
|
20
|
+
from dstack._internal.core.errors import (
|
|
21
|
+
CLIError,
|
|
22
|
+
ConfigurationError,
|
|
23
|
+
ResourceNotExistsError,
|
|
24
|
+
ServerClientError,
|
|
25
|
+
URLNotFoundError,
|
|
26
|
+
)
|
|
21
27
|
from dstack._internal.core.models.configurations import ApplyConfigurationType
|
|
22
28
|
from dstack._internal.core.models.fleets import (
|
|
23
29
|
Fleet,
|
|
@@ -31,6 +37,7 @@ from dstack._internal.core.models.repos.base import Repo
|
|
|
31
37
|
from dstack._internal.utils.common import local_time
|
|
32
38
|
from dstack._internal.utils.logging import get_logger
|
|
33
39
|
from dstack._internal.utils.ssh import convert_ssh_key_to_pem, generate_public_key, pkey_from_str
|
|
40
|
+
from dstack.api._public import Client
|
|
34
41
|
from dstack.api.utils import load_profile
|
|
35
42
|
|
|
36
43
|
logger = get_logger(__name__)
|
|
@@ -109,11 +116,11 @@ class FleetConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
|
|
|
109
116
|
else:
|
|
110
117
|
time.sleep(1)
|
|
111
118
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
119
|
+
try:
|
|
120
|
+
with console.status("Applying plan..."):
|
|
121
|
+
fleet = _apply_plan(self.api, plan)
|
|
122
|
+
except ServerClientError as e:
|
|
123
|
+
raise CLIError(e.msg)
|
|
117
124
|
if command_args.detach:
|
|
118
125
|
console.print("Fleet configuration submitted. Exiting...")
|
|
119
126
|
return
|
|
@@ -239,32 +246,34 @@ def _print_plan_header(plan: FleetPlan):
|
|
|
239
246
|
def th(s: str) -> str:
|
|
240
247
|
return f"[bold]{s}[/bold]"
|
|
241
248
|
|
|
249
|
+
spec = plan.get_effective_spec()
|
|
250
|
+
|
|
242
251
|
configuration_table = Table(box=None, show_header=False)
|
|
243
252
|
configuration_table.add_column(no_wrap=True) # key
|
|
244
253
|
configuration_table.add_column() # value
|
|
245
254
|
|
|
246
255
|
configuration_table.add_row(th("Project"), plan.project_name)
|
|
247
256
|
configuration_table.add_row(th("User"), plan.user)
|
|
248
|
-
configuration_table.add_row(th("Configuration"),
|
|
249
|
-
configuration_table.add_row(th("Type"),
|
|
257
|
+
configuration_table.add_row(th("Configuration"), spec.configuration_path or "?")
|
|
258
|
+
configuration_table.add_row(th("Type"), spec.configuration.type)
|
|
250
259
|
|
|
251
260
|
fleet_type = "cloud"
|
|
252
|
-
nodes =
|
|
253
|
-
placement =
|
|
254
|
-
reservation =
|
|
261
|
+
nodes = spec.configuration.nodes or "-"
|
|
262
|
+
placement = spec.configuration.placement or InstanceGroupPlacement.ANY
|
|
263
|
+
reservation = spec.configuration.reservation
|
|
255
264
|
backends = None
|
|
256
|
-
if
|
|
257
|
-
backends = ", ".join(b.value for b in
|
|
265
|
+
if spec.configuration.backends is not None:
|
|
266
|
+
backends = ", ".join(b.value for b in spec.configuration.backends)
|
|
258
267
|
regions = None
|
|
259
|
-
if
|
|
260
|
-
regions = ", ".join(
|
|
268
|
+
if spec.configuration.regions is not None:
|
|
269
|
+
regions = ", ".join(spec.configuration.regions)
|
|
261
270
|
resources = None
|
|
262
|
-
if
|
|
263
|
-
resources =
|
|
264
|
-
spot_policy =
|
|
265
|
-
if
|
|
271
|
+
if spec.configuration.resources is not None:
|
|
272
|
+
resources = spec.configuration.resources.pretty_format()
|
|
273
|
+
spot_policy = spec.merged_profile.spot_policy
|
|
274
|
+
if spec.configuration.ssh_config is not None:
|
|
266
275
|
fleet_type = "ssh"
|
|
267
|
-
nodes = len(
|
|
276
|
+
nodes = len(spec.configuration.ssh_config.hosts)
|
|
268
277
|
resources = None
|
|
269
278
|
spot_policy = None
|
|
270
279
|
|
|
@@ -350,3 +359,17 @@ def _failed_provisioning(fleet: Fleet) -> bool:
|
|
|
350
359
|
if instance.status == InstanceStatus.TERMINATED:
|
|
351
360
|
return True
|
|
352
361
|
return False
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _apply_plan(api: Client, plan: FleetPlan) -> Fleet:
|
|
365
|
+
try:
|
|
366
|
+
return api.client.fleets.apply_plan(
|
|
367
|
+
project_name=api.project,
|
|
368
|
+
plan=plan,
|
|
369
|
+
)
|
|
370
|
+
except URLNotFoundError:
|
|
371
|
+
# TODO: Remove in 0.20
|
|
372
|
+
return api.client.fleets.create(
|
|
373
|
+
project_name=api.project,
|
|
374
|
+
spec=plan.spec,
|
|
375
|
+
)
|
|
@@ -92,7 +92,7 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
|
|
|
92
92
|
profile=profile,
|
|
93
93
|
)
|
|
94
94
|
|
|
95
|
-
print_run_plan(run_plan,
|
|
95
|
+
print_run_plan(run_plan, max_offers=configurator_args.max_offers)
|
|
96
96
|
|
|
97
97
|
confirm_message = "Submit a new run?"
|
|
98
98
|
stop_run_name = None
|
|
@@ -274,7 +274,7 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
|
|
|
274
274
|
console.print(f"Run [code]{conf.name}[/] deleted")
|
|
275
275
|
|
|
276
276
|
@classmethod
|
|
277
|
-
def register_args(cls, parser: argparse.ArgumentParser):
|
|
277
|
+
def register_args(cls, parser: argparse.ArgumentParser, default_max_offers: int = 3):
|
|
278
278
|
configuration_group = parser.add_argument_group(f"{cls.TYPE.value} Options")
|
|
279
279
|
configuration_group.add_argument(
|
|
280
280
|
"-n",
|
|
@@ -286,7 +286,7 @@ class BaseRunConfigurator(ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator):
|
|
|
286
286
|
"--max-offers",
|
|
287
287
|
help="Number of offers to show in the run plan",
|
|
288
288
|
type=int,
|
|
289
|
-
default=
|
|
289
|
+
default=default_max_offers,
|
|
290
290
|
)
|
|
291
291
|
cls.register_env_args(configuration_group)
|
|
292
292
|
configuration_group.add_argument(
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
from typing import Any, Dict, List, Union
|
|
1
|
+
from typing import Any, Dict, List, Optional, Union
|
|
2
2
|
|
|
3
3
|
from rich.markup import escape
|
|
4
4
|
from rich.table import Table
|
|
5
5
|
|
|
6
6
|
from dstack._internal.cli.utils.common import NO_OFFERS_WARNING, add_row_from_dict, console
|
|
7
|
-
from dstack._internal.core.models.common import is_core_model_instance
|
|
8
7
|
from dstack._internal.core.models.configurations import DevEnvironmentConfiguration
|
|
9
8
|
from dstack._internal.core.models.instances import InstanceAvailability
|
|
10
9
|
from dstack._internal.core.models.profiles import (
|
|
@@ -25,7 +24,10 @@ from dstack._internal.utils.common import (
|
|
|
25
24
|
from dstack.api import Run
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
def print_run_plan(
|
|
27
|
+
def print_run_plan(
|
|
28
|
+
run_plan: RunPlan, max_offers: Optional[int] = None, include_run_properties: bool = True
|
|
29
|
+
):
|
|
30
|
+
run_spec = run_plan.get_effective_run_spec()
|
|
29
31
|
job_plan = run_plan.job_plans[0]
|
|
30
32
|
|
|
31
33
|
props = Table(box=None, show_header=False)
|
|
@@ -40,29 +42,30 @@ def print_run_plan(run_plan: RunPlan, offers_limit: int = 3):
|
|
|
40
42
|
if job_plan.job_spec.max_duration
|
|
41
43
|
else "-"
|
|
42
44
|
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
inactivity_duration
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
retry
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
45
|
+
if include_run_properties:
|
|
46
|
+
inactivity_duration = None
|
|
47
|
+
if isinstance(run_spec.configuration, DevEnvironmentConfiguration):
|
|
48
|
+
inactivity_duration = "-"
|
|
49
|
+
if isinstance(run_spec.configuration.inactivity_duration, int):
|
|
50
|
+
inactivity_duration = format_pretty_duration(
|
|
51
|
+
run_spec.configuration.inactivity_duration
|
|
52
|
+
)
|
|
53
|
+
if job_plan.job_spec.retry is None:
|
|
54
|
+
retry = "-"
|
|
55
|
+
else:
|
|
56
|
+
retry = escape(job_plan.job_spec.retry.pretty_format())
|
|
57
|
+
|
|
58
|
+
profile = run_spec.merged_profile
|
|
59
|
+
creation_policy = profile.creation_policy
|
|
60
|
+
# FIXME: This assumes the default idle_duration is the same for client and server.
|
|
61
|
+
# If the server changes idle_duration, old clients will see incorrect value.
|
|
62
|
+
termination_policy, termination_idle_time = get_termination(
|
|
63
|
+
profile, DEFAULT_RUN_TERMINATION_IDLE_TIME
|
|
64
|
+
)
|
|
65
|
+
if termination_policy == TerminationPolicy.DONT_DESTROY:
|
|
66
|
+
idle_duration = "-"
|
|
67
|
+
else:
|
|
68
|
+
idle_duration = format_pretty_duration(termination_idle_time)
|
|
66
69
|
|
|
67
70
|
if req.spot is None:
|
|
68
71
|
spot_policy = "auto"
|
|
@@ -76,30 +79,32 @@ def print_run_plan(run_plan: RunPlan, offers_limit: int = 3):
|
|
|
76
79
|
|
|
77
80
|
props.add_row(th("Project"), run_plan.project_name)
|
|
78
81
|
props.add_row(th("User"), run_plan.user)
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
if include_run_properties:
|
|
83
|
+
props.add_row(th("Configuration"), run_spec.configuration_path)
|
|
84
|
+
props.add_row(th("Type"), run_spec.configuration.type)
|
|
81
85
|
props.add_row(th("Resources"), pretty_req)
|
|
82
|
-
props.add_row(th("Max price"), max_price)
|
|
83
|
-
props.add_row(th("Max duration"), max_duration)
|
|
84
|
-
if inactivity_duration is not None: # None means n/a
|
|
85
|
-
props.add_row(th("Inactivity duration"), inactivity_duration)
|
|
86
86
|
props.add_row(th("Spot policy"), spot_policy)
|
|
87
|
-
props.add_row(th("
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
props.add_row(th("Max price"), max_price)
|
|
88
|
+
if include_run_properties:
|
|
89
|
+
props.add_row(th("Retry policy"), retry)
|
|
90
|
+
props.add_row(th("Creation policy"), creation_policy)
|
|
91
|
+
props.add_row(th("Idle duration"), idle_duration)
|
|
92
|
+
props.add_row(th("Max duration"), max_duration)
|
|
93
|
+
if inactivity_duration is not None: # None means n/a
|
|
94
|
+
props.add_row(th("Inactivity duration"), inactivity_duration)
|
|
95
|
+
props.add_row(th("Reservation"), run_spec.configuration.reservation or "-")
|
|
91
96
|
|
|
92
97
|
offers = Table(box=None)
|
|
93
98
|
offers.add_column("#")
|
|
94
99
|
offers.add_column("BACKEND")
|
|
95
100
|
offers.add_column("REGION")
|
|
96
|
-
offers.add_column("INSTANCE")
|
|
101
|
+
offers.add_column("INSTANCE TYPE")
|
|
97
102
|
offers.add_column("RESOURCES")
|
|
98
103
|
offers.add_column("SPOT")
|
|
99
104
|
offers.add_column("PRICE")
|
|
100
105
|
offers.add_column()
|
|
101
106
|
|
|
102
|
-
job_plan.offers = job_plan.offers[:
|
|
107
|
+
job_plan.offers = job_plan.offers[:max_offers] if max_offers else job_plan.offers
|
|
103
108
|
|
|
104
109
|
for i, offer in enumerate(job_plan.offers, start=1):
|
|
105
110
|
r = offer.instance.resources
|
|
@@ -4,7 +4,6 @@ from boto3.session import Session
|
|
|
4
4
|
|
|
5
5
|
from dstack._internal.core.backends.aws.models import AnyAWSCreds, AWSAccessKeyCreds
|
|
6
6
|
from dstack._internal.core.errors import BackendAuthError
|
|
7
|
-
from dstack._internal.core.models.common import is_core_model_instance
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
def authenticate(creds: AnyAWSCreds, region: str) -> Session:
|
|
@@ -14,7 +13,7 @@ def authenticate(creds: AnyAWSCreds, region: str) -> Session:
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def get_session(creds: AnyAWSCreds, region: str) -> Session:
|
|
17
|
-
if
|
|
16
|
+
if isinstance(creds, AWSAccessKeyCreds):
|
|
18
17
|
return boto3.session.Session(
|
|
19
18
|
region_name=region,
|
|
20
19
|
aws_access_key_id=creds.access_key,
|
|
@@ -28,7 +28,7 @@ from dstack._internal.core.backends.base.compute import (
|
|
|
28
28
|
from dstack._internal.core.backends.base.offers import get_catalog_offers
|
|
29
29
|
from dstack._internal.core.errors import ComputeError, NoCapacityError, PlacementGroupInUseError
|
|
30
30
|
from dstack._internal.core.models.backends.base import BackendType
|
|
31
|
-
from dstack._internal.core.models.common import CoreModel
|
|
31
|
+
from dstack._internal.core.models.common import CoreModel
|
|
32
32
|
from dstack._internal.core.models.gateways import (
|
|
33
33
|
GatewayComputeConfiguration,
|
|
34
34
|
GatewayProvisioningData,
|
|
@@ -79,7 +79,7 @@ class AWSCompute(
|
|
|
79
79
|
def __init__(self, config: AWSConfig):
|
|
80
80
|
super().__init__()
|
|
81
81
|
self.config = config
|
|
82
|
-
if
|
|
82
|
+
if isinstance(config.creds, AWSAccessKeyCreds):
|
|
83
83
|
self.session = boto3.Session(
|
|
84
84
|
aws_access_key_id=config.creds.access_key,
|
|
85
85
|
aws_secret_access_key=config.creds.secret_key,
|
|
@@ -169,14 +169,19 @@ class AWSCompute(
|
|
|
169
169
|
raise NoCapacityError("No eligible availability zones")
|
|
170
170
|
|
|
171
171
|
instance_name = generate_unique_instance_name(instance_config)
|
|
172
|
-
|
|
172
|
+
base_tags = {
|
|
173
173
|
"Name": instance_name,
|
|
174
174
|
"owner": "dstack",
|
|
175
175
|
"dstack_project": project_name,
|
|
176
176
|
"dstack_name": instance_config.instance_name,
|
|
177
177
|
"dstack_user": instance_config.user,
|
|
178
178
|
}
|
|
179
|
-
tags = merge_tags(
|
|
179
|
+
tags = merge_tags(
|
|
180
|
+
base_tags=base_tags,
|
|
181
|
+
backend_tags=self.config.tags,
|
|
182
|
+
resource_tags=instance_config.tags,
|
|
183
|
+
)
|
|
184
|
+
tags = aws_resources.filter_invalid_tags(tags)
|
|
180
185
|
|
|
181
186
|
disk_size = round(instance_offer.instance.resources.disk.size_mib / 1024)
|
|
182
187
|
max_efa_interfaces = _get_maximum_efa_interfaces(
|
|
@@ -326,15 +331,20 @@ class AWSCompute(
|
|
|
326
331
|
ec2_client = self.session.client("ec2", region_name=configuration.region)
|
|
327
332
|
|
|
328
333
|
instance_name = generate_unique_gateway_instance_name(configuration)
|
|
329
|
-
|
|
334
|
+
base_tags = {
|
|
330
335
|
"Name": instance_name,
|
|
331
336
|
"owner": "dstack",
|
|
332
337
|
"dstack_project": configuration.project_name,
|
|
333
338
|
"dstack_name": configuration.instance_name,
|
|
334
339
|
}
|
|
335
340
|
if settings.DSTACK_VERSION is not None:
|
|
336
|
-
|
|
337
|
-
tags = merge_tags(
|
|
341
|
+
base_tags["dstack_version"] = settings.DSTACK_VERSION
|
|
342
|
+
tags = merge_tags(
|
|
343
|
+
base_tags=base_tags,
|
|
344
|
+
backend_tags=self.config.tags,
|
|
345
|
+
resource_tags=configuration.tags,
|
|
346
|
+
)
|
|
347
|
+
tags = aws_resources.filter_invalid_tags(tags)
|
|
338
348
|
tags = aws_resources.make_tags(tags)
|
|
339
349
|
|
|
340
350
|
vpc_id, subnets_ids = get_vpc_id_subnet_id_or_error(
|
|
@@ -522,14 +532,19 @@ class AWSCompute(
|
|
|
522
532
|
ec2_client = self.session.client("ec2", region_name=volume.configuration.region)
|
|
523
533
|
|
|
524
534
|
volume_name = generate_unique_volume_name(volume)
|
|
525
|
-
|
|
535
|
+
base_tags = {
|
|
526
536
|
"Name": volume_name,
|
|
527
537
|
"owner": "dstack",
|
|
528
538
|
"dstack_project": volume.project_name,
|
|
529
539
|
"dstack_name": volume.name,
|
|
530
540
|
"dstack_user": volume.user,
|
|
531
541
|
}
|
|
532
|
-
tags = merge_tags(
|
|
542
|
+
tags = merge_tags(
|
|
543
|
+
base_tags=base_tags,
|
|
544
|
+
backend_tags=self.config.tags,
|
|
545
|
+
resource_tags=volume.configuration.tags,
|
|
546
|
+
)
|
|
547
|
+
tags = aws_resources.filter_invalid_tags(tags)
|
|
533
548
|
|
|
534
549
|
zones = aws_resources.get_availability_zones(
|
|
535
550
|
ec2_client=ec2_client, region=volume.configuration.region
|
|
@@ -29,7 +29,6 @@ from dstack._internal.core.errors import (
|
|
|
29
29
|
from dstack._internal.core.models.backends.base import (
|
|
30
30
|
BackendType,
|
|
31
31
|
)
|
|
32
|
-
from dstack._internal.core.models.common import is_core_model_instance
|
|
33
32
|
from dstack._internal.utils.logging import get_logger
|
|
34
33
|
|
|
35
34
|
logger = get_logger(__name__)
|
|
@@ -58,12 +57,12 @@ class AWSConfigurator(Configurator):
|
|
|
58
57
|
BACKEND_CLASS = AWSBackend
|
|
59
58
|
|
|
60
59
|
def validate_config(self, config: AWSBackendConfigWithCreds, default_creds_enabled: bool):
|
|
61
|
-
if
|
|
60
|
+
if isinstance(config.creds, AWSDefaultCreds) and not default_creds_enabled:
|
|
62
61
|
raise_invalid_credentials_error(fields=[["creds"]])
|
|
63
62
|
try:
|
|
64
63
|
session = auth.authenticate(creds=config.creds, region=MAIN_REGION)
|
|
65
64
|
except Exception:
|
|
66
|
-
if
|
|
65
|
+
if isinstance(config.creds, AWSAccessKeyCreds):
|
|
67
66
|
raise_invalid_credentials_error(
|
|
68
67
|
fields=[
|
|
69
68
|
["creds", "access_key"],
|
|
@@ -448,6 +448,16 @@ def make_tags(tags: Dict[str, str]) -> List[Dict[str, str]]:
|
|
|
448
448
|
return tags_list
|
|
449
449
|
|
|
450
450
|
|
|
451
|
+
def filter_invalid_tags(tags: Dict[str, str]) -> Dict[str, str]:
|
|
452
|
+
filtered_tags = {}
|
|
453
|
+
for k, v in tags.items():
|
|
454
|
+
if not _is_valid_tag(k, v):
|
|
455
|
+
logger.warning("Skipping invalid tag '%s: %s'", k, v)
|
|
456
|
+
continue
|
|
457
|
+
filtered_tags[k] = v
|
|
458
|
+
return filtered_tags
|
|
459
|
+
|
|
460
|
+
|
|
451
461
|
def validate_tags(tags: Dict[str, str]):
|
|
452
462
|
for k, v in tags.items():
|
|
453
463
|
if not _is_valid_tag(k, v):
|
|
@@ -9,7 +9,6 @@ from dstack._internal.core.backends.azure.models import (
|
|
|
9
9
|
AzureClientCreds,
|
|
10
10
|
)
|
|
11
11
|
from dstack._internal.core.errors import BackendAuthError
|
|
12
|
-
from dstack._internal.core.models.common import is_core_model_instance
|
|
13
12
|
|
|
14
13
|
AzureCredential = Union[ClientSecretCredential, DefaultAzureCredential]
|
|
15
14
|
|
|
@@ -21,7 +20,7 @@ def authenticate(creds: AnyAzureCreds) -> Tuple[AzureCredential, str]:
|
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
def get_credential(creds: AnyAzureCreds) -> Tuple[AzureCredential, str]:
|
|
24
|
-
if
|
|
23
|
+
if isinstance(creds, AzureClientCreds):
|
|
25
24
|
credential = ClientSecretCredential(
|
|
26
25
|
tenant_id=creds.tenant_id,
|
|
27
26
|
client_id=creds.client_id,
|