Commit a47a16f6 authored by hxquangnhat's avatar hxquangnhat

fixed issue#54

parent 2ef53fc7
......@@ -16,3 +16,4 @@ Developer documentation
scheduler
backend
stats
kube-backend
.. _kube_backend:
.. _kube-backend:
Kubernetes backend for Zoe
==========================
......@@ -23,11 +23,11 @@ How it works
2. Zoe:
* Zapp Description:
* Add new field: ``replicas``, if users doesn't specify this value, the default value for each service would be ``1``.
* In field ``require_resources``, the ``cores`` field can be float.
* Idea:
* Idea:
* Create each **replication controller** per each service of a Zapp. Replication Controller assures to have at least a number of **replicas** (pod) always running.
* Create a Kubernetes **service** per each **replication controller**, which has the same **labels** and **label selectors** with the associated **replication controller**. The service would help the zapp service be exposed to the network by exposing the same port of the service on all kubernetes nodes.
......@@ -37,5 +37,5 @@ References
* Kubernetes: https://kubernetes.io/
* Kubernetes Replication Controller : https://kubernetes.io/docs/user-guide/replication-controller/
* Kubernetes Service: https://kubernetes.io/docs/user-guide/services/
* Kubernetes Limit and Request: https://kubernetes.io/docs/user-guide/compute-resources/
* Kubernetes Limit and Request: https://kubernetes.io/docs/user-guide/compute-resources/
#!/usr/bin/env bash
pylint *.py zoe_*
doc8 docs/
set -e
pylint --ignore old_swarm *.py zoe_*
doc8 docs/
......@@ -21,14 +21,13 @@ import threading
import zoe_api.exceptions
import zoe_api.master_api
from zoe_api.proxy.apache import ApacheProxy
#from zoe_api.proxy.nginx import NginxProxy
import zoe_lib.applications
import zoe_lib.exceptions
import zoe_lib.state
from zoe_lib.config import get_conf
from zoe_api.proxy.apache import ApacheProxy
from zoe_api.proxy.nginx import NginxProxy
log = logging.getLogger(__name__)
......@@ -78,10 +77,10 @@ class APIEndpoint:
if get_conf().deployment_name != 'test':
if get_conf().proxy_type == 'apache':
proxy = zoe_api.proxy.apache.ApacheProxy(self)
else:
proxy = zoe_api.proxy.nginx.NginxProxy(self)
threading.Thread(target=proxy.proxify,args=(uid, role, new_id)).start()
proxy = ApacheProxy(self)
#else:
# proxy = NginxProxy(self)
threading.Thread(target=proxy.proxify, args=(uid, role, new_id)).start()
return new_id
......@@ -98,9 +97,9 @@ class APIEndpoint:
if e.is_active:
if get_conf().deployment_name != 'test':
if get_conf().proxy_type == 'apache':
proxy = zoe_api.proxy.apache.ApacheProxy(self)
else:
proxy = zoe_api.proxy.nginx.NginxProxy(self)
proxy = ApacheProxy(self)
#else:
# proxy = NginxProxy(self)
proxy.unproxify(uid, role, exec_id)
return self.master.execution_terminate(exec_id)
else:
......
......@@ -15,17 +15,18 @@
""" Store adapters to read/write data to from/to PostgresSQL. """
import datetime, time
import zoe_lib.state
from zoe_lib.config import get_conf
from oauth2.store import AccessTokenStore, AuthCodeStore, ClientStore
from oauth2.datatype import AccessToken, AuthorizationCode, Client
from oauth2.error import AccessTokenNotFound, AuthCodeNotFound, ClientNotFoundError
from oauth2.store import AccessTokenStore, ClientStore
from oauth2.datatype import AccessToken, Client
from oauth2.error import AccessTokenNotFound, ClientNotFoundError
class AccessTokenStore(AccessTokenStore):
class AccessTokenStorePg(AccessTokenStore):
""" AccessTokenStore for postgresql """
def fetch_by_refresh_token(self, refresh_token):
""" get accesstoken from refreshtoken """
sql = zoe_lib.state.SQLManager(get_conf())
data = sql.fetch_by_refresh_token(refresh_token)
......@@ -51,18 +52,21 @@ class AccessTokenStore(AccessTokenStore):
return res
def get_client_id_by_refresh_token(self, refresh_token):
""" get clientID from refreshtoken """
sql = zoe_lib.state.SQLManager(get_conf())
data = sql.get_client_id_by_refresh_token(refresh_token)
return data
def get_client_id_by_access_token(self, access_token):
""" get clientID from accesstoken """
sql = zoe_lib.state.SQLManager(get_conf())
data = sql.get_client_id_by_access_token(access_token)
return data
def fetch_existing_token_of_user(self, client_id, grant_type, user_id):
""" get accesstoken from userid """
sql = zoe_lib.state.SQLManager(get_conf())
data = sql.fetch_existing_token_of_user(client_id, grant_type, user_id)
......@@ -80,23 +84,26 @@ class AccessTokenStore(AccessTokenStore):
user_id=data["user_id"])
def save_token(self, access_token):
""" save accesstoken """
sql = zoe_lib.state.SQLManager(get_conf())
sql.save_token(access_token.client_id,
access_token.grant_type,
access_token.token,
access_token.data,
access_token.expires_at,
access_token.refresh_token,
access_token.refresh_expires_at,
access_token.scopes,
access_token.user_id)
access_token.grant_type,
access_token.token,
access_token.data,
access_token.expires_at,
access_token.refresh_token,
access_token.refresh_expires_at,
access_token.scopes,
access_token.user_id)
return True
class ClientStore(ClientStore):
class ClientStorePg(ClientStore):
""" ClientStore for postgres """
def save_client(self, identifier, secret, role, redirect_uris, authorized_grants, authorized_response_types):
""" save client to db """
sql = zoe_lib.state.SQLManager(get_conf())
sql.save_client(identifier,
secret,
......@@ -107,10 +114,11 @@ class ClientStore(ClientStore):
return True
def fetch_by_client_id(self, client_id):
""" get client by clientid """
sql = zoe_lib.state.SQLManager(get_conf())
client_data = sql.fetch_by_client_id(client_id)
client_data_grants= client_data["authorized_grants"].split(':')
client_data_grants = client_data["authorized_grants"].split(':')
if client_data is None:
raise ClientNotFoundError
......@@ -122,6 +130,7 @@ class ClientStore(ClientStore):
authorized_response_types=client_data["authorized_response_types"])
def get_role_by_client_id(self, client_id):
""" get client role by clientid """
sql = zoe_lib.state.SQLManager(get_conf())
client_data = sql.fetch_by_client_id(client_id)
......
......@@ -15,7 +15,6 @@
""" Token generator for oauth2."""
import base64
import hashlib
import os
import uuid
......@@ -64,7 +63,6 @@ class TokenGenerator(object):
"""
raise NotImplementedError
class URandomTokenGenerator(TokenGenerator):
"""
Create a token using ``os.urandom()``.
......@@ -85,7 +83,6 @@ class URandomTokenGenerator(TokenGenerator):
return hash_gen.hexdigest()[:self.token_length]
class Uuid4(TokenGenerator):
"""
Generate a token using uuid4.
......@@ -96,4 +93,3 @@ class Uuid4(TokenGenerator):
:rtype: str
"""
return str(uuid.uuid4())
......@@ -15,10 +15,10 @@
"""Proxifying using Apache2 Container."""
import docker
import time
import logging
import random
import docker
import zoe_api.proxy.base
import zoe_api.api_endpoint
......@@ -33,78 +33,77 @@ class ApacheProxy(zoe_api.proxy.base.BaseProxy):
def __init__(self, apiEndpoint):
self.api_endpoint = apiEndpoint
"""Proxify function."""
def proxify(self, uid, role, id):
def proxify(self, uid, role, execution_id): #pylint: disable=too-many-locals
"""Proxify function."""
try:
length_service = 0
#Wait until all the services get created and started to be able to get the backend_id
while self.api_endpoint.execution_by_id(uid, role, id).status != 'running':
while self.api_endpoint.execution_by_id(uid, role, execution_id).status != 'running':
log.info('Waiting for all services get started...')
length_service = len(self.api_endpoint.execution_by_id(uid, role, id).services)
time.sleep(1)
exe = self.api_endpoint.execution_by_id(uid, role, id)
l = len(exe.services)
while l != 0:
exe = self.api_endpoint.execution_by_id(uid, role, id)
l = len(exe.services)
exe = self.api_endpoint.execution_by_id(uid, role, execution_id)
lth = len(exe.services)
while lth != 0:
exe = self.api_endpoint.execution_by_id(uid, role, execution_id)
lth = len(exe.services)
for srv in exe.services:
if srv.backend_id == None:
if srv.backend_id is None:
time.sleep(1)
else:
l = l - 1
lth = lth - 1
#Start proxifying by adding entry to use proxypass and proxypassreverse in apache2 config file
for srv in exe.services:
ip, p = None, None
ip, port = None, None
if get_conf().backend == 'OldSwarm':
swarm = SwarmClient(get_conf())
s_info = swarm.inspect_container(srv.backend_id)
portList = s_info['ports']
for k,v in portList.items():
exposedPort = k.split('/tcp')[0]
if v != None:
ip = v[0]
p = v[1]
base_path = '/zoe/' + uid + '/' + str(id) + '/' + srv.name + '/' + exposedPort
original_path = str(ip) + ':' + str(p) + base_path
if ip is not None and p is not None:
log.info('Proxifying %s', srv.name + ' port ' + exposedPort)
port_list = s_info['ports']
for key, val in port_list.items():
exposed_port = key.split('/tcp')[0]
if val != None:
ip = val[0]
port = val[1]
base_path = '/zoe/' + uid + '/' + str(execution_id) + '/' + srv.name + '/' + exposed_port
original_path = str(ip) + ':' + str(port) + base_path
if ip is not None and port is not None:
log.info('Proxifying %s', srv.name + ' port ' + exposed_port)
self.dispatch_to_docker(base_path, original_path)
else:
kube = KubernetesClient(get_conf())
s_info = kube.inspect_service(srv.dns_name)
kubeNodes = kube.info().nodes
hostIP = random.choice(kubeNodes).name
kube_nodes = kube.info().nodes
host_ip = random.choice(kube_nodes).name
while 'nodePort' not in s_info['port_forwarding'][0]:
log.info('Waiting for service get started before proxifying...')
s_info = kube.inspect_service(srv.dns_name)
time.sleep(0.5)
ip = hostIP
p = s_info['port_forwarding'][0]['nodePort']
exposedPort = s_info['port_forwarding'][0]['port']
base_path = '/zoe/' + uid + '/' + str(id) + '/' + srv.name + '/' + str(exposedPort)
original_path = str(ip) + ':' + str(p) + base_path
ip = host_ip
port = s_info['port_forwarding'][0]['nodePort']
exposed_port = s_info['port_forwarding'][0]['port']
base_path = '/zoe/' + uid + '/' + str(execution_id) + '/' + srv.name + '/' + str(exposed_port)
original_path = str(ip) + ':' + str(port) + base_path
if ip is not None and p is not None:
log.info('Proxifying %s', srv.name + ' port ' + str(exposedPort))
if ip is not None and port is not None:
log.info('Proxifying %s', srv.name + ' port ' + str(exposed_port))
self.dispatch_to_docker(base_path, original_path)
except Exception as ex:
log.error(ex)
#The apache2 server is running inside a container
#Adding new entries with the proxy path and the ip:port of the application to the apache2 config file
def dispatch_to_docker(self, base_path, original_path):
"""
The apache2 server is running inside a container
Adding new entries with the proxy path and the ip:port of the application to the apache2 config file
"""
proxy = ['ProxyPass ' + base_path + '/api/kernels/ ws://' + original_path + '/api/kernels/',
'ProxyPassReverse ' + base_path + '/api/kernels/ ws://' + original_path + '/api/kernels/',
'ProxyPass ' + base_path + '/terminals/websocket/ ws://' + original_path + '/terminals/websocket/',
......@@ -116,24 +115,24 @@ class ApacheProxy(zoe_api.proxy.base.BaseProxy):
docker_client = docker.Client(base_url=get_conf().proxy_docker_sock)
delCommand = "sed -i '$ d' " + get_conf().proxy_config_file # /etc/apache2/sites-available/all.conf"
delID = docker_client.exec_create(get_conf().proxy_container, delCommand)
docker_client.exec_start(delID)
del_command = "sed -i '$ d' " + get_conf().proxy_config_file # /etc/apache2/sites-available/all.conf"
del_id = docker_client.exec_create(get_conf().proxy_container, del_command)
docker_client.exec_start(del_id)
for s in proxy:
command = 'bash -c "echo ' + "'" + s + "'" + ' >> /etc/apache2/sites-available/all.conf"'
id = docker_client.exec_create(get_conf().proxy_container, command)
docker_client.exec_start(id)
for entry in proxy:
command = 'bash -c "echo ' + "'" + entry + "'" + ' >> /etc/apache2/sites-available/all.conf"'
execution_id = docker_client.exec_create(get_conf().proxy_container, command)
docker_client.exec_start(execution_id)
reloadCommand = 'service apache2 reload'
reloadID = docker_client.exec_create(get_conf().proxy_container, reloadCommand)
docker_client.exec_start(reloadID)
reload_command = 'service apache2 reload'
reload_id = docker_client.exec_create(get_conf().proxy_container, reload_command)
docker_client.exec_start(reload_id)
#Simply remove the added entries at the apache2 config file when terminating applcations
def unproxify(self, uid, role, id):
log.info('Unproxifying for user %s - execution %s', uid, str(id))
pattern = '/zoe\/' + uid + '\/' + str(id) + '/d'
def unproxify(self, uid, role, execution_id):
log.info('Unproxifying for user %s - execution %s', uid, str(execution_id))
pattern = '/zoe\/' + uid + '\/' + str(execution_id) + '/d' #pylint: disable=anomalous-backslash-in-string
docker_client = docker.Client(base_url=get_conf().proxy_docker_sock)
delCommand = 'sed -i "' + pattern + '" ' + get_conf().proxy_config_file # /etc/apache2/sites-available/all.conf'
delID = docker_client.exec_create(get_conf().proxy_container, delCommand)
docker_client.exec_start(delID)
del_command = 'sed -i "' + pattern + '" ' + get_conf().proxy_config_file # /etc/apache2/sites-available/all.conf'
del_id = docker_client.exec_create(get_conf().proxy_container, del_command)
docker_client.exec_start(del_id)
......@@ -19,9 +19,10 @@
class BaseProxy:
"""Base proxy class."""
def proxify(self, uid, role, id):
def proxify(self, uid, role, execution_id):
"""The methods that needs to be overridden by implementations."""
raise NotImplementedError
def unproxify(self, uid, role, id):
def unproxify(self, uid, role, execution_id):
"""The methods that needs to be overridden by implementations."""
raise NotImplementedError
# Copyright (c) 2016, Quang-Nhat Hoang-Xuan
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Base authenticator class."""
import docker
import time
import logging
import zoe_api.proxy.base
import zoe_api.api_endpoint
from zoe_lib.config import get_conf
log = logging.getLogger(__name__)
class NginxProxy(zoe_api.proxy.base.BaseProxy):
"""Nginx proxy class."""
def __init__(self, apiEndpoint):
return {}
def proxify(self, uid, role, id):
return {}
def unproxify(self, uid, role, id):
return {}
......@@ -34,7 +34,7 @@ class ExecutionAPI(RequestHandler):
"""Set up the headers for enabling CORS."""
manage_cors_headers(self)
def options(self, execution_id):
def options(self, execution_id): # pylint: disable=unused-argument
"""Needed for CORS."""
self.set_status(204)
self.finish()
......@@ -121,8 +121,8 @@ class ExecutionCollectionAPI(RequestHandler):
if self.request.body:
filt_dict = tornado.escape.json_decode(self.request.body)
except ValueError:
raise zoe_api.exceptions.ZoeRestAPIExecution('Error decoding JSON data')
raise zoe_api.exceptions.ZoeRestAPIException('Error decoding JSON data')
if 'status' in filt_dict:
execs = self.api_endpoint.execution_list(uid, role, status=filt_dict['status'])
else:
......
......@@ -15,21 +15,16 @@
"""The oAuth2 API endpoints."""
from tornado.web import RequestHandler
import tornado.escape
import logging
import json
import psycopg2
import zoe_lib.config as config
import zoe_api.exceptions
from tornado.web import RequestHandler
import oauth2.grant
import json
import requests
import psycopg2
from zoe_api.rest_api.utils import catch_exceptions, get_auth
from zoe_api.rest_api.oauth_utils import auth_controller, client_store, token_store
from zoe_api.api_endpoint import APIEndpoint
log = logging.getLogger(__name__)
......@@ -66,13 +61,13 @@ class OAuthGetAPI(RequestHandler):
def post(self):
"""REQUEST/REFRESH token"""
uid, role = get_auth(self)
grant_type = oauth2.grant.RefreshToken.grant_type + ':' + oauth2.grant.ResourceOwnerGrant.grant_type
try:
self.client_store.save_client(uid, '', role, '', grant_type, '')
except psycopg2.IntegrityError as e:
log.warn('User is already had')
except psycopg2.IntegrityError:
log.info('User is already had')
response = self._dispatch_request(uid)
self._map_response(response)
......@@ -95,7 +90,6 @@ class OAuthGetAPI(RequestHandler):
request.post_param = lambda key: params[key]
return self.auth_controller.dispatch(request, environ={})
def _map_response(self, response):
for name, value in list(response.headers.items()):
self.set_header(name, value)
......@@ -109,7 +103,8 @@ class OAuthGetAPI(RequestHandler):
def data_received(self, chunk):
pass
class OAuthRevokeAPI(RequestHandler):
class OAuthRevokeAPI(RequestHandler): # pylint: disable=abstract-method
"""The OAuthRevokeAPI endpoint."""
def initialize(self, **kwargs):
"""Initializes the request handler."""
......@@ -120,8 +115,8 @@ class OAuthRevokeAPI(RequestHandler):
@catch_exceptions
def delete(self, token):
"""DELETE token (logout)"""
uid, role = get_auth(self)
get_auth(self)
res = self.token_store.delete_refresh_token(token)
if res == 0:
......
......@@ -16,38 +16,35 @@
"""Authentication controller for oauth2."""
import logging
from zoe_api.exceptions import ZoeRestAPIException
import oauth2.web
import oauth2.grant
from zoe_api.auth.oauth2.postgresql import AccessTokenStore, ClientStore
from zoe_api.auth.oauth2.postgresql import AccessTokenStorePg, ClientStorePg
from zoe_api.auth.oauth2.tokengenerator import Uuid4
import time
log = logging.getLogger(__name__)
class OAuthSiteAdapter(oauth2.web.ResourceOwnerGrantSiteAdapter):
"""OAuth Simple SiteAdapter"""
def authenticate(self, request, environ, scopes, client):
return {}
return {}
client_store = ClientStore()
token_store = AccessTokenStore()
client_store = ClientStorePg() #pylint: disable=invalid-name
token_store = AccessTokenStorePg() #pylint: disable=invalid-name
token_generator = Uuid4()
token_generator = Uuid4() #pylint: disable=invalid-name
token_generator.expires_in[oauth2.grant.ClientCredentialsGrant.grant_type] = 3600
auth_controller = oauth2.Provider(
auth_controller = oauth2.Provider( #pylint: disable=invalid-name
access_token_store=token_store,
auth_code_store=token_store,
client_store=client_store,
token_generator=token_generator
)
site_adapter = OAuthSiteAdapter()
site_adapter = OAuthSiteAdapter() #pylint: disable=invalid-name
auth_controller.token_path = '/api/0.7/oauth/token'
auth_controller.add_grant(oauth2.grant.ClientCredentialsGrant())
auth_controller.add_grant(oauth2.grant.RefreshToken(expires_in=3600))
auth_controller.add_grant(oauth2.grant.ResourceOwnerGrant(site_adapter=site_adapter))
......@@ -40,7 +40,7 @@ class ServiceAPI(RequestHandler):
manage_cors_headers(self)
@catch_exceptions
def options(self, service_id):
def options(self, service_id): # pylint: disable=unused-argument
"""Needed for CORS."""
self.set_status(204)
self.finish()
......
......@@ -15,6 +15,7 @@
"""Utility functions needed by the Zoe REST API."""
import time
import base64
import logging
import functools
......@@ -27,11 +28,7 @@ from zoe_api.exceptions import ZoeRestAPIException, ZoeNotFoundException, ZoeAut
from zoe_api.auth.ldap import LDAPAuthenticator
from zoe_api.auth.file import PlainTextAuthenticator
from zoe_api.auth.ldapsasl import LDAPSASLAuthenticator
from zoe_api.auth.base import BaseAuthenticator
from zoe_api.rest_api.oauth_utils import auth_controller, client_store, token_store
import json
import time
from zoe_api.rest_api.oauth_utils import client_store, token_store
log = logging.getLogger(__name__)
......
......@@ -129,7 +129,7 @@ def _service_check(data):
try:
float(data['required_resources']['cores'])
except ValueError:
raise InvalidApplicationDescription(msg="required_resources -> cores field should be a float")
raise InvalidApplicationDescription(msg="required_resources -> cores field should be a float")
if 'environment' in data:
if not hasattr(data['environment'], '__iter__'):
......@@ -157,7 +157,7 @@ def _service_check(data):
if get_conf().backend == 'Kubernetes':
if 'replicas' not in data:
data['replicas'] = 1
try:
int(data['replicas'])
except ValueError:
......
......@@ -55,7 +55,8 @@ class ExposedPort:
self.number = data['port_number']
self.expose = data['expose'] if 'expose' in data else False
def isExpose(self):
def is_expose(self):
""" return expose """
return self.expose
......@@ -184,12 +185,13 @@ class Service:
@property
def proxy_address(self):
for p in self.ports:
if p.isExpose():
proxyAdd = get_conf().proxy_path + "/" + self.user_id + "/" + str(self.execution_id) + "/" + self.name
"""Get proxy address path"""
for port in self.ports:
if port.is_expose():
proxy_addr = get_conf().proxy_path + "/" + self.user_id + "/" + str(self.execution_id) + "/" + self.name
else:
proxyAdd = None
return proxyAdd
proxy_addr = None
return proxy_addr
def is_dead(self):
"""Returns True if this service is not running."""
......
......@@ -181,9 +181,10 @@ class SQLManager:
self.conn.commit()
return cur.fetchone()[0]
""" The above section is used for Oauth2 authentication mechanism """
#The above section is used for Oauth2 authentication mechanism
def fetch_by_refresh_token(self, refresh_token):
""" get info from refreshtoken """
cur = self._cursor()
query = 'SELECT * FROM oauth_token WHERE refresh_token = %s'
cur.execute(query, (refresh_token,))
......@@ -191,6 +192,7 @@ class SQLManager:
return cur.fetchone()
def delete_refresh_token(self, refresh_token):
""" delete info by refreshtoken """
cur = self._cursor()
check_exists = 'SELECT * FROM oauth_token WHERE refresh_token = %s OR token = %s'
cur.execute(check_exists, (refresh_token, refresh_token))
......@@ -203,6 +205,7 @@ class SQLManager:
return res
def fetch_existing_token_of_user(self, client_id, grant_type, user_id):
""" get info from clientid granttype userid """
cur = self._cursor()
query = 'SELECT * FROM oauth_token WHERE client_id = %s AND grant_type = %s AND user_id = %s'
cur.execute(query, (client_id, grant_type, user_id,))
......@@ -210,6 +213,7 @@ class SQLManager:
return cur.fetchone()
def get_client_id_by_access_token(self, access_token):
""" get clientid from accesstoken """
cur = self._cursor()
query = 'SELECT * FROM oauth_token WHERE token = %s'
cur.execute(query, (access_token,))
......@@ -217,16 +221,18 @@ class SQLManager:
return cur.fetchone()
def get_client_id_by_refresh_token(self, refresh_token):
""" get clientid from refreshtoken """
cur = self._cursor()
query = 'SELECT * FROM oauth_token WHERE refresh_token = %s'
cur.execute(query, (refresh_token,))
return cur.fetchone()
def save_token(self, client_id, grant_type, token, data, expires_at, refresh_token, refresh_expires_at, scopes, user_id):
def save_token(self, client_id, grant_type, token, data, expires_at, refresh_token, refresh_expires_at, scopes, user_id): #pylint: disable=too-many-arguments
""" save token to db """
cur = self._cursor()
expires_at = datetime.datetime.fromtimestamp(expires_at)
if refresh_expires_at == None:
if refresh_expires_at is None:
query = cur.mogrify('UPDATE oauth_token SET token = %s, expires_at = %s WHERE client_id=%s', (token, expires_at, client_id))
else:
refresh_token_expires_at = datetime.datetime.fromtimestamp(refresh_expires_at)
......@@ -236,12 +242,14 @@ class SQLManager:
self.conn.commit()