skypilot-nightly 1.0.0.dev20250101__py3-none-any.whl → 1.0.0.dev20250103__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 = '3b0b11011aefa882539e346629d0644fbd59cfe0'
8
+ _SKYPILOT_COMMIT_SHA = '6a6d6671958c9fce56ffa314144daa1156a43342'
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.dev20250101'
38
+ __version__ = '1.0.0.dev20250103'
39
39
  __root_dir__ = os.path.dirname(os.path.abspath(__file__))
40
40
 
41
41
 
sky/adaptors/do.py ADDED
@@ -0,0 +1,20 @@
1
+ """Digital Ocean cloud adaptors"""
2
+
3
+ # pylint: disable=import-outside-toplevel
4
+
5
+ from sky.adaptors import common
6
+
7
+ _IMPORT_ERROR_MESSAGE = ('Failed to import dependencies for DO. '
8
+ 'Try pip install "skypilot[do]"')
9
+ pydo = common.LazyImport('pydo', import_error_message=_IMPORT_ERROR_MESSAGE)
10
+ azure = common.LazyImport('azure', import_error_message=_IMPORT_ERROR_MESSAGE)
11
+ _LAZY_MODULES = (pydo, azure)
12
+
13
+
14
+ # `pydo`` inherits Azure exceptions. See:
15
+ # https://github.com/digitalocean/pydo/blob/7b01498d99eb0d3a772366b642e5fab3d6fc6aa2/examples/poc_droplets_volumes_sshkeys.py#L6
16
+ @common.load_lazy_modules(modules=_LAZY_MODULES)
17
+ def exceptions():
18
+ """Azure exceptions."""
19
+ from azure.core import exceptions as azure_exceptions
20
+ return azure_exceptions
@@ -1000,6 +1000,7 @@ def _add_auth_to_cluster_config(cloud: clouds.Cloud, cluster_config_file: str):
1000
1000
  clouds.Cudo,
1001
1001
  clouds.Paperspace,
1002
1002
  clouds.Azure,
1003
+ clouds.DO,
1003
1004
  )):
1004
1005
  config = auth.configure_ssh_info(config)
1005
1006
  elif isinstance(cloud, clouds.GCP):
@@ -178,6 +178,7 @@ def _get_cluster_config_template(cloud):
178
178
  clouds.SCP: 'scp-ray.yml.j2',
179
179
  clouds.OCI: 'oci-ray.yml.j2',
180
180
  clouds.Paperspace: 'paperspace-ray.yml.j2',
181
+ clouds.DO: 'do-ray.yml.j2',
181
182
  clouds.RunPod: 'runpod-ray.yml.j2',
182
183
  clouds.Kubernetes: 'kubernetes-ray.yml.j2',
183
184
  clouds.Vsphere: 'vsphere-ray.yml.j2',
sky/clouds/__init__.py CHANGED
@@ -15,6 +15,7 @@ from sky.clouds.cloud_registry import CLOUD_REGISTRY
15
15
  from sky.clouds.aws import AWS
16
16
  from sky.clouds.azure import Azure
17
17
  from sky.clouds.cudo import Cudo
18
+ from sky.clouds.do import DO
18
19
  from sky.clouds.fluidstack import Fluidstack
19
20
  from sky.clouds.gcp import GCP
20
21
  from sky.clouds.ibm import IBM
@@ -34,6 +35,7 @@ __all__ = [
34
35
  'Cudo',
35
36
  'GCP',
36
37
  'Lambda',
38
+ 'DO',
37
39
  'Paperspace',
38
40
  'SCP',
39
41
  'RunPod',
sky/clouds/do.py ADDED
@@ -0,0 +1,303 @@
1
+ """ Digital Ocean Cloud. """
2
+
3
+ import json
4
+ import typing
5
+ from typing import Dict, Iterator, List, Optional, Tuple, Union
6
+
7
+ from sky import clouds
8
+ from sky.adaptors import do
9
+ from sky.clouds import service_catalog
10
+ from sky.provision.do import utils as do_utils
11
+ from sky.utils import resources_utils
12
+
13
+ if typing.TYPE_CHECKING:
14
+ from sky import resources as resources_lib
15
+
16
+ _CREDENTIAL_FILE = 'config.yaml'
17
+
18
+
19
+ @clouds.CLOUD_REGISTRY.register(aliases=['digitalocean'])
20
+ class DO(clouds.Cloud):
21
+ """Digital Ocean Cloud"""
22
+
23
+ _REPR = 'DO'
24
+ _CLOUD_UNSUPPORTED_FEATURES = {
25
+ clouds.CloudImplementationFeatures.CLONE_DISK_FROM_CLUSTER:
26
+ 'Migrating '
27
+ f'disk is not supported in {_REPR}.',
28
+ clouds.CloudImplementationFeatures.SPOT_INSTANCE:
29
+ 'Spot instances are '
30
+ f'not supported in {_REPR}.',
31
+ clouds.CloudImplementationFeatures.CUSTOM_DISK_TIER:
32
+ 'Custom disk tiers'
33
+ f' is not supported in {_REPR}.',
34
+ }
35
+ # DO maximum node name length defined as <= 255
36
+ # https://docs.digitalocean.com/reference/api/api-reference/#operation/droplets_create
37
+ # 255 - 8 = 247 characters since
38
+ # our provisioner adds additional `-worker`.
39
+ _MAX_CLUSTER_NAME_LEN_LIMIT = 247
40
+ _regions: List[clouds.Region] = []
41
+
42
+ # Using the latest SkyPilot provisioner API to provision and check status.
43
+ PROVISIONER_VERSION = clouds.ProvisionerVersion.SKYPILOT
44
+ STATUS_VERSION = clouds.StatusVersion.SKYPILOT
45
+
46
+ @classmethod
47
+ def _unsupported_features_for_resources(
48
+ cls, resources: 'resources_lib.Resources'
49
+ ) -> Dict[clouds.CloudImplementationFeatures, str]:
50
+ """The features not supported based on the resources provided.
51
+
52
+ This method is used by check_features_are_supported() to check if the
53
+ cloud implementation supports all the requested features.
54
+
55
+ Returns:
56
+ A dict of {feature: reason} for the features not supported by the
57
+ cloud implementation.
58
+ """
59
+ del resources # unused
60
+ return cls._CLOUD_UNSUPPORTED_FEATURES
61
+
62
+ @classmethod
63
+ def _max_cluster_name_length(cls) -> Optional[int]:
64
+ return cls._MAX_CLUSTER_NAME_LEN_LIMIT
65
+
66
+ @classmethod
67
+ def regions_with_offering(
68
+ cls,
69
+ instance_type: str,
70
+ accelerators: Optional[Dict[str, int]],
71
+ use_spot: bool,
72
+ region: Optional[str],
73
+ zone: Optional[str],
74
+ ) -> List[clouds.Region]:
75
+ assert zone is None, 'DO does not support zones.'
76
+ del accelerators, zone # unused
77
+ if use_spot:
78
+ return []
79
+ regions = service_catalog.get_region_zones_for_instance_type(
80
+ instance_type, use_spot, 'DO')
81
+ if region is not None:
82
+ regions = [r for r in regions if r.name == region]
83
+ return regions
84
+
85
+ @classmethod
86
+ def get_vcpus_mem_from_instance_type(
87
+ cls,
88
+ instance_type: str,
89
+ ) -> Tuple[Optional[float], Optional[float]]:
90
+ return service_catalog.get_vcpus_mem_from_instance_type(instance_type,
91
+ clouds='DO')
92
+
93
+ @classmethod
94
+ def zones_provision_loop(
95
+ cls,
96
+ *,
97
+ region: str,
98
+ num_nodes: int,
99
+ instance_type: str,
100
+ accelerators: Optional[Dict[str, int]] = None,
101
+ use_spot: bool = False,
102
+ ) -> Iterator[None]:
103
+ del num_nodes # unused
104
+ regions = cls.regions_with_offering(instance_type,
105
+ accelerators,
106
+ use_spot,
107
+ region=region,
108
+ zone=None)
109
+ for r in regions:
110
+ assert r.zones is None, r
111
+ yield r.zones
112
+
113
+ def instance_type_to_hourly_cost(
114
+ self,
115
+ instance_type: str,
116
+ use_spot: bool,
117
+ region: Optional[str] = None,
118
+ zone: Optional[str] = None,
119
+ ) -> float:
120
+ return service_catalog.get_hourly_cost(
121
+ instance_type,
122
+ use_spot=use_spot,
123
+ region=region,
124
+ zone=zone,
125
+ clouds='DO',
126
+ )
127
+
128
+ def accelerators_to_hourly_cost(
129
+ self,
130
+ accelerators: Dict[str, int],
131
+ use_spot: bool,
132
+ region: Optional[str] = None,
133
+ zone: Optional[str] = None,
134
+ ) -> float:
135
+ """Returns the hourly cost of the accelerators, in dollars/hour."""
136
+ # the acc price is include in the instance price.
137
+ del accelerators, use_spot, region, zone # unused
138
+ return 0.0
139
+
140
+ def get_egress_cost(self, num_gigabytes: float) -> float:
141
+ return 0.0
142
+
143
+ def __repr__(self):
144
+ return self._REPR
145
+
146
+ @classmethod
147
+ def get_default_instance_type(
148
+ cls,
149
+ cpus: Optional[str] = None,
150
+ memory: Optional[str] = None,
151
+ disk_tier: Optional[resources_utils.DiskTier] = None,
152
+ ) -> Optional[str]:
153
+ """Returns the default instance type for DO."""
154
+ return service_catalog.get_default_instance_type(cpus=cpus,
155
+ memory=memory,
156
+ disk_tier=disk_tier,
157
+ clouds='DO')
158
+
159
+ @classmethod
160
+ def get_accelerators_from_instance_type(
161
+ cls, instance_type: str) -> Optional[Dict[str, Union[int, float]]]:
162
+ return service_catalog.get_accelerators_from_instance_type(
163
+ instance_type, clouds='DO')
164
+
165
+ @classmethod
166
+ def get_zone_shell_cmd(cls) -> Optional[str]:
167
+ return None
168
+
169
+ def make_deploy_resources_variables(
170
+ self,
171
+ resources: 'resources_lib.Resources',
172
+ cluster_name: resources_utils.ClusterName,
173
+ region: 'clouds.Region',
174
+ zones: Optional[List['clouds.Zone']],
175
+ num_nodes: int,
176
+ dryrun: bool = False) -> Dict[str, Optional[str]]:
177
+ del zones, dryrun, cluster_name
178
+
179
+ r = resources
180
+ acc_dict = self.get_accelerators_from_instance_type(r.instance_type)
181
+ if acc_dict is not None:
182
+ custom_resources = json.dumps(acc_dict, separators=(',', ':'))
183
+ else:
184
+ custom_resources = None
185
+ image_id = None
186
+ if (resources.image_id is not None and
187
+ resources.extract_docker_image() is None):
188
+ if None in resources.image_id:
189
+ image_id = resources.image_id[None]
190
+ else:
191
+ assert region.name in resources.image_id
192
+ image_id = resources.image_id[region.name]
193
+ return {
194
+ 'instance_type': resources.instance_type,
195
+ 'custom_resources': custom_resources,
196
+ 'region': region.name,
197
+ **({
198
+ 'image_id': image_id
199
+ } if image_id else {})
200
+ }
201
+
202
+ def _get_feasible_launchable_resources(
203
+ self, resources: 'resources_lib.Resources'
204
+ ) -> resources_utils.FeasibleResources:
205
+ """Returns a list of feasible resources for the given resources."""
206
+ if resources.use_spot:
207
+ # TODO: Add hints to all return values in this method to help
208
+ # users understand why the resources are not launchable.
209
+ return resources_utils.FeasibleResources([], [], None)
210
+ if resources.instance_type is not None:
211
+ assert resources.is_launchable(), resources
212
+ resources = resources.copy(accelerators=None)
213
+ return resources_utils.FeasibleResources([resources], [], None)
214
+
215
+ def _make(instance_list):
216
+ resource_list = []
217
+ for instance_type in instance_list:
218
+ r = resources.copy(
219
+ cloud=DO(),
220
+ instance_type=instance_type,
221
+ accelerators=None,
222
+ cpus=None,
223
+ )
224
+ resource_list.append(r)
225
+ return resource_list
226
+
227
+ # Currently, handle a filter on accelerators only.
228
+ accelerators = resources.accelerators
229
+ if accelerators is None:
230
+ # Return a default instance type
231
+ default_instance_type = DO.get_default_instance_type(
232
+ cpus=resources.cpus,
233
+ memory=resources.memory,
234
+ disk_tier=resources.disk_tier)
235
+ return resources_utils.FeasibleResources(
236
+ _make([default_instance_type]), [], None)
237
+
238
+ assert len(accelerators) == 1, resources
239
+ acc, acc_count = list(accelerators.items())[0]
240
+ (instance_list, fuzzy_candidate_list) = (
241
+ service_catalog.get_instance_type_for_accelerator(
242
+ acc,
243
+ acc_count,
244
+ use_spot=resources.use_spot,
245
+ cpus=resources.cpus,
246
+ memory=resources.memory,
247
+ region=resources.region,
248
+ zone=resources.zone,
249
+ clouds='DO',
250
+ ))
251
+ if instance_list is None:
252
+ return resources_utils.FeasibleResources([], fuzzy_candidate_list,
253
+ None)
254
+ return resources_utils.FeasibleResources(_make(instance_list),
255
+ fuzzy_candidate_list, None)
256
+
257
+ @classmethod
258
+ def check_credentials(cls) -> Tuple[bool, Optional[str]]:
259
+ """Verify that the user has valid credentials for DO."""
260
+ try:
261
+ # attempt to make a CURL request for listing instances
262
+ do_utils.client().droplets.list()
263
+ except do.exceptions().HttpResponseError as err:
264
+ return False, str(err)
265
+ except do_utils.DigitalOceanError as err:
266
+ return False, str(err)
267
+
268
+ return True, None
269
+
270
+ def get_credential_file_mounts(self) -> Dict[str, str]:
271
+ try:
272
+ do_utils.client()
273
+ return {
274
+ f'~/.config/doctl/{_CREDENTIAL_FILE}': do_utils.CREDENTIALS_PATH
275
+ }
276
+ except do_utils.DigitalOceanError:
277
+ return {}
278
+
279
+ @classmethod
280
+ def get_current_user_identity(cls) -> Optional[List[str]]:
281
+ # NOTE: used for very advanced SkyPilot functionality
282
+ # Can implement later if desired
283
+ return None
284
+
285
+ @classmethod
286
+ def get_image_size(cls, image_id: str, region: Optional[str]) -> float:
287
+ del region
288
+ try:
289
+ response = do_utils.client().images.get(image_id=image_id)
290
+ return response['image']['size_gigabytes']
291
+ except do.exceptions().HttpResponseError as err:
292
+ raise do_utils.DigitalOceanError(
293
+ 'HTTP error while retrieving size of '
294
+ f'image_id {response}: {err.error.message}') from err
295
+ except KeyError as err:
296
+ raise do_utils.DigitalOceanError(
297
+ f'No image_id `{image_id}` found') from err
298
+
299
+ def instance_type_exists(self, instance_type: str) -> bool:
300
+ return service_catalog.instance_type_exists(instance_type, 'DO')
301
+
302
+ def validate_region_zone(self, region: Optional[str], zone: Optional[str]):
303
+ return service_catalog.validate_region_zone(region, zone, clouds='DO')
@@ -4,4 +4,4 @@ CATALOG_SCHEMA_VERSION = 'v6'
4
4
  CATALOG_DIR = '~/.sky/catalogs'
5
5
  ALL_CLOUDS = ('aws', 'azure', 'gcp', 'ibm', 'lambda', 'scp', 'oci',
6
6
  'kubernetes', 'runpod', 'vsphere', 'cudo', 'fluidstack',
7
- 'paperspace')
7
+ 'paperspace', 'do')
@@ -0,0 +1,111 @@
1
+ """Digital ocean service catalog.
2
+
3
+ This module loads the service catalog file and can be used to
4
+ query instance types and pricing information for digital ocean.
5
+ """
6
+
7
+ import typing
8
+ from typing import Dict, List, Optional, Tuple, Union
9
+
10
+ from sky.clouds.service_catalog import common
11
+ from sky.utils import ux_utils
12
+
13
+ if typing.TYPE_CHECKING:
14
+ from sky.clouds import cloud
15
+
16
+ _df = common.read_catalog('do/vms.csv')
17
+
18
+
19
+ def instance_type_exists(instance_type: str) -> bool:
20
+ return common.instance_type_exists_impl(_df, instance_type)
21
+
22
+
23
+ def validate_region_zone(
24
+ region: Optional[str],
25
+ zone: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
26
+ if zone is not None:
27
+ with ux_utils.print_exception_no_traceback():
28
+ raise ValueError('DO does not support zones.')
29
+ return common.validate_region_zone_impl('DO', _df, region, zone)
30
+
31
+
32
+ def get_hourly_cost(
33
+ instance_type: str,
34
+ use_spot: bool = False,
35
+ region: Optional[str] = None,
36
+ zone: Optional[str] = None,
37
+ ) -> float:
38
+ """Returns the cost, or the cheapest cost among all zones for spot."""
39
+ if zone is not None:
40
+ with ux_utils.print_exception_no_traceback():
41
+ raise ValueError('DO does not support zones.')
42
+ return common.get_hourly_cost_impl(_df, instance_type, use_spot, region,
43
+ zone)
44
+
45
+
46
+ def get_vcpus_mem_from_instance_type(
47
+ instance_type: str,) -> Tuple[Optional[float], Optional[float]]:
48
+ return common.get_vcpus_mem_from_instance_type_impl(_df, instance_type)
49
+
50
+
51
+ def get_default_instance_type(
52
+ cpus: Optional[str] = None,
53
+ memory: Optional[str] = None,
54
+ disk_tier: Optional[str] = None,
55
+ ) -> Optional[str]:
56
+ # NOTE: After expanding catalog to multiple entries, you may
57
+ # want to specify a default instance type or family.
58
+ del disk_tier # unused
59
+ return common.get_instance_type_for_cpus_mem_impl(_df, cpus, memory)
60
+
61
+
62
+ def get_accelerators_from_instance_type(
63
+ instance_type: str) -> Optional[Dict[str, Union[int, float]]]:
64
+ return common.get_accelerators_from_instance_type_impl(_df, instance_type)
65
+
66
+
67
+ def get_instance_type_for_accelerator(
68
+ acc_name: str,
69
+ acc_count: int,
70
+ cpus: Optional[str] = None,
71
+ memory: Optional[str] = None,
72
+ use_spot: bool = False,
73
+ region: Optional[str] = None,
74
+ zone: Optional[str] = None,
75
+ ) -> Tuple[Optional[List[str]], List[str]]:
76
+ """Returns a list of instance types that have the given accelerator."""
77
+ if zone is not None:
78
+ with ux_utils.print_exception_no_traceback():
79
+ raise ValueError('DO does not support zones.')
80
+ return common.get_instance_type_for_accelerator_impl(
81
+ df=_df,
82
+ acc_name=acc_name,
83
+ acc_count=acc_count,
84
+ cpus=cpus,
85
+ memory=memory,
86
+ use_spot=use_spot,
87
+ region=region,
88
+ zone=zone,
89
+ )
90
+
91
+
92
+ def get_region_zones_for_instance_type(instance_type: str,
93
+ use_spot: bool) -> List['cloud.Region']:
94
+ df = _df[_df['InstanceType'] == instance_type]
95
+ return common.get_region_zones(df, use_spot)
96
+
97
+
98
+ def list_accelerators(
99
+ gpus_only: bool,
100
+ name_filter: Optional[str],
101
+ region_filter: Optional[str],
102
+ quantity_filter: Optional[int],
103
+ case_sensitive: bool = True,
104
+ all_regions: bool = False,
105
+ require_price: bool = True,
106
+ ) -> Dict[str, List[common.InstanceTypeInfo]]:
107
+ """Returns all instance types in DO offering GPUs."""
108
+ del require_price # unused
109
+ return common.list_accelerators_impl('DO', _df, gpus_only, name_filter,
110
+ region_filter, quantity_filter,
111
+ case_sensitive, all_regions)
@@ -6,6 +6,10 @@ History:
6
6
  configuration.
7
7
  - Hysun He (hysun.he@oracle.com) @ Nov.12, 2024: Add the constant
8
8
  SERVICE_PORT_RULE_TAG
9
+ - Hysun He (hysun.he@oracle.com) @ Jan.01, 2025: Set the default image
10
+ from ubuntu 20.04 to ubuntu 22.04, including:
11
+ - GPU: skypilot:gpu-ubuntu-2004 -> skypilot:gpu-ubuntu-2204
12
+ - CPU: skypilot:cpu-ubuntu-2004 -> skypilot:cpu-ubuntu-2204
9
13
  """
10
14
  import os
11
15
 
@@ -117,7 +121,7 @@ class OCIConfig:
117
121
  # the sky's user-config file (if not specified, use the hardcode one at
118
122
  # last)
119
123
  return skypilot_config.get_nested(('oci', 'default', 'image_tag_gpu'),
120
- 'skypilot:gpu-ubuntu-2004')
124
+ 'skypilot:gpu-ubuntu-2204')
121
125
 
122
126
  @classmethod
123
127
  def get_default_image_tag(cls) -> str:
@@ -125,7 +129,7 @@ class OCIConfig:
125
129
  # set the default image tag in the sky's user-config file. (if not
126
130
  # specified, use the hardcode one at last)
127
131
  return skypilot_config.get_nested(
128
- ('oci', 'default', 'image_tag_general'), 'skypilot:cpu-ubuntu-2004')
132
+ ('oci', 'default', 'image_tag_general'), 'skypilot:cpu-ubuntu-2204')
129
133
 
130
134
  @classmethod
131
135
  def get_sky_user_config_file(cls) -> str:
@@ -0,0 +1,11 @@
1
+ """DO provisioner for SkyPilot."""
2
+
3
+ from sky.provision.do.config import bootstrap_instances
4
+ from sky.provision.do.instance import cleanup_ports
5
+ from sky.provision.do.instance import get_cluster_info
6
+ from sky.provision.do.instance import open_ports
7
+ from sky.provision.do.instance import query_instances
8
+ from sky.provision.do.instance import run_instances
9
+ from sky.provision.do.instance import stop_instances
10
+ from sky.provision.do.instance import terminate_instances
11
+ from sky.provision.do.instance import wait_instances
@@ -0,0 +1,14 @@
1
+ """Paperspace configuration bootstrapping."""
2
+
3
+ from sky import sky_logging
4
+ from sky.provision import common
5
+
6
+ logger = sky_logging.init_logger(__name__)
7
+
8
+
9
+ def bootstrap_instances(
10
+ region: str, cluster_name: str,
11
+ config: common.ProvisionConfig) -> common.ProvisionConfig:
12
+ """Bootstraps instances for the given cluster."""
13
+ del region, cluster_name
14
+ return config
@@ -0,0 +1,10 @@
1
+ """DO cloud constants
2
+ """
3
+
4
+ POLL_INTERVAL = 5
5
+ WAIT_DELETE_VOLUMES = 5
6
+
7
+ GPU_IMAGES = {
8
+ 'gpu-h100x1-80gb': 'gpu-h100x1-base',
9
+ 'gpu-h100x8-640gb': 'gpu-h100x8-base',
10
+ }