Imported Upstream version 4.6.2
This commit is contained in:
997
doc/guide/guide.org
Normal file
997
doc/guide/guide.org
Normal file
@@ -0,0 +1,997 @@
|
||||
#+OPTIONS: ^:{}
|
||||
#+EMAIL: abokovoy@redhat.com
|
||||
#+AUTHOR: Alexander Bokovoy
|
||||
#+STYLE: <style type="text/css">
|
||||
#+STYLE: pre {
|
||||
#+STYLE: border: 1pt solid #000000;
|
||||
#+STYLE: background-color: #404040;
|
||||
#+STYLE: color: white;
|
||||
#+STYLE: }
|
||||
#+STYLE: .src {width: 940px;}
|
||||
#+STYLE: dt {width: 400px; margin 25px auto;}
|
||||
#+STYLE: dd {width: 940px;}
|
||||
#+STYLE: p {text-align:justify;}
|
||||
#+STYLE: body {width: 960px;
|
||||
#+STYLE: margin: 0 auto;
|
||||
#+STYLE: }
|
||||
#+STYLE: div#content {margin: 0 10px 0 10px;
|
||||
#+STYLE: display: inline;
|
||||
#+STYLE: float: left;
|
||||
#+STYLE: width: 940px;
|
||||
#+STYLE: overflow: hidden;}
|
||||
#+STYLE: </style>
|
||||
Extending FreeIPA
|
||||
* Introduction
|
||||
FreeIPA is an integrated security information management solution. There is a common
|
||||
framework written in Python to command LDAP server provided by a 389-ds project, certificate
|
||||
services of a Dogtag project, and a MIT Kerberos server, as well as configuring various other
|
||||
services typically used to maintain integrity of an enterprise environment, like DNS and
|
||||
time management (NTP). The framework is written in Python, runs at a server side, and
|
||||
provides access via command line tools or web-based user interface.
|
||||
|
||||
As core parts of the framework are implemented as pluggable modules, it is possible to
|
||||
extend FreeIPA on multiple levels. This document attempts to present general ideas and
|
||||
ways to make use of most of extensibility points in FreeIPA.
|
||||
|
||||
For information management solutions extensibility could mean multiple things. Information
|
||||
objects that are managed could be extended themselves or new objects could be added. New
|
||||
operations on existing objects might become needed or certain aspects of an object should
|
||||
be hidden in a specific environment. All these tasks may require quite different approaches
|
||||
to implement.
|
||||
|
||||
Following chapters will cover high-level design of FreeIPA and dive into details of its core
|
||||
framework. Knowledge of Python programming language basics is required. Understanding
|
||||
LDAP concepts is desirable, though it is not required for simple
|
||||
extensions as FreeIPA attempts to provide sufficient mapping of LDAP concepts onto less
|
||||
complex structures and Python objects, lowering a barrier to fine tune FreeIPA for
|
||||
the specific use cases.
|
||||
* High level design
|
||||
FreeIPA core is written in Python programming language. The data is stored in LDAP
|
||||
database, and client-server paradigm is used for managing it. A FreeIPA server instance
|
||||
runs its own LDAP database, provided by 389-ds project (formerly Fedora Directory
|
||||
Server). A single instance of LDAP database corresponds to the single FreeIPA
|
||||
domain. Access to all information stored in the database is provided via FreeIPA server
|
||||
core which is run as a simple WSGI application which uses XML-RPC and JSON to exchange
|
||||
requests with its own clients.
|
||||
|
||||
Multiple replicas of the FreeIPA instance can be created on different servers, they are
|
||||
managed with the help of replication mechanisms of 389-ds directory server.
|
||||
|
||||
As LDAP database is used for data storage, LDAP's Access Control Model is used to provide
|
||||
privilege separation and Kerberos tickets are used to pass-through assertion of
|
||||
authenticity. As Kerberos server is using the same LDAP database instance, use of Kerberos
|
||||
tickets allows to perform operations against the database on the server if a client is
|
||||
capable to forward such tickets via communication channels selected for the operation.
|
||||
|
||||
When FreeIPA client connects to FreeIPA server, a Kerberos ticket is forwarded
|
||||
to the server and operations against LDAP database are performed under identity
|
||||
authenticated when the ticket was issued. As LDAP database also uses Kerberos to establish
|
||||
identity of a client, Access Control Information attributes can be used to limit what
|
||||
entries could be accessed and what operations could be performed.
|
||||
|
||||
The approach allows to delegate operations from a FreeIPA client to the FreeIPA server
|
||||
and in general gives FreeIPA server ability to interact with any Kerberos-aware service on
|
||||
behalf of the client. It also allows to keep FreeIPA client side implementation relatively
|
||||
light-weight: all it needs to do is to be able to forward Kerberos ticket, process XML-RPC or
|
||||
JSON, and present resulting responses to the user.
|
||||
|
||||
Besides run-time core, FreeIPA includes few configuration tools. These tools
|
||||
are split between server and client. Server-side tools are used when an instance of
|
||||
FreeIPA server is set up and configured, while client-side tools are used to configure client
|
||||
systems. While the server tools are used to configure LDAP database, put proper schema
|
||||
definitions in use, create Kerberos domain, Certificate Authority and configure all
|
||||
corresponding services, client side is more limited to configure PAM/NSS modules to work
|
||||
against FreeIPA server, and make sure that appropriate information about the client host
|
||||
is recorded in FreeIPA databases.
|
||||
* Core plug-in framework
|
||||
FreeIPA core defines few fundamentals. These are managed objects, their properties, and
|
||||
methods to apply actions to the objects. Methods, in turn, are commands that are
|
||||
associated with a specific object. Additionally, there are commands that do not have
|
||||
directly associated objects and may perform actions over few of those. Objects are stored
|
||||
using data store represented by a back end, and one of most useful back ends is LDAP store
|
||||
back end.
|
||||
|
||||
Altogether, set of =Object=, =Method=, =Command=, and =Backend= instances
|
||||
represent application programming interface, API, of FreeIPA core framework.
|
||||
|
||||
In Python programming language object oriented support is implemented using a fairly
|
||||
simple concept that allows to modify instances in place, extending or removing their
|
||||
properties and methods. While this concept is highly useful, in security-oriented
|
||||
frameworks ability to lock down and trace origins of changes is also important. FreeIPA core
|
||||
attempts to implement locking down feature by artificially making instances of foundation
|
||||
classes read-only after their initialization has happened. If an attempt to modify object
|
||||
happens after it was locked down, an exception is thrown. There are many classes
|
||||
following this pattern.
|
||||
|
||||
For example, =ipalib.frontend.Command= class is derived from =ipalib.frontend.HasParam= class
|
||||
that derives from =ipalib.plugable.Plugin= class which, in turn, is derived from
|
||||
=ipalib.base.ReadOnly= class.
|
||||
|
||||
As result, every command has typed parameters and can dynamically be added to the
|
||||
framework. At the same time, one cannot modify the properties of the command accidentally
|
||||
once it is instantiated. This protects from modifications and enforces true nature of the
|
||||
commands: they cannot have state that is carried over across multiple calls to the same
|
||||
command unless the state is changing globally the whole environment around.
|
||||
|
||||
Environment also holds information about the context of execution. The /context/ is
|
||||
important part of the FreeIPA framework as it also defines which methods of
|
||||
the command instance are called in order to perform action. /Context/ in itself is defined
|
||||
by the /environment/ which gives means to catch and store certain information about execution.
|
||||
As with commands themselves, once instantiated, environment cannot be changed.
|
||||
|
||||
By default, for primary FreeIPA use, there are three major contexts defined: server,
|
||||
client, and installer/updates.
|
||||
|
||||
- /server context/ :: plugins are registered and communicate with clients via XML-RPC and JSON
|
||||
listeners. They validate any arguments and options defined and then execute whatever
|
||||
action they supposed to perform
|
||||
- /client context/ :: plugins are used to validate any arguments and options they take and
|
||||
then forward the request to the FreeIPA server.
|
||||
- /installer context/, /updates context/ :: plugins specific to installation and update
|
||||
are loaded and registered. This context can be used to extend possible operations
|
||||
during set up of FreeIPA server.
|
||||
|
||||
A user may define any context they want. FreeIPA names server context as '~server~'. When
|
||||
using the ~ipa~ command line tool the context is '~cli~'. Server installation tools, in
|
||||
particular, '~ipa-ldap-updater~', use special '~updates~' context to load specialized
|
||||
plugins useful during update of the installed FreeIPA server.
|
||||
|
||||
Because these utilities use the same framework they will do the same validation, set default
|
||||
values, and perform other basic actions in all contexts. This can help to save a
|
||||
round-trip when testing for invalid data. However, for client-server communication, the
|
||||
server is always authoritative and can re-define what the client has sent.
|
||||
|
||||
** Name space
|
||||
FreeIPA has one special type of read-only objects: =NameSpace=. =NameSpace= class gives an
|
||||
ordered, immutable mapping object whose values can also be accessed as attributes. A
|
||||
=NameSpace= instance is constructed from iterable providing its members, which are simply
|
||||
arbitrary objects with =name= attribute. This attribute must conform to two following
|
||||
rules:
|
||||
- Its value must be unique among the members of the name space
|
||||
- Its value must pass the =check_name()= function =ipalib.base= module.
|
||||
|
||||
=check_name()= function encodes a simple rule of a lower-case Python identifier that
|
||||
neither starts nor ends with an underscore. Actual regular expression that codifies this
|
||||
rule is =NAME_REGEX= within =ipalib.constants= module.
|
||||
|
||||
Once name space is created, it locks itself down and becomes read-only. It means that
|
||||
while original objects accessed through the name space might change, the references to
|
||||
them via name space will stay intact. They cannot be removed or changed to point to other
|
||||
objects.
|
||||
|
||||
The name spaces are used widely in FreeIPA core framework. As mentioned earlier, API
|
||||
includes set of objects, commands, and methods. Objects include properties that are
|
||||
defined before lock-down. At object's lock-down parameters are placed into a name space
|
||||
and that locks them down so that no parameter specification can change. Command's
|
||||
parameters and options also locked down and cannot change once command instance is
|
||||
instantiated.
|
||||
|
||||
** Parameters
|
||||
=Param= class is used to define attributes, arguments, or options throughout FreeIPA core
|
||||
framework. The =Param= base class is not used directly but rather sub-classed to define
|
||||
properties like passwords or specific data types like =Str= or =Int=.
|
||||
|
||||
Instances of classes inherited from =Param= base class give uniform access to the
|
||||
properties required to command line interface, Web UI, and internally to FreeIPA
|
||||
code. Following properties are most important:
|
||||
- /name/ :: name of the parameter used internally to address the parameter in Python
|
||||
code. The /name/ could include special characters to designate a =Param= spec.
|
||||
- /cli_name/ :: optional name of the parameter to use in command line
|
||||
interface. FreeIPA's CLI sets a mechanism to automatically translate
|
||||
from a command line option name to a parameter's /name/ if /cli_name/
|
||||
is specified.
|
||||
- /label/ :: A short phrase describing the parameter. It is used on the CLI when
|
||||
interactively prompting for the values, and as a label for the form inputs
|
||||
in the Web UI. The /label/ should start with an initial capital letter.
|
||||
- /doc/ :: A long description of the parameter. It is used by the CLI when displaying the
|
||||
help information for a command, and as an extra instruction for the form input
|
||||
on the Web UI. By default the /doc/ is the same as the /label/ but can be
|
||||
overridden when a =Param= instance is created. As with /label/, /doc/ should
|
||||
start with an initial capital letter and additionally should not end with any
|
||||
punctuation.
|
||||
- /required/ :: If set to =True=, means this parameter is required to supply. All
|
||||
parameters are required by default and that means that /required/
|
||||
property should only be specified when parameter *is not required*.
|
||||
- /multivalue/ :: if set to =True=, means this parameter can accept a Python's tuple of
|
||||
values. By default all parameters are *single-valued*.
|
||||
|
||||
When parameter /name/ has any of ~?~, ~*~, or ~+~ characters, it is treated as parameter
|
||||
spec and is used to specify whether parameter is required, and should it be
|
||||
multivalued. Following syntax is used:
|
||||
|
||||
| Spec | Name | Required | Multivalue |
|
||||
|--------+-------+----------+------------|
|
||||
| 'var' | 'var' | True | False |
|
||||
| 'var?' | 'var' | False | False |
|
||||
| 'var*' | 'var' | False | True |
|
||||
| 'var+' | 'var' | True | True |
|
||||
|
||||
Access to the value stored by the =Param= class is given through a callable interface:
|
||||
|
||||
#+BEGIN_SRC python
|
||||
age = Int('age', label='Age', default=100)
|
||||
print age(10)
|
||||
#+END_SRC
|
||||
|
||||
Following parameter classes are defined and used throughout FreeIPA framework:
|
||||
- /Bool/ :: boolean parameters that are stored in Python's ~bool~ type, therefore, they
|
||||
return either ~True~ or ~False~ value. However, they accept ~1~, ~True~
|
||||
(Python boolean), or Unicode strings '~1~', '~true~' and '~TRUE~' as truth value, and ~0~,
|
||||
~False~ (Python boolean), or Unicode strings '~0~', '~false~', and '~FALSE~' as false.
|
||||
- /Flag/ :: boolean parameters which always have default value. Property /default/ can be
|
||||
used to set the value. Defaults to ~False~:
|
||||
#+BEGIN_SRC python
|
||||
verbose = Flag('verbose', default=True)
|
||||
#+END_SRC
|
||||
- /Int/ :: integer parameters that are stored in Python's int type. Two additional properties can be
|
||||
specified when constructing =Int= parameter:
|
||||
- /minvalue/ :: minimal value that this parameter accepts, defaults to =MININT=
|
||||
- /maxvalue/ :: maximum value this parameter can accept, defaults to =MAXINT=
|
||||
- /Decimal/ :: floating point parameters that are stored in Python's Decimal type. =Decimal= has
|
||||
the same two additional properties as =Int=. Unlike =Int=, there are no
|
||||
default values for the minimal and maximum boundaries.
|
||||
- /Bytes/ :: a parameter to represent binary data.
|
||||
- /Str/ :: parameter representing a Unicode text. Both /Bytes/ and /Str/ parameters accept
|
||||
following additional properties:
|
||||
- /minlength/ :: minimal length of the parameter
|
||||
- /maxlength/ :: maximum length of the parameter
|
||||
- /length/ :: length of the parameters
|
||||
- /pattern/ :: regular expression applied to the parameter's value to check its
|
||||
validness
|
||||
- /pattern_errmsg/ :: an error message to show when regular expression check fails
|
||||
- /IA5Str/ :: string parameter as defined by RFC 4517. It means all characters of the
|
||||
string must be ASCII characters (7-bit).
|
||||
- /Password/ :: parameter to store passwords in Python =unicode= type. /Password/ has one
|
||||
additional property:
|
||||
- /confirm/ :: boolean specifying whether password should be confirmed
|
||||
when entered. The confirmation is enabled by default.
|
||||
- /Enum/ :: parameter can have one of predefined values that are specified with /values/
|
||||
property which is a Python's =tuple=.
|
||||
|
||||
For most common case of enumerable strings there are two parameters:
|
||||
- /BytesEnum/ :: parameter value should be one of predefined =unicode= strings
|
||||
- /StrEnum/ :: equivalent to /BytesEnum/. Originally /BytesEnum/ was stored in Python's
|
||||
=str= class instances but to be aligned with Python 3.0 changes both
|
||||
classes moved to store as =unicode=.
|
||||
|
||||
When more than one value should be accepted, there is /List/ parameter that allows to
|
||||
provide list of strings separated by a separator, default to ','. Also, the /List/
|
||||
parameter skips spaces before the next item in the list unless property /skipspace/ is set to False:
|
||||
#+BEGIN_SRC python
|
||||
names = List('names', separator=',', skipspace=True)
|
||||
names_list = names(u'John Doe, John Lee, Brad Moe')
|
||||
# names_list is (u'John Doe', u'John Lee', u'Brad Moe')
|
||||
names = List('names', separator=',', skipspace=False)
|
||||
names_list = names(u'John Doe, John Lee, Brad Moe')
|
||||
# names_list is (u'John Doe', u' John Lee', u' Brad Moe')
|
||||
#+END_SRC
|
||||
|
||||
** Objects
|
||||
The data manipulated by FreeIPA is represented by an Object class instances. Instance of
|
||||
an Object class is a collection of properties, accepted parameters, action methods, and a
|
||||
reference to where this object's data is preserved. Each object also has a reference to a
|
||||
property that represents a primary key for retrieving the object.
|
||||
|
||||
In addition to properties and parameters, Object class instances hold their labels to use
|
||||
in user interfaces. In practice, there are few differences in how labels are presented
|
||||
depending on whether it is command line interface or a Web UI, but they can be ignored at
|
||||
this point.
|
||||
|
||||
To be useful, all Object sub-classes need to override =takes_param= property. This is
|
||||
where most of flexibility of FreeIPA comes from.
|
||||
|
||||
*** takes_param attribute
|
||||
Properties of every object derived from Object class can be specified manually but FreeIPA
|
||||
gives a handy mechanism to perform descriptive specification. Each =Object= class has
|
||||
=Object.takes_param= attribute which defines a specification of all parameters this object
|
||||
type is accepting.
|
||||
|
||||
Next example shows how to create new object type. We create an aquarium tank by defining
|
||||
its dimensions and specifying which fish is living there.
|
||||
#+BEGIN_SRC python -n -r -l '(%s)'
|
||||
from ipalib import api, Object
|
||||
class tank(Object):
|
||||
takes_params = (
|
||||
StrEnum('species*', label=u'Species', doc=u'Fish species',
|
||||
values=(u'Angelfish', u'Betta', u'Cichlid', u'Firemouth')),
|
||||
Decimal('height', label=u'Height', doc=u'height in mm', default='400.0'),
|
||||
Decimal('width', label=u'Width', doc=u'width in mm', default='400.0'),
|
||||
Decimal('depth', label=u'Depth', doc=u'Depth in mm', default='300.0')
|
||||
)
|
||||
|
||||
api.register(tank) (ref:register)
|
||||
api.finalize() (ref:finalize)
|
||||
print list(api.Object.tank.params)
|
||||
# ['species', 'height', 'width', 'depth']
|
||||
#+END_SRC
|
||||
|
||||
First we define new class, =tank=, that takes four parameters. On line [[(register)]] we register the class
|
||||
in FreeIPA's API instance, api. This creates =tank= object in =api.Object= name
|
||||
space. Many objects can be added into the API up until =api.finalize()= is called as we do
|
||||
on line [[(finalize)]].
|
||||
|
||||
When =api.finalize()= is called, all name spaces are locked down and all registered Python
|
||||
objects in those name spaces are also finalized which in turn locks their structure down
|
||||
as well.
|
||||
|
||||
As result, once we have finalized our API instance, every registered Object can be
|
||||
accessed through =api.Object.<name>=. Our aquarium tank object now has defined =params=
|
||||
attribute which is a name space holding all =Param= instances. Thus we can introspect and
|
||||
see which parameters this object has.
|
||||
|
||||
At this point we can't do anything reasonable with our aquarium tank yet because we
|
||||
haven't defined methods to handle it. In addition, our object isn't very useful as it does
|
||||
not know how to store the information about aquarium's dimensions and species living in
|
||||
it.
|
||||
|
||||
*** Object methods
|
||||
Methods perform actions on the associated objects. The association of methods and objects
|
||||
is done through naming convention rather than using programming language features. FreeIPA
|
||||
expects methods operating on an object =<name>= to be named =<name>_<action>=:
|
||||
#+BEGIN_SRC python
|
||||
class tank_create(Method):
|
||||
def execute(self, **options):
|
||||
# create new aquarium tank
|
||||
|
||||
api.register(tank_create)
|
||||
|
||||
class tank_populate(Method):
|
||||
def execute(self, **options):
|
||||
# populate the aquarium tank with fish
|
||||
|
||||
api.register(tank_populate)
|
||||
#+END_SRC
|
||||
|
||||
As can be seen, each method is a separate Python class. This approach allows to maintain
|
||||
complexity of methods isolated from each other and from the complexity of the objects and
|
||||
their storage which is probably most important aspect due to LDAP complexity overall.
|
||||
|
||||
The linking between objects and their methods goes further. All parameters defined for an
|
||||
object, may be used as arguments of the methods without explicit declaration. This means
|
||||
=api.Method.tank_populate= will accept ~species~ argument.
|
||||
|
||||
*** Methods with storage back ends
|
||||
In order to store the information, =Object= class instances require a back end. FreeIPA
|
||||
defines several back ends but the ones that could store data are derived of
|
||||
=ipalib.CrudBackend=. CRUD, or /Create/, /Retrieve/, /Update/, and /Delete/, are basic
|
||||
operations that could be performed with corresponding objects. =ipalib.crud.CrudBackend=
|
||||
is an abstract class, it only defines functions that should be overridden in classes that
|
||||
actually implement the back end operations.
|
||||
|
||||
As back end is not used directly, FreeIPA defines methods that could use back end and
|
||||
operate on object's defined by certain criteria. Each method is defined as a separate
|
||||
Python class. As CRUD acronym suggests, there are four base operations:
|
||||
=ipalib.crud.Create=, =ipalib.crud.Retrieve=, =ipalib.crud.Update=,
|
||||
=ipalib.crud.Delete=. In addition, method =ipalib.crud.Search= allows to retrieve all
|
||||
entries that match a given search criteria.
|
||||
|
||||
When objects are defined and the back end is known, methods can be used to manipulate
|
||||
information stored by the back end. Most of useful operations combine some of CRUD base
|
||||
operations to perform their tasks.
|
||||
|
||||
In order to support flexible way to extend methods, FreeIPA gives special treatment for
|
||||
the LDAP back end. Methods using LDAP back end hide complexity of handling LDAP queries and
|
||||
allow to register user-provided functions that are called before or after method. This
|
||||
mechanism is defined by ipalib.plugins.baseldap.CallbackInterface and used by LDAP-aware
|
||||
CRUD classes, =LDAPCreate=, =LDAPRetrieve=, =LDAPUpdate=, =LDAPDelete=, and an analogue to
|
||||
=ipalib.crud.Search=, =LDAPSearch=. There are also classes that define methods to operate
|
||||
on reverse relationships between objects in LDAP to allow addition or removal of
|
||||
membership information both in forward and reverse directions: =LDAPAddMember=,
|
||||
=LDAPModMember=, =LDAPRemoveMember=, =LDAPAddReverseMember=, =LDAPModReverseMember=, =LDAPRemoveReverseMember=.
|
||||
|
||||
Most of CRUD classes are based on a =LDAPQuery= class which generalizes concept of
|
||||
querying a record addressed with a primary key and supports JSON marshalling of the
|
||||
queried attributes and their values.
|
||||
|
||||
Base LDAP operation classes implement everything needed to create typical methods to
|
||||
work with self-contained objects stored in LDAP.
|
||||
|
||||
*** LDAPObject class
|
||||
A large class of objects is LDAPObject. LDAPObject instances represent entries stored in
|
||||
FreeIPA LDAP database instance. They are referenced by their distinguished name, DN, and
|
||||
able to represent complex relationships between entries in LDAP like direct and indirect
|
||||
membership.
|
||||
|
||||
Any class derived from LDAPObject needs to re-define few properties so that base class can
|
||||
properly function for the specific object that is defined by the class. Below are commonly
|
||||
redefined properties:
|
||||
- /container_dn/ :: DN of the container for this object entries in LDAP. This one
|
||||
usually comes from the environment associated with the API and by default is populated
|
||||
from the =DEFAULT_CONFIG= of =ipalibs.constants=. For example, all accounts are
|
||||
stored under =cn=accounts=, with users are under =cn=users,cn=accounts= and groups
|
||||
are under =cn=groups,cn=accounts=. In case of a new object added, it
|
||||
is reasonable to select its container coordinated to default configuration.
|
||||
- /object_class/ :: list of LDAP object classes associated with the object
|
||||
- /search_attributes/ :: list of attributes that will be used for search
|
||||
- /default_attributes/ :: list of attributes that are always returned by searches
|
||||
- /uuid_attribute/ :: an attribute that defines uniqueness of the entry
|
||||
- /attribute_members/ :: a dict defining relations between other objects and this
|
||||
one. Key is the name of attribute and value is a list of objects this attribute may
|
||||
refer to. For example, =host= object defines that =memberof= attribute of a
|
||||
host may refer to a =hostgroup=, =netgroup=, =role=, =hbacrule=, or =sudorule=
|
||||
object. In other words, it means that =host= could be a member of any of those
|
||||
objects.
|
||||
- /reverse_members/ :: a dict defining reverse relations between this object and other
|
||||
objects. Key is the name of attribute and value is the name of an object that refers
|
||||
to this object with the attribute. For example, =role= object defines that =member=
|
||||
attribute of a =privilege= refers to a =role= object.
|
||||
- /password_attributes/ :: list of pairs defining an attribute in LDAP and a property of
|
||||
a Python dictionary representing the LDAP object attributes that will be set
|
||||
accordingly if such attribute exists in the LDAP entry. As passwords have restricted
|
||||
access, often one needs only to know that there is a password set on the entry to
|
||||
perform additional processing.
|
||||
- /relationships/ :: a dict defining existing relationship criteria associated with the
|
||||
object. These are used in Web UI to allow filtering of objects by the criteria. The
|
||||
value is defined as a tuple of an UI label and two prefixes: inclusive and exclusive
|
||||
that are prepended to the attribute parameter when options are generated by the
|
||||
framework. LDAPObject defines few default criteria: /member/, /memberof/,
|
||||
/memberindirect/, /memberofindirect/, and objects can redefine or append more. Due
|
||||
to regularity of the design of LDAP objects, default criteria already makes it
|
||||
possible to apply searches almost uniformly: one can ask for membership of a user in
|
||||
a group, as well as for a membership of a role in a privilege without explicitly
|
||||
defining those relationships.
|
||||
|
||||
|
||||
These properties define how translation would go from Python side to and from an LDAP
|
||||
backend.
|
||||
|
||||
As an example, let's see how role is defined. This is fully functioning plugin that
|
||||
provides operations on roles:
|
||||
#+INCLUDE "role.py.txt" src python -n
|
||||
|
||||
* Extending existing object
|
||||
As said earlier, until API instance is finalized, objects, methods, and commands can be
|
||||
added, removed, or modified freely. This allows to extend existing objects. Before API is
|
||||
finalized, we cannot address objects through the unified interface as =api.Object.foo=,
|
||||
but for almost all cases an object named =foo= is defined in a plugin
|
||||
=ipalib.plugins.foo=.
|
||||
|
||||
1. Add new parameter:
|
||||
#+BEGIN_SRC python -n
|
||||
from ipalib.plugins.user import user
|
||||
from ipalib import Str, _
|
||||
user.takes_params += (
|
||||
Str('foo',
|
||||
cli_name='foo',
|
||||
label=_('Foo'),
|
||||
),
|
||||
)
|
||||
#+END_SRC
|
||||
2. Re-define User object label to use organisation-specific terminology in Web UI:
|
||||
#+BEGIN_SRC python -n
|
||||
from ipalib.plugins.user import user
|
||||
from ipalib import text
|
||||
|
||||
_ = text.GettextFactory(domain='extend-ipa')
|
||||
user.label = _('Staff')
|
||||
user.label_singular = _('Engineer')
|
||||
#+END_SRC
|
||||
Note that we re-defined locally =_= method to use different ~GettextFactory~. As
|
||||
GettextFactory is supporting a single translation domain, all new translation terms need
|
||||
to be placed in a separate translation domain and referred accordingly. Python rules for
|
||||
scoping will keep this symbol as ~<package>._~ and as nobody imports it explicitly, it
|
||||
will not interfere with the framework's provided ~text._~.
|
||||
3. Assume =/dev/null= as default shell for all new users:
|
||||
#+BEGIN_SRC python -n -r
|
||||
from ipalib.plugins.user import user_add
|
||||
|
||||
def override_default_shell_cb(self, ldap, dn.
|
||||
entry_attrs, attrs_list,
|
||||
*keys, **options):
|
||||
if 'loginshell' in entry_attrs:
|
||||
default_shell = [self.api.Object.user.params['loginshell'].default]
|
||||
if entry_attrs['loginshell'] == default_shell:
|
||||
entry_attrs['loginshell'] = [u'/dev/null']
|
||||
|
||||
user_add.register_pre_callback(override_default_shell_cb)
|
||||
#+END_SRC
|
||||
|
||||
The last example exploits a powerful feature available for every method of LDAPObject:
|
||||
registered callbacks.
|
||||
* Extending existing method
|
||||
For objects stored in LDAP database instance all methods support adding callbacks. A
|
||||
/callback/ is a user-provided function that is called at certain point of execution of a
|
||||
method.
|
||||
|
||||
There are four types of callbacks:
|
||||
- /PRE callback/ :: called before executing the method's action. Allows to modify passed
|
||||
arguments, do additional validation or data transformation and
|
||||
specific access control beyond what is provided by the framework.
|
||||
- /POST callback/ :: called after executing the method's action. Allows to analyze results
|
||||
of the action and perform additional actions or modify output.
|
||||
- /EXC callback/ :: called in case execution of the method's action caused an execution
|
||||
error. These callbacks provide means to recover from an erroneous execution.
|
||||
- /INTERACTIVE callback/ :: called at a client context to allow a command to decide if
|
||||
additional parameters should be requested from an user. This mechanism especially
|
||||
useful to simplify complex interaction when there are several levels of possible
|
||||
scenarios depending on what was provided at a client side.
|
||||
|
||||
All callback types are available to any class derived from =CallbackInterface=
|
||||
class. These include all LDAP-based CRUD methods.
|
||||
|
||||
Callback registration methods accept a reference to callable and optionally ordering
|
||||
argument =first= (~False~ by default) to allow the callback be executed before previously
|
||||
registered callbacks of this type.
|
||||
|
||||
=CallbackInterface= class provides following class methods:
|
||||
- =register_pre_callback= :: registers /PRE/ callback
|
||||
- =register_post_callback= :: registers /POST/ callback
|
||||
- =register_exc_callback= :: registers /EXC/ callback for purpose of recovering from
|
||||
execution errors
|
||||
- =register_interactive_prompt_callback= :: registers callbacks called by the client
|
||||
context.
|
||||
|
||||
Let's look again at the last example:
|
||||
#+BEGIN_SRC python -n -r
|
||||
from ipalib.plugins.user import user_add
|
||||
|
||||
def override_default_shell_cb(self, ldap, dn.
|
||||
entry_attrs, attrs_list,
|
||||
*keys, **options):
|
||||
if 'loginshell' in entry_attrs:
|
||||
default_shell = [self.api.Object.user.params['loginshell'].default]
|
||||
if entry_attrs['loginshell'] == default_shell:
|
||||
entry_attrs['loginshell'] = [u'/dev/null']
|
||||
|
||||
user_add.register_pre_callback(override_default_shell_cb)
|
||||
#+END_SRC
|
||||
|
||||
This extension defines a pre-processing callback that accepts number of arguments:
|
||||
- /ldap/ :: reference to the back end to store and retrieve the object's data
|
||||
- /dn/ :: reference to the object data in LDAP
|
||||
- /entry_attrs/ :: arguments and options of the command and their values as a
|
||||
dictionary. All values in /entry_attrs/ will be used for communicating
|
||||
with LDAP store, thus replacing values should be done with care. For
|
||||
details please see Python LDAP module documentation
|
||||
- /attrs_list/ :: list of all attributes we intend to fetch from the back end
|
||||
- /keys/ :: arguments of the command
|
||||
- /options/ :: all other unidentified parameters passed to the method
|
||||
|
||||
Arguments of a post-processing callback, /POST/, are slightly different. As action is
|
||||
already performed and the attributes of the entry are fetched back from the back end,
|
||||
there is no need to provide =attrs_list=:
|
||||
#+BEGIN_SRC python -n -r
|
||||
from ipalib.plugins.user import user_add
|
||||
def verify_shell_cb(self, ldap, dn. entry_attrs,
|
||||
*keys, **options):
|
||||
if 'loginshell' in entry_attrs:
|
||||
default_shell = [self.api.Object.user.params['loginshell'].default]
|
||||
if entry_attrs['loginshell'] == default_shell:
|
||||
# report that default shell is assigned
|
||||
|
||||
user_add.register_post_callback(verify_shell_cb)
|
||||
#+END_SRC
|
||||
|
||||
Execution error callback, /EXC/, has following signature:
|
||||
#+BEGIN_SRC python -n
|
||||
def user_add_error_cb(self, args, options, exc,
|
||||
call_func, *call_args, **call_kwargs):
|
||||
return
|
||||
#+END_SRC
|
||||
|
||||
where arguments have following meaning:
|
||||
- /args/ :: arguments of the original method
|
||||
- /options/ :: options of the original method
|
||||
- /exc/ :: exception object thrown by a /call_func/
|
||||
- /call_func/ :: function that was called by the method and caused the error of
|
||||
execution. In case of LDAP-based methods this is often =ldap.add_entry()=
|
||||
or =ldap.modify_entry()=, or a similar function
|
||||
- /call_args/ :: first argument passed to the /call_func/
|
||||
- /call_kwargs/ :: remaining arguments of /call_func/
|
||||
|
||||
Finally, interactive prompt callback receives /kw/ argument which is a dictionary of all
|
||||
arguments of the command.
|
||||
|
||||
All callbacks are supplied with a reference to the method instance, ~self~, unless the
|
||||
callback itself has an attribute called '~im_self~'. As can be seen in callback examples,
|
||||
self reference recursively provides access to the whole FreeIPA API structure.
|
||||
|
||||
This approach gives complete control of existing FreeIPA methods without
|
||||
deep dive into details of LDAP programming even if the framework allows such a deep dive.
|
||||
|
||||
* Web UI
|
||||
FreeIPA framework has two major client applications: Web UI and command line-based client
|
||||
tool, ~ipa~. Web UI communicates with a FreeIPA server running WSGI application that
|
||||
accepts JSON-formatted requests and translates them to calls to FreeIPA plugins.
|
||||
|
||||
A following code in ~install/share/ui/wsgi.py~ defines FreeIPA web application:
|
||||
#+INCLUDE "wsgi.py.txt" src python -n -r
|
||||
|
||||
At line [[(wsgi-app-bootstrap)]] we set up FreeIPA framework with server context. This means
|
||||
plugins are loaded and initialized from following locations:
|
||||
- ~ipalib/plugins/~ -- general FreeIPA plugins, available for all contexts
|
||||
- ~ipaserver/plugins/~ -- server-specific plugins, available in '~server~' context
|
||||
|
||||
With =api.finalize()= call at line [[(wsgi-app-finalize)]] FreeIPA framework is locked down and all
|
||||
components provided by plugins are registered at ~api~ name spaces: =api.Object=,
|
||||
=api.Method=, =api.Command=, =api.Backend=.
|
||||
|
||||
At this point, ~api~ name spaces become usable and our WSGI entry point, defined on lines
|
||||
[[(wsgi-app-start)]] to [[(wsgi-app-end)]] can access =api.Backend.session()= to generate
|
||||
response for WSGI request.
|
||||
|
||||
Web UI itself is written in JavaScript and utilizes JQuery framework. It can be split into
|
||||
three major parts:
|
||||
- /communication/ :: tools defined in ~ipa.js~ to allow talking with FreeIPA server using
|
||||
AJAX requests and JSON formatting
|
||||
- /presentation/ :: tools in ~facet.js~, ~entity.js~, ~search.js~, ~widget.js~, ~add.js~,
|
||||
and ~details.js~ to give basic building blocks of Web UI
|
||||
- /objects/ :: actual implementation of Web UI for FreeIPA objects (user, group, host,
|
||||
rule, and other available objects registered at =api.Object= by the server
|
||||
side)
|
||||
|
||||
The code of these JavaScript files is loaded in ~index.html~ and kicked into work by
|
||||
~webui.js~ where main navigation and document's ~onready~ event handler are defined. In
|
||||
addition, ~index.html~ imports ~extension.js~ file where all extensions to Web UI can be
|
||||
registered or referenced. As ~extension.js~ is loaded after all other Web UI JavaScript
|
||||
files but before ~webui.js~, it can already use all tools of the Web UI.
|
||||
|
||||
The execution of Web UI starts with the call of =IPA.init()= function which does
|
||||
following:
|
||||
1. Set up AJAX asynchronous communication via POST method using JSON format.
|
||||
2. Fetches meta-data about FreeIPA methods available on the server using JSON format and
|
||||
makes them available as =IPA.methods=.
|
||||
3. Fetches meta-data about FreeIPA objects available on the server using JSON format and
|
||||
makes them available as =IPA.objects=.
|
||||
4. Fetches translations of messages used in the Web UI and makes them available as
|
||||
=IPA.messages=.
|
||||
5. Fetches identity of the user running the Web UI, accessible as =IPA.whoami=.
|
||||
6. Fetches FreeIPA environment specific for Web UI, accessible as =IPA.env=.
|
||||
|
||||
The communication with FreeIPA server is done using =IPA.command()= function. Commands
|
||||
created with =IPA.command()= can later be executed with =execute()= method. This
|
||||
separation of construction and actual execution allows to create multiple commands and
|
||||
combine them together in a single request. Batch requests are created with
|
||||
=IPA.batch_command()= function and command are added to them with =add_command()=
|
||||
method. In addition, FreeIPA Web UI allows to run commands concurrently with
|
||||
=IPA.concurrent_command()= function.
|
||||
|
||||
Web UI has following DOM structure:
|
||||
|-----------------------+-----------------------------------+------------+-----------|
|
||||
| | Container | | |
|
||||
|-----------------------+-----------------------------------+------------+-----------|
|
||||
| background | header | navigation | content |
|
||||
| background-header | header-logo | | |
|
||||
| background-navigation | header-network-activity-indicator | | |
|
||||
| background-left | loggedinas | | |
|
||||
| background-right | | | |
|
||||
|-----------------------+-----------------------------------+------------+-----------|
|
||||
|
||||
~Container~ div is a top-level one, it includes background, header, navigation, content
|
||||
divs. These divs and their parts can be manipulated from the JavaScript code to represent
|
||||
the UI. However, FreeIPA gives an easier way to accomplish this.
|
||||
|
||||
** Facets
|
||||
Facet is a smallest block of FreeIPA Web UI. When facet is defined, it has name, label,
|
||||
link to an entity it is part of, and methods to create, show, load, and hide itself.
|
||||
|
||||
** Entities
|
||||
Entity is addressable group of facets. FreeIPA Web UI provides a declarative way of
|
||||
creating entities and defining their facets based on JavaScript's syntax. Following
|
||||
example is a complete definition of a netgroup facet:
|
||||
#+INCLUDE "netgroup.js" src js2-mode -n
|
||||
|
||||
This definition of a netgroup facet describes:
|
||||
- /details facet/ :: a facet named '~identity~' and three fields, ~cn~, ~description~,
|
||||
and ~nisdomainname~. In addition, ~description~ field is a text area widget. This
|
||||
facet is used to display existing netgroup information.
|
||||
- /association facets/ :: number of facets, linking this one with others. In case of a
|
||||
netgroup, netgroups are linked to facet group ~member~ via different attributes. The
|
||||
definition also adds standard association facets defined in ~entity.js~.
|
||||
- /adder dialog/ :: a dialog to create a new netgroup. The dialog has two fields: ~cn~ and
|
||||
~description~ where ~description~ is again a text area widget.
|
||||
|
||||
Similarly to FreeIPA core framework, created entity needs to be registered to the Web UI
|
||||
via =IPA.register()= method.
|
||||
|
||||
In order to add new entity to the Web UI, one can use ~extension.js~. This file in
|
||||
~/usr/share/ipa/html~ is empty and provided specifically for this purpose.
|
||||
|
||||
As an example, let's define an entity 'Tank' corresponding to our aquarium tank:
|
||||
#+BEGIN_SRC js2-mode -n
|
||||
IPA.tank = {};
|
||||
IPA.tank.entity = function(spec) {
|
||||
var that = IPA.entity(spec);
|
||||
that.init = function(params) {
|
||||
details_facet({
|
||||
sections: [
|
||||
{
|
||||
name: 'identity',
|
||||
fields: [
|
||||
'species', 'height', 'width', 'depth'
|
||||
]
|
||||
}
|
||||
]
|
||||
}).
|
||||
standard_association_facets().
|
||||
adder_dialog({
|
||||
fields: [
|
||||
'species', 'height', 'width', 'depth'
|
||||
]
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
IPA.register('tank', IPA.tank.entity);
|
||||
#+END_SRC
|
||||
|
||||
* Command line tools
|
||||
As an alternative to Web UI, FreeIPA server can be controlled via command-line interface
|
||||
provided by the ~ipa~ utility. This utility is operating under '~client~' context and
|
||||
looks even simpler than Web UI's ~wsgi.py~:
|
||||
#+BEGIN_SRC python -n
|
||||
import sys
|
||||
from ipalib import api, cli
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli.run(api)
|
||||
#+END_SRC
|
||||
|
||||
=cli.run()= is the central running point defined in ~ipalib/cli.py~:
|
||||
#+BEGIN_SRC python -n
|
||||
# <cli.py code> ....
|
||||
cli_plugins = (
|
||||
cli,
|
||||
textui,
|
||||
console,
|
||||
help,
|
||||
show_mappings,
|
||||
)
|
||||
|
||||
|
||||
def run(api):
|
||||
error = None
|
||||
try:
|
||||
(_options, argv) = api.bootstrap_with_global_options(context='cli')
|
||||
for klass in cli_plugins:
|
||||
api.add_plugin(klass)
|
||||
api.finalize()
|
||||
if not 'config_loaded' in api.env and not 'help' in argv:
|
||||
raise NotConfiguredError()
|
||||
sys.exit(api.Backend.cli.run(argv))
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
logger.info('operation aborted')
|
||||
except PublicError as e:
|
||||
error = e
|
||||
except Exception as e:
|
||||
logger.exception('%s: %s', e.__class__.__name__, str(e))
|
||||
error = InternalError()
|
||||
if error is not None:
|
||||
assert isinstance(error, PublicError)
|
||||
logger.error(error.strerror)
|
||||
sys.exit(error.rval)
|
||||
#+END_SRC
|
||||
|
||||
As with WSGI, =api= is bootstraped, though with a client context and using global options
|
||||
from ~/etc/ipa/default.conf~, and command line arguments. In addition to common plugins
|
||||
available in ~ipalib/plugins~, ~cli.py~ adds few command-line specific classes defined in
|
||||
the module itself:
|
||||
- ~cli~ :: a backend for executing from command line interface which does translation of
|
||||
command line option names, basic verification of commands and fallback to show
|
||||
help messages with ~help~ command, execution of the command, and translation of
|
||||
the output to command-line friendly format if this is defined for the command.
|
||||
- ~textui~ :: a backend to nicely format output to stdout which handles conversion from
|
||||
binary to base64, prints text word-wrapped to the terminal width, formats
|
||||
returned complex values so that they can be easily understood by a human
|
||||
being.
|
||||
#+BEGIN_EXAMPLE
|
||||
>>> entry = {'name' : u'Test example', 'age' : u'100'}
|
||||
>>> api.Backend.textui.print_entry(entry)
|
||||
age: 100
|
||||
name: Test example
|
||||
#+END_EXAMPLE
|
||||
- ~console~ :: starts interactive Python console with FreeIPA commands
|
||||
- ~help~ :: generates help for every command and method of FreeIPA and structures it into
|
||||
sections according to the registered FreeIPA objects.
|
||||
#+BEGIN_EXAMPLE
|
||||
>>> api.Command.help(u'user-show')
|
||||
Purpose: Display information about a user.
|
||||
Usage: ipa [global-options] user-show LOGIN [options]
|
||||
|
||||
Options:
|
||||
-h, --help show this help message and exit
|
||||
--rights Display the access rights of this entry (requires --all). See
|
||||
ipa man page for details.
|
||||
--all Retrieve and print all attributes from the server. Affects
|
||||
command output.
|
||||
--raw Print entries as stored on the server. Only affects output
|
||||
format.
|
||||
#+END_EXAMPLE
|
||||
- ~show_mappings~ :: displays mappings between command's parameters and LDAP attributes:
|
||||
#+BEGIN_EXAMPLE
|
||||
>>> api.Command.show_mappings(command_name=u"role-find")
|
||||
Parameter : LDAP attribute
|
||||
========= : ==============
|
||||
name : cn
|
||||
desc : description
|
||||
timelimit : timelimit?
|
||||
sizelimit : sizelimit?
|
||||
#+END_EXAMPLE
|
||||
|
||||
** Extending command line utility
|
||||
Since ~ipa~ utility operates under client context, it loads all command plugins from
|
||||
~ipalib/plugins~. A simple way to extend command line is to drop its plugin file into
|
||||
~ipalib/plugins~ on the machine where ~ipa~ utility is executed. Next time ~ipa~ is
|
||||
started, new plugin will be loaded together with all other plugins from ~ipalib/plugins~
|
||||
and commands provided by it will be added to the =api=.
|
||||
|
||||
Let's add a command line plugin that allows to ping a server and measures round trip time:
|
||||
#+BEGIN_SRC python -n
|
||||
from ipalib import frontend
|
||||
from ipalib import output
|
||||
from ipalib import _, ngettext
|
||||
from ipalib import api
|
||||
import time
|
||||
|
||||
__doc__ = _("""
|
||||
Local extensions to FreeIPA commands
|
||||
""")
|
||||
|
||||
class timed_ping(frontend.Command):
|
||||
__doc__ = _('Ping remote FreeIPA server and measure round-trip')
|
||||
|
||||
has_output = (
|
||||
output.summary,
|
||||
)
|
||||
def run(self):
|
||||
t1 = time.time()
|
||||
result = self.api.Command.ping()
|
||||
t2 = time.time()
|
||||
summary = u"""Round-trip to the server is %f ms.
|
||||
Server response is %s"""
|
||||
return dict(summary=summary % ((t2-t1)*1000.0, result['summary']))
|
||||
|
||||
api.register(timed_ping)
|
||||
#+END_SRC
|
||||
|
||||
When this plugin code is placed into ~ipalib/plugins/extend-cli.py~ (name of the plugin
|
||||
file can be set arbitrarily), ~ipa timed-ping~ will produce following output:
|
||||
#+BEGIN_EXAMPLE
|
||||
$ ipa timed-ping
|
||||
-----------------------------------------------------------------------------
|
||||
Round-trip to the server is 286.306143 ms.
|
||||
Server response is IPA server version 2.1.3GIT8a254ca. API version 2.13
|
||||
-----------------------------------------------------------------------------
|
||||
#+END_EXAMPLE
|
||||
|
||||
In this example we have created ~timed-ping~ command and overrode its =run()=
|
||||
method. Effectively, this command will only work properly on the client. If the client is
|
||||
also FreeIPA server (all FreeIPA servers are enrolled as FreeIPA clients), the same code
|
||||
will also be loaded by the server context and will be accessible to the Web UI as well,
|
||||
albeit its usefulness will be questionable as it will be measuring the round-trip to the
|
||||
server from the server itself.
|
||||
|
||||
* File paths
|
||||
Finally, it should be noted that depending on installed Python version and operating
|
||||
system, paths where plugins are loaded from may differ. Usually Python extensions are
|
||||
placed in ~site-packages~ Python sub-directory. In Fedora and RHEL distributions, this is
|
||||
~/usr/lib/python<version>/site-packages~. Thus, full path to ~extend-cli.py~ would be
|
||||
~/usr/lib/python<version>/site-packages/ipalib/plugins/extend-cli.py~.
|
||||
|
||||
On recent Fedora distribution, following paths are used:
|
||||
|--------------------+---------------------------+------------------------------------------------------------|
|
||||
| Plugins | Python module prefix | File path |
|
||||
|--------------------+---------------------------+------------------------------------------------------------|
|
||||
| common | ipalib/plugins | /usr/lib/python2.7/site-packages/ipalib/plugins |
|
||||
| server | ipaserver/plugins | /usr/lib/python2.7/site-packages/ipaserver/plugins |
|
||||
| installer, updates | ipaserver/install/plugins | /usr/lib/python2.7/site-packages/ipaserver/install/plugins |
|
||||
|--------------------+---------------------------+------------------------------------------------------------|
|
||||
|
||||
Next table explains use of contexts in FreeIPA applications:
|
||||
|---------+------------------+-------------------------+----------------------------------------|
|
||||
| Context | Application | Plugins | Description |
|
||||
|---------+------------------+-------------------------+----------------------------------------|
|
||||
| server | wsgi.py | common, server | Main FreeIPA server, server context |
|
||||
| cli | ipa | common | Command line interface, client context |
|
||||
| updates | ipa-ldap-updater | common, server, updates | LDAP schema updater |
|
||||
|---------+------------------+-------------------------+----------------------------------------|
|
||||
|
||||
|
||||
* Platform portability
|
||||
Originally FreeIPA was created utilizing packages available in Fedora and RHEL
|
||||
distributions. During configuration stages multiple system services need to be stopped
|
||||
and started again, scheduled to start after reboot and re-configured. In addition, when
|
||||
operating system utilizing security measures to harden the server setup, appropriate
|
||||
activities need to be done as well for preserving proper security contexts. As
|
||||
configuration details, service names, security features and management tools differ
|
||||
substantially between various GNU/Linux distributions and other operating systems, porting
|
||||
FreeIPA project's code to other environment has proven to be problematic.
|
||||
|
||||
When Fedora project has decided to migrate to systemd for services management, FreeIPA
|
||||
packages for Fedora needed to be updated as well, at the same time preserving support for
|
||||
older SystemV initialization scheme used in older releases. This prompted to develop a
|
||||
'platformization' support allowing to abstract services management between different
|
||||
platforms.
|
||||
|
||||
FreeIPA 2.1.3 includes first cut of platformization work to support Fedora 16 distribution
|
||||
based on systemd. At the same time, there is an effort to port FreeIPA client side code to
|
||||
Ubuntu distributions.
|
||||
|
||||
Platform portability in FreeIPA means centralization of code to manage system-provided
|
||||
services, authentication setup, and means to manage security context and host names. It is
|
||||
going to be extended in future to cover other areas as well, both client- and server-side.
|
||||
|
||||
The code that implements platform-specific adaptation is placed under
|
||||
~ipaplatform~. As of FreeIPA 4.4.2, there are two major "platforms" supported:
|
||||
- /rhel/ :: Red Hat Enterprise Linux 7-based distributions utilizing Systemd
|
||||
such as CentOS 7 and Scientific Linux 7.
|
||||
- /fedora/ :: Fedora distribution version 23 above are supported by this platform
|
||||
module. It is based on ~systemd~ system management tool and utilizes
|
||||
common code in ~ipaplatform/base/services.py~. ~fedora~ contains
|
||||
only differentiation required to cover Fedora 23-specific implementation
|
||||
of systemd use, depending on changes to Dogtag, Tomcat6, and 389-ds
|
||||
packages.
|
||||
|
||||
Each platform-specific adaptation should provide few basic building blocks:
|
||||
|
||||
*** AuthConfig class and tasks module
|
||||
|
||||
=ipaplatform.tasks= module implements system-independent interface to configure system
|
||||
resources. In Red Hat systems some of these tasks are done with authconfig(8) utility.
|
||||
|
||||
=AuthConfig= class is nothing more than a tool to gather configuration options and execute
|
||||
their processing. These options then converted by an actual implementation to series of a
|
||||
system calls to appropriate utilities performing real configuration.
|
||||
|
||||
From FreeIPA code perspective, the system configuration should be done with
|
||||
use of ~ipaplatform.tasks.tasks~:
|
||||
|
||||
#+BEGIN_SRC python -n
|
||||
from ipaplatform.tasks import tasks
|
||||
|
||||
tasks.set_nisdomain('nisdomain.example')
|
||||
#+END_SRC
|
||||
|
||||
The actual implementation can differ. ~redhat~ platform module builds up arguments to
|
||||
authconfig(8) tool and on =execute()= method runs it with those arguments. Other systems
|
||||
will need to have processing based on their respective tools.
|
||||
|
||||
*** PlatformService class
|
||||
=PlatformService= class abstracts out an external process running on the system which is
|
||||
possible to administer: start, stop, check its status, schedule for automatic startup,
|
||||
etc.
|
||||
|
||||
Services are used thoroughly through FreeIPA server and client install tools. There are
|
||||
several services that are used especially often and they are selected to be accessible via
|
||||
Python properties of =ipaplatform.services.knownservices= instance.
|
||||
|
||||
To facilitate more expressive way of working with often used services, ipaplatform.services
|
||||
module provides a shortcut to access them by name via
|
||||
ipaplatform.services.knownservices.<service>. A typical code change looks like this:
|
||||
#+BEGIN_EXAMPLE
|
||||
import ipaplatform.services.knownservices
|
||||
....
|
||||
- service.restart("dirsrv")
|
||||
- service.restart("krb5kdc")
|
||||
- service.restart("httpd")
|
||||
+ ipaplatform.services.knownservices.dirsrv.restart()
|
||||
+ ipaplatform.services.knownservices.krb5kdc.restart()
|
||||
+ ipaplatform.services.knownservices.httpd.restart()
|
||||
#+END_EXAMPLE
|
||||
|
||||
Besides expression change this also makes more explicit to platform providers access to
|
||||
what services they have to implement. Service names are defined in
|
||||
ipaplatform.platform.base.wellknownservices and represent definitive names to access these
|
||||
services from FreeIPA code. Of course, platform provider should remap those names to
|
||||
platform-specific ones -- for ipaplatform.redhat provider mapping is identity.
|
||||
|
||||
Porting to a new platform may be hard as can be witnessed by this example:
|
||||
https://www.redhat.com/archives/freeipa-devel/2011-September/msg00408.html
|
||||
|
||||
If there is doubt, always consult existing providers. ~redhat/services.py~ is canonical -- it
|
||||
represents the code which was used throughout FreeIPA v2 development.
|
||||
|
||||
*** Enabling new platform provider
|
||||
When support for new platform is implemented and appropriate provider is placed to
|
||||
~ipaplatform/platform/~, it is time to enable its use by the FreeIPA. Since FreeIPA is
|
||||
supposed to be rolled out uniformly on multiple clients and servers, best approach is to
|
||||
build and distribute software packages using platform-provided package management tools.
|
||||
|
||||
With this in mind, platform code selection in FreeIPA is static and run at package
|
||||
production time. In order to select proper platform provider, one needs to pass
|
||||
~--with-ipaplatform~ argument to FreeIPA's configure process:
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
./configure --with-ipaplatform=fedora
|
||||
#+END_EXAMPLE
|
||||
Reference in New Issue
Block a user