
This patch set makes the following changes: * using the new standards * Update doc/source/index.rst with new links * Move content of install-guide to the doc/source/install Change-Id: Ic16671191832d949d2ea0626dcbf334ba60c56ef
14 KiB
Part 4: Refactoring code to use the Application Framework
Up until this point we wrote the Plone application in a manner that was common to all applications that were written before the application framework was introduced.
In this last tutorial step we are going to refactor the Plone code in order to take advantage of the framework.
Application framework was written in order to simplify the application development and encapsulate common deployment workflows. This gives things primitives for application scaling and high availability without the need to develop them over and over again for each application.
When using the frameworks, an application developer only has to inherit the class that best suits him and provide it only with the code that is specific to the application, while leaving the rest to the framework. This typically includes:
- instructions on how to provision the software on each node (server)
- instructions on how to configure the provisioned software
- server group onto which the software should be installed. This may be a fixed server list, a shared server pool, or a scalable server group that creates servers using the given instance template, or one of the several other implementations provided by the framework
The framework is located in a separate library package
io.murano.applications
that is shipped with Murano. We are
going to use the apps
namespace prefix to refer to this
namespace through the code.
Step 1: Add dependency on the App Framework
In order to use one Murano Package from another, the former must be
explicitly specified as a requirement for the latter. This is done by
filling the Require
section in the package's manifest
file.
Open the Plone's manifest.yaml file and append the following lines:
Require:
io.murano.applications:
Requirements are specified as a mapping from package name to the
desired version of that package (or version range). The missing value
indicates the dependency on the latest 0.*.*
version of the
package which is exactly what we need since the current version of the
app framework library is 0.
Step 2: Get rid of the instance
Since we are going to have a multi-sever Plone application there won't be a single instance belonging to the application. Instead, we are going to provide it with the server group that abstracts the server management from the application.
So instead of
Properties:
instance:
Contract: $.class(res:Instance)
we are going to have
Properties:
servers:
Contract: $.class(apps:ServerGroup).notNull()
Step 3: Change the base classes
Another change that we are going to make to the main application
class is to change its base classes. Regular applications inherit from
the std:Application
which only has the method
deploy
that does all the work.
Application framework provides us with its own implementation of that class and method. Instead of one monolithic method that does everything, with the framework, the application provides only the code needed to provision and configure the software on each server.
So instead of std:Application
class we are going to
inherit two of the framework classes:
Extends:
- apps:MultiServerApplicationWithScaling
- apps:OpenStackSecurityConfigurable
The first class tells us that we are going to have an application
that runs on multiple servers. In the following section we are going to
split out deploy
method into two smaller methods that are
going to be invoked by the framework to install the software on each of
the servers. By inheriting the
apps:MultiServerApplicationWithScaling
, the application
automatically gets all the UI buttons to scale it out and in.
The second class is a mix-in class that tells the framework that we are going to provide the OpenStack-specific security group configuration for the application.
Step 4: Split the deployment logic
In this step we are going to split the installation into two phases: provisioning and configuration.
Provisioning is implemented by overriding the
onInstallServer
method, which is called every time a new
server is added to the server group. In this method we are going to
install the Plone software bits onto the server (which is provided as a
method parameter).
Configuration is done through the onConfigureServer
,
which is called upon the first installation on the server, and every
time any of the application settings change, and
onCompleteConfiguration
which is executed on each server
after everything was configured so that we can perform
post-configuration steps like starting application daemons and reporting
messages to the user.
Thus we are going to split the install-plone.sh
script
into two scripts: installPlone.sh
and
configureServer.sh
and execute each one in their
corresponding methods:
onInstallServer:
Arguments:
- server:
Contract: $.class(res:Instance).notNull()
- serverGroup:
Contract: $.class(apps:ServerGroup).notNull()
Body:
- $file: sys:Resources.string('installPlone.sh').replace({
"$1" => $this.deploymentPath,
"$2" => $this.adminPassword
})
- conf:Linux.runCommand($server.agent, $file)
onConfigureServer:
Arguments:
- server:
Contract: $.class(res:Instance).notNull()
- serverGroup:
Contract: $.class(apps:ServerGroup).notNull()
Body:
- $primaryServer: $serverGroup.getServers().first()
- If: $server = $primaryServer
Then:
- $file: sys:Resources.string('configureServer.sh').replace({
"$1" => $this.deploymentPath,
"$2" => $primaryServer.ipAddresses[0]
})
Else:
- $file: sys:Resources.string('configureClient.sh').replace({
"$1" => $this.deploymentPath,
"$2" => $this.servers.primaryServer.ipAddresses[0],
"$3" => $this.listeningPort})
- conf:Linux.runCommand($server.agent, $file)
onCompleteConfiguration:
Arguments:
- servers:
Contract:
- $.class(res:Instance).notNull()
- serverGroup:
Contract: $.class(apps:ServerGroup).notNull()
- failedServers:
Contract:
- $.class(res:Instance).notNull()
Body:
- $startCommand: format('{0}/zeocluster/bin/plonectl start', $this.deploymentPath)
- $primaryServer: $serverGroup.getServers().first()
- If: $primaryServer in $servers
Then:
- $this.report('Starting DB node')
- conf:Linux.runCommand($primaryServer.agent, $startCommand)
- conf:Linux.runCommand($primaryServer.agent, 'sleep 10')
- $otherServers: $servers.where($ != $primaryServer)
- If: $otherServers.any()
Then:
- $this.report('Starting Client nodes')
# run command on all other nodes in parallel with pselect
- $otherServers.pselect(conf:Linux.runCommand($.agent, $startCommand))
# build an address string with IPs of all our servers
- $addresses: $serverGroup.getServers().
select(
switch($.assignFloatingIp => $.floatingIpAddress,
true => $.ipAddresses[0])
+ ':' + str($this.listeningPort)
).join(', ')
- $this.report('Plone listeners are running at ' + str($addresses))
During configuration phase we distinguish the first server in the server group from the rest of the servers. The first server is going to be the primary node and treated differently from the others.
Step 5: Configuring OpenStack security group
The last change to the main class is to set up the security group
rules. We are going to do this by overriding the
getSecurityRules
method that we inherited from the
apps:OpenStackSecurityConfigurable
class:
getSecurityRules:
Body:
- Return:
- FromPort: $this.listeningPort
ToPort: $this.listeningPort
IpProtocol: tcp
External: true
- FromPort: 8100
ToPort: 8100
IpProtocol: tcp
External: false
The code is very similar to that of the old deploy
method with the only difference being that it returns the rules rather
than sets them on its own.
Step 6: Provide the server group instance
Do you remember, that previously we replaced the
instance
property with servers
of type
apps:ServerGroup
? Since the object is coming from the UI
definition, we must change the latter in order to provide the class with
the apps:ServerReplicationGroup
instance rather than
resources:Instance
.
To do this we are going to replace the instance
property
in the Application template with the following snippet:
servers:
?:
type: io.murano.applications.ServerReplicationGroup
numItems: $.ploneConfiguration.numNodes
provider:
?:
type: io.murano.applications.TemplateServerProvider
template:
?:
type: io.murano.resources.LinuxMuranoInstance
flavor: $.instanceConfiguration.flavor
image: $.instanceConfiguration.osImage
assignFloatingIp: $.instanceConfiguration.assignFloatingIP
serverNamePattern: $.instanceConfiguration.unitNamingPattern
If you take a closer look at the code above you will find out that
the new declaration is very similar to the old one. But now instead of
providing the Instance
property values directly, we are
providing them as a template for the TemplateServerProvider
server provider. ServerReplicationGroup
is going to use the
provider each time it requires another server. In turn, the provider is
going to use the familiar template for the new instances.
Besides the instance template we also specify the initial number of
Plone nodes using the numItems
property and the name
pattern for the servers. Thus we must also add it to the list of our
controls:
Forms:
- instanceConfiguration:
fields:
...
- name: unitNamingPattern
type: string
label: Instance Naming Pattern
required: false
maxLength: 64
initial: 'plone-{0}'
description: >-
Specify a string, that will be used in instance hostname.
Just A-Z, a-z, 0-9, dash and underline are allowed.
- ploneConfiguration:
fields:
...
- name: numNodes
type: integer
label: Initial number of Client Nodes
initial: 1
minValue: 1
required: true
description: >-
Select the initial number of Plone Client Nodes
Step 6: Using server group composition
By this step we should already have a working Plone application. But let's go one step further and enhance our sample application.
Since we are running the database on the first server group server
only, we might want it to have different properties. For example we
might want to give it a bigger flavor or just a special name. This is a
perfect opportunity for us to demonstrate how to construct complex
server groups. All we need to do is to just use another implementation
of apps:ServerGroup
. Instead of
apps:ServerReplicationGroup
we are going to use the
apps:CompositeServerGroup
class, which allows us to compose
several server groups together. One of them is going to be a
single-server server group consisting of our primary server, and the
second is going to be the scalable server group that we used to create
in the previous step.
So again, we change the Application
section of our UI
definition file with even a more advanced servers
property
definition:
servers:
?:
type: io.murano.applications.CompositeServerGroup
serverGroups:
- ?:
type: io.murano.applications.SingleServerGroup
server:
?:
type: io.murano.resources.LinuxMuranoInstance
name: format($.instanceConfiguration.unitNamingPattern, 'db')
image: $.instanceConfiguration.image
flavor: $.instanceConfiguration.flavor
assignFloatingIp: $.instanceConfiguration.assignFloatingIp
- ?:
type: io.murano.applications.ServerReplicationGroup
numItems: $.ploneConfiguration.numNodes
provider:
?:
type: io.murano.applications.TemplateServerProvider
template:
?:
type: io.murano.resources.LinuxMuranoInstance
flavor: $.instanceConfiguration.flavor
image: $.instanceConfiguration.osImage
assignFloatingIp: $.instanceConfiguration.assignFloatingIP
serverNamePattern: $.instanceConfiguration.unitNamingPattern
Here the instance definition for the SingleServerGroup
(our primary server) differs from the servers in the
ServerReplicationGroup
by its name only. However the same
technique might be used to customize other properties as well as to
create even more sophisticated server group topologies. For example, we
could implement region bursting by composing several scalable server
groups that allocate servers in different regions. And all of that
without making any changes to the application code itself!