Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New installer 2019 #849

Closed
wants to merge 46 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
984200b
2 Bugs I found in the current installer
martin-bts Jul 22, 2019
0184961
Don't put a default for dj_database_url!
martin-bts Jul 22, 2019
be49e20
Validating setup parameters with ConfigManagers and ConfigFields
martin-bts Jul 22, 2019
5b5f16a
Code refinement and testcases
martin-bts Jul 24, 2019
0904b83
Fixed types and added ConfigManagerCollection
martin-bts Jul 28, 2019
8258e7b
Bugfix for the old installer
martin-bts Jul 28, 2019
01c0a2a
Shuffled existing argparser arguments and added 3
martin-bts Jul 28, 2019
ccfde5c
Add force attribute to base class
martin-bts Jul 29, 2019
33018aa
Fixed int conversion of *_engine paramters
martin-bts Jul 29, 2019
63712ed
Fix docstrings to allign code description with code
martin-bts Jul 29, 2019
19ab8fe
Introduced FilesystemConfigManager
martin-bts Jul 29, 2019
76adba6
Add --app-name as parameter to overwrite 'askbot_app'
martin-bts Jul 30, 2019
1256931
Add convenience function to access registered handlers
martin-bts Jul 30, 2019
f2e433e
Improved FilesystemConfigManager (WiP)
martin-bts Jul 30, 2019
fa35388
Added tests for FilesystemConfigManager
martin-bts Jul 30, 2019
b04a8c3
Use new validors in dry runs
martin-bts Jul 30, 2019
77a3b98
Fix parser args definition indent and define type=int where appropriate
martin-bts Jul 31, 2019
becede8
AskbotSetup computes parameter-choices form ConfigManagers
martin-bts Jul 31, 2019
16ede42
Switch installer to using only ConfigManagers
martin-bts Aug 1, 2019
eafd10a
Added a few shy test for the Askbot installer
martin-bts Aug 2, 2019
a46e4ed
Added deployment unit test for the Askbot installer
martin-bts Aug 2, 2019
32f3796
Use the app-name parameter, make --force defunct and housekeeping
martin-bts Aug 2, 2019
3f01102
Extended tox ini to have PGSQL and SQLITE based deployment tests
martin-bts Aug 5, 2019
528ec75
Commited WiP files for publication
martin-bts Aug 10, 2019
fd6e7d9
created directory structure for deployables
martin-bts Aug 13, 2019
0999094
Added unit tests for deployables.objects and made them work
martin-bts Aug 14, 2019
a93715e
Added unittests for DeployableComponents
martin-bts Aug 14, 2019
a37d07e
Refined deployables unittests
martin-bts Aug 14, 2019
726dc7c
Integrated deployables into installer (BROKEN!)
martin-bts Aug 14, 2019
46d2829
Modified settings.py.jinja2 to reflect the most recent change in dire…
martin-bts Aug 14, 2019
47afce7
Adopted ObjectWithOutput from structure_installer_using_classes
martin-bts Aug 15, 2019
280a438
Added implementation for LinkedDir; refined unittests
martin-bts Aug 15, 2019
a7ccb60
Implemented ProjectRoot.deploy to mimic the actual/current installer
martin-bts Aug 15, 2019
cef90cc
Fixed indent error
martin-bts Aug 15, 2019
52e51ed
Merge branch 'deployment_scenarios' into new_installer_2019
martin-bts Aug 15, 2019
45a8d00
Deleted unused files
martin-bts Aug 15, 2019
1b96f5e
Common ObjectwithOutput and deploy container files
martin-bts Oct 3, 2019
8c59c6a
setup files for container installations
martin-bts Oct 3, 2019
5bed3d9
Moved hardcoded value from installer into template
martin-bts Oct 3, 2019
3ddbd71
New merged installer
martin-bts Oct 3, 2019
fded777
Fix tests and manaagement commands
martin-bts Oct 3, 2019
e5f3b94
adapt tox.ini
martin-bts Oct 3, 2019
d1597db
Refactor installer [1/3] -- base module
martin-bts Oct 10, 2019
cd6dca4
Refactor installer [2/3] -- use base module
martin-bts Oct 10, 2019
d967fa5
Refactor installer [3/3] -- hardcode default
martin-bts Oct 10, 2019
5cbf4e7
Lost patches and a bugfix
martin-bts Nov 16, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
808 changes: 279 additions & 529 deletions askbot/deployment/__init__.py

Large diffs are not rendered by default.

18 changes: 0 additions & 18 deletions askbot/deployment/assertions.py

This file was deleted.

14 changes: 14 additions & 0 deletions askbot/deployment/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from .objectwithoutput import ObjectWithOutput
from .deployablecomponent import DeployableComponent
from .deployableobject import DeployableObject, DeployableFile, DeployableDir
from .configfield import ConfigField
from .configmanager import ConfigManager, ConfigManagerCollection

from askbot.deployment.base import exceptions



__all__ = ['exceptions', 'DeployableComponent', 'ObjectWithOutput',
'DeployableObject', 'DeployableFile', 'DeployableDir',
'ConfigField', 'ConfigManager', 'ConfigManagerCollection',
]
33 changes: 33 additions & 0 deletions askbot/deployment/base/configfield.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from .objectwithoutput import ObjectWithOutput
from askbot.utils import console

class ConfigField(ObjectWithOutput):
defaultOk = True
default = None
user_prompt = 'Please enter something'

def __init__(self, defaultOk=None, default=None, user_prompt=None, verbosity=1):
super(ConfigField, self).__init__(verbosity=verbosity)
self.defaultOk = self.__class__.defaultOk if defaultOk is None else defaultOk
self.default = self.__class__.default if default is None else default
self.user_prompt = self.__class__.user_prompt if user_prompt is None else user_prompt

def acceptable(self, value):
"""High level sanity check for a specific value. This method is called
for an installation parameter with the value provided by the user, or
the default value, if the user didn't provide any value. There must be
a boolean response, if the installation can proceed with :var:value as
the setting for this ConfigField."""
#self.print(f'This is {cls.__name__}.acceptable({value}) {cls.defaultOk}', 2)
if value is None and self.default is None or value == self.default:
return self.defaultOk
return True

def ask_user(self, current):
"""Prompt the user to provide a value for this installation
parameter."""
user_prompt = self.user_prompt
if self.defaultOk is True:
user_prompt += ' (Just press ENTER, to use the current '\
+ f'value "{current}")'
return console.simple_dialog(user_prompt)
173 changes: 173 additions & 0 deletions askbot/deployment/base/configmanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from .objectwithoutput import ObjectWithOutput

class ConfigManager(ObjectWithOutput):
"""ConfigManagers are used to ensure the installation can proceed.

Each ConfigManager looks at some installation parameters, usually
grouped by the aspect of Askbot they configure. For instance there is
a ConfigManager for the database backend and another one for the cache
backend. The task of a ConfigManager is to ensure that the combination
of install parameters it analyses is sensible for an Askbot installation.

The installer calls a ConfigManager's complete() method and passes it
its entire collection of installation parameters, a dictionary.
A ConfigManager only looks at those installation parameters which have
been register()-ed with the ConfigManager.

For each installation parameter there is registered class, derived from
ConfigField, which can determine if a parameter's value is acceptable and
ask the user to provide a value for the parameter.

The ConfigManager knows in which order to process its registered
installation parameters and contains all the logic determining if a user
should be asked for a(nother) value. It also remembers all values it
accepts and in the process may also modify the behaviour of the
ConfigField classes, to fit with the previously accepted values. For
instance, usually the DbConfigManager insists on credentials for accessing
the database, but if a user selects SQLite as database backend, the
DbConfigManager will *NOT* insist on credentials for accessing the
database, because SQLite does not need authentication.
"""
strings = {
'eNoValue': 'You must specify a value for "{name}"!',
}

def __init__(self, interactive=True, verbosity=1, force=False):
self._interactive = interactive
self._catalog = dict() # we use this to hold the ConfigFields
self.keys = set() # we use this for scoping and consistency
self._ordered_keys = list() # we use this for ordering our keys
self._managed_config = dict() # we use this as regestry for completed work
super(ConfigManager, self).__init__(verbosity=verbosity, force=force)
self.interactive = interactive

@property
def interactive(self):
return self._interactive

@interactive.setter
def interactive(self, interactive):
self._interactive = interactive
for name, handler in self._catalog.items():
if hasattr(handler,'interactive'):
handler.interactive = interactive

@property
def force(self):
return self._force

@force.setter
def force(self, force):
self._force = force
for name, handler in self._catalog.items():
if hasattr(handler,'force'):
handler.force = force

@ObjectWithOutput.verbosity.setter
def verbosity(self, verbosity):
self._verbosity = verbosity
for name, handler in self._catalog.items():
if hasattr(handler, 'verbosity'):
handler.verbosity = verbosity

def register(self, name, handler):
"""Add the ability to handle a specific install parameter.
Parameters:
- name: the install parameter to handle
- handler: the class to handle the parameter"""
handler.verbosity = self.verbosity
self._catalog[name] = handler
self.keys.update({name})
self._ordered_keys.append(name)


def configField(self, name):
if name not in self.keys:
raise KeyError(f'{self.__class__.__name__}: No handler for {name} registered.')
return self._catalog[name]

def _remember(self, name, value):
"""With this method, instances remember the accepted piece of
information for a given name. Making this a method allows derived
classes to perform additional work on accepting a piece of
information."""
self._managed_config.setdefault(name, value)

def _complete(self, name, current_value):
"""The generic procedure to ensure an installation parameter is
sensible and bug the user until a sensible value is provided.

If this is not an interactive installation, a not acceptable() value
raises a ValueError"""
if name not in self.keys:
raise KeyError

configField = self._catalog[name]

while not configField.acceptable(current_value):
self.print(f'Current value {current_value} not acceptable!', 2)
if not self.interactive:
raise ValueError(self.strings['eNoValue'].format(name=name))
current_value = configField.ask_user(current_value)

# remember the piece of information we just determined acceptable()
self._remember(name, current_value)
return current_value

def _order(self, keys):
"""Gives implementations control over the order in which they process
installation parameters. A ConfigManager should restrict itself to
the ConfigFields it knows about and ensure each field is only
consulted once."""
ordered_keys = []
known_fields = list(self.keys & set(keys)) # only handle keys we know
for f in self._ordered_keys: # use this order
if f in known_fields: # only fields the caller wants sorted
ordered_keys.append(f)
known_fields.remove(f) # avoid duplicates
return ordered_keys

def complete(self, collection):
"""Main method of this :class:ConfigManager.
Consumers use this method to ensure their data in :dict:collection is
sensible for installing Askbot.
"""
contribution = dict()
keys = self.keys & set(collection.keys()) # scope to this instance
for k in self._order(keys):
v = self._complete(k, collection[k])
contribution.setdefault(k, v)
collection.update(contribution)

def reset(self):
"""ConfigManagers may keep a state. This method shall be used to reset
the state to whatever that means for the specific config manager. This
implementation merely flushes the memory about completed work."""
self._managed_config = dict()



# one could make a case for not deriving ConfigManagerCollection from
# ConfigManager because the collection serves a different purpose than the
# individual manager, but they are still quite similar
class ConfigManagerCollection(ConfigManager):
"""
Container class for ConfigManagers.
"""
def __init__(self, interactive=False, verbosity=1):
super(ConfigManagerCollection, self).__init__(interactive=interactive, verbosity=verbosity)

def configManager(self, name):
return super(ConfigManagerCollection, self).configField(name)

def complete(self, *args, **kwargs):
for manager in self._order(self.keys):
handler = self.configManager(manager)
handler.complete(*args, **kwargs)

# these should never be called. we keep these implementations, just in case
def _remember(self, name, value):
raise NotImplementedError(f'Not implemented in {self.__class__.__name__}.')

def _complete(self, name, value):
raise NotImplementedError(f'Not implemented in {self.__class__.__name__}.')
68 changes: 68 additions & 0 deletions askbot/deployment/base/deployablecomponent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from askbot.deployment.base import ObjectWithOutput
from .deployableobject import DeployableDir as Directory

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

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

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

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