skypilot-nightly 1.0.0.dev20250320__py3-none-any.whl → 1.0.0.dev20250322__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.
Files changed (50) hide show
  1. sky/__init__.py +2 -2
  2. sky/adaptors/cloudflare.py +16 -5
  3. sky/adaptors/kubernetes.py +2 -1
  4. sky/adaptors/nebius.py +128 -6
  5. sky/backends/cloud_vm_ray_backend.py +3 -1
  6. sky/benchmark/benchmark_utils.py +3 -2
  7. sky/check.py +114 -114
  8. sky/cloud_stores.py +66 -0
  9. sky/clouds/aws.py +14 -7
  10. sky/clouds/azure.py +13 -6
  11. sky/clouds/cloud.py +34 -10
  12. sky/clouds/cudo.py +3 -2
  13. sky/clouds/do.py +3 -2
  14. sky/clouds/fluidstack.py +3 -2
  15. sky/clouds/gcp.py +8 -9
  16. sky/clouds/ibm.py +15 -6
  17. sky/clouds/kubernetes.py +3 -1
  18. sky/clouds/lambda_cloud.py +3 -1
  19. sky/clouds/nebius.py +59 -11
  20. sky/clouds/oci.py +15 -6
  21. sky/clouds/paperspace.py +3 -2
  22. sky/clouds/runpod.py +7 -1
  23. sky/clouds/scp.py +3 -1
  24. sky/clouds/service_catalog/kubernetes_catalog.py +3 -1
  25. sky/clouds/vast.py +3 -2
  26. sky/clouds/vsphere.py +3 -2
  27. sky/core.py +6 -4
  28. sky/data/data_transfer.py +75 -0
  29. sky/data/data_utils.py +34 -0
  30. sky/data/mounting_utils.py +18 -0
  31. sky/data/storage.py +540 -10
  32. sky/data/storage_utils.py +102 -84
  33. sky/exceptions.py +2 -0
  34. sky/global_user_state.py +12 -33
  35. sky/jobs/server/core.py +1 -1
  36. sky/jobs/utils.py +5 -0
  37. sky/optimizer.py +10 -5
  38. sky/resources.py +6 -1
  39. sky/setup_files/dependencies.py +3 -1
  40. sky/task.py +16 -5
  41. sky/utils/command_runner.py +2 -0
  42. sky/utils/controller_utils.py +8 -5
  43. sky/utils/kubernetes/gpu_labeler.py +4 -4
  44. sky/utils/kubernetes/kubernetes_deploy_utils.py +4 -3
  45. {skypilot_nightly-1.0.0.dev20250320.dist-info → skypilot_nightly-1.0.0.dev20250322.dist-info}/METADATA +16 -7
  46. {skypilot_nightly-1.0.0.dev20250320.dist-info → skypilot_nightly-1.0.0.dev20250322.dist-info}/RECORD +50 -50
  47. {skypilot_nightly-1.0.0.dev20250320.dist-info → skypilot_nightly-1.0.0.dev20250322.dist-info}/WHEEL +1 -1
  48. {skypilot_nightly-1.0.0.dev20250320.dist-info → skypilot_nightly-1.0.0.dev20250322.dist-info}/entry_points.txt +0 -0
  49. {skypilot_nightly-1.0.0.dev20250320.dist-info → skypilot_nightly-1.0.0.dev20250322.dist-info}/licenses/LICENSE +0 -0
  50. {skypilot_nightly-1.0.0.dev20250320.dist-info → skypilot_nightly-1.0.0.dev20250322.dist-info}/top_level.txt +0 -0
sky/cloud_stores.py CHANGED
@@ -19,6 +19,7 @@ from sky.adaptors import aws
19
19
  from sky.adaptors import azure
20
20
  from sky.adaptors import cloudflare
21
21
  from sky.adaptors import ibm
22
+ from sky.adaptors import nebius
22
23
  from sky.adaptors import oci
23
24
  from sky.clouds import gcp
24
25
  from sky.data import data_utils
@@ -543,6 +544,70 @@ class OciCloudStorage(CloudStorage):
543
544
  return download_via_ocicli
544
545
 
545
546
 
547
+ class NebiusCloudStorage(CloudStorage):
548
+ """Nebius Cloud Storage."""
549
+
550
+ # List of commands to install AWS CLI
551
+ _GET_AWSCLI = [
552
+ 'aws --version >/dev/null 2>&1 || '
553
+ f'{constants.SKY_UV_PIP_CMD} install awscli',
554
+ ]
555
+
556
+ def is_directory(self, url: str) -> bool:
557
+ """Returns whether nebius 'url' is a directory.
558
+
559
+ In cloud object stores, a "directory" refers to a regular object whose
560
+ name is a prefix of other objects.
561
+ """
562
+ nebius_s3 = nebius.resource('s3')
563
+ bucket_name, path = data_utils.split_nebius_path(url)
564
+ bucket = nebius_s3.Bucket(bucket_name)
565
+
566
+ num_objects = 0
567
+ for obj in bucket.objects.filter(Prefix=path):
568
+ num_objects += 1
569
+ if obj.key == path:
570
+ return False
571
+ # If there are more than 1 object in filter, then it is a directory
572
+ if num_objects == 3:
573
+ return True
574
+
575
+ # A directory with few or no items
576
+ return True
577
+
578
+ def make_sync_dir_command(self, source: str, destination: str) -> str:
579
+ """Downloads using AWS CLI."""
580
+ # AWS Sync by default uses 10 threads to upload files to the bucket.
581
+ # To increase parallelism, modify max_concurrent_requests in your
582
+ # aws config file (Default path: ~/.aws/config).
583
+ endpoint_url = nebius.create_endpoint()
584
+ assert 'nebius://' in source, 'nebius:// is not in source'
585
+ source = source.replace('nebius://', 's3://')
586
+ download_via_awscli = (f'{constants.SKY_REMOTE_PYTHON_ENV}/bin/aws s3 '
587
+ 'sync --no-follow-symlinks '
588
+ f'{source} {destination} '
589
+ f'--endpoint {endpoint_url} '
590
+ f'--profile={nebius.NEBIUS_PROFILE_NAME}')
591
+
592
+ all_commands = list(self._GET_AWSCLI)
593
+ all_commands.append(download_via_awscli)
594
+ return ' && '.join(all_commands)
595
+
596
+ def make_sync_file_command(self, source: str, destination: str) -> str:
597
+ """Downloads a file using AWS CLI."""
598
+ endpoint_url = nebius.create_endpoint()
599
+ assert 'nebius://' in source, 'nebius:// is not in source'
600
+ source = source.replace('nebius://', 's3://')
601
+ download_via_awscli = (f'{constants.SKY_REMOTE_PYTHON_ENV}/bin/aws s3 '
602
+ f'cp {source} {destination} '
603
+ f'--endpoint {endpoint_url} '
604
+ f'--profile={nebius.NEBIUS_PROFILE_NAME}')
605
+
606
+ all_commands = list(self._GET_AWSCLI)
607
+ all_commands.append(download_via_awscli)
608
+ return ' && '.join(all_commands)
609
+
610
+
546
611
  def get_storage_from_path(url: str) -> CloudStorage:
547
612
  """Returns a CloudStorage by identifying the scheme:// in a URL."""
548
613
  result = urllib.parse.urlsplit(url)
@@ -559,6 +624,7 @@ _REGISTRY = {
559
624
  'r2': R2CloudStorage(),
560
625
  'cos': IBMCosCloudStorage(),
561
626
  'oci': OciCloudStorage(),
627
+ 'nebius': NebiusCloudStorage(),
562
628
  # TODO: This is a hack, as Azure URL starts with https://, we should
563
629
  # refactor the registry to be able to take regex, so that Azure blob can
564
630
  # be identified with `https://(.*?)\.blob\.core\.windows\.net`
sky/clouds/aws.py CHANGED
@@ -558,11 +558,23 @@ class AWS(clouds.Cloud):
558
558
  return resources_utils.FeasibleResources(_make(instance_list),
559
559
  fuzzy_candidate_list, None)
560
560
 
561
+ @classmethod
562
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
563
+ """Checks if the user has access credentials to this AWS's compute service."""
564
+ return cls._check_credentials()
565
+
566
+ @classmethod
567
+ def _check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
568
+ """Checks if the user has access credentials to this AWS's storage service."""
569
+ # TODO(seungjin): Implement separate check for
570
+ # if the user has access to S3.
571
+ return cls._check_credentials()
572
+
561
573
  @classmethod
562
574
  @annotations.lru_cache(scope='global',
563
575
  maxsize=1) # Cache since getting identity is slow.
564
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
565
- """Checks if the user has access credentials to this cloud."""
576
+ def _check_credentials(cls) -> Tuple[bool, Optional[str]]:
577
+ """Checks if the user has access credentials to AWS."""
566
578
 
567
579
  dependency_installation_hints = (
568
580
  'AWS dependencies are not installed. '
@@ -666,11 +678,6 @@ class AWS(clouds.Cloud):
666
678
  f'{common_utils.format_exception(e, use_bracket=True)}')
667
679
  return True, hints
668
680
 
669
- @classmethod
670
- def check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
671
- # TODO(seungjin): Check if the user has access to S3.
672
- return cls.check_credentials()
673
-
674
681
  @classmethod
675
682
  def _current_identity_type(cls) -> Optional[AWSIdentityType]:
676
683
  stdout = cls._aws_configure_list()
sky/clouds/azure.py CHANGED
@@ -512,7 +512,19 @@ class Azure(clouds.Cloud):
512
512
  fuzzy_candidate_list, None)
513
513
 
514
514
  @classmethod
515
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
515
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
516
+ """Checks if the user has access credentials to this cloud's compute service."""
517
+ return cls._check_credentials()
518
+
519
+ @classmethod
520
+ def _check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
521
+ """Checks if the user has access credentials to this cloud's storage service."""
522
+ # TODO(seungjin): Implement separate check for
523
+ # if the user has access to Azure Blob Storage.
524
+ return cls._check_credentials()
525
+
526
+ @classmethod
527
+ def _check_credentials(cls) -> Tuple[bool, Optional[str]]:
516
528
  """Checks if the user has access credentials to this cloud."""
517
529
  help_str = (
518
530
  ' Run the following commands:'
@@ -574,11 +586,6 @@ class Azure(clouds.Cloud):
574
586
  return service_catalog.instance_type_exists(instance_type,
575
587
  clouds='azure')
576
588
 
577
- @classmethod
578
- def check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
579
- # TODO(seungjin): Check if the user has access to Azure Blob Storage.
580
- return cls.check_credentials()
581
-
582
589
  @classmethod
583
590
  @annotations.lru_cache(scope='global',
584
591
  maxsize=1) # Cache since getting identity is slow.
sky/clouds/cloud.py CHANGED
@@ -13,6 +13,8 @@ import math
13
13
  import typing
14
14
  from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple, Union
15
15
 
16
+ from typing_extensions import assert_never
17
+
16
18
  from sky import exceptions
17
19
  from sky import skypilot_config
18
20
  from sky.clouds import service_catalog
@@ -48,6 +50,17 @@ class CloudImplementationFeatures(enum.Enum):
48
50
  AUTO_TERMINATE = 'auto_terminate' # Pod/VM can stop or down itself
49
51
 
50
52
 
53
+ # Use str, enum.Enum to allow CloudCapability to be used as a string.
54
+ class CloudCapability(str, enum.Enum):
55
+ # Compute capability.
56
+ COMPUTE = 'compute'
57
+ # Storage capability.
58
+ STORAGE = 'storage'
59
+
60
+
61
+ ALL_CAPABILITIES = [CloudCapability.COMPUTE, CloudCapability.STORAGE]
62
+
63
+
51
64
  class Region(collections.namedtuple('Region', ['name'])):
52
65
  """A region."""
53
66
  name: str
@@ -435,25 +448,36 @@ class Cloud:
435
448
  return {reservation: 0 for reservation in specific_reservations}
436
449
 
437
450
  @classmethod
438
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
451
+ def check_credentials(
452
+ cls,
453
+ cloud_capability: CloudCapability) -> Tuple[bool, Optional[str]]:
439
454
  """Checks if the user has access credentials to this cloud.
440
455
 
441
456
  Returns a boolean of whether the user can access this cloud, and a
442
457
  string describing the reason if the user cannot access.
458
+
459
+ Raises NotSupportedError if the capability is
460
+ not supported by this cloud.
443
461
  """
444
- raise NotImplementedError
462
+ if cloud_capability == CloudCapability.COMPUTE:
463
+ return cls._check_compute_credentials()
464
+ elif cloud_capability == CloudCapability.STORAGE:
465
+ return cls._check_storage_credentials()
466
+ assert_never(cloud_capability)
445
467
 
446
468
  @classmethod
447
- def check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
448
- """Checks if the user has access credentials to this cloud's storage.
469
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
470
+ """Checks if the user has access credentials to
471
+ this cloud's compute service."""
472
+ raise exceptions.NotSupportedError(
473
+ f'{cls._REPR} does not support {CloudCapability.COMPUTE.value}.')
449
474
 
450
- Returns a boolean of whether the user can access this cloud's storage,
451
- and a string describing the reason if the user cannot access.
452
- """
453
- # A given cloud does not support storage
454
- # unless it overrides this method.
475
+ @classmethod
476
+ def _check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
477
+ """Checks if the user has access credentials to
478
+ this cloud's storage service."""
455
479
  raise exceptions.NotSupportedError(
456
- f'{cls._REPR} does not support storage.')
480
+ f'{cls._REPR} does not support {CloudCapability.STORAGE.value}.')
457
481
 
458
482
  # TODO(zhwu): Make the return type immutable.
459
483
  @classmethod
sky/clouds/cudo.py CHANGED
@@ -267,8 +267,9 @@ class Cudo(clouds.Cloud):
267
267
  fuzzy_candidate_list, None)
268
268
 
269
269
  @classmethod
270
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
271
-
270
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
271
+ """Checks if the user has access credentials to
272
+ Cudo's compute service."""
272
273
  try:
273
274
  # pylint: disable=import-outside-toplevel,unused-import
274
275
  from cudo_compute import cudo_api
sky/clouds/do.py CHANGED
@@ -259,8 +259,9 @@ class DO(clouds.Cloud):
259
259
  fuzzy_candidate_list, None)
260
260
 
261
261
  @classmethod
262
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
263
- """Verify that the user has valid credentials for DO."""
262
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
263
+ """Verify that the user has valid credentials for
264
+ DO's compute service."""
264
265
 
265
266
  try:
266
267
  do.exceptions()
sky/clouds/fluidstack.py CHANGED
@@ -255,8 +255,9 @@ class Fluidstack(clouds.Cloud):
255
255
  fuzzy_candidate_list, None)
256
256
 
257
257
  @classmethod
258
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
259
-
258
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
259
+ """Checks if the user has access credentials to
260
+ FluidStack's compute service."""
260
261
  try:
261
262
  assert os.path.exists(
262
263
  os.path.expanduser(fluidstack_utils.FLUIDSTACK_API_KEY_PATH))
sky/clouds/gcp.py CHANGED
@@ -719,9 +719,9 @@ class GCP(clouds.Cloud):
719
719
  return DEFAULT_GCP_APPLICATION_CREDENTIAL_PATH
720
720
 
721
721
  @classmethod
722
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
723
- """Checks if the user has compute access credentials to this cloud."""
724
- return cls._check_credentials( # Check APIs.
722
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
723
+ """Checks if the user has access credentials to this cloud's compute service."""
724
+ return cls._check_credentials(
725
725
  [
726
726
  ('compute', 'Compute Engine'),
727
727
  ('cloudresourcemanager', 'Cloud Resource Manager'),
@@ -731,12 +731,11 @@ class GCP(clouds.Cloud):
731
731
  gcp_utils.get_minimal_compute_permissions())
732
732
 
733
733
  @classmethod
734
- def check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
735
- """Checks if the user has compute access credentials to this cloud."""
736
- return cls._check_credentials( # Check APIs.
737
- [
738
- ('storage', 'Cloud Storage'),
739
- ], gcp_utils.get_minimal_storage_permissions())
734
+ def _check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
735
+ """Checks if the user has access credentials to this cloud's storage service."""
736
+ return cls._check_credentials(
737
+ [('storage', 'Cloud Storage')],
738
+ gcp_utils.get_minimal_storage_permissions())
740
739
 
741
740
  @classmethod
742
741
  def _check_credentials(
sky/clouds/ibm.py CHANGED
@@ -395,7 +395,21 @@ class IBM(clouds.Cloud):
395
395
  return image_size
396
396
 
397
397
  @classmethod
398
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
398
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
399
+ """Checks if the user has access credentials to
400
+ IBM's compute service."""
401
+ return cls._check_credentials()
402
+
403
+ @classmethod
404
+ def _check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
405
+ """Checks if the user has access credentials to
406
+ IBM's storage service."""
407
+ # TODO(seungjin): Implement separate check for
408
+ # if the user has access to IBM COS.
409
+ return cls._check_credentials()
410
+
411
+ @classmethod
412
+ def _check_credentials(cls) -> Tuple[bool, Optional[str]]:
399
413
  """Checks if the user has access credentials to this cloud."""
400
414
 
401
415
  required_fields = ['iam_api_key', 'resource_group_id']
@@ -433,11 +447,6 @@ class IBM(clouds.Cloud):
433
447
  except Exception as e:
434
448
  return (False, f'{str(e)}' + help_str)
435
449
 
436
- @classmethod
437
- def check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
438
- # TODO(seungjin): Check if the user has access to IBM COS.
439
- return cls.check_credentials()
440
-
441
450
  def get_credential_file_mounts(self) -> Dict[str, str]:
442
451
  """Returns a {remote:local} credential path mapping
443
452
  written to the cluster's file_mounts segment
sky/clouds/kubernetes.py CHANGED
@@ -655,7 +655,9 @@ class Kubernetes(clouds.Cloud):
655
655
  [], None)
656
656
 
657
657
  @classmethod
658
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
658
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
659
+ """Checks if the user has access credentials to
660
+ Kubernetes."""
659
661
  # Test using python API
660
662
  try:
661
663
  existing_allowed_contexts = cls.existing_allowed_contexts()
@@ -241,7 +241,9 @@ class Lambda(clouds.Cloud):
241
241
  fuzzy_candidate_list, None)
242
242
 
243
243
  @classmethod
244
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
244
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
245
+ """Checks if the user has access credentials to
246
+ Lambda's compute service."""
245
247
  try:
246
248
  lambda_utils.LambdaCloudClient().list_instances()
247
249
  except (AssertionError, KeyError, lambda_utils.LambdaCloudError):
sky/clouds/nebius.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """ Nebius Cloud. """
2
2
  import logging
3
+ import os
3
4
  import typing
4
5
  from typing import Dict, Iterator, List, Optional, Tuple, Union
5
6
 
@@ -20,6 +21,22 @@ _CREDENTIAL_FILES = [
20
21
  nebius.NEBIUS_CREDENTIALS_FILENAME
21
22
  ]
22
23
 
24
+ _INDENT_PREFIX = ' '
25
+
26
+
27
+ def nebius_profile_in_aws_cred() -> bool:
28
+ """Checks if Nebius Object Storage profile is set in aws credentials."""
29
+
30
+ profile_path = os.path.expanduser('~/.aws/credentials')
31
+ nebius_profile_exists = False
32
+ if os.path.isfile(profile_path):
33
+ with open(profile_path, 'r', encoding='utf-8') as file:
34
+ for line in file:
35
+ if f'[{nebius.NEBIUS_PROFILE_NAME}]' in line:
36
+ nebius_profile_exists = True
37
+
38
+ return nebius_profile_exists
39
+
23
40
 
24
41
  @registry.CLOUD_REGISTRY.register
25
42
  class Nebius(clouds.Cloud):
@@ -250,17 +267,19 @@ class Nebius(clouds.Cloud):
250
267
  fuzzy_candidate_list, None)
251
268
 
252
269
  @classmethod
253
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
254
- """ Verify that the user has valid credentials for Nebius. """
270
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
271
+ """Checks if the user has access credentials to
272
+ Nebius's compute service."""
255
273
  logging.debug('Nebius cloud check credentials')
256
- token_cred_msg = (' Credentials can be set up by running: \n'\
257
- f' $ nebius iam get-access-token > {nebius.NEBIUS_IAM_TOKEN_PATH} \n'\
258
- ' or generate ~/.nebius/credentials.json') # pylint: disable=line-too-long
259
-
260
- tenant_msg = (' Copy your tenat ID from the web console and save it to file \n' # pylint: disable=line-too-long
261
- f' $ echo $NEBIUS_TENANT_ID_PATH > {nebius.NEBIUS_TENANT_ID_PATH} \n' # pylint: disable=line-too-long
262
- ' Or if you have 1 tenant you can run:\n' # pylint: disable=line-too-long
263
- f' $ nebius --format json iam whoami|jq -r \'.user_profile.tenants[0].tenant_id\' > {nebius.NEBIUS_TENANT_ID_PATH} \n') # pylint: disable=line-too-long
274
+ token_cred_msg = (
275
+ f'{_INDENT_PREFIX}Credentials can be set up by running: \n'
276
+ f'{_INDENT_PREFIX} $ nebius iam get-access-token > {nebius.NEBIUS_IAM_TOKEN_PATH} \n' # pylint: disable=line-too-long
277
+ f'{_INDENT_PREFIX} or generate ~/.nebius/credentials.json')
278
+
279
+ tenant_msg = (f'{_INDENT_PREFIX}Copy your tenat ID from the web console and save it to file \n' # pylint: disable=line-too-long
280
+ f'{_INDENT_PREFIX} $ echo $NEBIUS_TENANT_ID_PATH > {nebius.NEBIUS_TENANT_ID_PATH} \n' # pylint: disable=line-too-long
281
+ f'{_INDENT_PREFIX} Or if you have 1 tenant you can run:\n' # pylint: disable=line-too-long
282
+ f'{_INDENT_PREFIX} $ nebius --format json iam whoami|jq -r \'.user_profile.tenants[0].tenant_id\' > {nebius.NEBIUS_TENANT_ID_PATH} \n') # pylint: disable=line-too-long
264
283
  if not nebius.is_token_or_cred_file_exist():
265
284
  return False, f'{token_cred_msg}'
266
285
  sdk = nebius.sdk()
@@ -278,11 +297,40 @@ class Nebius(clouds.Cloud):
278
297
  f'{tenant_msg}')
279
298
  return True, None
280
299
 
300
+ @classmethod
301
+ def _check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
302
+ """Checks if the user has access credentials to Nebius Object Storage.
303
+
304
+ Returns:
305
+ A tuple of a boolean value and a hint message where the bool
306
+ is True when credentials needed for Nebius Object Storage is set.
307
+ It is False when either of those are not set, which would hint
308
+ with a string on unset credential.
309
+ """
310
+ hints = None
311
+ if not nebius_profile_in_aws_cred():
312
+ hints = (f'[{nebius.NEBIUS_PROFILE_NAME}] profile '
313
+ 'is not set in ~/.aws/credentials.')
314
+ if hints:
315
+ hints += ' Run the following commands:'
316
+ if not nebius_profile_in_aws_cred():
317
+ hints += (
318
+ f'\n{_INDENT_PREFIX} $ pip install boto3'
319
+ f'\n{_INDENT_PREFIX} $ aws configure --profile nebius')
320
+ hints += (
321
+ f'\n{_INDENT_PREFIX}For more info: '
322
+ 'https://docs.skypilot.co/en/latest/getting-started/installation.html#nebius' # pylint: disable=line-too-long
323
+ )
324
+ return (False, hints) if hints else (True, hints)
325
+
281
326
  def get_credential_file_mounts(self) -> Dict[str, str]:
282
- return {
327
+ credential_file_mounts = {
283
328
  f'~/.nebius/{filename}': f'~/.nebius/{filename}'
284
329
  for filename in _CREDENTIAL_FILES
285
330
  }
331
+ credential_file_mounts['~/.aws/credentials'] = '~/.aws/credentials'
332
+
333
+ return credential_file_mounts
286
334
 
287
335
  @classmethod
288
336
  def get_current_user_identity(cls) -> Optional[List[str]]:
sky/clouds/oci.py CHANGED
@@ -396,7 +396,21 @@ class OCI(clouds.Cloud):
396
396
  fuzzy_candidate_list, None)
397
397
 
398
398
  @classmethod
399
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
399
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
400
+ """Checks if the user has access credentials to
401
+ OCI's compute service."""
402
+ return cls._check_credentials()
403
+
404
+ @classmethod
405
+ def _check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
406
+ """Checks if the user has access credentials to
407
+ OCI's storage service."""
408
+ # TODO(seungjin): Implement separate check for
409
+ # if the user has access to OCI Object Storage.
410
+ return cls._check_credentials()
411
+
412
+ @classmethod
413
+ def _check_credentials(cls) -> Tuple[bool, Optional[str]]:
400
414
  """Checks if the user has access credentials to this cloud."""
401
415
 
402
416
  short_credential_help_str = (
@@ -456,11 +470,6 @@ class OCI(clouds.Cloud):
456
470
  f'{cls._INDENT_PREFIX}Error details: '
457
471
  f'{common_utils.format_exception(e, use_bracket=True)}')
458
472
 
459
- @classmethod
460
- def check_storage_credentials(cls) -> Tuple[bool, Optional[str]]:
461
- # TODO(seungjin): Check if the user has access to OCI Object Storage.
462
- return cls.check_credentials()
463
-
464
473
  @classmethod
465
474
  def check_disk_tier(
466
475
  cls, instance_type: Optional[str],
sky/clouds/paperspace.py CHANGED
@@ -249,8 +249,9 @@ class Paperspace(clouds.Cloud):
249
249
  fuzzy_candidate_list, None)
250
250
 
251
251
  @classmethod
252
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
253
- """Verify that the user has valid credentials for Paperspace."""
252
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
253
+ """Checks if the user has access credentials to
254
+ Paperspace's compute service."""
254
255
  try:
255
256
  # attempt to make a CURL request for listing instances
256
257
  utils.PaperspaceCloudClient().list_instances()
sky/clouds/runpod.py CHANGED
@@ -248,7 +248,13 @@ class RunPod(clouds.Cloud):
248
248
  fuzzy_candidate_list, None)
249
249
 
250
250
  @classmethod
251
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
251
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
252
+ """Checks if the user has access credentials to
253
+ RunPod's compute service."""
254
+ return cls._check_credentials()
255
+
256
+ @classmethod
257
+ def _check_credentials(cls) -> Tuple[bool, Optional[str]]:
252
258
  """ Verify that the user has valid credentials for RunPod. """
253
259
  try:
254
260
  import runpod # pylint: disable=import-outside-toplevel
sky/clouds/scp.py CHANGED
@@ -312,7 +312,9 @@ class SCP(clouds.Cloud):
312
312
  fuzzy_candidate_list, None)
313
313
 
314
314
  @classmethod
315
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
315
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
316
+ """Checks if the user has access credentials to
317
+ SCP's compute service."""
316
318
  try:
317
319
  scp_utils.SCPClient().list_instances()
318
320
  except (AssertionError, KeyError, scp_utils.SCPClientError,
@@ -12,6 +12,7 @@ from sky import clouds as sky_clouds
12
12
  from sky import sky_logging
13
13
  from sky.adaptors import common as adaptors_common
14
14
  from sky.adaptors import kubernetes
15
+ from sky.clouds import cloud
15
16
  from sky.clouds.service_catalog import CloudFilter
16
17
  from sky.clouds.service_catalog import common
17
18
  from sky.provision.kubernetes import utils as kubernetes_utils
@@ -132,7 +133,8 @@ def _list_accelerators(
132
133
 
133
134
  # First check if Kubernetes is enabled. This ensures k8s python client is
134
135
  # installed. Do not put any k8s-specific logic before this check.
135
- enabled_clouds = sky_check.get_cached_enabled_clouds_or_refresh()
136
+ enabled_clouds = sky_check.get_cached_enabled_clouds_or_refresh(
137
+ cloud.CloudCapability.COMPUTE)
136
138
  if not sky_clouds.cloud_in_iterable(sky_clouds.Kubernetes(),
137
139
  enabled_clouds):
138
140
  return {}, {}, {}
sky/clouds/vast.py CHANGED
@@ -234,8 +234,9 @@ class Vast(clouds.Cloud):
234
234
  fuzzy_candidate_list, None)
235
235
 
236
236
  @classmethod
237
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
238
- """ Verify that the user has valid credentials for Vast. """
237
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
238
+ """Checks if the user has valid credentials for
239
+ Vast's compute service. """
239
240
  try:
240
241
  import vastai_sdk as _vast # pylint: disable=import-outside-toplevel
241
242
  vast = _vast.VastAI()
sky/clouds/vsphere.py CHANGED
@@ -254,8 +254,9 @@ class Vsphere(clouds.Cloud):
254
254
  fuzzy_candidate_list, None)
255
255
 
256
256
  @classmethod
257
- def check_credentials(cls) -> Tuple[bool, Optional[str]]:
258
- """Checks if the user has access credentials to this cloud."""
257
+ def _check_compute_credentials(cls) -> Tuple[bool, Optional[str]]:
258
+ """Checks if the user has access credentials to
259
+ vSphere's compute service."""
259
260
 
260
261
  try:
261
262
  # pylint: disable=import-outside-toplevel,unused-import
sky/core.py CHANGED
@@ -19,6 +19,7 @@ from sky import optimizer
19
19
  from sky import sky_logging
20
20
  from sky import task as task_lib
21
21
  from sky.backends import backend_utils
22
+ from sky.clouds import cloud as sky_cloud
22
23
  from sky.clouds import service_catalog
23
24
  from sky.jobs.server import core as managed_jobs_core
24
25
  from sky.provision.kubernetes import constants as kubernetes_constants
@@ -1001,7 +1002,8 @@ def storage_delete(name: str) -> None:
1001
1002
  # ===================
1002
1003
  @usage_lib.entrypoint
1003
1004
  def enabled_clouds() -> List[clouds.Cloud]:
1004
- return global_user_state.get_cached_enabled_clouds()
1005
+ return global_user_state.get_cached_enabled_clouds(
1006
+ sky_cloud.CloudCapability.COMPUTE)
1005
1007
 
1006
1008
 
1007
1009
  @usage_lib.entrypoint
@@ -1134,9 +1136,9 @@ def local_down() -> None:
1134
1136
  # Run sky check
1135
1137
  with rich_utils.safe_status(
1136
1138
  ux_utils.spinner_message('Running sky check...')):
1137
- sky_check.check(clouds=['kubernetes'],
1138
- quiet=True,
1139
- capability=sky_check.CloudCapability.COMPUTE)
1139
+ sky_check.check_capability(sky_cloud.CloudCapability.COMPUTE,
1140
+ clouds=['kubernetes'],
1141
+ quiet=True)
1140
1142
  logger.info(
1141
1143
  ux_utils.finishing_message('Local cluster removed.',
1142
1144
  log_path=log_path,