-
Notifications
You must be signed in to change notification settings - Fork 0
#14 fixture operational saas database #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
2005c86
4e51522
6c274ef
7e1cb91
de6a01f
dad0f11
ebef57c
97f44cc
8a400e4
0985a0d
bb93dda
6b328bf
b21c96a
2da0580
66026b0
26cf321
d822647
2b2b4a3
aace19e
5af0e1d
bd0fcc9
2534a4a
381e404
fb30fa7
db989b6
3e23db0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,52 @@ | ||
| import getpass | ||
| import logging | ||
| import time | ||
|
|
||
| from typing import Iterable | ||
| from contextlib import contextmanager | ||
| from datetime import datetime | ||
| from datetime import datetime, timedelta | ||
| from tenacity.wait import wait_fixed | ||
| from tenacity.stop import stop_after_delay | ||
|
|
||
| from exasol.saas.client import openapi | ||
| from exasol.saas.client.openapi.models.status import Status | ||
| from exasol.saas.client.openapi.api.databases import ( | ||
| create_database, | ||
| delete_database, | ||
| list_databases, | ||
| get_database, | ||
| ) | ||
| from exasol.saas.client.openapi.api.security import ( | ||
| list_allowed_i_ps, | ||
| add_allowed_ip, | ||
| delete_allowed_ip, | ||
| ) | ||
| from tenacity import retry, TryAgain | ||
|
|
||
|
|
||
| LOG = logging.getLogger(__name__) | ||
| LOG.setLevel(logging.INFO) | ||
|
|
||
|
|
||
| # For auto-stopping idle database clusters | ||
| MINIMUM_IDLE_TIME = timedelta(minutes=15) | ||
|
ckunki marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| # If deleting a database too early, then logging and accounting could be invalid. | ||
| MINIMUM_LIFETIME = timedelta(seconds=30) | ||
|
ckunki marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| def _timestamp_name() -> str: | ||
| username = getpass.getuser() | ||
| timestamp = f'{datetime.now().timestamp():.0f}' | ||
| return f"{username}-{timestamp}" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still can break for long user names, we need to shorten it differently. Furthermore, we still should mention from where the DB is from. So, mu suggestion would be something like the following
@tomuben what is the max length for the DB name in SaaS
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thx
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ckunki maybe we put the unshortened name as main cluster name
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the other hand, I wonder if the user name is helpful in CI builds.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think, the user name was runner, this would indicate the CI |
||
|
|
||
|
|
||
| def timestamp() -> str: | ||
| return f'{datetime.now().timestamp():.0f}' | ||
| class DatabaseStartupFailure(Exception): | ||
| """ | ||
| If a SaaS database instance during startup reports a status other than | ||
| successful. | ||
| """ | ||
|
|
||
|
|
||
| def create_saas_client( | ||
|
|
@@ -46,12 +76,18 @@ def create_database(self, cluster_size: str = "XS") -> openapi.models.database.D | |
| cluster_spec = openapi.models.CreateCluster( | ||
| name="my-cluster", | ||
| size=cluster_size, | ||
| auto_stop=openapi.models.AutoStop( | ||
| enabled=True, | ||
| idle_time=int(MINIMUM_IDLE_TIME.seconds / 60), | ||
|
ckunki marked this conversation as resolved.
Outdated
|
||
| ), | ||
| ) | ||
| db_name = _timestamp_name() | ||
| LOG.info(f"Creating database {db_name}") | ||
| return create_database.sync( | ||
| self._account_id, | ||
| client=self._client, | ||
| body=openapi.models.CreateDatabase( | ||
| name=f"pytest-{timestamp()}", | ||
| name=db_name, | ||
|
ckunki marked this conversation as resolved.
Outdated
|
||
| initial_cluster=cluster_spec, | ||
| provider="aws", | ||
| region='us-east-1', | ||
|
|
@@ -81,12 +117,52 @@ def database( | |
| ignore_delete_failure: bool = False, | ||
| ): | ||
| db = None | ||
| start = datetime.now() | ||
| try: | ||
| db = self.create_database() | ||
| yield db | ||
| self.wait_for_delete_clearance(start) | ||
| finally: | ||
| if not keep and db: | ||
| self.delete_database(db.id, ignore_delete_failure) | ||
| if db and not keep: | ||
| LOG.info(f"Deleting database {db.name}") | ||
| response = self.delete_database(db.id, ignore_delete_failure) | ||
|
ckunki marked this conversation as resolved.
|
||
| if response.status_code == 200: | ||
| LOG.info(f"Successfully deleted database {db.name}.") | ||
| else: | ||
| LOG.info(f"Ignoring status code {response.status_code}.") | ||
|
ckunki marked this conversation as resolved.
Outdated
|
||
| elif not db: | ||
| LOG.warning("Cannot delete db None") | ||
| else: | ||
| LOG.info(f"Keeping database {db.name} as keep = {keep}") | ||
|
|
||
| def get_database(self, database_id: str) -> openapi.models.database.Database: | ||
| return get_database.sync( | ||
| self._account_id, | ||
| database_id, | ||
| client=self._client, | ||
| ) | ||
|
|
||
| def wait_until_running( | ||
| self, | ||
| database_id: str, | ||
| timeout: timedelta = timedelta(minutes=30), | ||
| interval: timedelta = timedelta(minutes=2), | ||
| ) -> str: | ||
| success = [ | ||
| Status.RUNNING, | ||
| ] | ||
|
|
||
| @retry(wait=wait_fixed(interval), stop=stop_after_delay(timeout)) | ||
| def poll_status(): | ||
|
ckunki marked this conversation as resolved.
|
||
| db = self.get_database(database_id) | ||
| if db.status not in success: | ||
| print(f'status = {db.status}') | ||
| raise TryAgain | ||
| return db.status | ||
|
|
||
| if poll_status() not in success: | ||
|
ckunki marked this conversation as resolved.
|
||
| raise DatabaseStartupFailure() | ||
|
|
||
|
|
||
| def list_allowed_ip_ids(self) -> Iterable[openapi.models.allowed_ip.AllowedIP]: | ||
| ips = list_allowed_i_ps.sync( | ||
|
|
@@ -95,6 +171,14 @@ def list_allowed_ip_ids(self) -> Iterable[openapi.models.allowed_ip.AllowedIP]: | |
| ) | ||
| return (x.id for x in ips) | ||
|
|
||
| def wait_for_delete_clearance(self, start: datetime.time): | ||
|
ckunki marked this conversation as resolved.
Outdated
|
||
| lifetime = datetime.now() - start | ||
| if lifetime < MINIMUM_LIFETIME: | ||
| wait = MINIMUM_LIFETIME - lifetime | ||
| LOG.info(f"Waiting {int(wait.seconds)} seconds" | ||
| " before deleting the database.") | ||
| time.sleep(wait.seconds) | ||
|
|
||
| def add_allowed_ip(self, cidr_ip: str = "0.0.0.0/0") -> openapi.models.allowed_ip.AllowedIP: | ||
| """ | ||
| Suggested values for cidr_ip: | ||
|
|
@@ -103,7 +187,7 @@ def add_allowed_ip(self, cidr_ip: str = "0.0.0.0/0") -> openapi.models.allowed_i | |
| * ::/0 = all ipv6 | ||
| """ | ||
| rule = openapi.models.create_allowed_ip.CreateAllowedIP( | ||
| name=f"pytest-{timestamp()}", | ||
| name=_timestamp_name(), | ||
| cidr_ip=cidr_ip, | ||
| ) | ||
| return add_allowed_ip.sync( | ||
|
|
@@ -129,5 +213,5 @@ def allowed_ip( | |
| ip = self.add_allowed_ip(cidr_ip) | ||
| yield ip | ||
| finally: | ||
| if not keep and ip: | ||
| if ip and not keep: | ||
| self.delete_allowed_ip(ip.id, ignore_delete_failure) | ||
Uh oh!
There was an error while loading. Please reload this page.