Skip to content
This repository has been archived by the owner on Aug 22, 2022. It is now read-only.

Commit

Permalink
Split Instance from AppServer, split out Open edX specific parts
Browse files Browse the repository at this point in the history
  • Loading branch information
bradenmacdonald committed Apr 29, 2016
1 parent f2e1eb6 commit 975e092
Show file tree
Hide file tree
Showing 12 changed files with 1,030 additions and 678 deletions.
54 changes: 46 additions & 8 deletions instance/logger_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,50 @@

import logging

# Helper methods ##############################################################


def format_instance(instance):
""" Given any concrete subclass of Instance, get a short ID string to put into the log """
if instance:
return 'instance={} ({!s:.15})'.format(instance.ref.pk, instance.ref.name)
return 'Unknown Instance'


def format_appserver(app_server):
""" Given an AppServer subclass, get a short ID string to put into the log """
if app_server:
return 'app_server={} ({!s:.15})'.format(app_server.pk, app_server.name)
return 'Unknown AppServer'


def format_server(server):
""" Given a Server subclass, get a short ID string to put into the log """
if server:
return 'server={!s:.20}'.format(server.name)
return 'Unknown Server'

# Adapters ####################################################################

class InstanceLoggerAdapter(logging.LoggerAdapter):

class AppServerLoggerAdapter(logging.LoggerAdapter):
"""
Custom LoggerAdapter for Instance objects
Include the instance name in the output
"""
def process(self, msg, kwargs):
msg, kwargs = super().process(msg, kwargs)

instance = self.extra['obj']
if instance.sub_domain:
return 'instance={} | {}'.format(instance.sub_domain, msg), kwargs
app_server = self.extra['obj']
if app_server.sub_domain:
return 'app_server={} | {}'.format(app_server.name, msg), kwargs
else:
return msg, kwargs

app_server = self.extra['obj']
msg = '{},{} | {}'.format(format_instance(app_server.instance), format_appserver(app_server), msg)
return msg, kwargs


class ServerLoggerAdapter(logging.LoggerAdapter):
"""
Expand All @@ -51,7 +78,18 @@ def process(self, msg, kwargs):
msg, kwargs = super().process(msg, kwargs)

server = self.extra['obj']
if server.instance and server.instance.sub_domain:
return 'instance={!s:.15},server={!s:.8} | {}'.format(server.instance.sub_domain, server, msg), kwargs
else:
return msg, kwargs
msg = '{} | {}'.format(format_server(server), msg)
return msg, kwargs


class InstanceLoggerAdapter(logging.LoggerAdapter):
"""
Custom LoggerAdapter for Instance objects
Include the InstanceReference ID in the output
"""
def process(self, msg, kwargs):
msg, kwargs = super().process(msg, kwargs)

instance = self.extra['obj']
msg = '{} | {}'.format(format_instance(instance), msg)
return msg, kwargs
254 changes: 254 additions & 0 deletions instance/models/appserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# -*- coding: utf-8 -*-
#
# OpenCraft -- tools to aid developing and hosting free software projects
# Copyright (C) 2015 OpenCraft <xavier@opencraft.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Instance app models - App Server
"""

# Imports #####################################################################

import logging

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import Q
from django_extensions.db.models import TimeStampedModel

from instance.logger_adapter import AppServerLoggerAdapter
from .instance import InstanceReference
from .log_entry import LogEntry
from .server import OpenStackServer
from .utils import ModelResourceStateDescriptor, ResourceState, ValidateModelMixin


# Logging #####################################################################

logger = logging.getLogger(__name__)


# States ######################################################################

class AppServerState(ResourceState):
"""
A [finite state machine] state describing an instance.
"""
# A state is "steady" if we don't expect it to change.
# This information can be used to:
# - delay execution of an operation until the target server reaches a steady state
# - raise an exception when trying to schedule an operation that depends on a state change
# while the target server is in a steady state
# Steady states include:
# - Status.New
# - Status.Running
# - Status.ConfigurationFailed
# - Status.Error
# - Status.Terminated
is_steady_state = True

# An instance is healthy if it is part of a normal (expected) workflow.
# This information can be used to detect problems and highlight them in the UI or notify users.
# Healthy states include:
# - Status.New
# - Status.WaitingForServer
# - Status.ConfiguringServer
# - Status.Running
# - Status.Terminated
is_healthy_state = True


class Status(ResourceState.Enum):
"""
The states that an instance can be in.
"""

class New(AppServerState):
""" Newly created """
state_id = 'new'

class WaitingForServer(AppServerState):
""" Server not yet accessible """
state_id = 'waiting'
name = 'Waiting for server'
is_steady_state = False

class ConfiguringServer(AppServerState):
""" Running Ansible playbooks on server """
state_id = 'configuring'
name = 'Configuring server'
is_steady_state = False

class Running(AppServerState):
""" Instance is up and running """
state_id = 'running'

class ConfigurationFailed(AppServerState):
""" Instance was not configured successfully (but may be partially online) """
state_id = 'failed'
name = 'Configuration failed'
is_healthy_state = False

class Error(AppServerState):
""" Instance never got up and running (something went wrong when trying to build new VM) """
state_id = 'error'
is_healthy_state = False

class Terminated(AppServerState):
""" Instance was running successfully and has been shut down """
state_id = 'terminated'


# Models ######################################################################


class AppServer(ValidateModelMixin, TimeStampedModel):
"""
AppServer - One or more distinct web applications running on a single VM.
Owned by an Instance.
Characteristics of an AppServer:
* An AppServer instance's configuration fields are *immutable*. If you want to change
configuration, change hte Instance's configuration and have it create a new AppServer.
* An AppServer owns exactly one VM (Server), onto which it installs its applications.
"""
Status = Status
status = ModelResourceStateDescriptor(
state_classes=Status.states, default_state=Status.New, model_field_name='_status'
)
_status = models.CharField(
max_length=20,
default=status.default_state_class.state_id,
choices=status.model_field_choices,
db_index=True,
db_column='status',
)
# State transitions:
_status_to_waiting_for_server = status.transition(
from_states=Status.New, to_state=Status.WaitingForServer
)
_status_to_configuring_server = status.transition(
from_states=Status.WaitingForServer, to_state=Status.ConfiguringServer
)
_status_to_error = status.transition(
from_states=Status.WaitingForServer, to_state=Status.Error
)
_status_to_running = status.transition(
from_states=Status.ConfiguringServer, to_state=Status.Running
)
_status_to_configuration_failed = status.transition(
from_states=Status.ConfiguringServer, to_state=Status.ConfigurationFailed
)
_status_to_terminated = status.transition(
from_states=Status.Running, to_state=Status.Terminated
)

name = models.CharField(max_length=250, blank=False)
server = models.OneToOneField(OpenStackServer, on_delete=models.CASCADE, related_name='+')
# The Instance that owns this. Instance will get related_name accessors like 'openedxappserver_set'
owner = models.ForeignKey(InstanceReference, on_delete=models.CASCADE, related_name='%(class)s_set')

class Meta:
abstract = True

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.logger = AppServerLoggerAdapter(logger, {'obj': self})

def __str__(self):
return self.name

@property
def instance(self):
"""
Get the Instance that owns this AppServer
"""
return self.owner.instance

@property
def event_context(self):
"""
Context dictionary to include in events
"""
context = self.instance.event_context # dict with instance_id
context.update({'appserver_id': self.pk, 'appserver_type': self.__class__.__name__})
return context

def set_field_defaults(self):
"""
Set default values.
We don't use django field 'defaults=callable' because changes to the field defaults
affect automatically generated migrations, generating a new one when settings don't match
This method is called by clean_fields() before validation and save()
"""
pass

def clean_fields(self, exclude=None):
"""
Set default values before validation.
"""
if not self.pk:
self.set_field_defaults()
super().clean_fields(exclude=exclude)

def save(self, **kwargs):
if self.pk:
# We are changing an existing instance. But most AppServer fields are meant to be
# immutable. Only 'status' can change.
if not set(kwargs.get('update_fields', [])) < set(['_status', 'modified']):
raise RuntimeError("Error: Attempted to modify an AppServer instance. AppServers are immutable.")
super().save(**kwargs)

def _get_log_entries(self, level_list=None, limit=None):
"""
Return the list of log entry instances for this AppServer and the server it manages,
optionally filtering by logging level. If a limit is given, only the latest records are
returned.
Returns oldest entries first.
"""
# TODO: Filter out log entries for which the user doesn't have view rights
appserver_type = ContentType.objects.get_for_model(self)
server_type = ContentType.objects.get_for_model(self.server)
entries = LogEntry.objects.filter(
(Q(content_type=appserver_type) & Q(object_id=self.pk)) |
(Q(content_type=server_type) & Q(object_id=self.server_id))
)
if level_list:
entries = entries.filter(level__in=level_list)
if limit:
return reversed(list(entries[:limit]))
return entries.order_by('created')

@property
def log_entries(self):
"""
Return the list of log entry instances for this AppServer and the server it manages
"""
return self._get_log_entries(limit=settings.LOG_LIMIT)

@property
def log_error_entries(self):
"""
Return the list of error or critical log entry instances for this AppServer and the
server it manages
"""
return self._get_log_entries(level_list=['ERROR', 'CRITICAL'])
Loading

0 comments on commit 975e092

Please sign in to comment.