skypilot-nightly 1.0.0.dev20241228__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 +2 -2
- sky/adaptors/oci.py +32 -1
- sky/cloud_stores.py +61 -0
- sky/data/data_transfer.py +37 -0
- sky/data/data_utils.py +11 -0
- sky/data/mounting_utils.py +43 -0
- sky/data/storage.py +454 -4
- sky/task.py +10 -0
- {skypilot_nightly-1.0.0.dev20241228.dist-info → skypilot_nightly-1.0.0.dev20241229.dist-info}/METADATA +1 -1
- {skypilot_nightly-1.0.0.dev20241228.dist-info → skypilot_nightly-1.0.0.dev20241229.dist-info}/RECORD +14 -14
- {skypilot_nightly-1.0.0.dev20241228.dist-info → skypilot_nightly-1.0.0.dev20241229.dist-info}/LICENSE +0 -0
- {skypilot_nightly-1.0.0.dev20241228.dist-info → skypilot_nightly-1.0.0.dev20241229.dist-info}/WHEEL +0 -0
- {skypilot_nightly-1.0.0.dev20241228.dist-info → skypilot_nightly-1.0.0.dev20241229.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20241228.dist-info → skypilot_nightly-1.0.0.dev20241229.dist-info}/top_level.txt +0 -0
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 = '
|
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.
|
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=
|
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
|
sky/data/mounting_utils.py
CHANGED
@@ -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()),
|
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} '
|
{skypilot_nightly-1.0.0.dev20241228.dist-info → skypilot_nightly-1.0.0.dev20241229.dist-info}/RECORD
RENAMED
@@ -1,9 +1,9 @@
|
|
1
|
-
sky/__init__.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
92
|
-
sky/data/data_utils.py,sha256=
|
93
|
-
sky/data/mounting_utils.py,sha256=
|
94
|
-
sky/data/storage.py,sha256=
|
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.
|
283
|
-
skypilot_nightly-1.0.0.
|
284
|
-
skypilot_nightly-1.0.0.
|
285
|
-
skypilot_nightly-1.0.0.
|
286
|
-
skypilot_nightly-1.0.0.
|
287
|
-
skypilot_nightly-1.0.0.
|
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,,
|
File without changes
|
{skypilot_nightly-1.0.0.dev20241228.dist-info → skypilot_nightly-1.0.0.dev20241229.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|
File without changes
|