From b620f06521101472ee6bfa62d1d1ed524b4feee1 Mon Sep 17 00:00:00 2001 From: Adam Tengler Date: Tue, 6 Mar 2018 18:08:55 +0100 Subject: [PATCH 1/6] AsyncIO fix to actually call the functions in paralel --- kqueen/blueprints/api/views.py | 30 ++++++++++++++++++++++-------- kqueen/models.py | 11 +++++++---- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/kqueen/blueprints/api/views.py b/kqueen/blueprints/api/views.py index 1b440120..6b646ef0 100644 --- a/kqueen/blueprints/api/views.py +++ b/kqueen/blueprints/api/views.py @@ -71,9 +71,16 @@ def index(): class ListClusters(ListView): object_class = Cluster - async def _update_cluster(self, cluster): - cluster.get_state() - return True + async def _update_clusters(self, clusters, loop): + futures = [ + loop.run_in_executor( + None, + cluster.get_state + ) + for cluster in clusters + ] + for result in await asyncio.gather(*futures): + pass def get_content(self, *args, **kwargs): clusters = self.obj @@ -88,7 +95,7 @@ def get_content(self, *args, **kwargs): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # run coroutines and close loop - loop.run_until_complete(asyncio.gather(*[self._update_cluster(c) for c in clusters])) + loop.run_until_complete(self._update_clusters(clusters, loop)) loop.close() except Exception as e: logger.exception('Asyncio loop is NOT available, fallback to simple looping: ') @@ -210,9 +217,16 @@ def cluster_resize(pk): class ListProvisioners(ListView): object_class = Provisioner - async def _update_provisioner(self, provisioner): - provisioner.engine_status() - return True + async def _update_provisioners(self, provisioners, loop): + futures = [ + loop.run_in_executor( + None, + provisioner.engine_status + ) + for provisioner in provisioners + ] + for result in await asyncio.gather(*futures): + pass def get_content(self, *args, **kwargs): provisioners = self.obj @@ -227,7 +241,7 @@ def get_content(self, *args, **kwargs): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # run coroutines and close loop - loop.run_until_complete(asyncio.gather(*[self._update_provisioner(p) for p in provisioners])) + loop.run_until_complete(self._update_provisioners(provisioners, loop)) loop.close() except Exception as e: logger.exception('Asyncio loop is NOT available, fallback to simple looping: ') diff --git a/kqueen/models.py b/kqueen/models.py index 784b4546..79dab60d 100644 --- a/kqueen/models.py +++ b/kqueen/models.py @@ -350,10 +350,13 @@ def engine_status(self, save=True): return state def save(self, check_status=True): - if check_status: - self.state = self.engine_status(save=False) - self.verbose_name = getattr(self.get_engine_cls(), 'verbose_name', self.engine) - return super(Provisioner, self).save() + from kqueen.server import create_app + app = create_app() + with app.app_context(): + if check_status: + self.state = self.engine_status(save=False) + self.verbose_name = getattr(self.get_engine_cls(), 'verbose_name', self.engine) + return super(Provisioner, self).save() # From b6aed797bb3ac762bdf6440a51ca370c0ed372ee Mon Sep 17 00:00:00 2001 From: Adam Tengler Date: Wed, 7 Mar 2018 16:04:45 +0100 Subject: [PATCH 2/6] Don't require Project ID as user input, its part of service_account.json --- kqueen/engines/gce.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/kqueen/engines/gce.py b/kqueen/engines/gce.py index 6753f9c4..eacaf8fe 100644 --- a/kqueen/engines/gce.py +++ b/kqueen/engines/gce.py @@ -46,13 +46,6 @@ class GceEngine(BaseEngine): 'token_uri' ] } - }, - 'project': { - 'type': 'text', - 'label': 'Project ID', - 'validators': { - 'required': True, - } } }, 'cluster': { @@ -116,7 +109,7 @@ def __init__(self, cluster, **kwargs): super(GceEngine, self).__init__(cluster, **kwargs) # Client initialization self.service_account_info = kwargs.get('service_account_info', {}) - self.project = kwargs.get('project', '') + self.project = self.service_account_info.get('project_id', '') self.zone = kwargs.get('zone', '-') self.cluster_id = 'a' + self.cluster.id.replace('-', '') self.cluster_config = { @@ -323,7 +316,7 @@ def cluster_list(self): @classmethod def engine_status(cls, **kwargs): service_account_info = kwargs.get('service_account_info', {}) - project = kwargs.get('project', '') + project = service_account_info.get('project_id', '') project_zone = kwargs.get('zone', '-') credentials = service_account.Credentials.from_service_account_info(service_account_info) client = googleapiclient.discovery.build('container', 'v1', credentials=credentials) From 8701f7c621b2167fbb497f32b84a4f2814323987 Mon Sep 17 00:00:00 2001 From: Adam Tengler Date: Wed, 7 Mar 2018 16:05:03 +0100 Subject: [PATCH 3/6] Use fixed version of oauth2client to prevent google.auth errors --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 14853b8e..0cb48d7a 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ 'flask-swagger-ui', 'gunicorn', 'kubernetes', + 'oauth2client==3.0.0', 'pycrypto', 'prometheus_client', 'python-etcd', From 5589753c72eb31f78226507be344145e6fe32e18 Mon Sep 17 00:00:00 2001 From: Adam Tengler Date: Wed, 7 Mar 2018 14:39:25 +0100 Subject: [PATCH 4/6] Change app injection to work with tests --- kqueen/models.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/kqueen/models.py b/kqueen/models.py index 79dab60d..ee18c26b 100644 --- a/kqueen/models.py +++ b/kqueen/models.py @@ -350,9 +350,16 @@ def engine_status(self, save=True): return state def save(self, check_status=True): + # while used in async method, app context is not available by default and needs to be imported + from flask import current_app as app from kqueen.server import create_app - app = create_app() - with app.app_context(): + try: + if not app.testing: + app = create_app() + except RuntimeError: + app = create_app() + + with app.app_context(): if check_status: self.state = self.engine_status(save=False) self.verbose_name = getattr(self.get_engine_cls(), 'verbose_name', self.engine) From 7f4d17aafcbd2cbb2a15086e8ddf5897bbb0292c Mon Sep 17 00:00:00 2001 From: Adam Tengler Date: Wed, 7 Mar 2018 16:23:55 +0100 Subject: [PATCH 5/6] Inject app context to Cluster.save and prevent doublechecking of Provisioner status --- kqueen/models.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/kqueen/models.py b/kqueen/models.py index ee18c26b..f23c1541 100644 --- a/kqueen/models.py +++ b/kqueen/models.py @@ -82,7 +82,7 @@ def delete(self): deprov_status, deprov_msg = self.engine.deprovision() if deprov_status: - super(Cluster, self).delete() + super().delete() else: raise Exception('Unable to deprovision cluster: {}'.format(deprov_msg)) @@ -94,6 +94,19 @@ def get_kubeconfig(self): self.save() return kubeconfig + def save(self, **kwargs): + # while used in async method, app context is not available by default and needs to be imported + from flask import current_app as app + from kqueen.server import create_app + try: + if not app.testing: + app = create_app() + except RuntimeError: + app = create_app() + + with app.app_context(): + return super().save(**kwargs) + def status(self): """Return information about Kubernetes cluster""" try: @@ -346,10 +359,10 @@ def engine_status(self, save=True): state = engine_class.engine_status(**self.parameters) if save: self.state = state - self.save() + self.save(check_status=False) return state - def save(self, check_status=True): + def save(self, check_status=True, **kwargs): # while used in async method, app context is not available by default and needs to be imported from flask import current_app as app from kqueen.server import create_app @@ -363,7 +376,7 @@ def save(self, check_status=True): if check_status: self.state = self.engine_status(save=False) self.verbose_name = getattr(self.get_engine_cls(), 'verbose_name', self.engine) - return super(Provisioner, self).save() + return super().save(**kwargs) # From c42a81187ed6d6b8fdbb280167cf4a04f9ac78f0 Mon Sep 17 00:00:00 2001 From: Vnaumov Date: Sun, 11 Mar 2018 20:38:00 +0400 Subject: [PATCH 6/6] change log-level for dev/prod/demo cases prod/demo should have DEBUG level by default dev should have INFO level by default --- kqueen/config/demo.py | 2 +- kqueen/config/dev.py | 2 +- kqueen/config/prod.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kqueen/config/demo.py b/kqueen/config/demo.py index 8dabc757..0b91d073 100644 --- a/kqueen/config/demo.py +++ b/kqueen/config/demo.py @@ -3,7 +3,7 @@ class Config(BaseConfig): DEBUG = False - LOG_LEVEL = 'INFO' + LOG_LEVEL = 'DEBUG' LOG_CONFIG = 'kqueen/utils/logger_config.yml' KQUEEN_HOST = '0.0.0.0' diff --git a/kqueen/config/dev.py b/kqueen/config/dev.py index 71a96dfb..fef7e082 100644 --- a/kqueen/config/dev.py +++ b/kqueen/config/dev.py @@ -3,7 +3,7 @@ class Config(BaseConfig): DEBUG = True - LOG_LEVEL = 'DEBUG' + LOG_LEVEL = 'INFO' LOG_CONFIG = 'kqueen/utils/logger_config.yml' # App secret diff --git a/kqueen/config/prod.py b/kqueen/config/prod.py index d341dc5d..d1c25f6f 100644 --- a/kqueen/config/prod.py +++ b/kqueen/config/prod.py @@ -3,7 +3,7 @@ class Config(BaseConfig): DEBUG = False - LOG_LEVEL = 'INFO' + LOG_LEVEL = 'DEBUG' LOG_CONFIG = 'kqueen/utils/logger_config.yml' KQUEEN_HOST = '0.0.0.0'