From 0ba42010206a28ab85c3616a285cb5e8a3cabba6 Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Thu, 29 May 2014 11:24:10 -0400 Subject: [PATCH] Add a module which will create notifications for all users when the license is at its expiration period, and terminate the process when the license expires. --- app.py | 2 + data/database.py | 10 +-- .../5a07499ce53f_set_up_initial_database.py | 1 + data/model/legacy.py | 42 +++++++--- initdb.py | 1 + static/js/app.js | 6 ++ test/data/test.db | Bin 200704 -> 200704 bytes util/expiration.py | 76 ++++++++++++++++++ 8 files changed, 120 insertions(+), 18 deletions(-) create mode 100644 util/expiration.py diff --git a/app.py b/app.py index a8b701c66..b2f41cd69 100644 --- a/app.py +++ b/app.py @@ -16,6 +16,7 @@ from data.users import UserAuthentication from util.analytics import Analytics from util.exceptionlog import Sentry from util.queuemetrics import QueueMetrics +from util.expiration import Expiration from data.billing import Billing from data.buildlogs import BuildLogs from data.queue import WorkQueue @@ -64,6 +65,7 @@ sentry = Sentry(app) build_logs = BuildLogs(app) queue_metrics = QueueMetrics(app) authentication = UserAuthentication(app) +expiration = Expiration(app) tf = app.config['DB_TRANSACTION_FACTORY'] image_diff_queue = WorkQueue(app.config['DIFFS_QUEUE_NAME'], tf) diff --git a/data/database.py b/data/database.py index 71f88fb91..ffa8c909e 100644 --- a/data/database.py +++ b/data/database.py @@ -117,7 +117,7 @@ class FederatedLogin(BaseModel): class Visibility(BaseModel): - name = CharField(index=True) + name = CharField(index=True, unique=True) class Repository(BaseModel): @@ -136,7 +136,7 @@ class Repository(BaseModel): class Role(BaseModel): - name = CharField(index=True) + name = CharField(index=True, unique=True) class RepositoryPermission(BaseModel): @@ -189,7 +189,7 @@ class AccessToken(BaseModel): class BuildTriggerService(BaseModel): - name = CharField(index=True) + name = CharField(index=True, unique=True) class RepositoryBuildTrigger(BaseModel): @@ -283,7 +283,7 @@ class QueueItem(BaseModel): class LogEntryKind(BaseModel): - name = CharField(index=True) + name = CharField(index=True, unique=True) class LogEntry(BaseModel): @@ -330,7 +330,7 @@ class OAuthAccessToken(BaseModel): class NotificationKind(BaseModel): - name = CharField(index=True) + name = CharField(index=True, unique=True) class Notification(BaseModel): diff --git a/data/migrations/versions/5a07499ce53f_set_up_initial_database.py b/data/migrations/versions/5a07499ce53f_set_up_initial_database.py index 3c4a8de5d..23aaf506a 100644 --- a/data/migrations/versions/5a07499ce53f_set_up_initial_database.py +++ b/data/migrations/versions/5a07499ce53f_set_up_initial_database.py @@ -140,6 +140,7 @@ def upgrade(): [ {'id':1, 'name':'password_required'}, {'id':2, 'name':'over_private_usage'}, + {'id':3, 'name':'expiring_license'}, ]) op.create_table('teamrole', diff --git a/data/model/legacy.py b/data/model/legacy.py index 76d0123be..f27e29170 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -1622,14 +1622,20 @@ def list_trigger_builds(namespace_name, repository_name, trigger_uuid, .where(RepositoryBuildTrigger.uuid == trigger_uuid)) -def create_notification(kind, target, metadata={}): - kind_ref = NotificationKind.get(name=kind) +def create_notification(kind_name, target, metadata={}): + kind_ref = NotificationKind.get(name=kind_name) notification = Notification.create(kind=kind_ref, target=target, metadata_json=json.dumps(metadata)) return notification -def list_notifications(user, kind=None): +def create_unique_notification(kind_name, target, metadata={}): + with config.app_config['DB_TRANSACTION_FACTORY'](db): + if list_notifications(target, kind_name).count() == 0: + create_notification(kind_name, target, metadata) + + +def list_notifications(user, kind_name=None): Org = User.alias() AdminTeam = Team.alias() AdminTeamMember = TeamMember.alias() @@ -1647,20 +1653,30 @@ def list_notifications(user, kind=None): .join(AdminTeamMember, JOIN_LEFT_OUTER, on=(AdminTeam.id == AdminTeamMember.team)) .join(AdminUser, JOIN_LEFT_OUTER, on=(AdminTeamMember.user == - AdminUser.id))) + AdminUser.id)) + .where((Notification.target == user) | + ((AdminUser.id == user) & (TeamRole.name == 'admin'))) + .order_by(Notification.created) + .desc()) - where_clause = ((Notification.target == user) | - ((AdminUser.id == user) & - (TeamRole.name == 'admin'))) - - if kind: - where_clause = where_clause & (NotificationKind.name == kind) + if kind_name: + query = (query + .switch(Notification) + .join(NotificationKind) + .where(NotificationKind.name == kind_name)) - return query.where(where_clause).order_by(Notification.created).desc() + return query -def delete_notifications_by_kind(target, kind): - kind_ref = NotificationKind.get(name=kind) +def delete_all_notifications_by_kind(kind_name): + kind_ref = NotificationKind.get(name=kind_name) + (Notification.delete() + .where(Notification.kind == kind_ref) + .execute()) + + +def delete_notifications_by_kind(target, kind_name): + kind_ref = NotificationKind.get(name=kind_name) Notification.delete().where(Notification.target == target, Notification.kind == kind_ref).execute() diff --git a/initdb.py b/initdb.py index d3b1daf09..33d2f048e 100644 --- a/initdb.py +++ b/initdb.py @@ -233,6 +233,7 @@ def initialize_database(): NotificationKind.create(name='password_required') NotificationKind.create(name='over_private_usage') + NotificationKind.create(name='expiring_license') NotificationKind.create(name='test_notification') diff --git a/static/js/app.js b/static/js/app.js index 75220520c..e41c06a74 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -913,6 +913,12 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading return '/user'; } } + }, + 'expiring_license': { + 'level': 'error', + 'message': 'Your license will expire at: {expires_at} ' + + '

Please contact Quay.io support to purchase a new license.', + 'page': '/contact/' } }; diff --git a/test/data/test.db b/test/data/test.db index 8b38304645fa22e2a68341b75d1c56e3b3e0f8a3..801d3a28a5c7daae5d8ee678ae00d991b218c1ba 100644 GIT binary patch delta 6169 zcmb_gdstN0wV$)k5g0(lR}fJ^18S55=l$^BRAv}v7#_oGW|#pZo;h<4paLQg&?Msc z`E0&4H51u+D4JZ8`iM=G+#!#u?Y;f(*Tl59H>S2Xy-Auj?_N{$j!7tY4Om;1FUc1# zAAi7Jd#~TxzqQtH?U~I7Qa2w+{b44tZ};3{V&Cyqk7lNt7K=$(h&$0va&k0ns}L

H8MT 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 Ta)Pi+r;_(9{5BtD7TocFO6Uk~ delta 5601 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 diff --git a/util/expiration.py b/util/expiration.py new file mode 100644 index 000000000..b3f5732fe --- /dev/null +++ b/util/expiration.py @@ -0,0 +1,76 @@ +import calendar +import sys + +from email.utils import formatdate +from apscheduler.scheduler import Scheduler +from datetime import datetime, timedelta + +from data import model + + +class ExpirationScheduler(object): + def __init__(self, utc_create_notifications_date, utc_terminate_processes_date): + self._scheduler = Scheduler() + self._termination_date = utc_terminate_processes_date + + soon = datetime.now() + timedelta(seconds=1) + + if utc_create_notifications_date > datetime.utcnow(): + self._scheduler.add_date_job(model.delete_all_notifications_by_kind, soon, + ['expiring_license']) + + local_notifications_date = self._utc_to_local(utc_create_notifications_date) + self._scheduler.add_date_job(self._generate_notifications, local_notifications_date) + else: + self._scheduler.add_date_job(self._generate_notifications, soon) + + local_termination_date = self._utc_to_local(utc_terminate_processes_date) + self._scheduler.add_date_job(self._terminate, local_termination_date) + + @staticmethod + def _format_date(date): + """ Output an RFC822 date format. """ + if date is None: + return None + return formatdate(calendar.timegm(date.utctimetuple())) + + @staticmethod + def _utc_to_local(utc_dt): + # get integer timestamp to avoid precision lost + timestamp = calendar.timegm(utc_dt.timetuple()) + local_dt = datetime.fromtimestamp(timestamp) + return local_dt.replace(microsecond=utc_dt.microsecond) + + def _generate_notifications(self): + for user in model.get_active_users(): + model.create_unique_notification('expiring_license', user, + {'expires_at': self._format_date(self._termination_date)}) + + @staticmethod + def _terminate(): + sys.exit(1) + + def start(self): + self._scheduler.start() + + +class Expiration(object): + def __init__(self, app=None): + self.app = app + if app is not None: + self.state = self.init_app(app) + else: + self.state = None + + def init_app(self, app): + expiration = ExpirationScheduler(app.config['LICENSE_EXPIRATION_WARNING'], + app.config['LICENSE_EXPIRATION']) + expiration.start() + + # register extension with app + app.extensions = getattr(app, 'extensions', {}) + app.extensions['expiration'] = expiration + return expiration + + def __getattr__(self, name): + return getattr(self.state, name, None)