About October 2007

This page contains all entries posted to /blog/wvk in October 2007. They are listed from oldest to newest.

September 2007 is the previous archive.

November 2007 is the next archive.

Many more can be found on the main index page or by looking through the archives.

Powered by
Movable Type 3.31

« September 2007 | Main | November 2007 »

October 2007 Archives

October 2, 2007

Assoziation und Aggregation in Rails

Weil ich immer mal wieder eine Weile überlegen muss, wie nun genau die verschiedenenen Assoziationen wie has_one, has_one und has_and_belongs_to_many gebildet werden (im Sinne der Fremdschlüssel in der Datenbank), hab ich hier mal eine Grafik aus meiner Bachelorarbeit angefügt, die vielleicht auch dem einen oder anderen Rubyisten unter euch eine Hilfe sein wird.

Well, here goes:
RailsClassAssociations.png
Oben ist die Klassen-Assoziation angegeben, in der Mitte der verwendete Ruby-Code und unten die Tabellen mit den Fremdschlüsseln.

Ergänzung zu Philipps Kommentar: Wenn man 10 Aggregationen hat, kommen die einfach nacheinander, genau. Man kann sich auch über die Namenskonventionen hinwegsetzen und z.B. die Fremdschlüssel- und Tabellennamen explizit angeben. Allerdings wollte ich hier jetzt keine Einführung in ActiveRecord geben, da verweise ich gerne auf die Doku ;-). Hier aber mal eine Beispielklasse meines Systembenutzers (siehe Klassendiagramm für Details):

class SystemUser < ActiveRecord::Base
  belongs_to :client
  has_and_belongs_to_many :system_groups
  has_many :user_access_rights
  has_one :reseller_account, :foreign_key => 'reseller_id'
end

Oder hier die Assoziationen des AccessRights, hier sind die Zusammenhänge im Datenmodell etwas komplizierter. Die Vergknüpfung erfolgt über eine Tabelle, die mehr informationen als nur die Fremdschlüssel beeitstellt. Außerdem, um Daniels Kommentar zu würdigen, kann man so auch mit Verknüpfungstabellen arbeiten, deren Namen sich nicht aus der in alphabetisch richtiger Reihenfolge zusammengesetzten Namen der verknüpften Entitäten/Tabellen besteht:

class AccessRight < ActiveRecord::Base
  belongs_to :consolvix_module
  has_and_belongs_to_many :system_users,
                          :join_table => 'user_access_rights',
                          :foreign_key => 'access_right_id',
                          :association_foreign_key => 'user_id'
  has_and_belongs_to_many :system_groups,
                          :join_table => 'group_access_rights',
                          :association_foreign_key => 'access_right_id',
                          :foreign_key => 'group_id'
end
class UserAccessRight < ActiveRecord::Base
  belongs_to :system_user, :foreign_key => 'user_id'
  has_one :access_right
end
usw.

October 3, 2007

Ein paar Gedanken zu Benutzerrollen

Da neulich ja schon das Thema Benutzerrollen angesprochen wurde, habe ich nun mal ein paar Spezifikationen zur Benutzerverwaltung in meiner Software zusammengetragen. Diese Liste ist vorläufig, sie kann sich jederzeit ändern.

Folgende Benutzer-Rollen sind in Consolvix vorgesehen: Buchhaltungs-Administrator, Systemadministrator, Kunde, Reseller, E-Mailbenutzer, Hostingbenutzer, FTP-Benutzer, Systembenutzer. Rollen können sich überschneiden; manche Rollen sind Unter-Rollen von anderen Rollen. Buchhalterische und technische Rollen sind von einander getrennt.

Detaillierte Beschreibung

Der Kunde
erfüllt rein buchhalterische Aufgaben wie Abrechnungswesen, Domain-Eigentum etc. Der Kunde übernimmt die rechtliche Verantwortung für alle ihm zugeordneten Benutzer-Accounts und deren Inhalte. Im System (also technisch) spielt der Kunde keine Rolle.
Der Systembenutzer
ist die zentrale Entität. Jede Art von (Hosting-)Account wird durch Erweiterung der Rechte des Systembenutzers gebildet. Ein Systembenutzer erhält ein eigenes Heimat-Verzeichnis und einen System-E-Mailaccount (@). Zugriffsrechte, Quota-Regelungen etc. werden an den Systembenutzer vergeben. Der Systembenutzer spielt sowohl im (Betriebs-)System als auch in der Verwaltungssoftware (Consolvix) eine wichtige Rolle.
Der Systemadministrator
ist eine nur im Kontext der Verwaltungssoftware existierende Entität, die sämtliche Zugriffsrechte sowohl auf administrativer (kann sämtliche Konfigurationseinstellungen vornehmen) als auch auf Benutzerdatenebene besitzt. Der Systemadministrator hat jedoch a priori keinen Zugriff auf buchhalterische Daten (lässt sich durch eine Konfigurationsoption aber ändern). Administrationsrechte können Modulweise vergeben werden.
Der Buchhaltungs-Administrator
ist verantwortlich für das Rechnungswesen. Im Rahmen der Diplomarbeit nicht weiter relevant.
Reseller
sind Kunden mit zusätzlichen Rechten: sie dürfen andere Kunden und Systembenutzer anlegen und sind für deren Verwaltung verantwortlich. Reseller können im Rahmen ihrer eigenen Verbrauchsgrenzen (Quotas) auch solche für ihre eigenen Benutzer anlegen. Sie können jedoch keine technischen Verwaltungsaufgaben übernehmen (s. Sysadmin). Ein Reseller muss mind. einen Systembenutzeraccount und eine Domain besitzen
E-Mailbenutzer
sind in erster Linie für den E-Mailserver relevant. Über die Verwaltungssoftware können Konten erstellt, konfiguriert und gelöscht werden, das Abrufen von E-Mails geschieht jedoch über vorerst nur über den Mailserver direkt (via IMAP oder POP3).
Hostingbenutzer
sind Systembenutzer mit der Möglichkeit, Inhalte im Web freizugeben, sprich VirtualHosts einzurichten.
FTP-Benutzer
sind virtuelle Benutzer, die auf Systemebene nur für den FTP-Server relevant sind. Auf Verwaltungsebene können einem Systembenutzer beliebig viele FTP-Benutzerkonten zugeordnet werden.

Die Rollen werden hier nicht (nur) über einzelne Rechte-Flags, sondern teilweise über ganze eigene Entitäten in der Datenbank definiert. Beispielsweise wird ein Systembenutze rnicht einfach zum Hostingbenutzer indem er Hosting-Rechte über ein Boolean Flag freigeshaltet bekommt, sondern indem das Systembenutzer-Objekt einen ganzen Satz neuer Eigenschaften aggregiert, die diese Rechte und Grenzen quantitativ erfassen. Ebenso beim Reseller: ein ganzer Datensatz in der Reseller-Tabelle beschreibt, welche neuen Rechte ein Reseller-Kunde bekommt. Fehlt dieser Datensatz (bzw. ist der Fremdschlüsselverweis im Kunden-Objekt gleich NULL), dann handelt es sich nicht um einen Reseller-Kunden.

Ein paar Anforderungen:

  • Jeder Kunde bekommt 1...n Systembenutzer zugeordnet; Standard ist 1.
  • Ein Kunde kann Reseller-Rechte bekommen; Reseller sind Kunden mit Reseller-Rechten.
  • Ein Reseller ist berechtigt, andere Kunden und Systembenutzer anzulegen.
  • Jeder Kunde gehört zu genau einem Reseller.
  • Jeder Benutzer kann sein eigenes Kennwort/persönliche Daten ändern, Reseller die ihrer Kunden, Kunden die aller ihnen direkt zugeordneter Benutzer
  • Benutzer- und Resellerkonten können gesperrt werden.
  • Bei Sperrung eines Resellerkontos werden dessen Kundenkonten ebenfalls gesperrt
  • Für einen Kunden können alle E-Mail, FTP- und Systemkonten einzeln oder kollektiv gesperrt werden.
  • Kunden haben ein Benutzerkonto für das Online-Vertrags- und Rechnungswesen.

October 7, 2007

Unklare Gedanken zum Thema "Module"

Es folgt eine wirre Sammlung an Gedanken zum Thema Module/Plugins. Ideen und Ratschläge sind stets herzlich willkommen!

Ein Modul betseht aus einer Menge von Klassen, die Aufgaben aus einem Bestimmten Bereich übernehmen sollen. Beispiele:
- HTTP: CRUD von Virtual Hosts, sperren von VirtualHosts, CRUD von Subdomains, Zuweisung von Domains zu VirtualHosts, setzen von Zugriff-Limits (Quota)
- FTP: CRUD von FTP-Accounts, setzen von Quota
- Benutzerverwaltung ...

Fragen:
- Soll jedes Modul Zugriff haben auf sämtliche in der gesamten Software bestehenden Models/Controller?
- Welche Authorisierungs-Methoden braucht jedes Modul?
- Welche Module müssen einander kennen?
- Welche Module sind voneinander abhängig?
- Macht es Sinn, abhängige Module zu gestalten, oder ist das ein Zeichen für schlechte Aufgabentrennung?
- Lassen sich alle Aufgabenbereiche überhaupt so genau abgrenzen? [s. erste Frage]
- Sollen Module aus Benutzer-, Administrator- oder Entwicklersicht erstellt werden? Ein Benutzer sieht den Unteschied zwischen einer "Domain", einem "VirtualHost" und einem "FTP-Account" vielleicht nicht (=> möglichst einfaches Interface, gutes Maß an Abstraktion der darunterliegenden Technologien/Anwendungen, hohes Maß an Automatisierung und sinnvolle Default-Werte). Ein Administrator sollte (?) jedoch tiefergehende Möglichkeiten haben und auf Applikationsebene Einstellungen vornehmen können (=> möglichst viele Einstellungsmöglichkeiten, auch auf Technischer Ebene). Ein Entwickler benötigt hingegen eine streng abgegrenzte Schnittstelle auf Code-Ebene, betrachtet "das Model Domain" und "das Model VirtualHost" als Datenbankentitäten usw. (=> ?????)
- Module sollen natürlich (?) bei der Installation keine Änderungen an anderen Modulen vornehmen dürfen. Um eine gute Erweiterbarkeit der Funktionalität bei stark vernetzten Modulen zu gewährleisten, müsste man Gebrauch machen von Callback-Funktionen. Rails bietet hierzu ein hervorragendes Mittel: Filter-Funktionen. Beispielsweise kann man Filter-Objekte (oder Methoden, Procs, ...) bauen, die vor, nach, oder um Methoden herum aufgerufen werden (also davor und danach), oder den Aufruf einer Funktion gänzlich ersetzen oder verhindern. So könnte ein Modul ein Callback-Objekt bei einem anderen Modul registrieren (z.B. für Authentifikationsaufgaben). Solche Möglichkeiten müssten in der API gut dokumentiert werden.

October 9, 2007

More impressive Views without Schuppen vor den Augen

Hallo Welt, ich lebe noch! Und heute kam ich mal wieder etwas mehr voran als die letzten Tage, wo ich im wesentlichen am Klassendesign herumgefeilt und mir gedanken zum Aufbau meiner Programm-Module gemacht habe. Zweiteres wäre einen eigenen Eintrag wert (der auch schon seit drei Tagen in Mache ist...), auf ersteres werde ich jetzt eingehen.
Mittlerweile besteht das Datenbankschema für Consolvix aus etwas über 40 Tabellen und Views, was sich natürlich auch auf Applikationsebene wiederspiegelt. Nicht gerade wenig, und viele Tabellen speichern doch in etwa das gleiche: Logindaten. Einfach, weil ich zu blind war um zu sehen wie man diesen ominösen universellen "User" nun auf Datenbankebene abbilden kann (siehe "Neues aus der Modellier-Ecke"). Nun fiel es mir heute wie Schuppen von den Augen, als ich ich feststellte dass trotz reiflicher Überlegungen zur Klassenhierarchie bei den Benutzerkonten (siehe "Benutzerverwaltung") meine Überlegungen nicht weit genug geführt hatte: Aus Polymorphie zwischen User, EmailAccount und SystemUser mach Aggregation, und zwar nicht vom Benutzer ausgehend, sondern zum Benutzer hin. Auf Datenbankebene bedeutet das: mache eine Tabelle User(id, name, real_name, uid, gid,...) und erstelle für jeden FTP-Account, E-Mail-Account etc. einen neuen Datensatz, dessen Inhalt dann aggregiert wird. Auf Applikationsebene "hat" ein EMailAccount dann ein User-Objekt, wo Logindaten etc. drinstehen. Für die Anwendungen wie der Mailserver, die direkt auf die Daten in einer Tabelle zugreifen können müssen, kommen dann einfach Views zum Tragen.
Eine schöne View ist z.B. die, die aus Systembenutzern, Benutzern und FTP-Kontodaten die Tabelle für die FTP-VirtualHosts bzw. FTP-Accounts zusammenbaut:

CREATE VIEW proftp_virtual_hosts AS
SELECT
  `users`.`name`     AS name,
  `users`.`password` AS password,
  IF (`ftp_accounts`.`home_dir`,
    `ftp_accounts`.`home_dir`,
    `system_users`.`home_dir`
  ) AS document_root,
  `ftp_accounts`.`login_count`   AS login_count,
  `ftp_accounts`.`last_accessed` AS last_accessed,
  `ftp_accounts`.`last_modified` AS last_modified,
  IF (`system_users`.`shell`,
    `system_users`.`shell`,
    '/bin/false'
  ) AS shell,
  `users`.`system_uid` AS uid,
  `users`.`system_gid` AS gid
FROM
  `ftp_accounts`,
  `system_users` RIGHT JOIN `users` ON (`system_users`.`user_id` = `users`.`id`)
--   access_rights   AS a
WHERE
      `users`.`id`                  = `ftp_accounts`.`user_id`
  AND `ftp_accounts`.`is_disabled?` = 0
  AND `users`.`is_disabled?`        = 0

Resultat:
| Field | Type | Null | Default |
| id | int(10) | No | |
| login_count | int(11) | No | 0 |
| last_accessed | datetime | No | 0000-00-00 00:00:00 |
| last_modified | datetime | No | 0000-00-00 00:00:00 |
| is_disabled? | tinyint(1) | No | 0 |
| user_id | int(11) | No | |
| home_dir | varchar(35) | No | |

Der FTP-Server kann dann auf die Daten zugreifen, als wäre es eine ganz normale Tabelle (ja, Zugriffszeiten und login_count lässt sich auch über die View aktualisieren), aber auf Applikationsebene ergeben sich durch das Auslagern der gemeinsamen "Benutzer"-Daten erhebliche Vorteile in der Rechteverwaltung. So kann dort bei der Authentisierung immer mit dem Benutzer-Objekt ohne Unterscheidung, ob es nun ein E-Mail oder FTP-Konto ist, gearbeitet werden. Im Klassendiagramm, sprich auf Applikationsebene, sieht obiger Zusammenhang so aus:

ftp-users-details

Ganz ähnlich, nur noch die eine oder andere Tabelle mehr erfordernd, sieht es mit den E-Mailkonten aus. Bei der Unten aufgeführten Lösung wird nicht mehr zwischen "virtuellen" und "echten" E-Mail-Konten unterschieden. Jedes E-Mailkonto "ist" bzw. "hat" auch einen Benutzer ("hate", weil Komposition -- obwohl eigentlich der "ist ein"-Gedanke durch Vererbung hadinter steckt und die Komposition an die Stelle der Polymorphie tritt). Dem E-Mailserver ist es egal, ob dieser "E-Mailbenutzer" nun auch ein Systemkonto besitzt (eindeutige UID/GID) oder ob es sich dabei um einen "virtuellen Benutzer" handelt, der in diesem Fall immer die UID/GID 5000 hat. Auch Consolvix' Authorisierungsfunktionen ist es herzlich egal, ob sich da via Web-Interface nun ein reiner E-Mailbenutzer oder etwas anderes einloggt, denn diese benutzen nur die im User-Objekt gespeicherten Informationen -- insbesondere sind sämtliche Zugriffsrechte allein am User gekoppelt.

Nundenn, so sieht der wichtigste Teil des E-Mail-"Subsystems" aus:

email-classes-details.png

Die E-Mailadresse stellt eine eigene Entität dar, weil offensichtlich mehrere E-Mailadresse für das gleiche POP/IMAP-Konto bestimmt sein können. Dass die Domain in der E-Mailadresse nochmal über einen Fremdschlüssel referenziert ist, macht die ganze Sache noch eine kleine Stufe komplifizierter, aber es soll ja ein konsistentes Datenbankschema sein. Dennoch beinhaltet obiges Scheme noch ein u.U. signifikantes Problem, und wer es herausfindet der kriegt von mir einen HTTP-Keks ;-)

Wie auch immer, der E-Mailserver benötigt eine einzige schön aufbereitete Tabelle, um seine Mails zu liefern. Deswegen übernimmt auch hier eine hübsche kleine View das. Bis auf die Subqueries, auf die ich unten kurz eingehe, sollte alles auch ohne Kommentare halbwegs selbsterklärend sein, ansonsten einfach einen Kommentar schreiben, ich erläutere gerne mehr.

CREATE VIEW `postfix_accounts` AS
SELECT
  CONCAT(`name`, '@', (SELECT `value` FROM `app_settings` WHERE `key` = 'mail_domain_name')) AS `account`,
  `name`,
  `real_name`,
  `password`,
  `quota`,
  IF (`email_accounts`.`mail_dir`,
      `email_accounts`.`mail_dir`,
      CONCAT(`home_dir`, '/mail/', `name`, '/Maildir/')
  ) AS `maildir`,
  (SELECT `system_uid`
    FROM `users` JOIN `app_settings` ON (`users`.`name` = `app_settings`.`value`)
    WHERE `app_settings`.`key` = 'mailbox_user_name'
  ) AS `uid`,
  (SELECT `system_gid`
    FROM `system_groups` JOIN `app_settings` ON (`system_groups`.`name` = `app_settings`.`value`)
    WHERE `app_settings`.`key` = 'mailbox_group_name'
  ) AS `gid`
FROM
 (`system_users` RIGHT JOIN `users` ON `system_users`.`user_id` = `users`.`id`),
  `email_accounts`
WHERE
      `users`.`id` = `email_accounts`.`user_id`
  AND `email_accounts`.`is_disabled?` = 0;

Resultat:
| Field | Type | Null | Default |
| account | varchar(301) | Yes | NULL |
| name | varchar(50) | No | |
| real_name | varchar(32) | No | |
| password | varchar(40) | No | x |
| quota | varchar(10) | No | |
| maildir | varchar(97) | Yes | NULL |
| uid | int(11) | Yes | NULL |
| gid | int(11) | Yes | NULL |

So, und was hat das mit den Subqueries auf sich? Wie ich noch nirgends erwähnt habe, gibt es für Consolvix eine eigene Entität, AppSettings, in der konfigurierbare Einstellunen gespeichert werden. Man kann ja nicht erwarten, dass die Systemkonfiguration in die Datenbank ausgelagert wird und dann die verwaltende Applikation für sich selbst wieder auf Konfigurationsdateien zurückgreift :-) -- jedenfalls wird in der Tabelle app_settings jeweils ein Schlüssel-Wert-Paar gespeichert, mit Angabe des zugehörigen Moduls. Innerhalb der Applikation ermöglicht die folgende Methode, diese Einstellungen schnell und einfach zu erhalten:

  def setting(setting_name)
    setting = AppSetting.find_by_key(setting_name,
                            :conditions => {:module_id => @current_module.id,
                                            :version   => @current_module.version} )
    setting.value
  end

Und wer genau hinschaut, entdeckt im wesentlichen genau die SQL-Abfrage als Subquery in den Views. Was im Datenbankschema noch nicht umgesetzt ist, ist jedoch die Versionierung der Module etc. Überhaupt bin ich noch unentschlossen, was die Versionierung der Applikationseinstellungen angeht. Denn nochmals je eine Subquery um Modul-ID und aktuelle Version herauszufinden scheint mir gegenärtig etwas übertrieben, auch wenn man da mit Stored Procedures ja einiges machen kann. Da mich zur Zeit aber ohnehin das Gefühl beschleicht, mehr ER-Modellierung als RoR zu machen, werde ich es erstmal dabei belassen.

Und für die UML-Liebhaber, hier ein Schnappschuss des wichtigsten Teils des Klassendiagramms:
SystemClasses-071010.png

Stay tuned, Folks!

October 18, 2007

Von befreienden Einschränkungen

Mittlerweile wurde endlich ein klarer(er) Rahmen, in dem ich mich mit der Diplomarbeit befassen soll, festgelegt. Es bleibt sozusagen recht wenig vom ursprünglichen Umfang übrig, was aber nicht bedeutet, dass ich nun nichts mehr zu tun hätte :-). Die Arbeit wird sich nun auf die nachfolgenden Punkte beschränken. Grob gesagt wären das:
- Kernbereich (Framework, Modul-API; Modul: core)
- Benutzerverwaltung (Versch. Benutzertypen, Gruppen, Rollen, Rechte. Module: domain, email, auth)
- Prototypartige Implementierung eines optionalen Moduls (http)
- Installationsroutinen (sehr wahrscheinlich über Rake)

Viele Überlegungen zur Implementierung machen m.E. nur mit Sicht auf das Gesamtsystem Sinn, darum werde ich weiterhin beim Erarbeiten des Datenbankschemas weitestgehend alle beteiligten Entitäten mit einbeziehen, auch wenn diese in der Diplomarbeit nicht konkret behandelt werden.

Als nächstes sollte ich mir nun ein paar Gedanken zur Modulverwaltung machen, sprich wie diese in die Applikation integroert werden sollen. Ein Blogientrag zu dem Thema ist noch immer in Arbeit...

ActiveRecord für Fortgeschrittenere

Neulich habe ich im Buch "Agile Web Development with Rails" eine ganz nette Sache entdeckt, die mir für die Benutzer-Rechteverwaltung sehr gelegen kommt. Ich habe die Entitäten User, Group und AccessRight, wobei sowohl Benutzer als auch Gruppen beliebig viele Zugriffsrechte haben können (habtm). Üblicherweise würde man zur Erstellung der Rechte-Beziehungen einfache Join-Tables mit zwei Fremdschlüsseln verwenden (users_access_rights(user_id, access_right_id)). Da zusätzlich aber gespeichert werden soll, wer wann von wem welche Rechte zugewiesen bekommen hat, macht es Sinn, eine zusätzliche Entität UserAccessRight (dito für Gruppen) zu erstellen. Klassischerweise würde das dann so aussehen:


class User < ActiveRecord::Base
  belongs_to :user_access_right
end
# dito für Group...

class AccessRight < ActiveRecord::Base
  belongs_to :user
end

class UserAccessRight < ActiveRecord::Base
  has_one :user
  has_one :access_right
end

# Liste alle Rechte von Benutzer 1 auf:
user = User.find 1
user.user_access_rights.each do |acc|
  p acc.right.name
end

# darf der Benutzer also Benutzer hinzufügen?
if user.user_access_rights.find {|r| r.access_right_name == 'add_user'}
  p "Benutzer #{user.name} darf Benutzer hinzufügen."
end

Das funktioniert zwar wunderbar, aber bei genauerem hinsehen ist es bestenfalls "etwas unhübsch", um immer über die UserAccessRight-Entität zuzugreifen, wenn man eigentlich mit dem AccessRight selbst arbeiten möchte -- denn eigentlich soll ja weiterhin lediglich eine HABTM-Beziehung zwischen User und AccessRight bestehen, die aber so aufgelöst wird.

Dafür gibt's aber in Rails die folgende Lösung:


class User < ActiveRecord::Base
  has_many :user_access_rights
  has_many :access_right,
          :through => :user_access_right
end

class AccessRight < ActiveRecord::Base
  has_many :user_access_rights
  has_many :access_rights,
          :through => :user_access_rights,
          :unique => true
end

class UserAccessRight < ActiveRecord::Base
  belongs_to :user
  belongs_to :access_right
end

# Liste alle Rechte von Benutzer 1 auf:
user = User.find 1
user.rights.each do |acc|
  p acc.name
end

# darf der Benutzer also Benutzer hinzufügen?
if user.rights.find {|r| r.name == 'add_user'}
  p "Benutzer #{user.name} darf Benutzer hinzufügen."
end

So kann man also direkt, wie bei einer klassischen HABTM-Beziehung, auf die Rechte eines Benutzers zugreifen. Zusätzlich stehen aber auch unmittelbar die Meta-Informationen zu den einzelnen Rechten (wann und von wem verliehen) zur Verfügung.

Also ich finde das toll ;-)

Als Nächstes muss ich nur mal sehen, wie man halbwegs elegant an die Rechte herankommt, die einem Benutzer über seine Gruppenzugehörigkeiten verliehen werden. Zur Not tut's natürlich eine einfache Suchschleife über alle Gruppen und deren Rechte, oder die Rechte werden bei der Objektinitialisierung geladen, oder alles wird in einer nifty-tricky-magic Funktion gekapselt mit deren Innenleben sich lieber keiner auseinandersetzen sollte ;-)

October 22, 2007

Die Geschichte mit der Vererbung

Wie mittlerweile klar rübergekommen sein dürfte, umfasst Consolvix diverse Arten von Benutzern. EmailAccount, FtpAccount, Client etc. aggregieren alle ein User-Objekt, um vom System authentifiziert werden zu können. Um nun all diesen Objekten nach außen hin das "Aussehen" eines von User erbenden Objektes zu verpassen, verwende ich jetzt einfach sowas wie ein Proxy-Pattern, realisiert über ein "Mixin", also ein Ruby-Modul. Am Beispiel SystemUser HAS_ONE User sieht das so aus:

module UserInheriter
  # übernehme alle aus der Tabelle ermittelten Attribute ohne ID
  attributes = User.new.attribute_names - ['id']

  attributes.each do |attr|
    # generiere Lesemethode:
    define_method("#{attr}") do
      return self.user.send("#{attr}")
    end

    # generiere Schreibmethode:
    define_method("#{attr}=") do |param|
      return self.user.send("#{attr}=", param)
    end
  end
end

class SystemUser < ActiveRecord::Base
    has_one :user, :dependent => :destroy
  # ...
  include UserInheriter
  # ...
end

So wird jede Klasse, die das Modul UserInheriter importiert, um die Lese- und Schreibmethoden der User-Klasse erweitert. Natürlich bleibt der direkte Zugriff auf das User-Objekt weiterhin möglich.

Was später noch implermentiert werden sollte, ist die Unterscheidung zwischen public und protected Attribute sowie die Weiterreichung von Aggregierten Objekten. Das tut momentan aber noch nicht Not und darum wird's vorerst bei diesem Umfang bleiben.

October 26, 2007

Neue Benutzer Schritt für Schritt

Die letzten zwei Tage habe ich mich mal von meinem Modell losgerissen und endlich eines der umfangreicheren Use-Cases implementiert: Das Schrittweise Anlegen eines neuen Kunden mit (fast) allem drum und dran. Meine Anforderungen an die Implementierung waren in etwa die folgenden:

  • Übersichtlichkeit: Der Administrator oder Reseller, der den Kunden anlegt, soll nicht von einem Monster-Formular erschlagen werden, sondern Schritt für Schritt durch den Prozess geführt werden.
  • REST lässt grüßen: Sowohl der Gesamtprozess als auch jeder einzelne Unterschritt ist direkt über eine eindeutige URL zu erreichen. Beim Aufruf des Gesamtprozesses (/admin/create_hosting_account) wird auf die letzte noch nicht bearbeitete Eingabemaske gewechselt (wenn z.B. Schritt 1 und 2 bearbeitet sind, springe zu Maske 3). Beim expliziten Aufruf eines Schrittes (z.B. /admin/create_hosting_account/3) wird, sofern alle vorangehenden Schritte erfolgreich waren, zu diesem gewechselt.
  • Es sollte jederzeit möglich sein, in eine bereits bearbeitete Maske zu wechseln und dort Änderungen vorzunehmen, ohne dass Eingaben aus einer anderenMaske dadurch verändert werden müssten. Nach Bearbeiten einer Maske wird immer in die letzte noch nicht fertig bearbeitete Maske gesprungen.
  • Überprüfung auf Fehler erfolgt beim Absenden jeder einzelnen Maske, nicht erst am Ende des Prozesses
  • Bis zum letzten Schritt werden keinerlei Daten in der Datenbank abgespeichert. Und selbst im letzten Schritt wird durch eine Datenbanktransaktion dafür gesorgt, dass entweder alles oder gar nichts übernommen wird -- im zweiteren Fall wird wieder zum letzten Schritt gewechselt.
  • Die Umsetzung im Code sollte mit möglichst wenig Wiederholung (DRY ;-)) und im Hinblick auf zukünftige Erweiterung um weitere Schritte geschehen.
  • nochmal REST: Wird ein Schritt mit GET aufgerufen, wird dieser angezeigt ohne jegliche Fehlermeldungen, sollten sich in der aktuellen Maske noch Fehler befinden. Geschieht der Aufruf mit POST (und hoffentlich Formular-Eingaben), wird versucht, diesen Schritt abzuschließen indem die eigegebenen Daten validiert und in der Session zwischengespeichert werden. Bei DELETE-Aufrufen wird die gesamte "Transaktion" abgebrochen, d.h. alle bisher getätigten Eingaben werden gelöscht und es wird zum ersten Schrit gesprungen. PUT wird nicht verwendet.

Das Oberflächendesign lässt sicherlich noch den einen oder anderen Wunsch offen, denn momentan verwende ich einfach noch die Standard-Formulare des Rails-Scaffolds (wer sich die fieldsets genauer anschaut, wird evtl. feststellen, dass sie sich genau mit den Datenbankentitäten decken -- im ersten Schritt wärden das Client und Address). Außerdem sind noch keinerlei "dynamischen" Elemente implementiert, so lässt sich z.B. erst eine Domain und ein E-Mailaccount (E-Mailadresse, Adresse,...) im Laufe des Prozesses anlegen. Ziel ist es, diese auf eine beliebige Zahl zu erhöhen (Stichwort Formularerweiterung mit JS).

Ansonsten funktioniert eigentlich alles wie gewünscht, besonders die garantierte Vollständigkeit aller vorherigen Schritte bei der Anzeige eines beliebigen Schrites (Die Überprüfung auf Vollständigkeit von Schritt n erfordert die vorherige Überprüfung von Schritt n-1, rekursiv bis Schritt 1 -- es ist so nicht möglich, dass irgend eine Überprüfung "aus Versehen" beim Programmieren ausgelassen werden könne).

Und hier noch ein paar Screenshots:

snap7.png Erster Schritt: Eingabe Der Kontaktdaten des Kunden

snap8.png Upps, da hat sich wohl wer vertan: Ausführliche Fehlermeldungen eind selbstverständlich.

snap9.png Alle Eingaben OK, weiter geht's mit dem zweiten Schritt.

System-Statusanzeige

Zu jeder ordentlichen Serververwaltungssoftware gehört natürlich auch eine Anzeige der wichtigsten Systemeigenschaften. Da mir gestern Abend soo langweilig war, habich dann auch prompt mal ein paar Statusanzeigen für Consolvix geschrieben. Natürlich alles schön Ajax-based, sprich die Anzeigen verändern sich im 5-10-Sekundentakt.

Viel Mehr gibt's dazu eigentlich nicht zu schreiben, denn ein Bild sagt mehr als 1024 Worte:

snap11.png
Admin-Startseite: Das wichtigste auf einen Blick, sprich Auslastung und Speicherverbrauch (aufgeschlüsselt nach Verwendung)

snap10.png
Syslog: zeige die letzten Einträge von /var/log/syslog an

snap12.png
Der aktuelle Prozessbaum, optional mit Anzeige der PIDs (wer erkennt hier die Ausgabe von pstree? keks)

snap14.png
Die Anzeige des freien/belegten Plattenplatzes, im wesentlichen eine grafisch aufgearbeitete Version von df.

Sollte demnächst mal wieder Langeweile aufkommen, wird diese "Toolbox" sicherlich noch erweitert werden (Vorschläge von zukünftigen Nutzern dieser Software sind willkommen!)

October 29, 2007

Konfigurationsmanagement

Eine wesentliche Funktion, die das Kernmodul von Consolvix bereit stellen soll (und die es in der Tat mittlerweile tut ;-)), ist das einheitliche Auslesen und Speichern von Programmeinstellungen (AppSettings). Beispiele hierfür sind etwa die Anzahl der standardmäßig anzuzeigenden Einträge in einer Liste, das default-Homeverzeichnis der Hostingkunden, die UID des Mailservers und vieles mehr. Angefangen hatte es also mit einer Tabelle in der Form

appsettings(id_, key, value).

Doch um etwas mehr Übersicht zu schaffen, ist es natürlich sinnvoll, die einzelnen Werte an das jeweilige Modul zu binden, wo sie ihre Gültigkeit erhalten. So würde es z.B. wenig Sinn ergeben, die default-Webroots der Webbenutzer zu laden, wenn man sich gegenwärtig im Modul "email" aufhält. Und wenn man schon von verschiedenen Modulen spricht, die ja auch noch in verschiedenen Versionen auftauchen können, kommt zusätzlich gleich noch eine Versionierungsinformation hinzu. Also wird aus der obigen Tabelle die folgende:

appsettings(id, key, value, version, moduleid).

Nebenbei bemerkt, die Modul-Tabelle sieht so aus:

consolvixmodule(id, name, description, version, isenabled?, navigationentry, controllername, created_at)

Es war so angedacht, dass bei einem Update über das [version]-Feld entschieden werden sollte, ob ggf. eine neuere Modulversion zum Download zur Verfügung steht. Wenn diese dann installiert wäre, so würde dann (je nach antwort auf die Frage ob die alte Config übernommen werden soll) für jedes AppSetting ein neuer Eintrag mit identischen/aktualisierten Attributen und der neuen Versionsnummer (der des neuen Moduls) angelegt werden. Das würde Downgrades einfacher machen, denn die alten Einstellungen blieben weiterhin erhalten.

Aber sowas kann man ja genausogut über das Datum machen: dieses wird nämlich beim Herunterladen der Modulliste vom Downloadserver gleich mitgeliefert.

Dieses Konzept geriet arg ins Wanken, als ich heute die Funktion zum Auslesen der Konfigurationswerte weiter auscodete. Um einen Hauch dessen zu vermitteln, was da auf mich zugekommen wäre, möchte ich nur die API-Kommentare anführen, die ich (VOR! dem Erstellen der Funktion) dazu schrieb:

class ApplicationController < ActionController::Base @@module_name = 'core'

# Get an application setting, optionally with module name and version.
# * setting_name: the name of the setting to be retrieved, either string or symbol
# * module_name: name of the module where to look for the setting.
#              If none specified, use current module.
#              If not found, return default value or throw exception (see options)
# * options: Optional ;-)
#  * :version (Integer) => look for this version.
#                          If none specified or 0, use current module version (default)
#                          If positive, look for this exact version (return default value or throw exception if not found).
#                          If -1, look for highest version available.
#                          If not found, raise not found exception (if no default value given)
#  * :default (Object)  => return this value instead of throwing an exception
#  * :core (true|false) => let the core option take precedence over the default value, if specified.

def setting(setting_name, module_name=@@module_name, *options)
  # ...
end

end

Das hätte schätzungsweise 50 zeilen Code bedeutet und irgendwie schien mir die Herangehensweise etwas dubios. Irgendwie so wie "hole dir den wert einer Einstellung, wenn du sie hier nicht findest schau mal dort, oder dann vielleicht drüben, oder sonstwo, notfalls bei 'ner alten version die eigentlich gar nicht mehr existieren sollte -- und wenn das alle snix wird, dann schmeiß eben 'ne Exception". Toll, was? :)

Also wenn Konfigurationsmanagement, dann bitte auch vernünftig. Das ist nun herausgekommen: Benenne "version" in "configuration_id" um. Mache eine neue Entität Configuration und lasse für jedes Modul sauber angelegte Konfigurationen zu, die ein Admin nach Belieben zusammenstellen und aktivieren/deaktivieren kann. Wenn ein neues Modul installiert wird, so kann entweder eine neue Konfiguration angelegt werden, oder allenfalls neu hinzugekommene AppSettings werden einfach in die bestehende Konfiguration übernommen. Wenn bestimmte Einstellungen in der neuen Modulversion nicht mehr verwendet werden, dann stören sie auch keinen, wenn sie weiterhin in der Datenbank herumgeistern (es gibt da allerdings auch eine "Cleanup Module Configuration"-Option ;-)). Die configurations-Tabelle schaut so aus:

configurations(id, name, description, moduleid, userid, createdat, updatedat)

Es folgt nun ein Wenig Pseudocode, der in etwa klar machen soll, nach welchen Regeln nun Einstellungen geladen werden:

if modulname_angegeben? and modul_vorhanden?(modulname)
  modul = modulname
else
  if not core_option_angegeben? and modul_vorhanden?(@@module_name)
    modul = @@module_name
  elsif core_option_angegeben?
    modul = 'core'
  end
end
if modul.nil?
  raise "Modul nicht vorhanden"
end

if einstellung_vorhanden?(für_modul) and modul != core
  return einstellung(für_modul)
elsif core_option_angegeben? or modul == core
  if einstellung_vorhanden?(für_core)
    return einstellung(für_core)
  end
end
if default_option_angegeben?
  return :default
end
raise Exception(nicht gefunden)

Die Versionsnummer taucht nirgends mehr auf, darum kümmert sich wie gesagt nun das Configuration-Objekt. Damit lassen sich Konfigurationen auch benennen. Beispielsweise ist es damit auch möglich, während Wartungsphasen den Zugriff au alle Module durch Auswahl einer entsprechenden Konfiguration zu sperren. Ach überhaupt ist vieles denkbar. Ich denke mal, dass sich heute wieder einiges an Flexiblität in der Anwendung etabliert hat ;-)

October 31, 2007

Ein Bisschen GUI muss sein...

Aus nicht weiter beschreibenswürdigem Anlass wurde ich heute mal wieder auf das schon länger nicht mehr von mir benutzte Programm Kommander aufmerksam. Für die nicht-KDE-Benutzer unter euch (O_o, ich fürchte hier tun sich gerade Abgründe auf... ;-)): Das ist ein tolles Programm um mal eben schnell GUIs für Konsolenprogramme, einfache (OK, gerne auch komplizierte) scriptbasierte Anwendungen und dergleichen mehr zu schreiben. Nachdem ich lange damit herumgespielt hatte und dabei die Innereien von KDE (genauer genommen die DCOP-Schnittstelle, eine einfach nur wundervolle Sache!) näher kennen gelernt hatte, kam ich auf die Idee, die mir schon vor ewigen Zeiten mal gekommen war: eine einfache GUI für script/generate zu schaffen. Nun mag das trivial klingen -- und das ist es auch. Tja, was gibt's mehr zu sagen... So sieht dat Dingen aus:

snap16.png

Und hier kann man sich dat Dingen herunterladen (yepp, I'ts GPLed): Download Rails Helper.

Und Aufrufen kann man dat Dingen mit:

kmdr-executor rails-helper.kmdr

Außerdem kann man das in KDE's hübsche Entwicklungsumgebung KDevelop als Menüpunkt einbinden, sodass man immer nur zwei Klicks oder eine Tastenkombination davon entfernt ist:

snap15.png

Zur Erklärung der trivialen GUI: "name" durch den Namen des zu erstellenden Controllers/Models/Scaffolds/Migration ersetzen, im Falle von Controller oder Scaffold bis zu 6 Methodennamen eingeben (die Felder werden sonst ausgeblendet, also selbsterklärender geht nicht...) und "GENERATE" aufrufen -- in der Konsole (da wo auf dem Screenshot noch nichts steht) landet die Ausgabe von script/generate.

Das war's mal wieder für heute! Gute Nacht und bis zum nächsten Mal ;-)