Path: blob/main/singlestoredb/management/cluster.py
469 views
#!/usr/bin/env python1"""SingleStoreDB Cluster Management."""2import datetime3import warnings4from typing import Any5from typing import Dict6from typing import List7from typing import Optional8from typing import Union910from .. import config11from .. import connection12from ..exceptions import ManagementError13from .manager import Manager14from .region import Region15from .utils import NamedList16from .utils import to_datetime17from .utils import vars_to_str181920class Cluster(object):21"""22SingleStoreDB cluster definition.2324This object is not instantiated directly. It is used in the results25of API calls on the :class:`ClusterManager`. Clusters are created using26:meth:`ClusterManager.create_cluster`, or existing clusters are accessed by either27:attr:`ClusterManager.clusters` or by calling :meth:`ClusterManager.get_cluster`.2829See Also30--------31:meth:`ClusterManager.create_cluster`32:meth:`ClusterManager.get_cluster`33:attr:`ClusterManager.clusters`3435"""3637def __init__(38self, name: str, id: str, region: Region, size: str,39units: float, state: str, version: str,40created_at: Union[str, datetime.datetime],41expires_at: Optional[Union[str, datetime.datetime]] = None,42firewall_ranges: Optional[List[str]] = None,43terminated_at: Optional[Union[str, datetime.datetime]] = None,44endpoint: Optional[str] = None,45):46"""Use :attr:`ClusterManager.clusters` or :meth:`ClusterManager.get_cluster`."""47#: Name of the cluster48self.name = name.strip()4950#: Unique ID of the cluster51self.id = id5253#: Region of the cluster (see :class:`Region`)54self.region = region5556#: Size of the cluster in cluster size notation (S-00, S-1, etc.)57self.size = size5859#: Size of the cluster in units such as 0.25, 1.0, etc.60self.units = units6162#: State of the cluster: PendingCreation, Transitioning, Active,63#: Terminated, Suspended, Resuming, Failed64self.state = state.strip()6566#: Version of the SingleStoreDB server67self.version = version.strip()6869#: Timestamp of when the cluster was created70self.created_at = to_datetime(created_at)7172#: Timestamp of when the cluster expires73self.expires_at = to_datetime(expires_at)7475#: List of allowed incoming IP addresses / ranges76self.firewall_ranges = firewall_ranges7778#: Timestamp of when the cluster was terminated79self.terminated_at = to_datetime(terminated_at)8081#: Hostname (or IP address) of the cluster database server82self.endpoint = endpoint8384self._manager: Optional[ClusterManager] = None8586def __str__(self) -> str:87"""Return string representation."""88return vars_to_str(self)8990def __repr__(self) -> str:91"""Return string representation."""92return str(self)9394@classmethod95def from_dict(cls, obj: Dict[str, Any], manager: 'ClusterManager') -> 'Cluster':96"""97Construct a Cluster from a dictionary of values.9899Parameters100----------101obj : dict102Dictionary of values103manager : ClusterManager, optional104The ClusterManager the Cluster belongs to105106Returns107-------108:class:`Cluster`109110"""111out = cls(112name=obj['name'], id=obj['clusterID'],113region=Region.from_dict(obj['region'], manager),114size=obj.get('size', 'Unknown'), units=obj.get('units', float('nan')),115state=obj['state'], version=obj['version'],116created_at=obj['createdAt'], expires_at=obj.get('expiresAt'),117firewall_ranges=obj.get('firewallRanges'),118terminated_at=obj.get('terminatedAt'),119endpoint=obj.get('endpoint'),120)121out._manager = manager122return out123124def refresh(self) -> 'Cluster':125"""Update the object to the current state."""126if self._manager is None:127raise ManagementError(128msg='No cluster manager is associated with this object.',129)130new_obj = self._manager.get_cluster(self.id)131for name, value in vars(new_obj).items():132setattr(self, name, value)133return self134135def update(136self, name: Optional[str] = None,137admin_password: Optional[str] = None,138expires_at: Optional[str] = None,139size: Optional[str] = None, firewall_ranges: Optional[List[str]] = None,140) -> None:141"""142Update the cluster definition.143144Parameters145----------146name : str, optional147Cluster name148admim_password : str, optional149Admin password for the cluster150expires_at : str, optional151Timestamp when the cluster expires152size : str, optional153Cluster size in cluster size notation (S-00, S-1, etc.)154firewall_ranges : Sequence[str], optional155List of allowed incoming IP addresses156157"""158if self._manager is None:159raise ManagementError(160msg='No cluster manager is associated with this object.',161)162data = {163k: v for k, v in dict(164name=name, adminPassword=admin_password,165expiresAt=expires_at, size=size,166firewallRanges=firewall_ranges,167).items() if v is not None168}169self._manager._patch(f'clusters/{self.id}', json=data)170self.refresh()171172def suspend(173self,174wait_on_suspended: bool = False,175wait_interval: int = 20,176wait_timeout: int = 600,177) -> None:178"""179Suspend the cluster.180181Parameters182----------183wait_on_suspended : bool, optional184Wait for the cluster to go into 'Suspended' mode before returning185wait_interval : int, optional186Number of seconds between each server check187wait_timeout : int, optional188Total number of seconds to check server before giving up189190Raises191------192ManagementError193If timeout is reached194195"""196if self._manager is None:197raise ManagementError(198msg='No cluster manager is associated with this object.',199)200self._manager._post(201f'clusters/{self.id}/suspend',202headers={'Content-Type': 'application/x-www-form-urlencoded'},203)204if wait_on_suspended:205self._manager._wait_on_state(206self._manager.get_cluster(self.id),207'Suspended', interval=wait_interval, timeout=wait_timeout,208)209self.refresh()210211def resume(212self,213wait_on_resumed: bool = False,214wait_interval: int = 20,215wait_timeout: int = 600,216) -> None:217"""218Resume the cluster.219220Parameters221----------222wait_on_resumed : bool, optional223Wait for the cluster to go into 'Resumed' or 'Active' mode before returning224wait_interval : int, optional225Number of seconds between each server check226wait_timeout : int, optional227Total number of seconds to check server before giving up228229Raises230------231ManagementError232If timeout is reached233234"""235if self._manager is None:236raise ManagementError(237msg='No cluster manager is associated with this object.',238)239self._manager._post(240f'clusters/{self.id}/resume',241headers={'Content-Type': 'application/x-www-form-urlencoded'},242)243if wait_on_resumed:244self._manager._wait_on_state(245self._manager.get_cluster(self.id),246['Resumed', 'Active'], interval=wait_interval, timeout=wait_timeout,247)248self.refresh()249250def terminate(251self,252wait_on_terminated: bool = False,253wait_interval: int = 10,254wait_timeout: int = 600,255) -> None:256"""257Terminate the cluster.258259Parameters260----------261wait_on_terminated : bool, optional262Wait for the cluster to go into 'Terminated' mode before returning263wait_interval : int, optional264Number of seconds between each server check265wait_timeout : int, optional266Total number of seconds to check server before giving up267268Raises269------270ManagementError271If timeout is reached272273"""274if self._manager is None:275raise ManagementError(276msg='No cluster manager is associated with this object.',277)278self._manager._delete(f'clusters/{self.id}')279if wait_on_terminated:280self._manager._wait_on_state(281self._manager.get_cluster(self.id),282'Terminated', interval=wait_interval, timeout=wait_timeout,283)284self.refresh()285286def connect(self, **kwargs: Any) -> connection.Connection:287"""288Create a connection to the database server for this cluster.289290Parameters291----------292**kwargs : keyword-arguments, optional293Parameters to the SingleStoreDB `connect` function except host294and port which are supplied by the cluster object295296Returns297-------298:class:`Connection`299300"""301if not self.endpoint:302raise ManagementError(303msg='An endpoint has not been set in '304'this cluster configuration',305)306kwargs['host'] = self.endpoint307return connection.connect(**kwargs)308309310class ClusterManager(Manager):311"""312SingleStoreDB cluster manager.313314This class should be instantiated using :func:`singlestoredb.manage_cluster`.315316Parameters317----------318access_token : str, optional319The API key or other access token for the cluster management API320version : str, optional321Version of the API to use322base_url : str, optional323Base URL of the cluster management API324325See Also326--------327:func:`singlestoredb.manage_cluster`328329"""330331#: Cluster management API version if none is specified.332default_version = 'v0beta'333334#: Base URL if none is specified.335default_base_url = config.get_option('management.base_url') \336or 'https://api.singlestore.com'337338#: Object type339obj_type = 'cluster'340341@property342def clusters(self) -> NamedList[Cluster]:343"""Return a list of available clusters."""344res = self._get('clusters')345return NamedList([Cluster.from_dict(item, self) for item in res.json()])346347@property348def regions(self) -> NamedList[Region]:349"""Return a list of available regions."""350res = self._get('regions')351return NamedList([Region.from_dict(item, self) for item in res.json()])352353def create_cluster(354self, name: str, region: Union[str, Region], admin_password: str,355firewall_ranges: List[str], expires_at: Optional[str] = None,356size: Optional[str] = None, plan: Optional[str] = None,357wait_on_active: bool = False, wait_timeout: int = 600,358wait_interval: int = 20,359) -> Cluster:360"""361Create a new cluster.362363Parameters364----------365name : str366Name of the cluster367region : str or Region368The region ID of the cluster369admin_password : str370Admin password for the cluster371firewall_ranges : Sequence[str], optional372List of allowed incoming IP addresses373expires_at : str, optional374Timestamp of when the cluster expires375size : str, optional376Cluster size in cluster size notation (S-00, S-1, etc.)377plan : str, optional378Internal use only379wait_on_active : bool, optional380Wait for the cluster to be active before returning381wait_timeout : int, optional382Maximum number of seconds to wait before raising an exception383if wait=True384wait_interval : int, optional385Number of seconds between each polling interval386387Returns388-------389:class:`Cluster`390391"""392if isinstance(region, Region) and region.id:393region = region.id394res = self._post(395'clusters', json=dict(396name=name, regionID=region, adminPassword=admin_password,397expiresAt=expires_at, size=size, firewallRanges=firewall_ranges,398plan=plan,399),400)401out = self.get_cluster(res.json()['clusterID'])402if wait_on_active:403out = self._wait_on_state(404out, 'Active', interval=wait_interval,405timeout=wait_timeout,406)407return out408409def get_cluster(self, id: str) -> Cluster:410"""411Retrieve a cluster definition.412413Parameters414----------415id : str416ID of the cluster417418Returns419-------420:class:`Cluster`421422"""423res = self._get(f'clusters/{id}')424return Cluster.from_dict(res.json(), manager=self)425426427def manage_cluster(428access_token: Optional[str] = None,429version: Optional[str] = None,430base_url: Optional[str] = None,431*,432organization_id: Optional[str] = None,433) -> ClusterManager:434"""435Retrieve a SingleStoreDB cluster manager.436437Parameters438----------439access_token : str, optional440The API key or other access token for the cluster management API441version : str, optional442Version of the API to use443base_url : str, optional444Base URL of the cluster management API445organization_id: str, optional446ID of organization, if using a JWT for authentication447448Returns449-------450:class:`ClusterManager`451452"""453warnings.warn(454'The cluster management API is deprecated; '455'use manage_workspaces instead.',456category=DeprecationWarning,457)458return ClusterManager(459access_token=access_token, base_url=base_url,460version=version, organization_id=organization_id,461)462463464