skypilot-nightly 1.0.0.dev20241227__py3-none-any.whl → 1.0.0.dev20241229__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.
sky/__init__.py CHANGED
@@ -5,7 +5,7 @@ from typing import Optional
5
5
  import urllib.request
6
6
 
7
7
  # Replaced with the current commit when building the wheels.
8
- _SKYPILOT_COMMIT_SHA = '138679859b9844a8737f8dff1bf5a739e77e96c4'
8
+ _SKYPILOT_COMMIT_SHA = '7ae2d25d3f1fa82d02f6f95d57e1cac6b928f425'
9
9
 
10
10
 
11
11
  def _get_git_commit():
@@ -35,7 +35,7 @@ def _get_git_commit():
35
35
 
36
36
 
37
37
  __commit__ = _get_git_commit()
38
- __version__ = '1.0.0.dev20241227'
38
+ __version__ = '1.0.0.dev20241229'
39
39
  __root_dir__ = os.path.dirname(os.path.abspath(__file__))
40
40
 
41
41
 
sky/adaptors/oci.py CHANGED
@@ -1,9 +1,11 @@
1
1
  """Oracle OCI cloud adaptor"""
2
2
 
3
+ import functools
3
4
  import logging
4
5
  import os
5
6
 
6
7
  from sky.adaptors import common
8
+ from sky.clouds.utils import oci_utils
7
9
 
8
10
  # Suppress OCI circuit breaker logging before lazy import, because
9
11
  # oci modules prints additional message during imports, i.e., the
@@ -30,10 +32,16 @@ def get_config_file() -> str:
30
32
 
31
33
  def get_oci_config(region=None, profile='DEFAULT'):
32
34
  conf_file_path = get_config_file()
35
+ if not profile or profile == 'DEFAULT':
36
+ config_profile = oci_utils.oci_config.get_profile()
37
+ else:
38
+ config_profile = profile
39
+
33
40
  oci_config = oci.config.from_file(file_location=conf_file_path,
34
- profile_name=profile)
41
+ profile_name=config_profile)
35
42
  if region is not None:
36
43
  oci_config['region'] = region
44
+
37
45
  return oci_config
38
46
 
39
47
 
@@ -54,6 +62,29 @@ def get_identity_client(region=None, profile='DEFAULT'):
54
62
  return oci.identity.IdentityClient(get_oci_config(region, profile))
55
63
 
56
64
 
65
+ def get_object_storage_client(region=None, profile='DEFAULT'):
66
+ return oci.object_storage.ObjectStorageClient(
67
+ get_oci_config(region, profile))
68
+
69
+
57
70
  def service_exception():
58
71
  """OCI service exception."""
59
72
  return oci.exceptions.ServiceError
73
+
74
+
75
+ def with_oci_env(f):
76
+
77
+ @functools.wraps(f)
78
+ def wrapper(*args, **kwargs):
79
+ # pylint: disable=line-too-long
80
+ enter_env_cmds = [
81
+ 'conda info --envs | grep "sky-oci-cli-env" || conda create -n sky-oci-cli-env python=3.10 -y',
82
+ '. $(conda info --base 2> /dev/null)/etc/profile.d/conda.sh > /dev/null 2>&1 || true',
83
+ 'conda activate sky-oci-cli-env', 'pip install oci-cli',
84
+ 'export OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True'
85
+ ]
86
+ operation_cmd = [f(*args, **kwargs)]
87
+ leave_env_cmds = ['conda deactivate']
88
+ return ' && '.join(enter_env_cmds + operation_cmd + leave_env_cmds)
89
+
90
+ return wrapper
sky/cloud_stores.py CHANGED
@@ -7,6 +7,7 @@ TODO:
7
7
  * Better interface.
8
8
  * Better implementation (e.g., fsspec, smart_open, using each cloud's SDK).
9
9
  """
10
+ import os
10
11
  import shlex
11
12
  import subprocess
12
13
  import time
@@ -18,6 +19,7 @@ from sky.adaptors import aws
18
19
  from sky.adaptors import azure
19
20
  from sky.adaptors import cloudflare
20
21
  from sky.adaptors import ibm
22
+ from sky.adaptors import oci
21
23
  from sky.clouds import gcp
22
24
  from sky.data import data_utils
23
25
  from sky.data.data_utils import Rclone
@@ -470,6 +472,64 @@ class IBMCosCloudStorage(CloudStorage):
470
472
  return self.make_sync_dir_command(source, destination)
471
473
 
472
474
 
475
+ class OciCloudStorage(CloudStorage):
476
+ """OCI Cloud Storage."""
477
+
478
+ def is_directory(self, url: str) -> bool:
479
+ """Returns whether OCI 'url' is a directory.
480
+ In cloud object stores, a "directory" refers to a regular object whose
481
+ name is a prefix of other objects.
482
+ """
483
+ bucket_name, path = data_utils.split_oci_path(url)
484
+
485
+ client = oci.get_object_storage_client()
486
+ namespace = client.get_namespace(
487
+ compartment_id=oci.get_oci_config()['tenancy']).data
488
+
489
+ objects = client.list_objects(namespace_name=namespace,
490
+ bucket_name=bucket_name,
491
+ prefix=path).data.objects
492
+
493
+ if len(objects) == 0:
494
+ # A directory with few or no items
495
+ return True
496
+
497
+ if len(objects) > 1:
498
+ # A directory with more than 1 items
499
+ return True
500
+
501
+ object_name = objects[0].name
502
+ if path.endswith(object_name):
503
+ # An object path
504
+ return False
505
+
506
+ # A directory with only 1 item
507
+ return True
508
+
509
+ @oci.with_oci_env
510
+ def make_sync_dir_command(self, source: str, destination: str) -> str:
511
+ """Downloads using OCI CLI."""
512
+ bucket_name, path = data_utils.split_oci_path(source)
513
+
514
+ download_via_ocicli = (f'oci os object sync --no-follow-symlinks '
515
+ f'--bucket-name {bucket_name} '
516
+ f'--prefix "{path}" --dest-dir "{destination}"')
517
+
518
+ return download_via_ocicli
519
+
520
+ @oci.with_oci_env
521
+ def make_sync_file_command(self, source: str, destination: str) -> str:
522
+ """Downloads a file using OCI CLI."""
523
+ bucket_name, path = data_utils.split_oci_path(source)
524
+ filename = os.path.basename(path)
525
+ destination = os.path.join(destination, filename)
526
+
527
+ download_via_ocicli = (f'oci os object get --bucket-name {bucket_name} '
528
+ f'--name "{path}" --file "{destination}"')
529
+
530
+ return download_via_ocicli
531
+
532
+
473
533
  def get_storage_from_path(url: str) -> CloudStorage:
474
534
  """Returns a CloudStorage by identifying the scheme:// in a URL."""
475
535
  result = urllib.parse.urlsplit(url)
@@ -485,6 +545,7 @@ _REGISTRY = {
485
545
  's3': S3CloudStorage(),
486
546
  'r2': R2CloudStorage(),
487
547
  'cos': IBMCosCloudStorage(),
548
+ 'oci': OciCloudStorage(),
488
549
  # TODO: This is a hack, as Azure URL starts with https://, we should
489
550
  # refactor the registry to be able to take regex, so that Azure blob can
490
551
  # be identified with `https://(.*?)\.blob\.core\.windows\.net`
sky/data/data_transfer.py CHANGED
@@ -200,3 +200,40 @@ def _add_bucket_iam_member(bucket_name: str, role: str, member: str) -> None:
200
200
  bucket.set_iam_policy(policy)
201
201
 
202
202
  logger.debug(f'Added {member} with role {role} to {bucket_name}.')
203
+
204
+
205
+ def s3_to_oci(s3_bucket_name: str, oci_bucket_name: str) -> None:
206
+ """Creates a one-time transfer from Amazon S3 to OCI Object Storage.
207
+ Args:
208
+ s3_bucket_name: str; Name of the Amazon S3 Bucket
209
+ oci_bucket_name: str; Name of the OCI Bucket
210
+ """
211
+ # TODO(HysunHe): Implement sync with other clouds (s3, gs)
212
+ raise NotImplementedError('Moving data directly from S3 to OCI bucket '
213
+ 'is currently not supported. Please specify '
214
+ 'a local source for the storage object.')
215
+
216
+
217
+ def gcs_to_oci(gs_bucket_name: str, oci_bucket_name: str) -> None:
218
+ """Creates a one-time transfer from Google Cloud Storage to
219
+ OCI Object Storage.
220
+ Args:
221
+ gs_bucket_name: str; Name of the Google Cloud Storage Bucket
222
+ oci_bucket_name: str; Name of the OCI Bucket
223
+ """
224
+ # TODO(HysunHe): Implement sync with other clouds (s3, gs)
225
+ raise NotImplementedError('Moving data directly from GCS to OCI bucket '
226
+ 'is currently not supported. Please specify '
227
+ 'a local source for the storage object.')
228
+
229
+
230
+ def r2_to_oci(r2_bucket_name: str, oci_bucket_name: str) -> None:
231
+ """Creates a one-time transfer from Cloudflare R2 to OCI Bucket.
232
+ Args:
233
+ r2_bucket_name: str; Name of the Cloudflare R2 Bucket
234
+ oci_bucket_name: str; Name of the OCI Bucket
235
+ """
236
+ raise NotImplementedError(
237
+ 'Moving data directly from Cloudflare R2 to OCI '
238
+ 'bucket is currently not supported. Please specify '
239
+ 'a local source for the storage object.')
sky/data/data_utils.py CHANGED
@@ -730,3 +730,14 @@ class Rclone():
730
730
  lines_to_keep.append(line)
731
731
 
732
732
  return lines_to_keep
733
+
734
+
735
+ def split_oci_path(oci_path: str) -> Tuple[str, str]:
736
+ """Splits OCI Path into Bucket name and Relative Path to Bucket
737
+ Args:
738
+ oci_path: str; OCI Path, e.g. oci://imagenet/train/
739
+ """
740
+ path_parts = oci_path.replace('oci://', '').split('/')
741
+ bucket = path_parts.pop(0)
742
+ key = '/'.join(path_parts)
743
+ return bucket, key
@@ -19,6 +19,7 @@ BLOBFUSE2_VERSION = '2.2.0'
19
19
  _BLOBFUSE_CACHE_ROOT_DIR = '~/.sky/blobfuse2_cache'
20
20
  _BLOBFUSE_CACHE_DIR = ('~/.sky/blobfuse2_cache/'
21
21
  '{storage_account_name}_{container_name}')
22
+ RCLONE_VERSION = 'v1.68.2'
22
23
 
23
24
 
24
25
  def get_s3_mount_install_cmd() -> str:
@@ -158,6 +159,48 @@ def get_cos_mount_cmd(rclone_config_data: str, rclone_config_path: str,
158
159
  return mount_cmd
159
160
 
160
161
 
162
+ def get_rclone_install_cmd() -> str:
163
+ """ RClone installation for both apt-get and rpm.
164
+ This would be common command.
165
+ """
166
+ # pylint: disable=line-too-long
167
+ install_cmd = (
168
+ f'(which dpkg > /dev/null 2>&1 && (which rclone > /dev/null || (cd ~ > /dev/null'
169
+ f' && curl -O https://downloads.rclone.org/{RCLONE_VERSION}/rclone-{RCLONE_VERSION}-linux-amd64.deb'
170
+ f' && sudo dpkg -i rclone-{RCLONE_VERSION}-linux-amd64.deb'
171
+ f' && rm -f rclone-{RCLONE_VERSION}-linux-amd64.deb)))'
172
+ f' || (which rclone > /dev/null || (cd ~ > /dev/null'
173
+ f' && curl -O https://downloads.rclone.org/{RCLONE_VERSION}/rclone-{RCLONE_VERSION}-linux-amd64.rpm'
174
+ f' && sudo yum --nogpgcheck install rclone-{RCLONE_VERSION}-linux-amd64.rpm -y'
175
+ f' && rm -f rclone-{RCLONE_VERSION}-linux-amd64.rpm))')
176
+ return install_cmd
177
+
178
+
179
+ def get_oci_mount_cmd(mount_path: str, store_name: str, region: str,
180
+ namespace: str, compartment: str, config_file: str,
181
+ config_profile: str) -> str:
182
+ """ OCI specific RClone mount command for oci object storage. """
183
+ # pylint: disable=line-too-long
184
+ mount_cmd = (
185
+ f'sudo chown -R `whoami` {mount_path}'
186
+ f' && rclone config create oos_{store_name} oracleobjectstorage'
187
+ f' provider user_principal_auth namespace {namespace}'
188
+ f' compartment {compartment} region {region}'
189
+ f' oci-config-file {config_file}'
190
+ f' oci-config-profile {config_profile}'
191
+ f' && sed -i "s/oci-config-file/config_file/g;'
192
+ f' s/oci-config-profile/config_profile/g" ~/.config/rclone/rclone.conf'
193
+ f' && ([ ! -f /bin/fusermount3 ] && sudo ln -s /bin/fusermount /bin/fusermount3 || true)'
194
+ f' && (grep -q {mount_path} /proc/mounts || rclone mount oos_{store_name}:{store_name} {mount_path} --daemon --allow-non-empty)'
195
+ )
196
+ return mount_cmd
197
+
198
+
199
+ def get_rclone_version_check_cmd() -> str:
200
+ """ RClone version check. This would be common command. """
201
+ return f'rclone --version | grep -q {RCLONE_VERSION}'
202
+
203
+
161
204
  def _get_mount_binary(mount_cmd: str) -> str:
162
205
  """Returns mounting binary in string given as the mount command.
163
206
 
sky/data/storage.py CHANGED
@@ -24,6 +24,7 @@ from sky.adaptors import azure
24
24
  from sky.adaptors import cloudflare
25
25
  from sky.adaptors import gcp
26
26
  from sky.adaptors import ibm
27
+ from sky.adaptors import oci
27
28
  from sky.data import data_transfer
28
29
  from sky.data import data_utils
29
30
  from sky.data import mounting_utils
@@ -54,7 +55,9 @@ STORE_ENABLED_CLOUDS: List[str] = [
54
55
  str(clouds.AWS()),
55
56
  str(clouds.GCP()),
56
57
  str(clouds.Azure()),
57
- str(clouds.IBM()), cloudflare.NAME
58
+ str(clouds.IBM()),
59
+ str(clouds.OCI()),
60
+ cloudflare.NAME,
58
61
  ]
59
62
 
60
63
  # Maximum number of concurrent rsync upload processes
@@ -115,6 +118,7 @@ class StoreType(enum.Enum):
115
118
  AZURE = 'AZURE'
116
119
  R2 = 'R2'
117
120
  IBM = 'IBM'
121
+ OCI = 'OCI'
118
122
 
119
123
  @classmethod
120
124
  def from_cloud(cls, cloud: str) -> 'StoreType':
@@ -128,6 +132,8 @@ class StoreType(enum.Enum):
128
132
  return StoreType.R2
129
133
  elif cloud.lower() == str(clouds.Azure()).lower():
130
134
  return StoreType.AZURE
135
+ elif cloud.lower() == str(clouds.OCI()).lower():
136
+ return StoreType.OCI
131
137
  elif cloud.lower() == str(clouds.Lambda()).lower():
132
138
  with ux_utils.print_exception_no_traceback():
133
139
  raise ValueError('Lambda Cloud does not provide cloud storage.')
@@ -149,6 +155,8 @@ class StoreType(enum.Enum):
149
155
  return StoreType.R2
150
156
  elif isinstance(store, IBMCosStore):
151
157
  return StoreType.IBM
158
+ elif isinstance(store, OciStore):
159
+ return StoreType.OCI
152
160
  else:
153
161
  with ux_utils.print_exception_no_traceback():
154
162
  raise ValueError(f'Unknown store type: {store}')
@@ -165,6 +173,8 @@ class StoreType(enum.Enum):
165
173
  return 'r2://'
166
174
  elif self == StoreType.IBM:
167
175
  return 'cos://'
176
+ elif self == StoreType.OCI:
177
+ return 'oci://'
168
178
  else:
169
179
  with ux_utils.print_exception_no_traceback():
170
180
  raise ValueError(f'Unknown store type: {self}')
@@ -564,6 +574,8 @@ class Storage(object):
564
574
  self.add_store(StoreType.R2)
565
575
  elif self.source.startswith('cos://'):
566
576
  self.add_store(StoreType.IBM)
577
+ elif self.source.startswith('oci://'):
578
+ self.add_store(StoreType.OCI)
567
579
 
568
580
  @staticmethod
569
581
  def _validate_source(
@@ -644,7 +656,7 @@ class Storage(object):
644
656
  'using a bucket by writing <destination_path>: '
645
657
  f'{source} in the file_mounts section of your YAML')
646
658
  is_local_source = True
647
- elif split_path.scheme in ['s3', 'gs', 'https', 'r2', 'cos']:
659
+ elif split_path.scheme in ['s3', 'gs', 'https', 'r2', 'cos', 'oci']:
648
660
  is_local_source = False
649
661
  # Storage mounting does not support mounting specific files from
650
662
  # cloud store - ensure path points to only a directory
@@ -668,7 +680,7 @@ class Storage(object):
668
680
  with ux_utils.print_exception_no_traceback():
669
681
  raise exceptions.StorageSourceError(
670
682
  f'Supported paths: local, s3://, gs://, https://, '
671
- f'r2://, cos://. Got: {source}')
683
+ f'r2://, cos://, oci://. Got: {source}')
672
684
  return source, is_local_source
673
685
 
674
686
  def _validate_storage_spec(self, name: Optional[str]) -> None:
@@ -683,7 +695,7 @@ class Storage(object):
683
695
  """
684
696
  prefix = name.split('://')[0]
685
697
  prefix = prefix.lower()
686
- if prefix in ['s3', 'gs', 'https', 'r2', 'cos']:
698
+ if prefix in ['s3', 'gs', 'https', 'r2', 'cos', 'oci']:
687
699
  with ux_utils.print_exception_no_traceback():
688
700
  raise exceptions.StorageNameError(
689
701
  'Prefix detected: `name` cannot start with '
@@ -798,6 +810,11 @@ class Storage(object):
798
810
  s_metadata,
799
811
  source=self.source,
800
812
  sync_on_reconstruction=self.sync_on_reconstruction)
813
+ elif s_type == StoreType.OCI:
814
+ store = OciStore.from_metadata(
815
+ s_metadata,
816
+ source=self.source,
817
+ sync_on_reconstruction=self.sync_on_reconstruction)
801
818
  else:
802
819
  with ux_utils.print_exception_no_traceback():
803
820
  raise ValueError(f'Unknown store type: {s_type}')
@@ -886,6 +903,8 @@ class Storage(object):
886
903
  store_cls = R2Store
887
904
  elif store_type == StoreType.IBM:
888
905
  store_cls = IBMCosStore
906
+ elif store_type == StoreType.OCI:
907
+ store_cls = OciStore
889
908
  else:
890
909
  with ux_utils.print_exception_no_traceback():
891
910
  raise exceptions.StorageSpecError(
@@ -1149,6 +1168,9 @@ class S3Store(AbstractStore):
1149
1168
  assert data_utils.verify_ibm_cos_bucket(self.name), (
1150
1169
  f'Source specified as {self.source}, a COS bucket. ',
1151
1170
  'COS Bucket should exist.')
1171
+ elif self.source.startswith('oci://'):
1172
+ raise NotImplementedError(
1173
+ 'Moving data from OCI to S3 is currently not supported.')
1152
1174
  # Validate name
1153
1175
  self.name = self.validate_name(self.name)
1154
1176
 
@@ -1260,6 +1282,8 @@ class S3Store(AbstractStore):
1260
1282
  self._transfer_to_s3()
1261
1283
  elif self.source.startswith('r2://'):
1262
1284
  self._transfer_to_s3()
1285
+ elif self.source.startswith('oci://'):
1286
+ self._transfer_to_s3()
1263
1287
  else:
1264
1288
  self.batch_aws_rsync([self.source])
1265
1289
  except exceptions.StorageUploadError:
@@ -1588,6 +1612,9 @@ class GcsStore(AbstractStore):
1588
1612
  assert data_utils.verify_ibm_cos_bucket(self.name), (
1589
1613
  f'Source specified as {self.source}, a COS bucket. ',
1590
1614
  'COS Bucket should exist.')
1615
+ elif self.source.startswith('oci://'):
1616
+ raise NotImplementedError(
1617
+ 'Moving data from OCI to GCS is currently not supported.')
1591
1618
  # Validate name
1592
1619
  self.name = self.validate_name(self.name)
1593
1620
  # Check if the storage is enabled
@@ -1696,6 +1723,8 @@ class GcsStore(AbstractStore):
1696
1723
  self._transfer_to_gcs()
1697
1724
  elif self.source.startswith('r2://'):
1698
1725
  self._transfer_to_gcs()
1726
+ elif self.source.startswith('oci://'):
1727
+ self._transfer_to_gcs()
1699
1728
  else:
1700
1729
  # If a single directory is specified in source, upload
1701
1730
  # contents to root of bucket by suffixing /*.
@@ -2122,6 +2151,9 @@ class AzureBlobStore(AbstractStore):
2122
2151
  assert data_utils.verify_ibm_cos_bucket(self.name), (
2123
2152
  f'Source specified as {self.source}, a COS bucket. ',
2124
2153
  'COS Bucket should exist.')
2154
+ elif self.source.startswith('oci://'):
2155
+ raise NotImplementedError(
2156
+ 'Moving data from OCI to AZureBlob is not supported.')
2125
2157
  # Validate name
2126
2158
  self.name = self.validate_name(self.name)
2127
2159
 
@@ -2474,6 +2506,8 @@ class AzureBlobStore(AbstractStore):
2474
2506
  raise NotImplementedError(error_message.format('R2'))
2475
2507
  elif self.source.startswith('cos://'):
2476
2508
  raise NotImplementedError(error_message.format('IBM COS'))
2509
+ elif self.source.startswith('oci://'):
2510
+ raise NotImplementedError(error_message.format('OCI'))
2477
2511
  else:
2478
2512
  self.batch_az_blob_sync([self.source])
2479
2513
  except exceptions.StorageUploadError:
@@ -2833,6 +2867,10 @@ class R2Store(AbstractStore):
2833
2867
  assert data_utils.verify_ibm_cos_bucket(self.name), (
2834
2868
  f'Source specified as {self.source}, a COS bucket. ',
2835
2869
  'COS Bucket should exist.')
2870
+ elif self.source.startswith('oci://'):
2871
+ raise NotImplementedError(
2872
+ 'Moving data from OCI to R2 is currently not supported.')
2873
+
2836
2874
  # Validate name
2837
2875
  self.name = S3Store.validate_name(self.name)
2838
2876
  # Check if the storage is enabled
@@ -2884,6 +2922,8 @@ class R2Store(AbstractStore):
2884
2922
  self._transfer_to_r2()
2885
2923
  elif self.source.startswith('r2://'):
2886
2924
  pass
2925
+ elif self.source.startswith('oci://'):
2926
+ self._transfer_to_r2()
2887
2927
  else:
2888
2928
  self.batch_aws_rsync([self.source])
2889
2929
  except exceptions.StorageUploadError:
@@ -3590,3 +3630,413 @@ class IBMCosStore(AbstractStore):
3590
3630
  if e.__class__.__name__ == 'NoSuchBucket':
3591
3631
  logger.debug('bucket already removed')
3592
3632
  Rclone.delete_rclone_bucket_profile(self.name, Rclone.RcloneClouds.IBM)
3633
+
3634
+
3635
+ class OciStore(AbstractStore):
3636
+ """OciStore inherits from Storage Object and represents the backend
3637
+ for OCI buckets.
3638
+ """
3639
+
3640
+ _ACCESS_DENIED_MESSAGE = 'AccessDeniedException'
3641
+
3642
+ def __init__(self,
3643
+ name: str,
3644
+ source: str,
3645
+ region: Optional[str] = None,
3646
+ is_sky_managed: Optional[bool] = None,
3647
+ sync_on_reconstruction: Optional[bool] = True):
3648
+ self.client: Any
3649
+ self.bucket: StorageHandle
3650
+ self.oci_config_file: str
3651
+ self.config_profile: str
3652
+ self.compartment: str
3653
+ self.namespace: str
3654
+
3655
+ # Bucket region should be consistence with the OCI config file
3656
+ region = oci.get_oci_config()['region']
3657
+
3658
+ super().__init__(name, source, region, is_sky_managed,
3659
+ sync_on_reconstruction)
3660
+
3661
+ def _validate(self):
3662
+ if self.source is not None and isinstance(self.source, str):
3663
+ if self.source.startswith('oci://'):
3664
+ assert self.name == data_utils.split_oci_path(self.source)[0], (
3665
+ 'OCI Bucket is specified as path, the name should be '
3666
+ 'the same as OCI bucket.')
3667
+ elif not re.search(r'^\w+://', self.source):
3668
+ # Treat it as local path.
3669
+ pass
3670
+ else:
3671
+ raise NotImplementedError(
3672
+ f'Moving data from {self.source} to OCI is not supported.')
3673
+
3674
+ # Validate name
3675
+ self.name = self.validate_name(self.name)
3676
+ # Check if the storage is enabled
3677
+ if not _is_storage_cloud_enabled(str(clouds.OCI())):
3678
+ with ux_utils.print_exception_no_traceback():
3679
+ raise exceptions.ResourcesUnavailableError(
3680
+ 'Storage \'store: oci\' specified, but ' \
3681
+ 'OCI access is disabled. To fix, enable '\
3682
+ 'OCI by running `sky check`. '\
3683
+ 'More info: https://skypilot.readthedocs.io/en/latest/getting-started/installation.html.' # pylint: disable=line-too-long
3684
+ )
3685
+
3686
+ @classmethod
3687
+ def validate_name(cls, name) -> str:
3688
+ """Validates the name of the OCI store.
3689
+
3690
+ Source for rules: https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/managingbuckets.htm#Managing_Buckets # pylint: disable=line-too-long
3691
+ """
3692
+
3693
+ def _raise_no_traceback_name_error(err_str):
3694
+ with ux_utils.print_exception_no_traceback():
3695
+ raise exceptions.StorageNameError(err_str)
3696
+
3697
+ if name is not None and isinstance(name, str):
3698
+ # Check for overall length
3699
+ if not 1 <= len(name) <= 256:
3700
+ _raise_no_traceback_name_error(
3701
+ f'Invalid store name: name {name} must contain 1-256 '
3702
+ 'characters.')
3703
+
3704
+ # Check for valid characters and start/end with a number or letter
3705
+ pattern = r'^[A-Za-z0-9-._]+$'
3706
+ if not re.match(pattern, name):
3707
+ _raise_no_traceback_name_error(
3708
+ f'Invalid store name: name {name} can only contain '
3709
+ 'upper or lower case letters, numeric characters, hyphens '
3710
+ '(-), underscores (_), and dots (.). Spaces are not '
3711
+ 'allowed. Names must start and end with a number or '
3712
+ 'letter.')
3713
+ else:
3714
+ _raise_no_traceback_name_error('Store name must be specified.')
3715
+ return name
3716
+
3717
+ def initialize(self):
3718
+ """Initializes the OCI store object on the cloud.
3719
+
3720
+ Initialization involves fetching bucket if exists, or creating it if
3721
+ it does not.
3722
+
3723
+ Raises:
3724
+ StorageBucketCreateError: If bucket creation fails
3725
+ StorageBucketGetError: If fetching existing bucket fails
3726
+ StorageInitError: If general initialization fails.
3727
+ """
3728
+ # pylint: disable=import-outside-toplevel
3729
+ from sky.clouds.utils import oci_utils
3730
+ from sky.provision.oci.query_utils import query_helper
3731
+
3732
+ self.oci_config_file = oci.get_config_file()
3733
+ self.config_profile = oci_utils.oci_config.get_profile()
3734
+
3735
+ ## pylint: disable=line-too-long
3736
+ # What's compartment? See thttps://docs.oracle.com/en/cloud/foundation/cloud_architecture/governance/compartments.html
3737
+ self.compartment = query_helper.find_compartment(self.region)
3738
+ self.client = oci.get_object_storage_client(region=self.region,
3739
+ profile=self.config_profile)
3740
+ self.namespace = self.client.get_namespace(
3741
+ compartment_id=oci.get_oci_config()['tenancy']).data
3742
+
3743
+ self.bucket, is_new_bucket = self._get_bucket()
3744
+ if self.is_sky_managed is None:
3745
+ # If is_sky_managed is not specified, then this is a new storage
3746
+ # object (i.e., did not exist in global_user_state) and we should
3747
+ # set the is_sky_managed property.
3748
+ # If is_sky_managed is specified, then we take no action.
3749
+ self.is_sky_managed = is_new_bucket
3750
+
3751
+ def upload(self):
3752
+ """Uploads source to store bucket.
3753
+
3754
+ Upload must be called by the Storage handler - it is not called on
3755
+ Store initialization.
3756
+
3757
+ Raises:
3758
+ StorageUploadError: if upload fails.
3759
+ """
3760
+ try:
3761
+ if isinstance(self.source, list):
3762
+ self.batch_oci_rsync(self.source, create_dirs=True)
3763
+ elif self.source is not None:
3764
+ if self.source.startswith('oci://'):
3765
+ pass
3766
+ else:
3767
+ self.batch_oci_rsync([self.source])
3768
+ except exceptions.StorageUploadError:
3769
+ raise
3770
+ except Exception as e:
3771
+ raise exceptions.StorageUploadError(
3772
+ f'Upload failed for store {self.name}') from e
3773
+
3774
+ def delete(self) -> None:
3775
+ deleted_by_skypilot = self._delete_oci_bucket(self.name)
3776
+ if deleted_by_skypilot:
3777
+ msg_str = f'Deleted OCI bucket {self.name}.'
3778
+ else:
3779
+ msg_str = (f'OCI bucket {self.name} may have been deleted '
3780
+ f'externally. Removing from local state.')
3781
+ logger.info(f'{colorama.Fore.GREEN}{msg_str}'
3782
+ f'{colorama.Style.RESET_ALL}')
3783
+
3784
+ def get_handle(self) -> StorageHandle:
3785
+ return self.client.get_bucket(namespace_name=self.namespace,
3786
+ bucket_name=self.name).data
3787
+
3788
+ def batch_oci_rsync(self,
3789
+ source_path_list: List[Path],
3790
+ create_dirs: bool = False) -> None:
3791
+ """Invokes oci sync to batch upload a list of local paths to Bucket
3792
+
3793
+ Use OCI bulk operation to batch process the file upload
3794
+
3795
+ Args:
3796
+ source_path_list: List of paths to local files or directories
3797
+ create_dirs: If the local_path is a directory and this is set to
3798
+ False, the contents of the directory are directly uploaded to
3799
+ root of the bucket. If the local_path is a directory and this is
3800
+ set to True, the directory is created in the bucket root and
3801
+ contents are uploaded to it.
3802
+ """
3803
+
3804
+ @oci.with_oci_env
3805
+ def get_file_sync_command(base_dir_path, file_names):
3806
+ includes = ' '.join(
3807
+ [f'--include "{file_name}"' for file_name in file_names])
3808
+ sync_command = (
3809
+ 'oci os object bulk-upload --no-follow-symlinks --overwrite '
3810
+ f'--bucket-name {self.name} --namespace-name {self.namespace} '
3811
+ f'--src-dir "{base_dir_path}" {includes}')
3812
+
3813
+ return sync_command
3814
+
3815
+ @oci.with_oci_env
3816
+ def get_dir_sync_command(src_dir_path, dest_dir_name):
3817
+ if dest_dir_name and not str(dest_dir_name).endswith('/'):
3818
+ dest_dir_name = f'{dest_dir_name}/'
3819
+
3820
+ excluded_list = storage_utils.get_excluded_files(src_dir_path)
3821
+ excluded_list.append('.git/*')
3822
+ excludes = ' '.join([
3823
+ f'--exclude {shlex.quote(file_name)}'
3824
+ for file_name in excluded_list
3825
+ ])
3826
+
3827
+ # we exclude .git directory from the sync
3828
+ sync_command = (
3829
+ 'oci os object bulk-upload --no-follow-symlinks --overwrite '
3830
+ f'--bucket-name {self.name} --namespace-name {self.namespace} '
3831
+ f'--object-prefix "{dest_dir_name}" --src-dir "{src_dir_path}" '
3832
+ f'{excludes} ')
3833
+
3834
+ return sync_command
3835
+
3836
+ # Generate message for upload
3837
+ if len(source_path_list) > 1:
3838
+ source_message = f'{len(source_path_list)} paths'
3839
+ else:
3840
+ source_message = source_path_list[0]
3841
+
3842
+ log_path = sky_logging.generate_tmp_logging_file_path(
3843
+ _STORAGE_LOG_FILE_NAME)
3844
+ sync_path = f'{source_message} -> oci://{self.name}/'
3845
+ with rich_utils.safe_status(
3846
+ ux_utils.spinner_message(f'Syncing {sync_path}',
3847
+ log_path=log_path)):
3848
+ data_utils.parallel_upload(
3849
+ source_path_list=source_path_list,
3850
+ filesync_command_generator=get_file_sync_command,
3851
+ dirsync_command_generator=get_dir_sync_command,
3852
+ log_path=log_path,
3853
+ bucket_name=self.name,
3854
+ access_denied_message=self._ACCESS_DENIED_MESSAGE,
3855
+ create_dirs=create_dirs,
3856
+ max_concurrent_uploads=1)
3857
+
3858
+ logger.info(
3859
+ ux_utils.finishing_message(f'Storage synced: {sync_path}',
3860
+ log_path))
3861
+
3862
+ def _get_bucket(self) -> Tuple[StorageHandle, bool]:
3863
+ """Obtains the OCI bucket.
3864
+ If the bucket exists, this method will connect to the bucket.
3865
+
3866
+ If the bucket does not exist, there are three cases:
3867
+ 1) Raise an error if the bucket source starts with oci://
3868
+ 2) Return None if bucket has been externally deleted and
3869
+ sync_on_reconstruction is False
3870
+ 3) Create and return a new bucket otherwise
3871
+
3872
+ Return tuple (Bucket, Boolean): The first item is the bucket
3873
+ json payload from the OCI API call, the second item indicates
3874
+ if this is a new created bucket(True) or an existing bucket(False).
3875
+
3876
+ Raises:
3877
+ StorageBucketCreateError: If creating the bucket fails
3878
+ StorageBucketGetError: If fetching a bucket fails
3879
+ """
3880
+ try:
3881
+ get_bucket_response = self.client.get_bucket(
3882
+ namespace_name=self.namespace, bucket_name=self.name)
3883
+ bucket = get_bucket_response.data
3884
+ return bucket, False
3885
+ except oci.service_exception() as e:
3886
+ if e.status == 404: # Not Found
3887
+ if isinstance(self.source,
3888
+ str) and self.source.startswith('oci://'):
3889
+ with ux_utils.print_exception_no_traceback():
3890
+ raise exceptions.StorageBucketGetError(
3891
+ 'Attempted to connect to a non-existent bucket: '
3892
+ f'{self.source}') from e
3893
+ else:
3894
+ # If bucket cannot be found (i.e., does not exist), it is
3895
+ # to be created by Sky. However, creation is skipped if
3896
+ # Store object is being reconstructed for deletion.
3897
+ if self.sync_on_reconstruction:
3898
+ bucket = self._create_oci_bucket(self.name)
3899
+ return bucket, True
3900
+ else:
3901
+ return None, False
3902
+ elif e.status == 401: # Unauthorized
3903
+ # AccessDenied error for buckets that are private and not
3904
+ # owned by user.
3905
+ command = (
3906
+ f'oci os object list --namespace-name {self.namespace} '
3907
+ f'--bucket-name {self.name}')
3908
+ with ux_utils.print_exception_no_traceback():
3909
+ raise exceptions.StorageBucketGetError(
3910
+ _BUCKET_FAIL_TO_CONNECT_MESSAGE.format(name=self.name) +
3911
+ f' To debug, consider running `{command}`.') from e
3912
+ else:
3913
+ # Unknown / unexpected error happened. This might happen when
3914
+ # Object storage service itself functions not normal (e.g.
3915
+ # maintainance event causes internal server error or request
3916
+ # timeout, etc).
3917
+ with ux_utils.print_exception_no_traceback():
3918
+ raise exceptions.StorageBucketGetError(
3919
+ f'Failed to connect to OCI bucket {self.name}') from e
3920
+
3921
+ def mount_command(self, mount_path: str) -> str:
3922
+ """Returns the command to mount the bucket to the mount_path.
3923
+
3924
+ Uses Rclone to mount the bucket.
3925
+
3926
+ Args:
3927
+ mount_path: str; Path to mount the bucket to.
3928
+ """
3929
+ install_cmd = mounting_utils.get_rclone_install_cmd()
3930
+ mount_cmd = mounting_utils.get_oci_mount_cmd(
3931
+ mount_path=mount_path,
3932
+ store_name=self.name,
3933
+ region=str(self.region),
3934
+ namespace=self.namespace,
3935
+ compartment=self.bucket.compartment_id,
3936
+ config_file=self.oci_config_file,
3937
+ config_profile=self.config_profile)
3938
+ version_check_cmd = mounting_utils.get_rclone_version_check_cmd()
3939
+
3940
+ return mounting_utils.get_mounting_command(mount_path, install_cmd,
3941
+ mount_cmd, version_check_cmd)
3942
+
3943
+ def _download_file(self, remote_path: str, local_path: str) -> None:
3944
+ """Downloads file from remote to local on OCI bucket
3945
+
3946
+ Args:
3947
+ remote_path: str; Remote path on OCI bucket
3948
+ local_path: str; Local path on user's device
3949
+ """
3950
+ if remote_path.startswith(f'/{self.name}'):
3951
+ # If the remote path is /bucket_name, we need to
3952
+ # remove the leading /
3953
+ remote_path = remote_path.lstrip('/')
3954
+
3955
+ filename = os.path.basename(remote_path)
3956
+ if not local_path.endswith(filename):
3957
+ local_path = os.path.join(local_path, filename)
3958
+
3959
+ @oci.with_oci_env
3960
+ def get_file_download_command(remote_path, local_path):
3961
+ download_command = (f'oci os object get --bucket-name {self.name} '
3962
+ f'--namespace-name {self.namespace} '
3963
+ f'--name {remote_path} --file {local_path}')
3964
+
3965
+ return download_command
3966
+
3967
+ download_command = get_file_download_command(remote_path, local_path)
3968
+
3969
+ try:
3970
+ with rich_utils.safe_status(
3971
+ f'[bold cyan]Downloading: {remote_path} -> {local_path}[/]'
3972
+ ):
3973
+ subprocess.check_output(download_command,
3974
+ stderr=subprocess.STDOUT,
3975
+ shell=True)
3976
+ except subprocess.CalledProcessError as e:
3977
+ logger.error(f'Download failed: {remote_path} -> {local_path}.\n'
3978
+ f'Detail errors: {e.output}')
3979
+ with ux_utils.print_exception_no_traceback():
3980
+ raise exceptions.StorageBucketDeleteError(
3981
+ f'Failed download file {self.name}:{remote_path}.') from e
3982
+
3983
+ def _create_oci_bucket(self, bucket_name: str) -> StorageHandle:
3984
+ """Creates OCI bucket with specific name in specific region
3985
+
3986
+ Args:
3987
+ bucket_name: str; Name of bucket
3988
+ region: str; Region name, e.g. us-central1, us-west1
3989
+ """
3990
+ logger.debug(f'_create_oci_bucket: {bucket_name}')
3991
+ try:
3992
+ create_bucket_response = self.client.create_bucket(
3993
+ namespace_name=self.namespace,
3994
+ create_bucket_details=oci.oci.object_storage.models.
3995
+ CreateBucketDetails(
3996
+ name=bucket_name,
3997
+ compartment_id=self.compartment,
3998
+ ))
3999
+ bucket = create_bucket_response.data
4000
+ return bucket
4001
+ except oci.service_exception() as e:
4002
+ with ux_utils.print_exception_no_traceback():
4003
+ raise exceptions.StorageBucketCreateError(
4004
+ f'Failed to create OCI bucket: {self.name}') from e
4005
+
4006
+ def _delete_oci_bucket(self, bucket_name: str) -> bool:
4007
+ """Deletes OCI bucket, including all objects in bucket
4008
+
4009
+ Args:
4010
+ bucket_name: str; Name of bucket
4011
+
4012
+ Returns:
4013
+ bool; True if bucket was deleted, False if it was deleted externally.
4014
+ """
4015
+ logger.debug(f'_delete_oci_bucket: {bucket_name}')
4016
+
4017
+ @oci.with_oci_env
4018
+ def get_bucket_delete_command(bucket_name):
4019
+ remove_command = (f'oci os bucket delete --bucket-name '
4020
+ f'{bucket_name} --empty --force')
4021
+
4022
+ return remove_command
4023
+
4024
+ remove_command = get_bucket_delete_command(bucket_name)
4025
+
4026
+ try:
4027
+ with rich_utils.safe_status(
4028
+ f'[bold cyan]Deleting OCI bucket {bucket_name}[/]'):
4029
+ subprocess.check_output(remove_command.split(' '),
4030
+ stderr=subprocess.STDOUT)
4031
+ except subprocess.CalledProcessError as e:
4032
+ if 'BucketNotFound' in e.output.decode('utf-8'):
4033
+ logger.debug(
4034
+ _BUCKET_EXTERNALLY_DELETED_DEBUG_MESSAGE.format(
4035
+ bucket_name=bucket_name))
4036
+ return False
4037
+ else:
4038
+ logger.error(e.output)
4039
+ with ux_utils.print_exception_no_traceback():
4040
+ raise exceptions.StorageBucketDeleteError(
4041
+ f'Failed to delete OCI bucket {bucket_name}.')
4042
+ return True
sky/task.py CHANGED
@@ -1031,6 +1031,16 @@ class Task:
1031
1031
  storage.name, data_utils.Rclone.RcloneClouds.IBM)
1032
1032
  blob_path = f'cos://{cos_region}/{storage.name}'
1033
1033
  self.update_file_mounts({mnt_path: blob_path})
1034
+ elif store_type is storage_lib.StoreType.OCI:
1035
+ if storage.source is not None and not isinstance(
1036
+ storage.source,
1037
+ list) and storage.source.startswith('oci://'):
1038
+ blob_path = storage.source
1039
+ else:
1040
+ blob_path = 'oci://' + storage.name
1041
+ self.update_file_mounts({
1042
+ mnt_path: blob_path,
1043
+ })
1034
1044
  else:
1035
1045
  with ux_utils.print_exception_no_traceback():
1036
1046
  raise ValueError(f'Storage Type {store_type} '
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: skypilot-nightly
3
- Version: 1.0.0.dev20241227
3
+ Version: 1.0.0.dev20241229
4
4
  Summary: SkyPilot: An intercloud broker for the clouds
5
5
  Author: SkyPilot Team
6
6
  License: Apache 2.0
@@ -1,9 +1,9 @@
1
- sky/__init__.py,sha256=aI3UP8tBRxx251e1cD3RByxgJoAM_BPOCMx567XLEdk,5944
1
+ sky/__init__.py,sha256=rgKaX_f8ZpGxGTSVcVywUjROuflkN25vsD-Yc9qHrCM,5944
2
2
  sky/admin_policy.py,sha256=hPo02f_A32gCqhUueF0QYy1fMSSKqRwYEg_9FxScN_s,3248
3
3
  sky/authentication.py,sha256=kACHmiZgWgRpYd1wx1ofbXRMErfMcFmWrkw4a9NxYrY,20988
4
4
  sky/check.py,sha256=s8deMVL-k9y8gd519K7NWZc3DqWsEySwiAr0uH3Vvcc,9459
5
5
  sky/cli.py,sha256=eFlx0PM6k2XnWN9DFOaqrfHVR8D7KkiGBACI1NIGExg,214034
6
- sky/cloud_stores.py,sha256=kqWqnldCtGJvGFmwspLsNF9AGEub1MyzvmYWZBUhARw,21147
6
+ sky/cloud_stores.py,sha256=WKy9u-ZVs07A4tQIFK9UVKI-NUUM6lD75r8TwTbDfWs,23276
7
7
  sky/core.py,sha256=CPwNZQlC5WKLzTb2Tjo2Uogg0EvOt-yLCRlegqK_92A,38598
8
8
  sky/dag.py,sha256=f3sJlkH4bE6Uuz3ozNtsMhcBpRx7KmC9Sa4seDKt4hU,3104
9
9
  sky/exceptions.py,sha256=rUi_au7QBNn3_wvwa8Y_MSHN3QDRpVLry8Mfa56LyGk,9197
@@ -14,7 +14,7 @@ sky/resources.py,sha256=zgUHgqCZGxvAABTe3JYukl4HrzQZi67D7ULFzAMk9YY,70325
14
14
  sky/sky_logging.py,sha256=7Zk9mL1TDxFkGsy3INMBKYlqsbognVGSMzAsHZdZlhw,5891
15
15
  sky/skypilot_config.py,sha256=FN93hSG-heQCHBnemlIK2TwrJngKbpx4vMXNUzPIzV8,9087
16
16
  sky/status_lib.py,sha256=J7Jb4_Dz0v2T64ttOdyUgpokvl4S0sBJrMfH7Fvo51A,1457
17
- sky/task.py,sha256=lS8ajVgqMZ1AtIWRzao2BXF2_KL4tPEdlmWg31TPhIo,49760
17
+ sky/task.py,sha256=sNgjSdOPmRkhoInuYpo7u3JpIO-xvMvlksCpS8SQlqE,50262
18
18
  sky/adaptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  sky/adaptors/aws.py,sha256=jz3E8YyeGscgLZvFE6A1SkxpH6o_inZ-0NiXdRnxSGA,6863
20
20
  sky/adaptors/azure.py,sha256=yjM8nAPW-mlSXfmA8OmJNnSIrZ9lQx2-GxiI-TIVrwE,21910
@@ -25,7 +25,7 @@ sky/adaptors/docker.py,sha256=_kzpZ0fkWHqqQAVVl0llTsCE31KYz3Sjn8psTBQHVkA,468
25
25
  sky/adaptors/gcp.py,sha256=OQ9RaqjR0r0iaWYpjvEtIx5vnEhyB4LhUCwbtdxsmVk,3115
26
26
  sky/adaptors/ibm.py,sha256=H87vD6izq_wQI8oQC7cx9iVtRgPi_QkAcrfa1Z3PNqU,4906
27
27
  sky/adaptors/kubernetes.py,sha256=5pRyPmXYpA0CrU5JFjh88TxC9TNemIaSUkSvaXysrCY,6510
28
- sky/adaptors/oci.py,sha256=stQ-BRukZsk_r4yjdeVedeHTJSW4lYSlLBMJPBCTfig,1772
28
+ sky/adaptors/oci.py,sha256=LfMSFUmkkNT6Yoz9FZHNl6UFSg4X1lJO4-x4ZbDdXTs,2831
29
29
  sky/adaptors/runpod.py,sha256=4Nt_BfZhJAKQNA3wO8cxvvNI8x4NsDGHu_4EhRDlGYQ,225
30
30
  sky/adaptors/vsphere.py,sha256=zJP9SeObEoLrpgHW2VHvZE48EhgVf8GfAEIwBeaDMfM,2129
31
31
  sky/backends/__init__.py,sha256=UDjwbUgpTRApbPJnNfR786GadUuwgRk3vsWoVu5RB_c,536
@@ -88,10 +88,10 @@ sky/clouds/utils/gcp_utils.py,sha256=h_ezG7uq2FogLJCPSu8GIEYLOEHmUtC_Xzt4fLOw19s
88
88
  sky/clouds/utils/oci_utils.py,sha256=e3k6GR0DBP6ansLtA3f1ojJXKrUbP-hBsLKZjJOjRtA,5792
89
89
  sky/clouds/utils/scp_utils.py,sha256=r4lhRLtNgoz5nmkfN2ctAXYugF_-Et8TYH6ZlbbFfo8,15791
90
90
  sky/data/__init__.py,sha256=Nhaf1NURisXpZuwWANa2IuCyppIuc720FRwqSE2oEwY,184
91
- sky/data/data_transfer.py,sha256=MBmjey9_p2L3IKNKTi8um09SlZe32n4wK3CkVnlTVvo,7346
92
- sky/data/data_utils.py,sha256=JTvhmB3Es9vTmWaRRCQJJ-ynKCzJl487Os65Lzrf0l0,28507
93
- sky/data/mounting_utils.py,sha256=HwBGg1NmX-2IJZV_6h2r1U3ajTGOyfmA3MqboA7znqU,11004
94
- sky/data/storage.py,sha256=osFIiFSsNHQqKccOmtAgL0m-KD8GHIuoMwg7URbLZ6k,165891
91
+ sky/data/data_transfer.py,sha256=wixC4_3_JaeJFdGKOp-O5ulcsMugDSgrCR0SnPpugGc,8946
92
+ sky/data/data_utils.py,sha256=GbHJy52f-iPmuWDGcwS4ftAXtFNR7xiDceUvY_ElN8c,28851
93
+ sky/data/mounting_utils.py,sha256=QjPBxGLnfkRrlKkQHHbTJxposzAWMDo5aRBe-dAa680,13111
94
+ sky/data/storage.py,sha256=qOdevFn9uPsUgBCjwakDjroZTx-ecLURHRXdPrAGBTk,185684
95
95
  sky/data/storage_utils.py,sha256=cM3kxlffYE7PnJySDu8huyUsMX_JYsf9uer8r5OYsjo,9556
96
96
  sky/jobs/__init__.py,sha256=yucibSB_ZimtJMvOhMxn6ZqwBIYNfcwmc6pSXtCqmNQ,1483
97
97
  sky/jobs/constants.py,sha256=YLgcCg_RHSYr_rfsI_4UIdXk78KKKOK29Oem88t5j8I,1350
@@ -279,9 +279,9 @@ sky/utils/kubernetes/k8s_gpu_labeler_job.yaml,sha256=k0TBoQ4zgf79-sVkixKSGYFHQ7Z
279
279
  sky/utils/kubernetes/k8s_gpu_labeler_setup.yaml,sha256=VLKT2KKimZu1GDg_4AIlIt488oMQvhRZWwsj9vBbPUg,3812
280
280
  sky/utils/kubernetes/rsync_helper.sh,sha256=h4YwrPFf9727CACnMJvF3EyK_0OeOYKKt4su_daKekw,1256
281
281
  sky/utils/kubernetes/ssh_jump_lifecycle_manager.py,sha256=Kq1MDygF2IxFmu9FXpCxqucXLmeUrvs6OtRij6XTQbo,6554
282
- skypilot_nightly-1.0.0.dev20241227.dist-info/LICENSE,sha256=emRJAvE7ngL6x0RhQvlns5wJzGI3NEQ_WMjNmd9TZc4,12170
283
- skypilot_nightly-1.0.0.dev20241227.dist-info/METADATA,sha256=n4ynEKFPKHO5eylf65HJhXVnNc8FGTxHJoW0S-gfT2Q,20149
284
- skypilot_nightly-1.0.0.dev20241227.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
285
- skypilot_nightly-1.0.0.dev20241227.dist-info/entry_points.txt,sha256=StA6HYpuHj-Y61L2Ze-hK2IcLWgLZcML5gJu8cs6nU4,36
286
- skypilot_nightly-1.0.0.dev20241227.dist-info/top_level.txt,sha256=qA8QuiNNb6Y1OF-pCUtPEr6sLEwy2xJX06Bd_CrtrHY,4
287
- skypilot_nightly-1.0.0.dev20241227.dist-info/RECORD,,
282
+ skypilot_nightly-1.0.0.dev20241229.dist-info/LICENSE,sha256=emRJAvE7ngL6x0RhQvlns5wJzGI3NEQ_WMjNmd9TZc4,12170
283
+ skypilot_nightly-1.0.0.dev20241229.dist-info/METADATA,sha256=p-B0B7WRK91Lc2eTecq9f66Dkkkrb8sAPqg90_c-zJQ,20149
284
+ skypilot_nightly-1.0.0.dev20241229.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
285
+ skypilot_nightly-1.0.0.dev20241229.dist-info/entry_points.txt,sha256=StA6HYpuHj-Y61L2Ze-hK2IcLWgLZcML5gJu8cs6nU4,36
286
+ skypilot_nightly-1.0.0.dev20241229.dist-info/top_level.txt,sha256=qA8QuiNNb6Y1OF-pCUtPEr6sLEwy2xJX06Bd_CrtrHY,4
287
+ skypilot_nightly-1.0.0.dev20241229.dist-info/RECORD,,