File: part4.rst

package info (click to toggle)
murano 1%3A6.0.0-2
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 10,644 kB
  • sloc: python: 34,127; sh: 717; pascal: 269; makefile: 83
file content (355 lines) | stat: -rw-r--r-- 14,037 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
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:

.. code-block:: yaml

   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

.. code-block:: yaml

   Properties:
     instance:
       Contract: $.class(res:Instance)

we are going to have

.. code-block:: yaml

   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:

.. code-block:: yaml

   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:

.. code-block:: yaml

   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:

.. code-block:: yaml

   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:

.. code-block:: yaml

   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:

.. code-block:: yaml

   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:

.. code-block:: yaml

   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!