Skip to content

Commit

Permalink
Refactor installer [2/3] -- use base module
Browse files Browse the repository at this point in the history
* removed all classes and logic from the deployables and parameters
  modules that are provided through the base module
* import from base module
  • Loading branch information
martin-bts committed Oct 10, 2019
1 parent d1597db commit cd6dca4
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 426 deletions.
3 changes: 1 addition & 2 deletions askbot/deployment/deployables/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
did up to the error is undone. Yet, this code has no means to undo anything.
"""

from askbot.deployment.common.base import AskbotDeploymentError
from .objects import RenderedFile, CopiedFile, EmptyFile, Directory, LinkedDir
from .components import AskbotApp, AskbotSite, ProjectRoot


__all__ = ['RenderedFile', 'CopiedFile', 'EmptyFile', 'Directory', 'LinkedDir',
'AskbotApp', 'AskbotSite', 'ProjectRoot', 'AskbotDeploymentError']
'AskbotApp', 'AskbotSite', 'ProjectRoot']
78 changes: 14 additions & 64 deletions askbot/deployment/deployables/components.py
Original file line number Diff line number Diff line change
@@ -1,71 +1,21 @@
from askbot.deployment.deployables.objects import *
from askbot.deployment.common.base import ObjectWithOutput
import os

class DeployableComponent(ObjectWithOutput):
"""These constitute sensible deployment chunks of Askbot. For instance,
one would never deploy just a settings.py, because Django projects need
additional files as well."""
default_name = 'unset'
contents = dict()
from .objects import *
from askbot.deployment.base import DeployableComponent

def __init__(self, name=None, contents=None):
super(DeployableComponent, self).__init__(verbosity=2)
name = name if name is not None else self.__class__.default_name
contents = contents if contents is not None else self.__class__.contents
"""The classes defined herein are what we want to do when we talk about
deploying askbot.
self.name = name
self.contents = contents
self._src_dir = None
self._dst_dir = None
self.context = dict()
self.skip_silently = list()
self.forced_overwrite = list()
We write dictionaries, mostly named "contents" where we specify file/directory
names as keys and put the deployment object type as the value. The deployment
object type is basically how a particular filesystem object is to be created.
Nested dictionaries will result in a nested directory structure.
def _grow_deployment_tree(self, component):
todo = list()
for name, deployable in component.items():
if isinstance(deployable,dict):
branch = self._grow_deployment_tree(deployable)
todo.append(
Directory(name, None, *branch)
)
else:
todo.append(deployable(name))
return todo

def _root_deployment_tree(self, tree):
root = Directory(self.name, None, *tree)
root.src_path = self.src_dir
root.dst_path = self.dst_dir
root.context = self.context
root.forced_overwrite = self.forced_overwrite
root.skip_silently = self.skip_silently
root.verbosity = self.verbosity
return root

@property
def src_dir(self):
return self._src_dir

@src_dir.setter
def src_dir(self, value):
self._src_dir = value

@property
def dst_dir(self):
return self._dst_dir

@dst_dir.setter
def dst_dir(self, value):
self._dst_dir = value

def deploy(self):
"""Recursively deploy all DeployItems we know about. This method is
meant to be overwritten by subclasses to provide more fine grained
configurations to individual branches and/or nodes."""
tree = self._grow_deployment_tree(self.contents)
root = self._root_deployment_tree(tree)
root.deploy()
Splitting the deployment into these three DeployableComponents is completely
arbitrary and seemed reasonable at the time. The intention was to keep
deployment details away from the main installer. However, the installer can
(and does) change the components before deploying them.
"""

class AskbotSite(DeployableComponent):
default_name = 'askbot_site'
Expand Down
229 changes: 16 additions & 213 deletions askbot/deployment/deployables/objects.py
Original file line number Diff line number Diff line change
@@ -1,231 +1,34 @@
import os.path
import shutil
from askbot.deployment.messages import print_message
from askbot.deployment.template_loader import DeploymentTemplate
from askbot.deployment.common.base import AskbotDeploymentError, ObjectWithOutput
from askbot.deployment.base import DeployableFile, DeployableDir

class DeployObject(ObjectWithOutput):
"""Base class for filesystem objects, i.e. files and directories, that can
be/are deployed using askbot-setup."""
def __init__(self, name, src_path=None, dst_path=None):
super(DeployObject, self).__init__(verbosity=2)
self.name = name
self._src_path = src_path
self._dst_path = dst_path

@property
def src_path(self):
return self._src_path

@src_path.setter
def src_path(self, value):
self._src_path = value

@property
def dst_path(self):
return self._dst_path

@dst_path.setter
def dst_path(self, value):
self._dst_path = value

@property
def src(self):
return os.path.join(self.src_path, self.name)

@property
def dst(self):
return os.path.join(self.dst_path, self.name)

def _deploy_now(self):
"""virtual protected"""

def deploy(self):
"""The main method of this class. DeployableComponents call this method
to have this object do the filesystem operations which deploys
whatever this class represents."""
self.print(f'* {self.dst} from {self.src}')
try:
self._deploy_now()
except AskbotDeploymentError as e:
self.print(e)

class DeployFile(DeployObject):
"""This class collects all logic w.r.t. a single files. It has to be
subclassed and the subclasses must/should overwrite _deploy_now to call
one of the methods defined in this class. At the time of this writing, this
is either _render_with_jinja2 or _copy.
The subclasses may then be used to deploy a single file.
class RenderedFile(DeployableFile):
"""Render a file using Jinja2.
This class expects the source file to have a .jinja2-suffix and uses a
common render method.
"""
def __init__(self, name, src_path=None, dst_path=None):
super(DeployFile, self).__init__(name, src_path, dst_path)
self.context = dict()
self.skip_silently = list()
self.forced_overwrite = list()

# the different approaches for force and skip feel a little odd.
def __validate(self):
matches = [self.dst for c in self.forced_overwrite
if self.dst.endswith(f'{os.path.sep}{c}')]
exists = os.path.exists(self.dst)
force = len(matches) > 0
skip = self.dst.split(os.path.sep)[-1] in self.skip_silently
return exists, force, skip

def _render_with_jinja2(self, context):
exists, force, skip = self.__validate()
if exists:
raise AskbotDeploymentError(f' You already have a file "{self.dst}" please merge the contents.')
template = DeploymentTemplate('dummy.name') # we use this a little differently than originally intended
template.tmpl_path = self.src
content = template.render(context) # do not put this in the context. If this statement fails, we do not want an empty self.dst to be created!
with open(self.dst, 'w+') as output_file:
output_file.write(content)

def _copy(self):
exists, force, skip = self.__validate()
if exists:
if skip: # this makes skip more important than force. This is good, because when in doubt, we rather leave the current installation the way it is. This is bad because we cannot explicitly force an overwrite.
return
elif force:
self.print(' ^^^ forced overwrite!')
else:
raise AskbotDeploymentError(f' You already have a file "{self.dst}", please add contents of {self.src}.')
shutil.copy(self.src, self.dst)

#####################
## from path_utils ##
#####################
def _touch(self, times=None):
"""implementation of unix ``touch`` in python"""
#http://stackoverflow.com/questions/1158076/implement-touch-using-python
try:
os.utime(self.dst, times)
except:
open(self.dst, 'a').close()


class DeployDir(DeployObject):
def __init__(self, name, parent=None, *content):
super(DeployDir, self).__init__(name)
self.content = list(content)
if parent is not None:
self.dst_path = self.__clean_directory(parent)

@DeployObject.verbosity.setter
def verbosity(self, value):
self._verbosity = value
for child in self.content:
child.verbosity = value

@DeployObject.src_path.setter
def src_path(self, value):
value = self.__clean_directory(value)
self._src_path = value
for child in self.content:
child.src_path = value

@DeployObject.dst_path.setter
def dst_path(self, value):
value = self.__clean_directory(value)
self._dst_path = value
for child in self.content:
child.dst_path = self.dst

@property
def context(self):
raise AttributeError(f'{self.__class__} does not store context information')

@property
def skip_silently(self):
raise AttributeError(f'{self.__class__} does not store file skip information')

@property
def forced_overwrite(self):
raise AttributeError(f'{self.__class__} does not store file overwrite information')

# maybe deepcopy() in the following three setters. maybe there's an advantage in not deepcopy()ing here. time will tell.
@context.setter
def context(self, value):
for child in self.content: # in theory, we only want this for directories and rendered files.
child.context = value

@skip_silently.setter
def skip_silently(self, value):
for child in self.content: # in practice, we only need this for directories and copied files.
child.skip_silently = value

@forced_overwrite.setter
def forced_overwrite(self, value):
for child in self.content: # in practice, we only need this for directories and copied files.
child.forced_overwrite = value

def _link_dir(self):
"""Derived from __create_path()"""
if os.path.isdir(self.dst):
return
elif os.path.exists(self.dst):
raise AskbotDeploymentError('expect directory or a non-existing path')
else:
os.symlink(self.src, self.dst)

#####################
## from path_utils ##
#####################
def __create_path(self):
"""equivalent to mkdir -p"""
if os.path.isdir(self.dst):
return
elif os.path.exists(self.dst):
raise AskbotDeploymentError('expect directory or a non-existing path')
else:
os.makedirs(self.dst)

@staticmethod
def __clean_directory(directory):
"""Returns normalized absolute path to the directory
regardless of whether it exists or not
or ``None`` - if the path is a file or if ``directory``
parameter is ``None``"""
if directory is None:
return None

directory = os.path.normpath(directory)
directory = os.path.abspath(directory)

#if os.path.isfile(directory):
# print(messages.CANT_INSTALL_INTO_FILE % {'path': directory})
# return None
return directory

def _deploy_now(self):
self.__create_path()

def deploy(self):
self._deploy_now() # we don't try-except this, b/c input validation is supposed to identify problems before this code runs. If an exception is raised here, it's an issue that must be handled elsewhere.
for node in self.content:
node.deploy()

class RenderedFile(DeployFile):
def _deploy_now(self):
self._render_with_jinja2(self.context)

@property
def src(self):
return f'{super().src}.jinja2'

class CopiedFile(DeployFile):
class CopiedFile(DeployableFile):
"""Copy a file from the source directory into the target directory."""
def _deploy_now(self):
self._copy()

class EmptyFile(DeployFile):
class EmptyFile(DeployableFile):
"""Create an empty file."""
def _deploy_now(self):
self._touch()

class Directory(DeployDir):
pass
# Changing this file will not(!) alter the behaviour of deployable components,
# as it uses the base class DeployableDir. We only have the Directory for
# completeness sake.
class Directory(DeployableDir):
"""Create an empty directory."""

class LinkedDir(DeployDir):
class LinkedDir(DeployableDir):
"""Create a symbolic link to an existing directory."""
def _deploy_now(self):
self._link_dir()
44 changes: 9 additions & 35 deletions askbot/deployment/parameters/cache.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,5 @@
from askbot.utils import console
from askbot.deployment.parameters.base import ConfigField, ConfigManager

class CacheConfigManager(ConfigManager):
"""A config manager for validating setup parameters pertaining to
the cache Askbot will use."""
def __init__(self, interactive=True, verbosity=1):
super(CacheConfigManager, self).__init__(interactive=interactive, verbosity=verbosity)
db = ConfigField(
defaultOk = True,
user_prompt = 'Please enter the cache database name to use',
)
password = ConfigField(
defaultOk = True,
user_prompt = 'Please enter the shared secret for accessing the cache',
)
self.register('cache_engine', CacheEngine())
self.register('cache_nodes', CacheNodes())
self.register('cache_db', db)
self.register('cache_password', password)

def _order(self, keys):
full_set = [ 'cache_engine', 'cache_nodes', 'cache_db',
'cache_password' ]
return [ item for item in full_set if item in keys ]

def _remember(self, name, value):
if name == 'cache_engine':
value = int(value)
super(CacheConfigManager, self)._remember(name, value)
if name == 'cache_engine':
if value == 3:
self._catalog['cache_nodes'].defaultOk = True
elif value == 2:
self._catalog['cache_db'].defaultOk = False
self._catalog['cache_password'].defaultOk = False
from askbot.deployment.base import ConfigField

class CacheEngine(ConfigField):
defaultOk = False
Expand Down Expand Up @@ -76,3 +42,11 @@ class CacheNodes(ConfigField):
def ask_user(self, current):
value = super(CacheNodes, self).ask_user(current)
return [ value ]

class CacheDb(ConfigField):
defaultOk = True
user_prompt = 'Please enter the cache database name to use'

class CachePass(ConfigField):
defaultOk=True
user_prompt='Please enter the shared secret for accessing the cache'
Loading

0 comments on commit cd6dca4

Please sign in to comment.