backend/bms_app/services/control_node.py (134 lines of code) (raw):

# Copyright 2022 Google LLC # # 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. import logging from abc import ABC, abstractmethod from flask import render_template from bms_app import settings from bms_app.services.gce import create_instance, get_zone logger = logging.getLogger(__name__) CREATE_INSTANCE_LOG = 'Starting GCE name: %s, zone: %s, project: %s, ' \ 'vpc: %s, subnet: %s, service_account: %s' class BaseCNService(ABC): """Base class for launching Control Node.""" OPERATION_TYPE = None @classmethod def run(cls, project, operation, gcs_config_dir, **context): """Start GCE control node.""" zone = get_zone(settings.GCP_PROJECT_NAME, project.subnet) name = cls._generate_name(operation, context) raw_startup_script = cls._generate_startup_script( operation, gcs_config_dir, context ) logger.debug( CREATE_INSTANCE_LOG, name, zone, settings.GCP_PROJECT_NAME, project.vpc, project.subnet, settings.GCP_SERVICE_ACCOUNT ) machine_type = cls._get_machine_type(context) create_instance( project=settings.GCP_PROJECT_NAME, zone=zone, vpc=project.vpc, subnet=project.subnet, name=name, startup_script=raw_startup_script, service_account=settings.GCP_SERVICE_ACCOUNT, machine_type=machine_type, ) @staticmethod @abstractmethod def _generate_name(operation, context): """Return GCE control node's name.""" pass @classmethod def _generate_startup_script(cls, operation, gcs_config_dir, context): """Return GCE control node's startup script.""" script_context = { 'operation_id': operation.id, 'bucket_name': settings.GCS_BUCKET, 'gcs_config_dir': gcs_config_dir, 'operation_type': cls.OPERATION_TYPE, 'gcp_project_name': settings.GCP_PROJECT_NAME, 'pubsub_topic': settings.GCP_PUBSUB_TOPIC, } extra_script_context = cls._get_extra_startup_script_context( operation, gcs_config_dir, context ) script_context.update(extra_script_context) raw_startup_script = render_template( 'runner.sh.j2', **script_context ) return raw_startup_script @staticmethod @abstractmethod def _get_extra_startup_script_context(operation, gcs_config_dir, context): """Return additional data to render startup srcipt.""" return {} @staticmethod def _get_machine_type(context): return 'e2-medium' class WaveBaseCNService(BaseCNService): """Base class for Deployment and Rollback operations.""" OPERATION_TYPE = None @staticmethod def _generate_name(operation, context): wave = context['wave'] return f'bms-app-control-node-{wave.id}-{operation.id}' @staticmethod def _get_extra_startup_script_context(operation, gcs_config_dir, context): return {'wave_id': context['wave'].id} class DeployControlNodeService(WaveBaseCNService): OPERATION_TYPE = 'DEPLOYMENT' @staticmethod def _get_machine_type(context): """Return machine type based on number of targets. Up to 25 tagrets - e2-standard-4 25-75 targets - e2-standard-8 75-100 targets - e2-standard-16 """ total_targets = context['total_targets'] if total_targets < 25: machine_type = 'e2-standard-4' elif total_targets < 75: machine_type = 'e2-standard-8' else: machine_type = 'e2-standard-16' return machine_type class RollbackConrolNodeService(WaveBaseCNService): OPERATION_TYPE = 'ROLLBACK' class PreRestoreControlNodeService(BaseCNService): OPERATION_TYPE = 'PRE_RESTORE' @staticmethod def _generate_name(operation, context): return f'bms-app-control-node-pre-{operation.id}' @staticmethod def _get_extra_startup_script_context(operation, gcs_config_dir, context): source_db = context['source_db'] return { 'wave_id': 0, # there is no 'wave' for restore operations 'backup_type': source_db.restore_config.backup_type.value } class RestoreControlNodeService(BaseCNService): OPERATION_TYPE = 'RESTORE' @staticmethod def _generate_name(operation, context): return f'bms-app-control-node-dt-{operation.id}' @staticmethod def _get_extra_startup_script_context(operation, gcs_config_dir, context): source_db = context['source_db'] return { 'wave_id': 0, # there is no 'wave' for restore operations 'backup_type': source_db.restore_config.backup_type.value } class RollbackRestoreControlNodeService(BaseCNService): OPERATION_TYPE = 'ROLLBACK_RESTORE' @staticmethod def _generate_name(operation, context): return f'bms-app-control-node-dt-rollback-{operation.id}' @staticmethod def _get_extra_startup_script_context(operation, gcs_config_dir, context): return { 'wave_id': 0, # there is no 'wave' for restore operations } class FailOverControlNodeService(BaseCNService): OPERATION_TYPE = 'DB_FAILOVER' @staticmethod def _generate_name(operation, context): return f'bms-app-control-node-dt-failover-{operation.id}' @staticmethod def _get_extra_startup_script_context(operation, gcs_config_dir, context): return { 'wave_id': 0, # there is no 'wave' for restore operations }