Merge pull request #1062 from dod-ccpo/env-provisioning-task

Environment provisioning background jobs
This commit is contained in:
richard-dds
2019-09-16 09:58:18 -04:00
committed by GitHub
22 changed files with 613 additions and 31 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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,

View File

@@ -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):

View File

@@ -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"],

View File

@@ -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"])