Merge pull request #1062 from dod-ccpo/env-provisioning-task
Environment provisioning background jobs
This commit is contained in:
@@ -19,13 +19,13 @@ class Applications(BaseDomainClass):
|
||||
resource_name = "application"
|
||||
|
||||
@classmethod
|
||||
def create(cls, portfolio, name, description, environment_names):
|
||||
def create(cls, user, portfolio, name, description, environment_names):
|
||||
application = Application(
|
||||
portfolio=portfolio, name=name, description=description
|
||||
)
|
||||
db.session.add(application)
|
||||
|
||||
Environments.create_many(application, environment_names)
|
||||
Environments.create_many(user, application, environment_names)
|
||||
|
||||
db.session.commit()
|
||||
return application
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from sqlalchemy.orm import load_only
|
||||
from typing import List
|
||||
|
||||
from atst.database import db
|
||||
from atst.models.environment import Environment
|
||||
from atst.models import Environment, Application, Portfolio, TaskOrder, CLIN
|
||||
from atst.domain.environment_roles import EnvironmentRoles
|
||||
|
||||
from .exceptions import NotFoundError
|
||||
@@ -9,17 +12,17 @@ from .exceptions import NotFoundError
|
||||
|
||||
class Environments(object):
|
||||
@classmethod
|
||||
def create(cls, application, name):
|
||||
environment = Environment(application=application, name=name)
|
||||
def create(cls, user, application, name):
|
||||
environment = Environment(application=application, name=name, creator=user)
|
||||
db.session.add(environment)
|
||||
db.session.commit()
|
||||
return environment
|
||||
|
||||
@classmethod
|
||||
def create_many(cls, application, names):
|
||||
def create_many(cls, user, application, names):
|
||||
environments = []
|
||||
for name in names:
|
||||
environment = Environments.create(application, name)
|
||||
environment = Environments.create(user, application, name)
|
||||
environments.append(environment)
|
||||
|
||||
db.session.add_all(environments)
|
||||
@@ -90,3 +93,48 @@ class Environments(object):
|
||||
# TODO: How do we work around environment deletion being a largely manual process in the CSPs
|
||||
|
||||
return environment
|
||||
|
||||
@classmethod
|
||||
def base_provision_query(cls, now):
|
||||
return (
|
||||
db.session.query(Environment)
|
||||
.join(Application)
|
||||
.join(Portfolio)
|
||||
.join(TaskOrder)
|
||||
.join(CLIN)
|
||||
.filter(CLIN.start_date <= now)
|
||||
.filter(CLIN.end_date > now)
|
||||
# select only these columns
|
||||
.options(load_only("id", "creator_id"))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_environments_pending_creation(cls, now) -> List[Environment]:
|
||||
"""
|
||||
Any environment with an active CLIN that doesn't yet have a `cloud_id`.
|
||||
"""
|
||||
return cls.base_provision_query(now).filter(Environment.cloud_id == None).all()
|
||||
|
||||
@classmethod
|
||||
def get_environments_pending_atat_user_creation(cls, now) -> List[Environment]:
|
||||
"""
|
||||
Any environment with an active CLIN that has a cloud_id but no `root_user_info`.
|
||||
"""
|
||||
return (
|
||||
cls.base_provision_query(now)
|
||||
.filter(Environment.cloud_id != None)
|
||||
.filter(Environment.root_user_info == text("'null'"))
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
def get_environments_pending_baseline_creation(cls, now) -> List[Environment]:
|
||||
"""
|
||||
Any environment with an active CLIN that has a `cloud_id` and `root_user_info`
|
||||
but no `baseline_info`.
|
||||
"""
|
||||
return (
|
||||
cls.base_provision_query(now)
|
||||
.filter(Environment.cloud_id != None)
|
||||
.filter(Environment.root_user_info != text("'null'"))
|
||||
.filter(Environment.baseline_info == text("'null'"))
|
||||
).all()
|
||||
|
||||
106
atst/jobs.py
106
atst/jobs.py
@@ -1,8 +1,12 @@
|
||||
from flask import current_app as app
|
||||
import pendulum
|
||||
|
||||
from atst.queue import celery
|
||||
from atst.database import db
|
||||
from atst.queue import celery
|
||||
from atst.models import EnvironmentJobFailure, EnvironmentRoleJobFailure
|
||||
from atst.domain.csp.cloud import CloudProviderInterface, GeneralCSPException
|
||||
from atst.domain.environments import Environments
|
||||
from atst.domain.users import Users
|
||||
|
||||
|
||||
class RecordEnvironmentFailure(celery.Task):
|
||||
@@ -38,3 +42,103 @@ def send_notification_mail(recipients, subject, body):
|
||||
)
|
||||
)
|
||||
app.mailer.send(recipients, subject, body)
|
||||
|
||||
|
||||
def do_create_environment(
|
||||
csp: CloudProviderInterface, environment_id=None, atat_user_id=None
|
||||
):
|
||||
environment = Environments.get(environment_id)
|
||||
|
||||
if environment.cloud_id is not None:
|
||||
# TODO: Return value for this?
|
||||
return
|
||||
|
||||
user = Users.get(atat_user_id)
|
||||
|
||||
# we'll need to do some checking in this job for cases where it's retrying
|
||||
# when a failure occured after some successful steps
|
||||
# (e.g. if environment.cloud_id is not None, then we can skip first step)
|
||||
|
||||
# credentials either from a given user or pulled from config?
|
||||
# if using global creds, do we need to log what user authorized action?
|
||||
atat_root_creds = csp.root_creds()
|
||||
|
||||
# user is needed because baseline root account in the environment will
|
||||
# be assigned to the requesting user, open question how to handle duplicate
|
||||
# email addresses across new environments
|
||||
csp_environment_id = csp.create_environment(atat_root_creds, user, environment)
|
||||
environment.cloud_id = csp_environment_id
|
||||
db.session.add(environment)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def do_create_atat_admin_user(csp: CloudProviderInterface, environment_id=None):
|
||||
environment = Environments.get(environment_id)
|
||||
atat_root_creds = csp.root_creds()
|
||||
|
||||
atat_remote_root_user = csp.create_atat_admin_user(
|
||||
atat_root_creds, environment.cloud_id
|
||||
)
|
||||
environment.root_user_info = atat_remote_root_user
|
||||
db.session.add(environment)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def do_create_environment_baseline(csp: CloudProviderInterface, environment_id=None):
|
||||
environment = Environments.get(environment_id)
|
||||
|
||||
# ASAP switch to use remote root user for provisioning
|
||||
atat_remote_root_creds = environment.root_user_info["credentials"]
|
||||
|
||||
baseline_info = csp.create_environment_baseline(
|
||||
atat_remote_root_creds, environment.cloud_id
|
||||
)
|
||||
environment.baseline_info = baseline_info
|
||||
db.session.add(environment)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def do_work(fn, task, csp, **kwargs):
|
||||
try:
|
||||
fn(csp, **kwargs)
|
||||
except GeneralCSPException as e:
|
||||
raise task.retry(exc=e)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def create_environment(self, environment_id=None, atat_user_id=None):
|
||||
do_work(do_create_environment, self, app.csp.cloud, **kwargs)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def create_atat_admin_user(self, environment_id=None):
|
||||
do_work(do_create_atat_admin_user, self, app.csp.cloud, **kwargs)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def create_environment_baseline(self, environment_id=None):
|
||||
do_work(do_create_environment_baseline, self, app.csp.cloud, **kwargs)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def dispatch_create_environment(self):
|
||||
for environment in Environments.get_environments_pending_creation(pendulum.now()):
|
||||
create_environment.delay(
|
||||
environment_id=environment.id, atat_user_id=environment.creator_id
|
||||
)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def dispatch_create_atat_admin_user(self):
|
||||
for environment in Environments.get_environments_pending_atat_user_creation(
|
||||
pendulum.now()
|
||||
):
|
||||
create_atat_admin_user.delay(environment_id=environment.id)
|
||||
|
||||
|
||||
@celery.task(bind=True)
|
||||
def dispatch_create_environment_baseline(self):
|
||||
for environment in Environments.get_environments_pending_baseline_creation(
|
||||
pendulum.now()
|
||||
):
|
||||
create_environment_baseline.delay(environment_id=environment.id)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from sqlalchemy import Column, ForeignKey, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from enum import Enum
|
||||
|
||||
from atst.models import Base
|
||||
from atst.models.types import Id
|
||||
@@ -17,10 +19,22 @@ class Environment(
|
||||
application_id = Column(ForeignKey("applications.id"), nullable=False)
|
||||
application = relationship("Application")
|
||||
|
||||
# User user.id as the foreign key here beacuse the Environment creator may
|
||||
# not have an application role. We may need to revisit this if we receive any
|
||||
# requirements around tracking an environment's custodian.
|
||||
creator_id = Column(ForeignKey("users.id"), nullable=False)
|
||||
creator = relationship("User")
|
||||
|
||||
cloud_id = Column(String)
|
||||
root_user_info = Column(JSONB)
|
||||
baseline_info = Column(JSONB)
|
||||
|
||||
job_failures = relationship("EnvironmentJobFailure")
|
||||
|
||||
class ProvisioningStatus(Enum):
|
||||
PENDING = "pending"
|
||||
COMPLETED = "completed"
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
return {r.application_role.user for r in self.roles}
|
||||
@@ -41,6 +55,17 @@ class Environment(
|
||||
def portfolio_id(self):
|
||||
return self.application.portfolio_id
|
||||
|
||||
@property
|
||||
def provisioning_status(self) -> ProvisioningStatus:
|
||||
if (
|
||||
self.cloud_id is None
|
||||
or self.root_user_info is None
|
||||
or self.baseline_info is None
|
||||
):
|
||||
return self.ProvisioningStatus.PENDING
|
||||
else:
|
||||
return self.ProvisioningStatus.COMPLETED
|
||||
|
||||
def __repr__(self):
|
||||
return "<Environment(name='{}', num_users='{}', application='{}', portfolio='{}', id='{}')>".format(
|
||||
self.name,
|
||||
|
||||
@@ -5,6 +5,7 @@ celery = Celery(__name__)
|
||||
|
||||
def update_celery(celery, app):
|
||||
celery.conf.update(app.config)
|
||||
celery.conf.CELERYBEAT_SCHEDULE = {}
|
||||
|
||||
class ContextTask(celery.Task):
|
||||
def __call__(self, *args, **kwargs):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import redirect, render_template, request as http_request, url_for
|
||||
from flask import redirect, render_template, request as http_request, url_for, g
|
||||
|
||||
from . import applications_bp
|
||||
from atst.domain.applications import Applications
|
||||
@@ -24,6 +24,7 @@ def create(portfolio_id):
|
||||
if form.validate():
|
||||
application_data = form.data
|
||||
Applications.create(
|
||||
g.current_user,
|
||||
portfolio,
|
||||
application_data["name"],
|
||||
application_data["description"],
|
||||
|
||||
@@ -227,7 +227,9 @@ def new_environment(application_id):
|
||||
env_form = EditEnvironmentForm(formdata=http_request.form)
|
||||
|
||||
if env_form.validate():
|
||||
Environments.create(application=application, name=env_form.name.data)
|
||||
Environments.create(
|
||||
g.current_user, application=application, name=env_form.name.data
|
||||
)
|
||||
|
||||
flash("environment_added", environment_name=env_form.data["name"])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user