diff --git a/orchestra/contrib/databases/backends.py b/orchestra/contrib/databases/backends.py index f845c838..29e764ba 100644 --- a/orchestra/contrib/databases/backends.py +++ b/orchestra/contrib/databases/backends.py @@ -121,6 +121,10 @@ class MysqlDisk(ServiceMonitor): model = 'databases.Database' verbose_name = _("MySQL disk") delete_old_equal_values = True + doc_settings = (settings, + ('DATABASES_MYSQL_DB_DIR',) + ) + mysql_db_dir = settings.DATABASES_MYSQL_DB_DIR def exceeded(self, db): if db.type != db.MYSQL: @@ -141,11 +145,14 @@ class MysqlDisk(ServiceMonitor): ) def prepare(self): - super(MysqlDisk, self).prepare() + super().prepare() + context = { + 'mysql_db_dir': self.mysql_db_dir, + } self.append(textwrap.dedent("""\ - function monitor () { - { du -bs "/var/lib/mysql/$1" || echo 0; } | awk {'print $1'} - }""")) + function monitor_mysql () { + { SIZE=$(du -bs "%(mysql_db_dir)s/$1") && echo $SIZE || echo 0; } | awk {'print $1'} + }""") % context) # Slower way #self.append(textwrap.dedent("""\ # function monitor () { @@ -160,12 +167,13 @@ class MysqlDisk(ServiceMonitor): if db.type != db.MYSQL: return context = self.get_context(db) - self.append('echo %(db_id)s $(monitor "%(db_dirname)s")' % context) + self.append('echo %(db_id)s $(monitor_%(db_type)s "%(db_dirname)s")' % context) def get_context(self, db): context = { 'db_name': db.name, 'db_dirname': db.name.replace('-', '@003f'), 'db_id': db.pk, + 'db_type': db.type, } return replace(replace(context, "'", '"'), ';', '') diff --git a/orchestra/contrib/databases/settings.py b/orchestra/contrib/databases/settings.py index fe69e8dc..24dcfa68 100644 --- a/orchestra/contrib/databases/settings.py +++ b/orchestra/contrib/databases/settings.py @@ -22,3 +22,8 @@ DATABASES_DEFAULT_HOST = Setting('DATABASES_DEFAULT_HOST', 'localhost', validators=[validate_hostname], ) + + +DATABASES_MYSQL_DB_DIR = Setting('DATABASES_MYSQL_DB_DIR', + '/var/lib/mysql', +) diff --git a/orchestra/contrib/saas/README.md b/orchestra/contrib/saas/README.md new file mode 100644 index 00000000..3aec7396 --- /dev/null +++ b/orchestra/contrib/saas/README.md @@ -0,0 +1,79 @@ +# SaaS - Software as a Service + + +This app provides support for services that follow the SaaS model. Traditionaly known as multi-site or multi-tenant web applications where a single installation of a CMS provides accounts for multiple isolated tenants. + + +## Service declaration + +Each service is defined by a `SoftwareService` subclass, you can find examples on the [`services` module](services). + +The minimal service declaration will be: + +```python +class DrupalService(SoftwareService): + name = 'drupal' + verbose_name = "Drupal" + icon = 'orchestra/icons/apps/Drupal.png' + site_domain = settings.SAAS_MOODLE_DOMAIN +``` + +Additional attributes can be used to further customize the service class to your needs. + +### Custom forms +If the service needs to keep track of additional information you should provide an extra form and serializer. For example, wordpress requires you to provide an email_address during account creation, and the assigned blog ID is required for effectively update account state or delete it. In this case we provide two forms: + +```python +class WordPressForm(SaaSBaseForm): + email = forms.EmailField(label=_("Email"), + help_text=_("A new user will be created if the above email address is not in the database.
" + "The username and password will be mailed to this email address.")) + +class WordPressChangeForm(WordPressForm): + blog_id = forms.IntegerField(label=("Blog ID"), widget=widgets.SpanWidget, required=False, + help_text=_("ID of this blog used by WordPress, the only attribute that doesn't change.")) +``` + +WordPressForm provides the email field, and WordPressChangeForm adds the `blog_id` on top of it. `blog_id` will be represented as a *readonly* field on the form, so no modification will be allowed. + +### Serializer for extra data + +Additionally, we should provide a serializer in order to save the form extra pices of information into the database (into field *data*). + +```python +class WordPressDataSerializer(serializers.Serializer): + email = serializers.EmailField(label=_("Email")) + blog_id = serializers.IntegerField(label=_("Blog ID"), allow_null=True, required=False) +``` + +Now we have everything needed for declaring the WordPress service. + +```python +class WordPressService(SoftwareService): + name = 'wordpress' + verbose_name = "WordPress" + form = WordPressForm + change_form = WordPressChangeForm + serializer = WordPressDataSerializer + icon = 'orchestra/icons/apps/WordPress.png' + change_readonly_fields = ('email', 'blog_id') + site_domain = settings.SAAS_WORDPRESS_DOMAIN + allow_custom_url = settings.SAAS_WORDPRESS_ALLOW_CUSTOM_URL +``` + +Notice that two optional forms can be provices `form` and `change_form`. When non of them is provided, SaaS will provide a default one for you. When only `form` is provided, it will be used for both, *add view* and *change view*. If both are provided, `form` will be used for the *add view* and `change_form` for the change view. This last option allows us to display the `blog_id` back to the user, only when we have it (after creation). + +`change_readonly_fields` is a tuple with the name of the fields that can not be edditied once the service has been created. + + +## Backend + + +A backend class is required to interface with the web application and perform `save()` and `delete()` operations on it. The more reliable way of interfacing with the application is by means of a CLI (e.g. [Moodle](backends/moodle.py), but not all CMS come with this tool. The second preferable way is using some sort of API, possibly HTTP-based (e.g. [gitLab](backends/gitlab.py). This is less realiable because additional moving parts are used underneeth the interface; a busy web server can timeout our requests. The least prefered way is interfacing with an HTTP-HTML interface designed for human consumption, really paintful to implement but sometimes is the only way (e.g. [WordPress](backends/wordpressmu.py)). + +Some applications do not support multi-tenancy by default, but we can hack the configuration file of such apps and generate *table prefix* or *database name* based on some property of the URL. Example of this services are [moodle](backends/moodle.py) and [phplist](backends/phplist.py) respectively. + + +## Settings + +Enabled services should be added into the `SAAS_ENABLED_SERVICES` settings tuple, providing its full module path, e.g. `'orchestra.contrib.saas.services.moodle.MoodleService'`. Parameters that should allow easy configuration on each deployment should be defined as settings. e.g. `SAAS_WORDPRESS_DOMAIN`. Take a look at the [`settings` module](settings.py).