anyscale 0.25.2__py3-none-any.whl → 0.25.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. anyscale/_private/anyscale_client/anyscale_client.py +5 -0
  2. anyscale/_private/anyscale_client/common.py +5 -0
  3. anyscale/_private/anyscale_client/fake_anyscale_client.py +11 -0
  4. anyscale/_private/workload/workload_config.py +20 -6
  5. anyscale/client/README.md +6 -0
  6. anyscale/client/openapi_client/__init__.py +4 -0
  7. anyscale/client/openapi_client/api/default_api.py +272 -2
  8. anyscale/client/openapi_client/models/__init__.py +4 -0
  9. anyscale/client/openapi_client/models/cluster_event_source.py +105 -0
  10. anyscale/client/openapi_client/models/clusterevent_list_response.py +147 -0
  11. anyscale/client/openapi_client/models/create_notification_channel_record.py +29 -3
  12. anyscale/client/openapi_client/models/notification_channel_slack_config.py +121 -0
  13. anyscale/client/openapi_client/models/update_cloud_collaborator.py +121 -0
  14. anyscale/commands/cloud_commands.py +24 -4
  15. anyscale/commands/command_examples.py +4 -0
  16. anyscale/commands/service_commands.py +60 -0
  17. anyscale/controllers/cloud_controller.py +29 -4
  18. anyscale/service/__init__.py +21 -0
  19. anyscale/service/_private/service_sdk.py +13 -0
  20. anyscale/service/commands.py +35 -0
  21. anyscale/shared_anyscale_utils/utils/id_gen.py +1 -0
  22. anyscale/version.py +1 -1
  23. {anyscale-0.25.2.dist-info → anyscale-0.25.5.dist-info}/METADATA +1 -1
  24. {anyscale-0.25.2.dist-info → anyscale-0.25.5.dist-info}/RECORD +29 -25
  25. {anyscale-0.25.2.dist-info → anyscale-0.25.5.dist-info}/LICENSE +0 -0
  26. {anyscale-0.25.2.dist-info → anyscale-0.25.5.dist-info}/NOTICE +0 -0
  27. {anyscale-0.25.2.dist-info → anyscale-0.25.5.dist-info}/WHEEL +0 -0
  28. {anyscale-0.25.2.dist-info → anyscale-0.25.5.dist-info}/entry_points.txt +0 -0
  29. {anyscale-0.25.2.dist-info → anyscale-0.25.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,147 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Managed Ray API
5
+
6
+ No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) # noqa: E501
7
+
8
+ The version of the OpenAPI document: 0.1.0
9
+ Generated by: https://openapi-generator.tech
10
+ """
11
+
12
+
13
+ import pprint
14
+ import re # noqa: F401
15
+
16
+ import six
17
+
18
+ from openapi_client.configuration import Configuration
19
+
20
+
21
+ class ClustereventListResponse(object):
22
+ """NOTE: This class is auto generated by OpenAPI Generator.
23
+ Ref: https://openapi-generator.tech
24
+
25
+ Do not edit the class manually.
26
+ """
27
+
28
+ """
29
+ Attributes:
30
+ openapi_types (dict): The key is attribute name
31
+ and the value is attribute type.
32
+ attribute_map (dict): The key is attribute name
33
+ and the value is json key in definition.
34
+ """
35
+ openapi_types = {
36
+ 'results': 'list[ClusterEvent]',
37
+ 'metadata': 'ListResponseMetadata'
38
+ }
39
+
40
+ attribute_map = {
41
+ 'results': 'results',
42
+ 'metadata': 'metadata'
43
+ }
44
+
45
+ def __init__(self, results=None, metadata=None, local_vars_configuration=None): # noqa: E501
46
+ """ClustereventListResponse - a model defined in OpenAPI""" # noqa: E501
47
+ if local_vars_configuration is None:
48
+ local_vars_configuration = Configuration()
49
+ self.local_vars_configuration = local_vars_configuration
50
+
51
+ self._results = None
52
+ self._metadata = None
53
+ self.discriminator = None
54
+
55
+ self.results = results
56
+ if metadata is not None:
57
+ self.metadata = metadata
58
+
59
+ @property
60
+ def results(self):
61
+ """Gets the results of this ClustereventListResponse. # noqa: E501
62
+
63
+
64
+ :return: The results of this ClustereventListResponse. # noqa: E501
65
+ :rtype: list[ClusterEvent]
66
+ """
67
+ return self._results
68
+
69
+ @results.setter
70
+ def results(self, results):
71
+ """Sets the results of this ClustereventListResponse.
72
+
73
+
74
+ :param results: The results of this ClustereventListResponse. # noqa: E501
75
+ :type: list[ClusterEvent]
76
+ """
77
+ if self.local_vars_configuration.client_side_validation and results is None: # noqa: E501
78
+ raise ValueError("Invalid value for `results`, must not be `None`") # noqa: E501
79
+
80
+ self._results = results
81
+
82
+ @property
83
+ def metadata(self):
84
+ """Gets the metadata of this ClustereventListResponse. # noqa: E501
85
+
86
+
87
+ :return: The metadata of this ClustereventListResponse. # noqa: E501
88
+ :rtype: ListResponseMetadata
89
+ """
90
+ return self._metadata
91
+
92
+ @metadata.setter
93
+ def metadata(self, metadata):
94
+ """Sets the metadata of this ClustereventListResponse.
95
+
96
+
97
+ :param metadata: The metadata of this ClustereventListResponse. # noqa: E501
98
+ :type: ListResponseMetadata
99
+ """
100
+
101
+ self._metadata = metadata
102
+
103
+ def to_dict(self):
104
+ """Returns the model properties as a dict"""
105
+ result = {}
106
+
107
+ for attr, _ in six.iteritems(self.openapi_types):
108
+ value = getattr(self, attr)
109
+ if isinstance(value, list):
110
+ result[attr] = list(map(
111
+ lambda x: x.to_dict() if hasattr(x, "to_dict") else x,
112
+ value
113
+ ))
114
+ elif hasattr(value, "to_dict"):
115
+ result[attr] = value.to_dict()
116
+ elif isinstance(value, dict):
117
+ result[attr] = dict(map(
118
+ lambda item: (item[0], item[1].to_dict())
119
+ if hasattr(item[1], "to_dict") else item,
120
+ value.items()
121
+ ))
122
+ else:
123
+ result[attr] = value
124
+
125
+ return result
126
+
127
+ def to_str(self):
128
+ """Returns the string representation of the model"""
129
+ return pprint.pformat(self.to_dict())
130
+
131
+ def __repr__(self):
132
+ """For `print` and `pprint`"""
133
+ return self.to_str()
134
+
135
+ def __eq__(self, other):
136
+ """Returns true if both objects are equal"""
137
+ if not isinstance(other, ClustereventListResponse):
138
+ return False
139
+
140
+ return self.to_dict() == other.to_dict()
141
+
142
+ def __ne__(self, other):
143
+ """Returns true if both objects are not equal"""
144
+ if not isinstance(other, ClustereventListResponse):
145
+ return True
146
+
147
+ return self.to_dict() != other.to_dict()
@@ -34,15 +34,17 @@ class CreateNotificationChannelRecord(object):
34
34
  """
35
35
  openapi_types = {
36
36
  'email_config': 'NotificationChannelEmailConfig',
37
- 'webhook_config': 'NotificationChannelWebhookConfig'
37
+ 'webhook_config': 'NotificationChannelWebhookConfig',
38
+ 'slack_config': 'NotificationChannelSlackConfig'
38
39
  }
39
40
 
40
41
  attribute_map = {
41
42
  'email_config': 'email_config',
42
- 'webhook_config': 'webhook_config'
43
+ 'webhook_config': 'webhook_config',
44
+ 'slack_config': 'slack_config'
43
45
  }
44
46
 
45
- def __init__(self, email_config=None, webhook_config=None, local_vars_configuration=None): # noqa: E501
47
+ def __init__(self, email_config=None, webhook_config=None, slack_config=None, local_vars_configuration=None): # noqa: E501
46
48
  """CreateNotificationChannelRecord - a model defined in OpenAPI""" # noqa: E501
47
49
  if local_vars_configuration is None:
48
50
  local_vars_configuration = Configuration()
@@ -50,12 +52,15 @@ class CreateNotificationChannelRecord(object):
50
52
 
51
53
  self._email_config = None
52
54
  self._webhook_config = None
55
+ self._slack_config = None
53
56
  self.discriminator = None
54
57
 
55
58
  if email_config is not None:
56
59
  self.email_config = email_config
57
60
  if webhook_config is not None:
58
61
  self.webhook_config = webhook_config
62
+ if slack_config is not None:
63
+ self.slack_config = slack_config
59
64
 
60
65
  @property
61
66
  def email_config(self):
@@ -99,6 +104,27 @@ class CreateNotificationChannelRecord(object):
99
104
 
100
105
  self._webhook_config = webhook_config
101
106
 
107
+ @property
108
+ def slack_config(self):
109
+ """Gets the slack_config of this CreateNotificationChannelRecord. # noqa: E501
110
+
111
+
112
+ :return: The slack_config of this CreateNotificationChannelRecord. # noqa: E501
113
+ :rtype: NotificationChannelSlackConfig
114
+ """
115
+ return self._slack_config
116
+
117
+ @slack_config.setter
118
+ def slack_config(self, slack_config):
119
+ """Sets the slack_config of this CreateNotificationChannelRecord.
120
+
121
+
122
+ :param slack_config: The slack_config of this CreateNotificationChannelRecord. # noqa: E501
123
+ :type: NotificationChannelSlackConfig
124
+ """
125
+
126
+ self._slack_config = slack_config
127
+
102
128
  def to_dict(self):
103
129
  """Returns the model properties as a dict"""
104
130
  result = {}
@@ -0,0 +1,121 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Managed Ray API
5
+
6
+ No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) # noqa: E501
7
+
8
+ The version of the OpenAPI document: 0.1.0
9
+ Generated by: https://openapi-generator.tech
10
+ """
11
+
12
+
13
+ import pprint
14
+ import re # noqa: F401
15
+
16
+ import six
17
+
18
+ from openapi_client.configuration import Configuration
19
+
20
+
21
+ class NotificationChannelSlackConfig(object):
22
+ """NOTE: This class is auto generated by OpenAPI Generator.
23
+ Ref: https://openapi-generator.tech
24
+
25
+ Do not edit the class manually.
26
+ """
27
+
28
+ """
29
+ Attributes:
30
+ openapi_types (dict): The key is attribute name
31
+ and the value is attribute type.
32
+ attribute_map (dict): The key is attribute name
33
+ and the value is json key in definition.
34
+ """
35
+ openapi_types = {
36
+ 'slack_urls': 'list[str]'
37
+ }
38
+
39
+ attribute_map = {
40
+ 'slack_urls': 'slack_urls'
41
+ }
42
+
43
+ def __init__(self, slack_urls=None, local_vars_configuration=None): # noqa: E501
44
+ """NotificationChannelSlackConfig - a model defined in OpenAPI""" # noqa: E501
45
+ if local_vars_configuration is None:
46
+ local_vars_configuration = Configuration()
47
+ self.local_vars_configuration = local_vars_configuration
48
+
49
+ self._slack_urls = None
50
+ self.discriminator = None
51
+
52
+ self.slack_urls = slack_urls
53
+
54
+ @property
55
+ def slack_urls(self):
56
+ """Gets the slack_urls of this NotificationChannelSlackConfig. # noqa: E501
57
+
58
+
59
+ :return: The slack_urls of this NotificationChannelSlackConfig. # noqa: E501
60
+ :rtype: list[str]
61
+ """
62
+ return self._slack_urls
63
+
64
+ @slack_urls.setter
65
+ def slack_urls(self, slack_urls):
66
+ """Sets the slack_urls of this NotificationChannelSlackConfig.
67
+
68
+
69
+ :param slack_urls: The slack_urls of this NotificationChannelSlackConfig. # noqa: E501
70
+ :type: list[str]
71
+ """
72
+ if self.local_vars_configuration.client_side_validation and slack_urls is None: # noqa: E501
73
+ raise ValueError("Invalid value for `slack_urls`, must not be `None`") # noqa: E501
74
+
75
+ self._slack_urls = slack_urls
76
+
77
+ def to_dict(self):
78
+ """Returns the model properties as a dict"""
79
+ result = {}
80
+
81
+ for attr, _ in six.iteritems(self.openapi_types):
82
+ value = getattr(self, attr)
83
+ if isinstance(value, list):
84
+ result[attr] = list(map(
85
+ lambda x: x.to_dict() if hasattr(x, "to_dict") else x,
86
+ value
87
+ ))
88
+ elif hasattr(value, "to_dict"):
89
+ result[attr] = value.to_dict()
90
+ elif isinstance(value, dict):
91
+ result[attr] = dict(map(
92
+ lambda item: (item[0], item[1].to_dict())
93
+ if hasattr(item[1], "to_dict") else item,
94
+ value.items()
95
+ ))
96
+ else:
97
+ result[attr] = value
98
+
99
+ return result
100
+
101
+ def to_str(self):
102
+ """Returns the string representation of the model"""
103
+ return pprint.pformat(self.to_dict())
104
+
105
+ def __repr__(self):
106
+ """For `print` and `pprint`"""
107
+ return self.to_str()
108
+
109
+ def __eq__(self, other):
110
+ """Returns true if both objects are equal"""
111
+ if not isinstance(other, NotificationChannelSlackConfig):
112
+ return False
113
+
114
+ return self.to_dict() == other.to_dict()
115
+
116
+ def __ne__(self, other):
117
+ """Returns true if both objects are not equal"""
118
+ if not isinstance(other, NotificationChannelSlackConfig):
119
+ return True
120
+
121
+ return self.to_dict() != other.to_dict()
@@ -0,0 +1,121 @@
1
+ # coding: utf-8
2
+
3
+ """
4
+ Managed Ray API
5
+
6
+ No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) # noqa: E501
7
+
8
+ The version of the OpenAPI document: 0.1.0
9
+ Generated by: https://openapi-generator.tech
10
+ """
11
+
12
+
13
+ import pprint
14
+ import re # noqa: F401
15
+
16
+ import six
17
+
18
+ from openapi_client.configuration import Configuration
19
+
20
+
21
+ class UpdateCloudCollaborator(object):
22
+ """NOTE: This class is auto generated by OpenAPI Generator.
23
+ Ref: https://openapi-generator.tech
24
+
25
+ Do not edit the class manually.
26
+ """
27
+
28
+ """
29
+ Attributes:
30
+ openapi_types (dict): The key is attribute name
31
+ and the value is attribute type.
32
+ attribute_map (dict): The key is attribute name
33
+ and the value is json key in definition.
34
+ """
35
+ openapi_types = {
36
+ 'permission_level': 'PermissionLevel'
37
+ }
38
+
39
+ attribute_map = {
40
+ 'permission_level': 'permission_level'
41
+ }
42
+
43
+ def __init__(self, permission_level=None, local_vars_configuration=None): # noqa: E501
44
+ """UpdateCloudCollaborator - a model defined in OpenAPI""" # noqa: E501
45
+ if local_vars_configuration is None:
46
+ local_vars_configuration = Configuration()
47
+ self.local_vars_configuration = local_vars_configuration
48
+
49
+ self._permission_level = None
50
+ self.discriminator = None
51
+
52
+ self.permission_level = permission_level
53
+
54
+ @property
55
+ def permission_level(self):
56
+ """Gets the permission_level of this UpdateCloudCollaborator. # noqa: E501
57
+
58
+
59
+ :return: The permission_level of this UpdateCloudCollaborator. # noqa: E501
60
+ :rtype: PermissionLevel
61
+ """
62
+ return self._permission_level
63
+
64
+ @permission_level.setter
65
+ def permission_level(self, permission_level):
66
+ """Sets the permission_level of this UpdateCloudCollaborator.
67
+
68
+
69
+ :param permission_level: The permission_level of this UpdateCloudCollaborator. # noqa: E501
70
+ :type: PermissionLevel
71
+ """
72
+ if self.local_vars_configuration.client_side_validation and permission_level is None: # noqa: E501
73
+ raise ValueError("Invalid value for `permission_level`, must not be `None`") # noqa: E501
74
+
75
+ self._permission_level = permission_level
76
+
77
+ def to_dict(self):
78
+ """Returns the model properties as a dict"""
79
+ result = {}
80
+
81
+ for attr, _ in six.iteritems(self.openapi_types):
82
+ value = getattr(self, attr)
83
+ if isinstance(value, list):
84
+ result[attr] = list(map(
85
+ lambda x: x.to_dict() if hasattr(x, "to_dict") else x,
86
+ value
87
+ ))
88
+ elif hasattr(value, "to_dict"):
89
+ result[attr] = value.to_dict()
90
+ elif isinstance(value, dict):
91
+ result[attr] = dict(map(
92
+ lambda item: (item[0], item[1].to_dict())
93
+ if hasattr(item[1], "to_dict") else item,
94
+ value.items()
95
+ ))
96
+ else:
97
+ result[attr] = value
98
+
99
+ return result
100
+
101
+ def to_str(self):
102
+ """Returns the string representation of the model"""
103
+ return pprint.pformat(self.to_dict())
104
+
105
+ def __repr__(self):
106
+ """For `print` and `pprint`"""
107
+ return self.to_str()
108
+
109
+ def __eq__(self, other):
110
+ """Returns true if both objects are equal"""
111
+ if not isinstance(other, UpdateCloudCollaborator):
112
+ return False
113
+
114
+ return self.to_dict() == other.to_dict()
115
+
116
+ def __ne__(self, other):
117
+ """Returns true if both objects are not equal"""
118
+ if not isinstance(other, UpdateCloudCollaborator):
119
+ return True
120
+
121
+ return self.to_dict() != other.to_dict()
@@ -1,7 +1,9 @@
1
+ from io import StringIO
1
2
  import re
2
3
  from typing import List, Optional
3
4
 
4
5
  import click
6
+ import yaml
5
7
 
6
8
  import anyscale
7
9
  from anyscale.cli_logger import BlockLogger
@@ -321,11 +323,12 @@ def cloud_config_get(
321
323
  "The positional argument CLOUD_NAME and the keyword argument --name "
322
324
  "were both provided. Please only provide one of these two arguments."
323
325
  )
324
- print(
325
- CloudController().get_cloud_config(
326
- cloud_name=cloud_name or name, cloud_id=cloud_id,
327
- )
326
+ config = CloudController().get_cloud_config(
327
+ cloud_name=cloud_name or name, cloud_id=cloud_id,
328
328
  )
329
+ stream = StringIO()
330
+ yaml.dump(config.spec, stream)
331
+ print(stream.getvalue())
329
332
 
330
333
 
331
334
  @cloud_config_group.command(
@@ -354,13 +357,26 @@ def cloud_config_get(
354
357
  "extra data transfer cost from the cloud provider by enabling this feature."
355
358
  ),
356
359
  )
360
+ @click.option(
361
+ "--spec-file",
362
+ type=str,
363
+ required=False,
364
+ help="Provide a path to a specification file.",
365
+ )
357
366
  def cloud_config_update(
358
367
  cloud_name: Optional[str],
359
368
  name: Optional[str],
360
369
  cloud_id: Optional[str],
361
370
  enable_log_ingestion: Optional[bool],
371
+ spec_file: Optional[str],
362
372
  ) -> None:
373
+ if any([enable_log_ingestion is not None]) and spec_file:
374
+ raise click.ClickException(
375
+ "Please provide only one of the following arguments: --enable-log-ingestion, --disable-log-ingestion, --spec-file."
376
+ )
377
+
363
378
  if any([enable_log_ingestion is not None]):
379
+ # TODO: enable_log_ingestion should be unified into cloud deployment config.
364
380
  if enable_log_ingestion is True:
365
381
  consent_message = click.prompt(
366
382
  "--enable-log-ingestion is specified. Please note the logs produced by "
@@ -390,6 +406,10 @@ def cloud_config_update(
390
406
  cloud_id=cloud_id,
391
407
  enable_log_ingestion=enable_log_ingestion,
392
408
  )
409
+ elif spec_file:
410
+ CloudController().update_cloud_config(
411
+ cloud_name=cloud_name or name, cloud_id=cloud_id, spec_file=spec_file,
412
+ )
393
413
  else:
394
414
  raise click.ClickException(
395
415
  "Please provide at least one of the following arguments: --enable-log-ingestion, --disable-log-ingestion."
@@ -560,3 +560,7 @@ collaborators:
560
560
  - email: "test2@anyscale.com"
561
561
  permission_level: "readonly"
562
562
  """
563
+
564
+ SERVICE_ARCHIVE_EXAMPLE = """\
565
+ $ anyscale service archive --name my_service
566
+ """
@@ -9,7 +9,9 @@ import yaml
9
9
 
10
10
  from anyscale._private.models.image_uri import ImageURI
11
11
  from anyscale.cli_logger import BlockLogger
12
+ from anyscale.commands import command_examples
12
13
  from anyscale.commands.util import (
14
+ AnyscaleCommand,
13
15
  convert_kv_strings_to_dict,
14
16
  LegacyAnyscaleCommand,
15
17
  override_env_vars,
@@ -736,3 +738,61 @@ def terminate(
736
738
  project_id=project_id,
737
739
  )
738
740
  service_controller.terminate(service_id)
741
+
742
+
743
+ @service_cli.command(
744
+ name="archive",
745
+ help="Archive a service.",
746
+ cls=AnyscaleCommand,
747
+ example=command_examples.SERVICE_ARCHIVE_EXAMPLE,
748
+ )
749
+ @click.option(
750
+ "-n", "--name", required=False, default=None, type=str, help="Name of the service.",
751
+ )
752
+ @click.option(
753
+ "-f",
754
+ "--config-file",
755
+ required=False,
756
+ default=None,
757
+ type=str,
758
+ help="Path to a YAML config file to read the name from.",
759
+ )
760
+ @click.option(
761
+ "--cloud",
762
+ required=False,
763
+ default=None,
764
+ type=str,
765
+ help="The Anyscale Cloud to run this workload on. If not provided, the organization default will be used (or, if running in a workspace, the cloud of the workspace).",
766
+ )
767
+ @click.option(
768
+ "--project",
769
+ required=False,
770
+ default=None,
771
+ type=str,
772
+ help="Named project to use for the service. If not provided, the default project for the cloud will be used (or, if running in a workspace, the project of the workspace).",
773
+ )
774
+ def archive(
775
+ name: Optional[str],
776
+ config_file: Optional[str],
777
+ cloud: Optional[str],
778
+ project: Optional[str],
779
+ ) -> None:
780
+ """Archive a service.
781
+
782
+ To specify the service by name, use the --name flag. To specify the service by id, use the --id flag. Either name or
783
+ id should be used, specifying both will result in an error.
784
+ """
785
+ if name is not None and config_file is not None:
786
+ raise click.ClickException(
787
+ "Only one of '--name' and '--config-file' can be provided."
788
+ )
789
+
790
+ if config_file is not None:
791
+ name = _read_name_from_config_file(config_file)
792
+
793
+ if name is None:
794
+ raise click.ClickException(
795
+ "Service name must be provided using '--name' or in a config file using '-f'."
796
+ )
797
+
798
+ anyscale.service.archive(name=name, cloud=cloud, project=project)
@@ -5,6 +5,7 @@ Fetches data required and formats output for `anyscale cloud` commands.
5
5
  import copy
6
6
  import json
7
7
  from os import getenv
8
+ import pathlib
8
9
  import re
9
10
  import secrets
10
11
  import time
@@ -15,6 +16,7 @@ import boto3
15
16
  from botocore.exceptions import ClientError, NoCredentialsError
16
17
  import click
17
18
  from click import Abort, ClickException
19
+ import yaml
18
20
 
19
21
  from anyscale import __version__ as anyscale_version
20
22
  from anyscale.aws_iam_policies import get_anyscale_iam_permissions_ec2_restricted
@@ -24,6 +26,7 @@ from anyscale.client.openapi_client.models import (
24
26
  CloudAnalyticsEventCloudResource,
25
27
  CloudAnalyticsEventCommandName,
26
28
  CloudAnalyticsEventName,
29
+ CloudDeploymentConfig,
27
30
  CloudProviders,
28
31
  CloudState,
29
32
  CloudWithCloudResource,
@@ -59,7 +62,6 @@ from anyscale.cloud_resource import (
59
62
  )
60
63
  from anyscale.cloud_utils import (
61
64
  get_cloud_id_and_name,
62
- get_cloud_json_from_id,
63
65
  get_cloud_resource_by_cloud_id,
64
66
  get_organization_id,
65
67
  )
@@ -1382,23 +1384,28 @@ class CloudController(BaseController):
1382
1384
 
1383
1385
  def get_cloud_config(
1384
1386
  self, cloud_name: Optional[str] = None, cloud_id: Optional[str] = None,
1385
- ) -> str:
1387
+ ) -> CloudDeploymentConfig:
1386
1388
  """Get a cloud's current JSON configuration."""
1387
1389
 
1388
1390
  cloud_id, cloud_name = get_cloud_id_and_name(
1389
1391
  self.api_client, cloud_id, cloud_name
1390
1392
  )
1391
1393
 
1392
- return str(get_cloud_json_from_id(cloud_id, self.api_client)["config"])
1394
+ # In the future we will expose cloud_deployment id as a parameter, for now, it's just a placeholder.
1395
+ config: CloudDeploymentConfig = self.api_client.get_cloud_deployment_config_api_v2_clouds_cloud_id_deployment_cloud_deployment_id_config_get(
1396
+ cloud_id=cloud_id, cloud_deployment_id="default"
1397
+ ).result
1398
+
1399
+ return config
1393
1400
 
1394
1401
  def update_cloud_config(
1395
1402
  self,
1396
1403
  cloud_name: Optional[str] = None,
1397
1404
  cloud_id: Optional[str] = None,
1398
1405
  enable_log_ingestion: Optional[bool] = None,
1406
+ spec_file: Optional[str] = None,
1399
1407
  ):
1400
1408
  """Update a cloud's configuration."""
1401
-
1402
1409
  cloud_id, cloud_name = get_cloud_id_and_name(
1403
1410
  self.api_client, cloud_id, cloud_name
1404
1411
  )
@@ -1410,6 +1417,24 @@ class CloudController(BaseController):
1410
1417
  f"Successfully updated log ingestion configuration for cloud, "
1411
1418
  f"{cloud_id} to {enable_log_ingestion}"
1412
1419
  )
1420
+ elif spec_file is not None:
1421
+ path = pathlib.Path(spec_file)
1422
+ if not path.exists():
1423
+ raise FileNotFoundError(f"File {spec_file} does not exist.")
1424
+
1425
+ if not path.is_file():
1426
+ raise ValueError(f"File {spec_file} is not a file.")
1427
+
1428
+ spec = yaml.safe_load(path.read_text())
1429
+ config = CloudDeploymentConfig(spec=spec)
1430
+ self.api_client.update_cloud_deployment_config_api_v2_clouds_cloud_id_deployment_cloud_deployment_id_config_put(
1431
+ cloud_id=cloud_id,
1432
+ cloud_deployment_id="default",
1433
+ cloud_deployment_config=config,
1434
+ )
1435
+ self.log.info(
1436
+ f"Successfully updated cloud configuration for cloud {cloud_name}"
1437
+ )
1413
1438
 
1414
1439
  def set_default_cloud(
1415
1440
  self, cloud_name: Optional[str], cloud_id: Optional[str],