Developing Murano Packages 101
Change-Id: Ia227903d5efcbda8a955920e3e4123f22aa17aee
@ -78,6 +78,9 @@ do not specify any parents or omit the key, then the class extends
|
||||
``io.murano.Object``. Thus, ``io.murano.Object`` is the root class
|
||||
for all class hierarchies.
|
||||
|
||||
.. _class_props:
|
||||
|
||||
|
||||
Properties
|
||||
----------
|
||||
|
||||
|
BIN
doc/source/appdev-guide/step-by-step/configure-step1.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
doc/source/appdev-guide/step-by-step/configure-step2.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
doc/source/appdev-guide/step-by-step/hello-world-desc.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
doc/source/appdev-guide/step-by-step/hello-world-screen-1.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
doc/source/appdev-guide/step-by-step/hello-world-screen-2.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
doc/source/appdev-guide/step-by-step/new-env-1.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
doc/source/appdev-guide/step-by-step/new-env-2.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
doc/source/appdev-guide/step-by-step/new-env-3.png
Normal file
After Width: | Height: | Size: 74 KiB |
390
doc/source/appdev-guide/step-by-step/part1.rst
Normal file
@ -0,0 +1,390 @@
|
||||
|
||||
Part 1: Creating your first Application Package
|
||||
-----------------------------------------------
|
||||
|
||||
All tutorials on programming languages start with a "Hello, World" example,
|
||||
and since Murano provides its own programming language, this guide will start
|
||||
the same way. Let's do a "Hello, World" application. It will not do anything
|
||||
useful yet, but will provide you with an understanding of how things work
|
||||
in Murano. We will add more logic to the package at later stages. Now let's
|
||||
start with the basics:
|
||||
|
||||
|
||||
Creating package manifest
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Let's start with creating an empty Murano Package. All packages consist of
|
||||
multiple files (two at least) organized into a special structure. So, let's
|
||||
create a directory somewhere in our file system and set it as our current
|
||||
working directory. This directory will contain our package:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ mkdir HelloWorld
|
||||
$ cd HelloWorld
|
||||
|
||||
|
||||
The main element of the package is its `manifest`. It is a description of the
|
||||
package, telling Murano how to display the package in the catalog. It is
|
||||
defined in a yaml file called ``manifest.yaml`` which should be placed right in
|
||||
the main package directory. Let's create this file and open it with any text
|
||||
editor:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ vim manifest.yaml
|
||||
|
||||
|
||||
This file may contain a number of sections (we will take a closer look at some
|
||||
of them later), but the mandatory ones are ``FullName`` and ``Type``.
|
||||
|
||||
The ``FullName`` should be a unique identifier of the package, the name which
|
||||
Murano uses to distinguish it among other packages in the catalog. It is very
|
||||
important for this name to be globally unique: if you publish your package and
|
||||
someone adds it to their catalog, there should be no chances that someone
|
||||
else's package has the same name. That's why it is recommended to give your
|
||||
packages Full Names based on the domain you (or the company your work for) own.
|
||||
We recommend using "reversed-domain-name" notation, similar to the one used in
|
||||
the world of Java development: if the `yourdomain.com` is the domain name you
|
||||
own, then you could name your package ``com.yourdomain.HellWorld``. This way
|
||||
your package name will not duplicate anybody else's, even if they also named
|
||||
their package "HelloWorld", because theirs will begin with a different
|
||||
domain-specific prefix.
|
||||
|
||||
``Type`` may have either of two values: ``Application`` or ``Library``.
|
||||
``Application`` indicates the standard package to deploy an application with
|
||||
Murano, while a ``Library`` is bundle of reusable scenarios which may be used
|
||||
by other packages. For now we just need a single standalone app, so let's
|
||||
choose an ``Application`` type.
|
||||
|
||||
Enter these values and save the file. You should have something like this:
|
||||
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
FullName: com.yourdomain.HelloWorld
|
||||
Type: Application
|
||||
|
||||
|
||||
This is the minimum required to start. We'll add more manifest data later.
|
||||
|
||||
Adding a class
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
While `manifests` describe Murano packages in the catalog, the actual logic of
|
||||
packages is put into `classes`, which are plain YAML files placed into the
|
||||
``Classes`` directory of the application package. So, let's create a directory
|
||||
to store the logic of our application, then create and edit the file to contain
|
||||
the first class of the package.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ mkdir Classes
|
||||
$ vim Classes/HelloWorld.yaml
|
||||
|
||||
|
||||
Murano classes follow standard patterns of object-oriented programming: they
|
||||
define the types of the objects which may be instantiated by Murano. The types
|
||||
are composed of `properties`, defining the data structure of objects, and
|
||||
`methods`, containing the logic that defines the way in which Murano executes
|
||||
the former. The types may be `extended`: the extended class contains all the
|
||||
methods and properties of the class it extends, or it may override some of
|
||||
them.
|
||||
|
||||
Let's type in the following YAML to create our first class:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Name: com.yourdomain.HelloWorld
|
||||
|
||||
Extends: io.murano.Application
|
||||
|
||||
Methods:
|
||||
deploy:
|
||||
Body:
|
||||
- $reporter: $this.find('io.murano.Environment').reporter
|
||||
- $reporter.report($this, "Hello, World!")
|
||||
|
||||
|
||||
Let's walk through this code line by line and see what this code does.
|
||||
The first line is pretty obvious: it states the name of our class,
|
||||
``com.yourdomain.HelloWorld``. Note that this name matches the name of the
|
||||
package - that's intentional. Although it is not mandatory, it is strongly
|
||||
recommended to give the main class of your application package the same name as
|
||||
the package itself.
|
||||
|
||||
Then, there is an ``Extends`` directive. It says that our class extends (or
|
||||
inherits) another class, called ``io.murano.Application``. That is the base
|
||||
class for all classes which should deploy Applications in Murano. As many other
|
||||
classes it is shipped with Murano itself, thus its name starts with
|
||||
`io.murano.` prefix: `murano.io` domain is controlled by the Murano development
|
||||
team and no one else should create packages or classes having names in that
|
||||
namespace.
|
||||
|
||||
Note that ``Extends`` directive may contain not only a single value, but a
|
||||
list. In that case the class we create will inherit multiple base classes.
|
||||
Yes, Murano has multiple inheritance, yay!
|
||||
|
||||
Now, the ``Methods`` block contains all the logic encapsulated in our class. In
|
||||
this example there is just one method, called ``deploy``. This method is
|
||||
defined in the base class we've just inherited - the ``io.murano.Application``,
|
||||
so here we `override` it. ``Body`` block of the method contains the
|
||||
implementation, the actual logic of the method. It's a list of instructions
|
||||
(note the dash-prefixed lines - that's how YAML defines lists), each executed
|
||||
one by one.
|
||||
|
||||
There are two instruction statements here. The first one declares a `variable`
|
||||
named ``$reporter`` (note the ``$`` character: all the words prefixed with it
|
||||
are variables in Murano language) and assigns it a value. Unlike other
|
||||
languages Murano uses colon (``:``) as an assignment operator: this makes it
|
||||
convenient to express Murano statements as regular YAML mappings.
|
||||
The expression to the right of the colon is executed and the result value is
|
||||
assigned to a variable to the left of the colon.
|
||||
|
||||
Let's take a closer look at the right-hand side of the expression in the first
|
||||
statement:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- $reporter: $this.find('io.murano.Environment').reporter
|
||||
|
||||
|
||||
It takes a value of a special variable called ``$this`` (which always contains
|
||||
a reference to the current object, i.e. the instance of our class for which the
|
||||
method was called; it is same as ``self`` in python or ``this`` in Java) and
|
||||
calls a method named ``find`` on it with a string parameter equal
|
||||
to 'io.murano.Environment'; from the call result it takes a "reporter"
|
||||
attribute; this value is assigned to the variable in the left-hand side of the
|
||||
expression.
|
||||
|
||||
The meaning of this code is simple: it `finds` the object of class
|
||||
``io.murano.Environment`` which owns the current application and returns its
|
||||
"reporter" object. This ``io.murano.Environment`` is a special object which
|
||||
groups multiple deployed applications. When the end-user interacts with Murano
|
||||
they create these `Environments` and place applications into them. So, every
|
||||
Application is able to get a reference to this object by calling ``find``
|
||||
method like we just did. Meanwhile, the ``io.murano.Environment`` class has
|
||||
various methods to interact with the "outer world", for example to report
|
||||
various messages to the end-user via the deployment log: this is done by the
|
||||
"reporter" property of that class.
|
||||
|
||||
So, our first statement just retrieved that reporter. The second one uses it to
|
||||
display a message to a user: it calls a method "report", passes the reference
|
||||
to a reporting object and a message as the arguments of the method:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- $reporter.report($this, "Hello, World!")
|
||||
|
||||
Note that the second statement is not a YAML-mapping: it does not have a colon
|
||||
inside. That's because this statement just makes a method call, it does not
|
||||
need to remember the result.
|
||||
|
||||
That's it: we've just made a class which greets the user with a traditional
|
||||
"Hello, World!" message. Now we need to include this class into the package we
|
||||
are creating. Although it is placed within a ``Classes`` subdirectory of the
|
||||
package, it still needs to be explicitly added to the package. To do that, add
|
||||
a ``Classes`` section to your manifest.yaml file. This should be a YAML
|
||||
mapping, having class names as keys and relative paths of files within the
|
||||
``Classes`` directory as the values. So, for our example class it should look
|
||||
like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Classes:
|
||||
com.yourdomain.HelloWorld: HelloWorld.yaml
|
||||
|
||||
Paste this block anywhere in the ``manifest.yaml``
|
||||
|
||||
Pack and upload your app
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Our application is ready. It's very simplistic and lacks many features required
|
||||
for real-world applications, but it already can be deployed into Murano and run
|
||||
there.
|
||||
To do that we need to pack it first. We use good old zip for it.
|
||||
That's it: just zip everything inside your package directory into a zip
|
||||
archive, and you'll get a ready-to-use Murano package:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ zip -r hello_world.zip *
|
||||
|
||||
This will add all the contents of our package directory to a zip archive called
|
||||
``hello_world.zip``. Do not forget the ``-r`` argument to include the files in
|
||||
subdirectories (the class file in our case).
|
||||
|
||||
Now, let's upload the package to murano. Ensure that your system has a
|
||||
murano-client installed and your OpenStack cloud credentials are exported as
|
||||
environmnet variables (if not, sourcing an `openrc` file, downloadable from
|
||||
your horizon dashboard will do the latter). Then execute the following command:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ murano package-import ./hello_world.zip
|
||||
Importing package com.yourdomain.HelloWorld
|
||||
+----------------------------------+---------------------------+---------------------------+-----------+--------+-----------+-------------+---------+
|
||||
| ID | Name | FQN | Author | Active | Is Public | Type | Version |
|
||||
+----------------------------------+---------------------------+---------------------------+-----------+--------+-----------+-------------+---------+
|
||||
| 251a409645d1444aa1ead8eaac451a1d | com.yourdomain.HelloWorld | com.yourdomain.HelloWorld | OpenStack | True | | Application | |
|
||||
+----------------------------------+---------------------------+---------------------------+-----------+--------+-----------+-------------+---------+
|
||||
|
||||
As you can see from the output, the package has been uploaded to Murano catalog
|
||||
and is now available there. Let's now deploy it.
|
||||
|
||||
Deploying your application
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To deploy an application with Murano one needs to create an `Environment` and
|
||||
add configured instances of your applications into it. It may be done either
|
||||
with the help of user interface (but that requires some extra effort from
|
||||
package developer) or by providing an explicit JSON, describing the exact
|
||||
application instance and its configuration. Let's do the latter option for now.
|
||||
|
||||
First, let's create a json snippet for our application. Since the app is very
|
||||
basic, the snippet is simple as well:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/-",
|
||||
"value": {
|
||||
"?": {
|
||||
"name": "Demo",
|
||||
"type": "com.yourdomain.HelloWorld",
|
||||
"id": "42"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
This json follows a standard json-patch notation, i.e. it defines a number of
|
||||
operations to edit a large json document. This particular one `adds` (note the
|
||||
value of ``op`` key) an object descibed in the ``value`` of the json to the
|
||||
`root` (note the ``path`` equal to ``/-`` - that's root) of our environment.
|
||||
The object we add has the `type` of ``com.yourdomain.HelloWorld`` - that's the
|
||||
class we just created two steps ago. Other keys in this json parameterize the
|
||||
object we create: they add a `name` and an `id` to the object. Id is mandatory,
|
||||
name is optional. Note that since the id, name and type are the `system
|
||||
properties` of our object, they are defined in a special section of the json -
|
||||
the so-called `?-header`. Non-system properties, if they existed, would be
|
||||
defined at a top-level of the object. We'll add them in a next chapter to see
|
||||
how they work.
|
||||
|
||||
For now, save this JSON to some local file (say, ``input.json``) and let's
|
||||
finally deploy the thing.
|
||||
|
||||
Execute the following sequence of commands:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ murano environment-create TestHello
|
||||
+----------------------------------+-----------+--------+---------------------+---------------------+
|
||||
| ID | Name | Status | Created | Updated |
|
||||
+----------------------------------+-----------+--------+---------------------+---------------------+
|
||||
| 34bf673a26a8439d906827dea328c99c | TestHello | ready | 2016-10-04T13:19:12 | 2016-10-04T13:19:12 |
|
||||
+----------------------------------+-----------+--------+---------------------+---------------------+
|
||||
|
||||
$ murano environment-session-create 34bf673a26a8439d906827dea328c99c
|
||||
Created new session:
|
||||
+----------+----------------------------------+
|
||||
| Property | Value |
|
||||
+----------+----------------------------------+
|
||||
| id | 6d4a8fa2a5f4484fbc07740ef3ab60dd |
|
||||
+----------+----------------------------------+
|
||||
|
||||
$ murano environment-apps-edit --session-id 6d4a8fa2a5f4484fbc07740ef3ab60dd 34bf673a26a8439d906827dea328c99c ./input.json
|
||||
|
||||
This first command creates a murano environment named ``TestHello``. Note the
|
||||
`id` of the created environment - we use it to reference it in subsequent
|
||||
operations.
|
||||
|
||||
The second command creates a "configuration session" for this environment.
|
||||
Configuration sessions allow one to edit environments in transactional isolated
|
||||
manner. Note the `id` of the created sessions: all subsequent calls to modify
|
||||
or deploy the environment use both ids of environment and session.
|
||||
|
||||
The third command applies the json-patch we've created before to our
|
||||
environment within the configuration session we created.
|
||||
|
||||
Now, let's deploy the changes we made:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ murano environment-deploy --session-id 6d4a8fa2a5f4484fbc07740ef3ab60dd 34bf673a26a8439d906827dea328c99c
|
||||
+------------------+---------------------------------------------+
|
||||
| Property | Value |
|
||||
+------------------+---------------------------------------------+
|
||||
| acquired_by | 7b0fe7c67ede443da9840adb2d518d5c |
|
||||
| created | 2016-10-04T13:39:34 |
|
||||
| description_text | |
|
||||
| id | 34bf673a26a8439d906827dea328c99c |
|
||||
| name | TestHello |
|
||||
| services | [ |
|
||||
| | { |
|
||||
| | "?": { |
|
||||
| | "name": "Demo", |
|
||||
| | "status": "deploying", |
|
||||
| | "type": "com.yourdomain.HelloWorld", |
|
||||
| | "id": "42" |
|
||||
| | } |
|
||||
| | } |
|
||||
| | ] |
|
||||
| status | deploying |
|
||||
| tenant_id | 60b7b5f7d4e64ff0b1c5f047d694d7ca |
|
||||
| updated | 2016-10-04T13:39:34 |
|
||||
| version | 0 |
|
||||
+------------------+---------------------------------------------+
|
||||
|
||||
This will deploy the environment. You may check for its status by executing
|
||||
the following command:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ murano environment-show 34bf673a26a8439d906827dea328c99c
|
||||
+------------------+-----------------------------------------------------------------------------+
|
||||
| Property | Value |
|
||||
+------------------+-----------------------------------------------------------------------------+
|
||||
| acquired_by | None |
|
||||
| created | 2016-10-04T13:39:34 |
|
||||
| description_text | |
|
||||
| id | 34bf673a26a8439d906827dea328c99c |
|
||||
| name | TestHello |
|
||||
| services | [ |
|
||||
| | { |
|
||||
| | "?": { |
|
||||
| | "status": "ready", |
|
||||
| | "name": "Demo", |
|
||||
| | "type": "com.yourdomain.HelloWorld/0.0.0@com.yourdomain.HelloWorld", |
|
||||
| | "_actions": {}, |
|
||||
| | "id": "42", |
|
||||
| | "metadata": null |
|
||||
| | } |
|
||||
| | } |
|
||||
| | ] |
|
||||
| status | ready |
|
||||
| tenant_id | 60b7b5f7d4e64ff0b1c5f047d694d7ca |
|
||||
| updated | 2016-10-04T13:40:29 |
|
||||
| version | 1 |
|
||||
+------------------+-----------------------------------------------------------------------------+
|
||||
|
||||
As you can see, the status of the Environment has changed to ``ready``: it
|
||||
means that the application has been deployed. Open Murano Dashboard, navigate
|
||||
to Environment list and browse the contents of the ``TestHello`` environment
|
||||
there.
|
||||
You'll see that the 'Last Operation' column near the "Demo" component says
|
||||
"Hello, World!" - that's the reporting made by our application:
|
||||
|
||||
.. image:: hello-world-screen-1.png
|
||||
|
||||
This concludes the first part of our course. We've created a Murano Application
|
||||
Package, added a manifest describing its contents, written a class which
|
||||
reports a "Hello, World" message, packed all of these into a package archive
|
||||
and uploaded it to Murano Catalog and finally deployed an Environment with this
|
||||
application added.
|
||||
|
||||
In the next part we will learn how to improve this application in various
|
||||
aspects, both from users' and developers' perspectives.
|
444
doc/source/appdev-guide/step-by-step/part2.rst
Normal file
@ -0,0 +1,444 @@
|
||||
Part 2: Customizing your Application Package
|
||||
--------------------------------------------
|
||||
|
||||
We've built a classic "Hello, World" application during the first part of
|
||||
this tutorial, now let's play a little with it and customize it for better
|
||||
user and developer experience - while learning some more Murano features,
|
||||
of course.
|
||||
|
||||
Adding user input
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Most deployment scenarios for cloud applications require user input. It may
|
||||
be various options which should be applied in software configuration files,
|
||||
passwords for default administrator's accounts, IP addresses of external
|
||||
services to register with and so on. Murano Application Packages may define
|
||||
the user inputs they expect, prompt the end-users to pass the values as these
|
||||
inputs, so that they may utilize these values during application lifecycle
|
||||
workflows.
|
||||
|
||||
In Murano user input is defined for each class as `input properties`.
|
||||
`Properties` are object-level variables of the class, they may be of different
|
||||
kinds, and the `input properties` are the ones which are expected to contain
|
||||
user input. See :ref:`class_props` for details on other kinds of them.
|
||||
|
||||
To define properties of the class you should add a ``Properties`` block
|
||||
somewhere in the YAML file of that class.
|
||||
|
||||
.. note::
|
||||
Usually it is better to place this block after the ``Name`` and ``Extends``
|
||||
blocks but before the ``Methods`` block. Following this suggestion will
|
||||
improve the overall readability of your code.
|
||||
|
||||
The ``Properties`` block should contain a YAML dictionary, mapping the names of
|
||||
the properties to their descriptions. These descriptions may specify the kind
|
||||
of properties, the restrictions on the type and value of the property
|
||||
(so-called `contracts`), provide default value for the property and so on.
|
||||
|
||||
Let's add some user input to our "Hello, World" application. Let's ask the end
|
||||
user to provide their name, so the application will greet the user instead of
|
||||
the whole world. To do that, we need to edit our ``com.yourdomain.HelloWorld``
|
||||
class to look the following way:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
:emphasize-lines: 5-8
|
||||
|
||||
Name: com.yourdomain.HelloWorld
|
||||
|
||||
Extends: io.murano.Application
|
||||
|
||||
Properties:
|
||||
username:
|
||||
Usage: In
|
||||
Contract: $.string().notNull()
|
||||
|
||||
Methods:
|
||||
deploy:
|
||||
Body:
|
||||
- $reporter: $this.find('io.murano.Environment').reporter
|
||||
- $reporter.report($this, "Hello, World!")
|
||||
|
||||
On line 6 we declare a property named ``username``, on line 7 we specify that
|
||||
it is an input property, and on line 8 we provide a contract, i.e. a
|
||||
restriction on the value. This particular one states that the property's value
|
||||
should be a string and should not be null (i.e. should be provided by the
|
||||
user).
|
||||
|
||||
.. note::
|
||||
Although there are a total of 7 different kinds of properties, it turns
|
||||
out that the input ones are the most common. So, for input properties you
|
||||
may omit the ``Usage`` part - all the properties without an explicit usage
|
||||
are considered to be input properties.
|
||||
|
||||
|
||||
Once the property is declared within the ``Properties`` block, you may access
|
||||
it in the code of the class methods. Since the properties are object-level
|
||||
variables they may be accessed by calling a ``$this`` variable (which is a
|
||||
reference to a current instance of your class) followed by a dot and a property
|
||||
name. So, our ``username`` property may be accessed as ``$this.username``.
|
||||
|
||||
Let's modify the ``deploy`` method of our class to make use of the property to
|
||||
greet the user by name:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Methods:
|
||||
deploy:
|
||||
Body:
|
||||
- $reporter: $this.find('io.murano.Environment').reporter
|
||||
- $reporter.report($this, "Hello, " + $this.username + "!")
|
||||
|
||||
OK, let's try it. Save the file and archive your package directory again, then
|
||||
re-import your zip-file to the Murano Catalog as a package.
|
||||
You'll probably get a warning, since the package with the same name already
|
||||
exists in the catalog (we imported it there in the previous part of the
|
||||
tutorial), so murano CLI will ask you if you want to update it. In production
|
||||
it is better to make a newer version of our application and thus to have both
|
||||
in the catalog, but for now let's just overwrite the old package with the new
|
||||
one.
|
||||
|
||||
But you cannot deploy it with the old json input we used in the previous part:
|
||||
since the property's contract has that ``.notNull()`` part it means that the
|
||||
input should contain the value for the property. If you attempt to deploy an
|
||||
application without this value, you'll get an error.
|
||||
|
||||
So, let's edit the ``input.json`` file we created in the previous part and add
|
||||
the value of the property to the input:
|
||||
|
||||
.. code-block:: json
|
||||
:linenos:
|
||||
:emphasize-lines: 11
|
||||
|
||||
[
|
||||
{
|
||||
"op": "add",
|
||||
"path": "/-",
|
||||
"value": {
|
||||
"?": {
|
||||
"name": "Demo",
|
||||
"type": "com.yourdomain.HelloWorld",
|
||||
"id": "42"
|
||||
},
|
||||
"username": "Alice"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Save the json file and repeat the steps from the previous part to create an
|
||||
environment, open a configuration session, add an application and deploy it.
|
||||
Now in the 'Last Operation' of Murano Dashboard you will see the updated
|
||||
reporting message, containing the username:
|
||||
|
||||
.. image:: hello-world-screen-2.png
|
||||
:width: 100%
|
||||
|
||||
|
||||
Adding user interface
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As you can see in all the examples above, deploying applications via Murano
|
||||
CLI is quite a cumbersome process: the user has to create environments and
|
||||
sessions and provide the appropriate json-based input for the application.
|
||||
|
||||
This is inconvenient for a real user, of course. The CLI is intended to be used
|
||||
by various external automation systems which interact with Murano via scripts,
|
||||
but the human users will use Murano Dashboard which simplifies all those
|
||||
actions and provides a nice interface for them.
|
||||
|
||||
Murano Dashboard provides a nice interface to create and deploy environments
|
||||
and manages sessions transparently for the end users, but when it comes to the
|
||||
generation of input JSON it can't do it out of the box: it needs some hints
|
||||
from the package developer. By having hints, Murano Dashboard will be able to
|
||||
generate nicely looking wizard-like dialogs to configure applications and add
|
||||
them to an environment. In this section we'll learn how to create these UI
|
||||
hints.
|
||||
|
||||
The UI hints (also called `UI definitions`) should be defined in a separate
|
||||
YAML file (yeah, YAML again) in your application package. The file should be
|
||||
named ``ui.yaml`` and placed in a special directory of your package called
|
||||
``UI``.
|
||||
|
||||
The main section which is mandatory for all the UI definitions is called
|
||||
``Application``: it defines the object structure which should be passed as the
|
||||
input to Murano. That's it: it is equivalent to the JSON ``input.json`` we were
|
||||
creating before. The data structure remains the same: ?-header is for system
|
||||
properties and all other properties belong inside the top level of the object.
|
||||
|
||||
The ``Application`` section for our modified "Hello, World" application should
|
||||
look like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Application:
|
||||
?:
|
||||
type: com.yourdomain.HelloWorld
|
||||
username: Alice
|
||||
|
||||
This input is almost the same as the ``input.json`` we used last time, except
|
||||
that the data is expressed in a different format. However, there are several
|
||||
important differences: there are not JSON-Patch related keywords ("op", "path"
|
||||
and "value") - that's because Murano Dashboard will generate them
|
||||
automatically.
|
||||
|
||||
Same is true for the missing ``id`` and ``name`` from the ?-header of the
|
||||
object: the dashboard will generate the id on its own and ask the end-user for
|
||||
the name, and then will insert both into the structure it sends to Murano.
|
||||
|
||||
However, there is one problem in the example above: it has the ``username``
|
||||
hardcoded to be Alice. Of course we do not want the user input to be hardcoded:
|
||||
it won't be an input then. So, let's define a user interface which will ask the
|
||||
end user for the actual value of this parameter.
|
||||
|
||||
Since Murano Dashboard works like a step-by-step wizard, we need to define at
|
||||
least one wizard step (so-called `form`) and place a single text-box control
|
||||
into it, so the end-user will be able to enter his/her name there.
|
||||
|
||||
These steps are defined in the ``Forms`` section of our ui definition file.
|
||||
This section should contain a list of key-value pairs. Keys are the identifiers
|
||||
of the forms, while values should define a list of `field` objects. Each field
|
||||
may define a name, a type, a description, a requirement indicator and some
|
||||
other attributes intended for advanced usage.
|
||||
|
||||
For our example we need a single step with a single text field. The ``Forms``
|
||||
section should look like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Forms:
|
||||
- step1:
|
||||
fields:
|
||||
- name: username
|
||||
type: string
|
||||
description: Username of the user to say 'hello' to
|
||||
required: true
|
||||
|
||||
This defines the needed textbox control in the ui. Finally, we need to bind
|
||||
the value user puts into that textbox to the appropriate position in our
|
||||
``Application`` section. To do that we replace the hardcoded value with an
|
||||
expression of form ``$.<formId>.<fieldName>``. In our case this will be
|
||||
``$step1.username``.
|
||||
|
||||
So, our final UI definition will look like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Application:
|
||||
?:
|
||||
type: com.yourdomain.HelloWorld
|
||||
username: $.step1.username
|
||||
|
||||
Forms:
|
||||
- step1:
|
||||
fields:
|
||||
- name: username
|
||||
type: string
|
||||
description: Username of the user to say 'hello' to
|
||||
required: true
|
||||
|
||||
Save this code into your ``UI/ui.yaml`` file and then re-zip your package
|
||||
directory and import the resulting archive to Murano Catalog again.
|
||||
|
||||
Now, let's deploy this application using Murano Dashboard.
|
||||
|
||||
Open Murano Dashboard with your browser, navigate to
|
||||
"Applications/Catalog/Environments" panel, click the "Create Environment"
|
||||
button, enter the name for your environment and click "Create". You'll be
|
||||
taken to the contents of your environment: you'll see that it is empty, but on
|
||||
top of the screen there is a list of components you may add to it. If your
|
||||
Murano Catalog was empty when you started this tutorial, this list will
|
||||
contain just one item: your "Hello, World" application. The screen should look
|
||||
like this:
|
||||
|
||||
.. image:: new-env-1.png
|
||||
:width: 100%
|
||||
|
||||
Drag-n-drop your "com.yourdomain.HelloWorld" application from the list on top
|
||||
of the screen to the "Drop components here" panel beneath it. You'll see a
|
||||
dialog, prompting you to enter a username:
|
||||
|
||||
.. image:: configure-step1.png
|
||||
:width: 100%
|
||||
|
||||
Enter the name and click "Next". Although you've configured just one step of
|
||||
the wizard, the actual interface will consist of two: the dashboard always adds
|
||||
a final step to prompt the user to enter the name of the application instance
|
||||
within the environment:
|
||||
|
||||
.. image:: configure-step2.png
|
||||
:width: 100%
|
||||
|
||||
When you click "Create" button an instance of your application will be added to
|
||||
the environment, you'll see it in the list of components:
|
||||
|
||||
.. image:: new-env-2.png
|
||||
:width: 100%
|
||||
|
||||
So, now you may click the "Deploy this Environment" button and the application
|
||||
will greet the user with the name you've entered.
|
||||
|
||||
.. image:: new-env-3.png
|
||||
:width: 100%
|
||||
|
||||
|
||||
Simplifying code: namespaces
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Now that we've learned how to simplify the user's life by adding a UI
|
||||
definition, let's simplify the developer's life a bit.
|
||||
|
||||
When you were working with Murano classes in the previous part you probably
|
||||
noticed that the long class names with all those domain-name-based segments
|
||||
were hard to write and that it was easy to make a mistake:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Name: com.yourdomain.HelloWorld
|
||||
|
||||
Extends: io.murano.Application
|
||||
|
||||
Methods:
|
||||
deploy:
|
||||
Body:
|
||||
- $reporter: $this.find('io.murano.Environment').reporter
|
||||
- $reporter.report($this, "Hello, World!")
|
||||
|
||||
|
||||
To simplify the code we may use the concept of `namespaces` and `short names`.
|
||||
All but last segments of a long class name are namespaces, while the last
|
||||
segment is a short name of a class. In our example ``com.yourdomain`` is a
|
||||
namespace while the ``HelloWorld`` is a short name.
|
||||
|
||||
Short names have to be unique only within their namespace, so they tend to be
|
||||
expressive, short and human readable, while the namespaces are globally unique
|
||||
and thus are usually long and too detailed.
|
||||
|
||||
Murano provides a capability to abbreviate long namespaces with a short alias.
|
||||
Unlike namespaces, aliases don't need to be globally unique: they have
|
||||
to be unique only within a single file which uses them. So, they may be very
|
||||
short. So, in your file you may abbreviate your ``com.yourdomain`` namespace
|
||||
as ``my``, and standard Murano's ``io.murano`` as ``std``. Then instead of a
|
||||
long class name you may write a namespace alias followed by a colon character
|
||||
and then a short name, e.g. ``my:HelloWorld`` or ``std:Application``. This
|
||||
becomes very helpful when you have lots of class names in your code.
|
||||
|
||||
To use this feature, declare a special section called ``Namespaces`` in your
|
||||
class file. Inside that section provide a mapping of namespace aliases to full
|
||||
namespaces, like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Namespaces:
|
||||
my: com.yourdomain
|
||||
std: io.murano
|
||||
|
||||
.. note::
|
||||
|
||||
Since namespaces are often used in all other sections of files it is
|
||||
considered good practice to declare this section at a very top of your
|
||||
class file.
|
||||
|
||||
Quite often there is a namespace which is used much more often than others in a
|
||||
given file. In this case it would be beneficial to declare this namespace as a
|
||||
`default namespace`. Default namespace does not need a prefix at all: you just
|
||||
type short name of the class and Murano will interpret it as being in your
|
||||
default namespace. Use '=' character to declare the default namespace in your
|
||||
namespaces block:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
:emphasize-lines: 2,5
|
||||
|
||||
Namespaces:
|
||||
=: com.yourdomain
|
||||
std: io.murano
|
||||
|
||||
Name: HelloWorld
|
||||
|
||||
Extends: std:Application
|
||||
|
||||
Methods:
|
||||
deploy:
|
||||
Body:
|
||||
- $reporter: $this.find(std:Environment).reporter
|
||||
- $reporter.report($this, "Hello, World!")
|
||||
|
||||
|
||||
Notice that ``Name`` definition at line 5 uses the default namespace: the
|
||||
``HelloWorld`` is not prefixed with any namespaces, but is properly resolved
|
||||
to ``com.yourdomain.HelloWorld`` because of the default namespace declaration
|
||||
at line 2. Also, because Murano recognizes the ``ns:Class`` syntax there is
|
||||
no need to enclose ``std:Environment`` in quote marks, though it will also
|
||||
work.
|
||||
|
||||
|
||||
Adding more info for the catalog
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As you could see while browsing Murano Catalog your application entry in it is
|
||||
not particularly informative: the user can't get any description about your
|
||||
app, and the long domain-based name is not very user-friendly aither.
|
||||
|
||||
This can easily be improved. The ``manifest.yaml`` which we wrote in the first
|
||||
part contained only mandatory fields. This is how it should look by now:
|
||||
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
FullName: com.yourdomain.HelloWorld
|
||||
Type: Application
|
||||
Classes:
|
||||
com.yourdomain.HelloWorld: HelloWorld.yaml
|
||||
|
||||
Let's add more fields here.
|
||||
|
||||
First, you can add a ``Name`` attribute. Unlike ``FullName``, it is not a
|
||||
unique identifier of the package. But, if specified, it overrides the name of
|
||||
the package that is displayed in the catalog.
|
||||
|
||||
Then there is a ``Description`` field. This is a multi-line text attribute,
|
||||
providing detailed info about your package.
|
||||
|
||||
Then an ``Author`` field: here you can put your name or the name of your
|
||||
company, so it will be displayed in catalog as the name of the package
|
||||
developer. If this field is omitted, the catalog will consider the package to
|
||||
be made by "OpenStack", so don't forget this field if you care about your
|
||||
copyright.
|
||||
|
||||
When you add these fields your manifest may look like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
FullName: com.yourdomain.HelloWorld
|
||||
Type: Application
|
||||
Name: 'Hello, World'
|
||||
Description: |
|
||||
A package which demonstrates
|
||||
development for Murano
|
||||
by greeting the user.
|
||||
Author: John Doe
|
||||
Classes:
|
||||
com.yourdomain.HelloWorld: HelloWorld.yaml
|
||||
|
||||
|
||||
You may also add an icon to be displayed for your application. To do that just
|
||||
place a ``logo.png`` file with an appropriate image into the root folder of
|
||||
your package.
|
||||
|
||||
Zip the package directory and re-upload the file to the catalog. Then use
|
||||
Murano Dashboard and navigate to Applications/Catalog/Browse panel. You'll see
|
||||
that your app gets a logo, a more appropriate name and a description:
|
||||
|
||||
.. image:: hello-world-desc.png
|
||||
:width: 50%
|
||||
|
||||
So, here we've learned how to improve both the user's and developer's
|
||||
experience with developing Murano application packages. That was all we could
|
||||
do with the oversimplistic "Hello World" app. Let's move forward and touch
|
||||
some real-life applications.
|
799
doc/source/appdev-guide/step-by-step/part3.rst
Normal file
@ -0,0 +1,799 @@
|
||||
Part 3: Creating a Plone CMS application package
|
||||
------------------------------------------------
|
||||
|
||||
If you've completed "Hello, World" scenarios in the previous parts and are
|
||||
ready for some serious tasks, we've got a good example here.
|
||||
|
||||
Let's automate the deployment of some real application. We've chosen a "Plone
|
||||
CMS" for this purpose. Plone is a simple, but powerful and flexible Content
|
||||
Management System which can efficiently run on cloud. Its deployment scenario
|
||||
can be very simple for demo cases and can become really complicated for
|
||||
production-grade usage. So it's a good playground: in this part we'll create a
|
||||
Murano application to address the simplest scenario, then we will gradually add
|
||||
more features of production-grade deployments.
|
||||
|
||||
.. note::
|
||||
To learn more about Plone, its features, capabilities and deployment
|
||||
scenarios you may visit the `Official website of Plone Foundation
|
||||
<http://www.plone.org/>`_.
|
||||
|
||||
The goal
|
||||
~~~~~~~~
|
||||
|
||||
Simplest deployment of Plone CMS requires a single server, or, in the case of
|
||||
OpenStack, a Virtual Machine, to run on. Then a software should be downloaded
|
||||
and configured to run on that server.
|
||||
|
||||
So, as a bare minimum our Plone application package for Murano should automate
|
||||
the following steps:
|
||||
|
||||
#. Provision a virtual machine in OpenStack (VM);
|
||||
#. Configure ths VM's network connectivity and security;
|
||||
#. Download a distribution of Plone from Internet to the virtual machine;
|
||||
#. Install the distribution and configure some of its parameters with user
|
||||
input.
|
||||
|
||||
|
||||
Preparation
|
||||
~~~~~~~~~~~
|
||||
|
||||
First let's revisit what we've learned in previous parts and create a new
|
||||
application package with its manifest and create a class file to contain the
|
||||
logic of your app.
|
||||
|
||||
Create a new directory for a package, call it ``PloneApp``. Create a
|
||||
``manifest.yaml`` file as described in part 1 of this tutorial in the root of
|
||||
the package and fill it with data: name your package ``com.yourdomain.Plone``,
|
||||
set its type to ``Application``, give it a display name of "Plone CMS" and put
|
||||
your name as the author of the package:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
FullName: com.yourdomain.Plone
|
||||
Name: Plone CMS
|
||||
Description: Simple Plone Deployment
|
||||
Type: Application
|
||||
Author: John Doe
|
||||
|
||||
Then create a ``Classes`` sub directory inside your package directory and
|
||||
create a ``plone.yaml`` there. This will be your application class.
|
||||
|
||||
At the top of this file declare a `Namespace` section: this will simplify the
|
||||
code and save time on typing long class names. Make your namespace
|
||||
(``com.yourdomain``) a default namespace of the file, also include the standard
|
||||
namespace for Murano applications - ``io.murano``, alias it as ``std``.
|
||||
|
||||
Don't forget to include the ``Name`` of your class. Since you've declared a
|
||||
default namespace for a file you can name your class without a need to type its
|
||||
long part, just using the shortname.
|
||||
|
||||
Also include the ``Extends`` section: same as in our "Hello, World" example
|
||||
this application will inherit the ``io.murano.Application`` class, but since
|
||||
we've aliased this namespace as well, it may be shortened to
|
||||
``std:Application``
|
||||
|
||||
By now your class file should look like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Namespaces:
|
||||
=: com.yourdomain
|
||||
std: io.murano
|
||||
|
||||
Name: Plone
|
||||
|
||||
Extends: std:Application
|
||||
|
||||
|
||||
We'll add the actual logic in the next section. Now, save the file and include
|
||||
it into the ``Classes`` section of your manifest.yaml, which should now look
|
||||
like this:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
:emphasize-lines: 6-7
|
||||
|
||||
FullName: com.yourdomain.Plone
|
||||
Name: Plone CMS
|
||||
Description: Simple Plone Deployment
|
||||
Type: Application
|
||||
Author: John Doe
|
||||
Classes:
|
||||
com.yourdomain.Plone: plone.yaml
|
||||
|
||||
|
||||
You are all set and ready to go. Let's add the actual deployment logic.
|
||||
|
||||
Library classes
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Murano comes bundled with a so-called "Murano Core Library" - a Murano Package
|
||||
containing the classes to automate different scenarios of interaction with
|
||||
other entities such as OpenStack services or virtual machines. They follow
|
||||
object-oriented design: for example, there is a Murano class called
|
||||
``Instance`` which represents an OpenStack virtual machine: if you create an
|
||||
object of this class and execute a method called ``deploy`` for it Murano will
|
||||
do all the needed system calls to OpenStack Services to orchestrate the
|
||||
provisioning of a virtual machine and its networking configuration. Then this
|
||||
object will contain information about the state and configuration of the VM,
|
||||
such as its hostname, ip addresses etc. After the VM is provisioned you can use
|
||||
its object to send the configuration scripts to the VM to install and configure
|
||||
software for your application.
|
||||
|
||||
Other OpenStack resources such as Volumes, Networks, Ports, Routers etc also
|
||||
have their corresponding classes in the core library.
|
||||
|
||||
|
||||
Provisioning a VM
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
When creating your application package you can `compose` your application out
|
||||
of the components of core library. For example for an application which
|
||||
should run on a VM you can define an input property called ``instance`` and
|
||||
restrict the value type of this property to the aforementioned ``Instance``
|
||||
class with a contract.
|
||||
|
||||
Let's do that in the ``plone.yaml`` class file you've created.
|
||||
First, add a new namespace alias to your ``Namespaces`` section:
|
||||
shorten ``io.murano.resources`` as ``res``. This namespace of the core
|
||||
library contains all the resource classes, including the
|
||||
``io.murano.resources.Instance`` which we need to define the virtual machine:
|
||||
|
||||
.. code-block:: yaml
|
||||
:emphasize-lines: 4
|
||||
|
||||
Namespaces:
|
||||
=: com.yourdomain
|
||||
std: io.murano
|
||||
res: io.murano.resources
|
||||
|
||||
Now, let's add an input property to your class:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Properties:
|
||||
instance:
|
||||
Usage: In
|
||||
Contract: $.class(res:Instance)
|
||||
|
||||
Notice the contract at line 4: it limits the values of this property to the
|
||||
objects of class ``io.murano.resources.Instance`` or its subclasses.
|
||||
|
||||
This defines that your application needs a virtual machine. Now let's ensure
|
||||
that it is provisioned - or provision it otherwise. Add a ``deploy`` method to
|
||||
your application class and call instance's deploy method from it:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Methods:
|
||||
deploy:
|
||||
Body:
|
||||
- $this.instance.deploy()
|
||||
|
||||
That's very simple: you just access the ``instance`` property of your current
|
||||
object and run a method ``deploy`` for it. The core library defines this method
|
||||
of the ``Instance`` class in an `idempotent` manner: you may call it as many
|
||||
times as you want: the first call will actually provision the virtual machine
|
||||
in the cloud, while all the subsequent calls will no nothing, thus you may
|
||||
always call this method to ensure that the VM was properly provisioned. It's
|
||||
important since we define it as an input property: theoretically a user can
|
||||
pass an already-provisioned VM object as input, but you need to be sure.
|
||||
Always calling the ``deploy`` method is the best practice to follow.
|
||||
|
||||
Running a command on the VM
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Once the VM has been provisioned you may execute various kinds of software
|
||||
configuration scenarios on it to install and configure the actual application
|
||||
on the VM. Murano supports different types of software configuration tools to
|
||||
be run on a VM, but the simplest and the most common type is just a shell
|
||||
script.
|
||||
|
||||
To run a shell script on a virtual machine you may use a `static method`
|
||||
``runCommand`` of class ``io.murano.configuration.Linux``. Since this method is
|
||||
static you do not need to create any objects of its class: you can just do
|
||||
something like:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- type('io.murano.configuration.Linux').runCommand($server.agent, 'sudo apt-get update')
|
||||
|
||||
or, if we declare another namespace prefix
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Namespaces:
|
||||
...
|
||||
conf: io.murano.configuration
|
||||
|
||||
this may be shortened to
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- conf:Linux.runCommand($server.agent, 'sudo apt-get update')
|
||||
|
||||
In this case ``$server`` should be a variable containing an object of
|
||||
``io.murano.resources.Instance`` class, everything you pass as a second
|
||||
argument (``apt get update`` in the example above) is the shell command to be
|
||||
executed on a VM. You may pass not just a single line, but a multi-line text:
|
||||
it will be treated as a shell script.
|
||||
|
||||
.. note::
|
||||
The shell scripts and commands you send to a VM are executed by a special
|
||||
software component running on the VM - a `murano agent`. For the most
|
||||
popular distributions of Linux (Debian, Ubuntu, Centos, Fedora, etc.) it
|
||||
automatically gets installed on the VM once it is provisioned, but for other
|
||||
distribution and non-Linux OSes it has to be manually pre-installed in the
|
||||
image. See :ref:`Building Murano Image <building_images>` for details.
|
||||
|
||||
|
||||
Loading a script from a resource file
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Passing strings as a second argument of a ``runCommand`` method is convenient
|
||||
for short commands like the ``apt-get update`` shown in an example above.
|
||||
However for larger scripts it is not that useful. Instead it is preferable
|
||||
to load a script text from a file and run it. You can do that in Murano.
|
||||
|
||||
For example, let's make a script which downloads, unpacks, installs and
|
||||
configures Plone CMS on our VM. First, create a directory called ``Resources``
|
||||
inside your package directory. Then, create a file named ``install-plone.sh``
|
||||
and put the following script there:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
#input parameters
|
||||
|
||||
PL_PATH="$1"
|
||||
PL_PASS="$2"
|
||||
PL_PORT="$3"
|
||||
|
||||
|
||||
# Write log. Redirect stdout & stderr into log file:
|
||||
exec &> /var/log/runPloneDeploy.log
|
||||
# echo "Update all packages."
|
||||
sudo apt-get update
|
||||
|
||||
# Install the operating system software and libraries needed to run Plone:
|
||||
sudo apt-get -y install python-setuptools python-dev build-essential libssl-dev libxml2-dev libxslt1-dev libbz2-dev libjpeg62-dev
|
||||
|
||||
# Install optional system packages for the handling of PDF and Office files. Can be omitted:
|
||||
sudo apt-get -y install libreadline-dev wv poppler-utils
|
||||
|
||||
# Download the latest Plone unified installer:
|
||||
wget --no-check-certificate https://launchpad.net/plone/5.0/5.0.4/+download/Plone-5.0.4-UnifiedInstaller.tgz
|
||||
|
||||
# Unzip the latest Plone unified installer:
|
||||
tar -xvf Plone-5.0.4-UnifiedInstaller.tgz
|
||||
cd Plone-5.0.4-UnifiedInstaller
|
||||
|
||||
# Set the port that Plone will listen to on available network interfaces. Editing "http-address" param in buildout.cfg file:
|
||||
sed -i "s/^http-address = [0-9]*$/http-address = ${PL_PORT}/" buildout_templates/buildout.cfg
|
||||
|
||||
# Run the Plone installer in standalone mode
|
||||
./install.sh --password="${PL_PASS}" --target="${PL_PATH}" standalone
|
||||
|
||||
# Start Plone
|
||||
cd "${PL_PATH}/zinstance"
|
||||
bin/plonectl start
|
||||
|
||||
.. note::
|
||||
As you can see, this script uses apt to install the prerequisite software
|
||||
packages, so it expects a Debian-compatible Linux distro as the VM operating
|
||||
system. This particular script was tested on Ubuntu 14.04. Other distros
|
||||
may have a different set of preinstalled software and thus require different
|
||||
additional prerequisites.
|
||||
|
||||
|
||||
The comments in the script give the needed explanation: the script installs all
|
||||
the prerequisites, downloads a targz archive with a distribution of Plone,
|
||||
unpacks it, edits the ``buildout.cfg`` file to specify the port Plone will
|
||||
listen at, then runs the installation script which is included in the
|
||||
distribution. When that script is finished, the Plone daemon is started.
|
||||
|
||||
Save the file as ``Resources/install-plone.sh``. Now you may load its contents
|
||||
into a string variable in your class file. To do that, you need to use another
|
||||
static method: a ``string()`` method of a ``io.murano.system.Resources`` class:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- $script: type('io.murano.system.Resources').string('install-plone.sh')
|
||||
|
||||
or, with the introduction of another namespace prefix
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- $script: sys:Resources.string('install-plone.sh')
|
||||
|
||||
But before sending this script to a VM, it needs to be parametrized: as you
|
||||
can see in the script snippet above, it declares three variables which are
|
||||
used to set the installation path in the VM's filesystem, a default
|
||||
administrator's password and a listening port. In the script these values are
|
||||
initialized with stubs ``$1``, ``$2`` and ``$3``, now we need to replace these
|
||||
stubs with the actual user input. To do that our class needs to define the
|
||||
appropriate input properties and then do string replacement.
|
||||
|
||||
First, let's define the appropriate input properties in the ``Properties``
|
||||
block of the class, right after the ``instance`` property:
|
||||
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
:emphasize-lines: 6-18
|
||||
|
||||
Properties:
|
||||
instance:
|
||||
Usage: In
|
||||
Contract: $.class(res:Instance)
|
||||
|
||||
installationPath:
|
||||
Usage: In
|
||||
Contract: $.string().notNull()
|
||||
Default: '/opt/plone'
|
||||
|
||||
defaultPassword:
|
||||
Usage: In
|
||||
Contract: $.string().notNull()
|
||||
|
||||
listeningPort:
|
||||
Usage: In
|
||||
Contract: $.int().notNull()
|
||||
Default: 8080
|
||||
|
||||
Now, let's replace the stub values in that script value we've loaded into the
|
||||
``$script`` variable. This may be done using a ``replace`` function:
|
||||
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- $script: $script.replace({"$1" => $this.installationPath,
|
||||
"$2" => $this.defaultPassword,
|
||||
"$3" => $this.listeningPort})
|
||||
|
||||
Finally, the resulting ``$script`` variable may be passed as a second argument
|
||||
of a ``runCommand`` method, while the first one should be the ``instance``
|
||||
property, containing our VM-object:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
- conf:Linux.runCommand($this.instance.agent, $script)
|
||||
|
||||
|
||||
Configuring OpenStack Security
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By now we've got code which provisions a VM and a script which deploys and
|
||||
configures Plone on it. However, in most OpenStack clouds this is not enough:
|
||||
usually all incoming traffic to all the VMs is blocked by default, so we need
|
||||
to configure security group of OpenStack to allow the incoming http calls to
|
||||
our VM on the port our Plone server listens at.
|
||||
|
||||
To do that we need to use a ``securityGroupManager`` property of the
|
||||
``Environment`` class which owns our application. That property contains an
|
||||
object of type ``io.murano.system.SecurityGroupManager``, which defines a
|
||||
``addGroupIngress`` method. This method allows us to add a security group rule
|
||||
to allow incoming traffic of some type through a specific port within a port
|
||||
range. It accepts a list of YAML objects, each having four keys: ``FromPort``
|
||||
and ``ToPort`` to define the boundaries of the port range, ``IpProtocol`` to
|
||||
define the type of the protocol and ``External`` boolean flag to indicate if
|
||||
the incoming traffic should be be allowed to originate from outside of the
|
||||
environment (if this flag is false, the traffic will be accepted only from the
|
||||
VMs deployed by the application in the same Murano environment).
|
||||
|
||||
Let's do this in code:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
- $environment: $this.find(std:Environment)
|
||||
- $manager: $environment.securityGroupManager
|
||||
- $rules:
|
||||
- FromPort: $this.listeningPort
|
||||
ToPort: $this.listeningPort
|
||||
IpProtocol: tcp
|
||||
External: true
|
||||
- $manager.addGroupIngress($rules)
|
||||
- $environment.stack.push()
|
||||
|
||||
It's quite straightforward, just notice the last line. It is required, because
|
||||
current implementation of ``SecurityGroupManager`` relies on Heat underneath -
|
||||
it modifies the `Heat Stack` associated with our environment, but does not
|
||||
apply the changes to the actual cloud. To apply them the stack needs to be
|
||||
`pushed`, i.e. submitted to Heat Orchestration service. The last line does
|
||||
exactly that.
|
||||
|
||||
|
||||
Notifying end-user on Plone location
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When the deployment is completed and our instance of Plone server starts
|
||||
listening on a provisioned virtual machine, the end user has one last question
|
||||
to solve: to find out where it is. Of course, the user may use OpenStack
|
||||
Dashboard to list all the provisioned VMs, find the one which has just been
|
||||
created and look for its IP address. But that's inconvenient. It would be much
|
||||
better if Murano notified the end-user on where to find Plone once it is ready.
|
||||
|
||||
We may utilize the same approach we used in the previous parts to say "Hello,
|
||||
World" - call a ``report`` method of ``reporter`` attribute of the
|
||||
``Environment`` class. The tricky part is getting the IP address.
|
||||
|
||||
Class ``io.murano.resources.Instance`` has an `output property` called
|
||||
``ipAddresses``. Unlike input properties the output ones are not provided by
|
||||
users but are set by objects themselves while their methods are executed. The
|
||||
``ipAddresses`` is assigned during the execution of ``deploy`` method of the
|
||||
VM. The value is the list of ip addresses assigned to different interfaces of
|
||||
the machine. Also, if the ``assignFloatingIp`` input property is set to
|
||||
``true``, another output property will be set during the execution of
|
||||
``deploy`` - a ``floatingIpAddress`` will contain the floating ip attached to
|
||||
the VM.
|
||||
|
||||
Let's use this knowledge and build a proper report message:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
- $message: 'Plone is up and running at '
|
||||
- If: $this.instance.assignFloatingIp
|
||||
Then:
|
||||
- $message: $message + $this.instance.floatingIpAddress
|
||||
Else:
|
||||
- $message: $message + $this.instance.ipAddresses.first()
|
||||
- $message: $message + ":" + str($this.listeningPort)
|
||||
- $environment.reporter.report($this, $message)
|
||||
|
||||
Note the usage of ``If`` expression: it is similar to other programming
|
||||
languages, just uses YAML keys to define the "if" and "else" blocks.
|
||||
|
||||
This code creates a string variable called ``$message``, initializes it with
|
||||
the beginning of the message string, then appends either a floating ip address
|
||||
of the VM (if it's set) or the first of the regular ips otherwise. Then it
|
||||
appends a listening port after a colon character - and reports the resulting
|
||||
message to the user.
|
||||
|
||||
Completing the Plone class
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We've got all the pieces to deploy our Plone application, now let's combine
|
||||
them together. Our final class file should look like this:
|
||||
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Namespaces:
|
||||
=: com.yourdomain
|
||||
std: io.murano
|
||||
res: io.murano.resources
|
||||
sys: io.murano.system
|
||||
|
||||
Name: Plone
|
||||
|
||||
Extends: std:Application
|
||||
|
||||
Properties:
|
||||
instance:
|
||||
Usage: In
|
||||
Contract: $.class(res:Instance)
|
||||
|
||||
installationPath:
|
||||
Usage: In
|
||||
Contract: $.string().notNull()
|
||||
Default: '/opt/plone'
|
||||
|
||||
defaultPassword:
|
||||
Usage: In
|
||||
Contract: $.string().notNull()
|
||||
|
||||
listeningPort:
|
||||
Usage: In
|
||||
Contract: $.int().notNull()
|
||||
Default: 8080
|
||||
|
||||
Methods:
|
||||
deploy:
|
||||
Body:
|
||||
- $this.instance.deploy()
|
||||
- $script: sys:Resources.string('install-plone.sh')
|
||||
- $script: $script.replace({
|
||||
"$1" => $this.installationPath,
|
||||
"$2" => $this.defaultPassword,
|
||||
"$3" => $this.listeningPort
|
||||
})
|
||||
- type('io.murano.configuration.Linux').runCommand($this.instance.agent, $script)
|
||||
- $environment: $this.find(std:Environment)
|
||||
- $manager: $environment.securityGroupManager
|
||||
- $rules:
|
||||
- FromPort: $this.listeningPort
|
||||
ToPort: $this.listeningPort
|
||||
IpProtocol: tcp
|
||||
External: true
|
||||
- $manager.addGroupIngress($rules)
|
||||
- $environment.stack.push()
|
||||
- $formatString: 'Plone is up and running at {0}:{1}'
|
||||
- If: $this.instance.assignFloatingIp
|
||||
Then:
|
||||
- $address: $this.instance.floatingIpAddress
|
||||
Else:
|
||||
- $address: $this.instance.ipAddresses.first()
|
||||
- $message: format($formatString, $address, $this.listeningPort)
|
||||
- $environment.reporter.report($this, $message)
|
||||
|
||||
|
||||
That's all, our class is ready.
|
||||
|
||||
|
||||
Providing a UI definition
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Last but not least, we need to add a UI definition file to define a template
|
||||
for the user input and create wizard steps.
|
||||
|
||||
This time both are a bit more complicated than they were for the "Hello, World"
|
||||
app.
|
||||
|
||||
First, let's create the wizard steps. It's better to decompose the UI into two
|
||||
steps: the first one will define the properties of a Virtual Machine, and the
|
||||
second one the configuration properties of the Plone application itself.
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Forms:
|
||||
- instanceConfiguration:
|
||||
fields:
|
||||
- name: hostname
|
||||
type: string
|
||||
required: true
|
||||
- name: image
|
||||
type: image
|
||||
imageType: linux
|
||||
- name: flavor
|
||||
type: flavor
|
||||
- name: assignFloatingIp
|
||||
type: boolean
|
||||
- ploneConfiguration:
|
||||
fields:
|
||||
- name: installationPath
|
||||
type: string
|
||||
- name: defaultPassword
|
||||
type: password
|
||||
required: true
|
||||
- name: listeningPort
|
||||
type: integer
|
||||
|
||||
This is familiar to what we had on the previous step, however there are several
|
||||
new types of fields: while the types ``integer`` and ``boolean`` are quite
|
||||
obvious - they will render a numeric up-and-down textbox and checkbox controls
|
||||
respectively - other field types are more specific.
|
||||
|
||||
Field of type ``image`` will render a drop-down list allowing you to choose an
|
||||
image for your VM, and the list of images will contain only the ones having
|
||||
appropriate metadata associated (the type of metadata is defined by the
|
||||
``imageType`` attribute: this particular example requires it to be tagged as
|
||||
"Generic Linux").
|
||||
|
||||
Field of type ``flavor`` will render a drop-down list allowing you to choose a
|
||||
flavor for your VM among the ones registered in Nova.
|
||||
|
||||
Field of type ``password`` will render a pair of text-boxes in a password
|
||||
input mode (i.e. hiding all the input with '*'-characters). The rendered field
|
||||
will have appropriate validation: it will ensure that the values entered in
|
||||
both fields are identical (thus providing a "repeat password" functionality)
|
||||
and will also enforce password complexity check.
|
||||
|
||||
This defines the basic UI, but it is not particularly user friendly: when
|
||||
MuranoDashboard renders the wizard it will label appropriate controls with the
|
||||
names of the fields, but they usually don't look informative and pretty.
|
||||
|
||||
So, to improve the user experience you may add additional attributes to field
|
||||
descriptors here. ``label`` attribute allows you to define a custom label to be
|
||||
rendered next to appropriate control, ``description`` allows you to provide a
|
||||
longer text to be displayed on the form as a description of the control, and,
|
||||
finally, an ``initial`` attribute allows you define the default value to be
|
||||
entered into the control when it is shown to the end-user.
|
||||
|
||||
Modify the ``Forms`` section to use these attributes:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
:emphasize-lines: 6-9,14-17,20-23,26-28,33-36,38-39,44-46
|
||||
|
||||
Forms:
|
||||
- instanceConfiguration:
|
||||
fields:
|
||||
- name: hostname
|
||||
type: string
|
||||
label: Host Name
|
||||
description: >-
|
||||
Enter a hostname for a virtual machine to be created
|
||||
initial: plone-vm
|
||||
required: true
|
||||
- name: image
|
||||
type: image
|
||||
imageType: linux
|
||||
label: Instance image
|
||||
description: >-
|
||||
Select valid image for the application. Image should already be prepared and
|
||||
registered in glance.
|
||||
- name: flavor
|
||||
type: flavor
|
||||
label: Instance flavor
|
||||
description: >-
|
||||
Select registered in Openstack flavor. Consider that application performance
|
||||
depends on this parameter.
|
||||
- name: assignFloatingIp
|
||||
type: boolean
|
||||
label: Assign Floating IP
|
||||
description: >-
|
||||
Check to assign floating IP automatically
|
||||
- ploneConfiguration:
|
||||
fields:
|
||||
- name: installationPath
|
||||
type: string
|
||||
label: Installation Path
|
||||
initial: '/opt/plone'
|
||||
description: >-
|
||||
Enter the path on the VM filesystem to deploy Plone into
|
||||
- name: defaultPassword
|
||||
label: Admin password
|
||||
description: Default administrator's password
|
||||
type: password
|
||||
required: true
|
||||
- name: listeningPort
|
||||
type: integer
|
||||
label: Listening Port
|
||||
description: Port to listen at
|
||||
initial: 8080
|
||||
|
||||
|
||||
Now, let's add an ``Application`` section to provide templated input for our
|
||||
app:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Application:
|
||||
?:
|
||||
type: com.yourdomain.Plone
|
||||
instance:
|
||||
?:
|
||||
type: io.murano.resources.LinuxMuranoInstance
|
||||
name: $.instanceConfiguration.hostname
|
||||
image: $.instanceConfiguration.image
|
||||
flavor: $.instanceConfiguration.flavor
|
||||
assignFloatingIp: $.instanceConfiguration.assignFloatingIp
|
||||
installationPath: $.ploneConfiguration.installationPath
|
||||
defaultPassword: $.ploneConfiguration.defaultPassword
|
||||
listeningPort: $.ploneConfiguration.listeningPort
|
||||
|
||||
Note the ``instance`` part here: since our ``instance`` input property is not
|
||||
a scalar value but rather an object, we are placing another object template
|
||||
inside the appropriate section. Note that the type of this object is not
|
||||
``io.murano.resources.Instance`` as you could expect based on the property
|
||||
contract, but a more specific class: ``LinuxMuranoInstance`` in the same
|
||||
namespace. Since this class inherits the former, it matches the contract, but
|
||||
it provides a more appropriate implementation than the base one.
|
||||
|
||||
|
||||
Let's combine the two snippets together, we'll get the final UI definition of
|
||||
our app:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Application:
|
||||
?:
|
||||
type: com.yourdomain.Plone
|
||||
instance:
|
||||
?:
|
||||
type: io.murano.resources.LinuxMuranoInstance
|
||||
name: $.instanceConfiguration.hostname
|
||||
image: $.instanceConfiguration.image
|
||||
flavor: $.instanceConfiguration.flavor
|
||||
assignFloatingIp: $.instanceConfiguration.assignFloatingIp
|
||||
installationPath: $.ploneConfiguration.installationPath
|
||||
defaultPassword: $.ploneConfiguration.defaultPassword
|
||||
listeningPort: $.ploneConfiguration.listeningPort
|
||||
Forms:
|
||||
- instanceConfiguration:
|
||||
fields:
|
||||
- name: hostname
|
||||
type: string
|
||||
label: Host Name
|
||||
description: >-
|
||||
Enter a hostname for a virtual machine to be created
|
||||
initial: 'plone-vm'
|
||||
required: true
|
||||
- name: image
|
||||
type: image
|
||||
imageType: linux
|
||||
label: Instance image
|
||||
description: >-
|
||||
Select valid image for the application. Image should already be prepared and
|
||||
registered in glance.
|
||||
- name: flavor
|
||||
type: flavor
|
||||
label: Instance flavor
|
||||
description: >-
|
||||
Select registered in Openstack flavor. Consider that application performance
|
||||
depends on this parameter.
|
||||
- name: assignFloatingIp
|
||||
type: boolean
|
||||
label: Assign Floating IP
|
||||
description: >-
|
||||
Check to assign floating IP automatically
|
||||
- ploneConfiguration:
|
||||
fields:
|
||||
- name: installationPath
|
||||
type: string
|
||||
label: Installation Path
|
||||
initial: '/opt/plone'
|
||||
description: >-
|
||||
Enter the path on the VM filesystem to deploy Plone into
|
||||
- name: defaultPassword
|
||||
label: Admin password
|
||||
description: Default administrator's password
|
||||
type: password
|
||||
required: true
|
||||
- name: listeningPort
|
||||
type: integer
|
||||
label: Listening Port
|
||||
description: Port to listen at
|
||||
initial: 8080
|
||||
|
||||
|
||||
Save this file as a ``ui.yaml`` in a ``UI`` folder of your package. As a final
|
||||
touch add a logo to the package - save the image below to the root directory of
|
||||
your package as ``logo.png``:
|
||||
|
||||
.. image:: plone-logo.png
|
||||
:width: 100
|
||||
|
||||
The package is ready. Zip it and import to Murano catalog. We are ready to try
|
||||
it.
|
||||
|
||||
Deploying the package
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Go to Murano Dashboard, create an environment and add a "Plone CMS" application
|
||||
to it. You'll see the nice wizard with all the field labels and descriptions
|
||||
you've added to the ui definition file:
|
||||
|
||||
.. image:: plone-simple-step1.png
|
||||
:width: 50%
|
||||
|
||||
.. image:: plone-simple-step2.png
|
||||
:width: 50%
|
||||
|
||||
After the app is added to the environment, click the "Deploy this environment"
|
||||
button. The deployment will take about 10 minutes, depending on the speed of
|
||||
the VM's internet connection and the amount of packages to be updated. When it
|
||||
is over, check the "Last operation" column in the environment's list of
|
||||
components near the Plone component. It should contain a message "Plone is up
|
||||
and running at ..." followed by ip address and port:
|
||||
|
||||
.. image:: plone-ready.png
|
||||
:width: 50%
|
||||
|
||||
Enter this address to the address bar of your browser. You'll see the default
|
||||
management interface of Plone:
|
||||
|
||||
.. image:: plone-admin.png
|
||||
:width: 50%
|
||||
|
||||
If you click a "Create a new Plone site" button you'll be prompted for username
|
||||
and password. Use ``admin`` username and the password which you entered in the
|
||||
Wizard. See `Plone Documentation <http://docs.plone.org>`_ for details on how
|
||||
to operate Plone.
|
||||
|
||||
This concludes this part of the course. The application package we created
|
||||
demonstrates the basic capabilities of Murano for the deployments of real-world
|
||||
applications. However, the deployed configuration of Plone is not of
|
||||
production-grade service: it is just a single VM with all-in-one service
|
||||
topology, which is not a scalable or fault-tolerant solution.
|
||||
In the next part we will learn some advanced features which may help to bring
|
||||
more production-grade capabilities to our package.
|
355
doc/source/appdev-guide/step-by-step/part4.rst
Normal file
@ -0,0 +1,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 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!
|
BIN
doc/source/appdev-guide/step-by-step/plone-admin.png
Normal file
After Width: | Height: | Size: 141 KiB |
BIN
doc/source/appdev-guide/step-by-step/plone-logo.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
doc/source/appdev-guide/step-by-step/plone-ready.png
Normal file
After Width: | Height: | Size: 44 KiB |
BIN
doc/source/appdev-guide/step-by-step/plone-simple-step1.png
Normal file
After Width: | Height: | Size: 136 KiB |
BIN
doc/source/appdev-guide/step-by-step/plone-simple-step2.png
Normal file
After Width: | Height: | Size: 111 KiB |
@ -1,608 +1,41 @@
|
||||
.. _step-by-step:
|
||||
|
||||
==================
|
||||
Step-by-step guide
|
||||
==================
|
||||
Developing Murano Packages 101
|
||||
==============================
|
||||
|
||||
The goal of this manual is to walk you through the steps
|
||||
that should be taken while composing an application package
|
||||
to get it ready for uploading to Murano.
|
||||
|
||||
This tutorial uses a demo application named ApacheHTTPServer
|
||||
to demonstrate how you can create your own Murano application
|
||||
from scratch. We will walk you through its source code and
|
||||
explain how to upload it.
|
||||
|
||||
ApacheHTTPServer is a simple Murano application that spawns
|
||||
a virtual machine and installs Apache HTTP Server on it.
|
||||
It may also install php if a user wants to.
|
||||
|
||||
The source code of ApacheHTTPServer is available at `github
|
||||
<https://github.com/openstack/murano-apps/tree/master/ApacheHTTPServer/package>`_.
|
||||
|
||||
ApacheHTTPServer's source code is written in MuranoPL.
|
||||
This programming language is object-oriented, and we will
|
||||
see classes, objects and object instances. The detailed
|
||||
explanation of its syntax can be found in the :ref:`MuranoPL
|
||||
reference <murano-pl>`.
|
||||
|
||||
.. warning::
|
||||
Before you start the Murano application creation process, please consider
|
||||
the :ref:`System prerequisites <system_prerequisites>` and
|
||||
:ref:`Lab requirements <lab_requirements>` in order you do not risk
|
||||
starting with a wrong environment
|
||||
Murano provides a very powerful and flexible platform to automate the
|
||||
provisioning, deployment, configuration and lifecycle management of
|
||||
applications in OpenStack clouds. However, the flexibility comes at cost: to
|
||||
manage an application with Murano one has to design and develop special
|
||||
scenarios which will tell Murano how to handle different aspects of application
|
||||
lifecycle. These scenarios are usually called "Murano Applications" or "Murano
|
||||
Packages". It is not hard to build them, but it requires some time to get
|
||||
familiar with Murano's DSL to define these scenarios and to learn the common
|
||||
patterns and best practices. This article provides a basic introductory course
|
||||
of these aspects and aims to be the starting point for the developers willing
|
||||
to learn how to develop Murano Application packages with ease.
|
||||
|
||||
The course consists of the following parts:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
Step 1. Create the structure of the package
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You should structure an application package very neatly in order
|
||||
the application could be managed and deployed in the catalog
|
||||
successfully.
|
||||
|
||||
The package structure of ApacheHTTPServer package is::
|
||||
|
||||
..
|
||||
|
||||
|_ Classes
|
||||
| |_ ApacheHttpServer.yaml
|
||||
|
|
||||
|_ Resources
|
||||
| |_ scripts
|
||||
| |_runApacheDeploy.sh
|
||||
| |_ DeployApache.template
|
||||
|
|
||||
|_ UI
|
||||
| |_ ui.yaml
|
||||
|
|
||||
|_ logo.png
|
||||
|
|
||||
|_ manifest.yaml
|
||||
|
||||
The detailed information regarding the package structure can be found
|
||||
in the :ref:`Murano packages <murano-packages>` section.
|
||||
|
||||
|
||||
Step 2. Create the manifest file
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The application manifest file contains general application metadata.
|
||||
It is an entry-point for each Murano application, and is very similar
|
||||
to the manifest of a jar archive. It has a fixed format based on YAML.
|
||||
|
||||
The ApacheHTTPServer's manifest file:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Format: 1.0
|
||||
Type: Application
|
||||
FullName: com.example.apache.ApacheHttpServer
|
||||
Name: Apache HTTP Server
|
||||
Description: |
|
||||
The Apache HTTP Server Project is an effort to develop and maintain an
|
||||
open-source HTTP server for modern operating systems including UNIX and
|
||||
Windows NT.
|
||||
...
|
||||
Author: Mirantis, Inc
|
||||
Tags: [HTTP, Server, WebServer, HTML, Apache]
|
||||
Classes:
|
||||
com.example.apache.ApacheHttpServer: ApacheHttpServer.yaml
|
||||
|
||||
Now, let's inspect ``manifest.yaml`` line-by-line.
|
||||
|
||||
Format
|
||||
------
|
||||
|
||||
Specifies the version of the format for manifest.yaml to track
|
||||
the syntax changes. Format key presents in each manifest file.
|
||||
Currently, available versions are: `1.0`, `1.1`, `1.2` and `1.3`.
|
||||
For more information, refer to :ref:`format documentation<ManifestFormat>`.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Format: 1.0
|
||||
|
||||
Type
|
||||
----
|
||||
|
||||
Specifies the type of the package:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Type: Application
|
||||
|
||||
.. note::
|
||||
``Application`` starts with the capital letter.
|
||||
This is the naming convention for all the pre-defined values
|
||||
in Murano code.
|
||||
|
||||
FullName
|
||||
--------
|
||||
|
||||
Stands for the unique service application name. That name
|
||||
allows to easily recognize to which scope an application
|
||||
belongs. All other applications can address to the Apache
|
||||
application methods by this name.
|
||||
|
||||
To ensure the global uniqueness, the same naming
|
||||
convention as the naming convention of Java packages and
|
||||
classes is followed.
|
||||
|
||||
The examples of such class names may include:
|
||||
|
||||
* ``com.example.Foo`` - for demo applications and packages
|
||||
|
||||
* ``org.openstack.projectName.Foo`` - for applications and packages developed
|
||||
and maintained by the teams of official OpenStack projects
|
||||
|
||||
* ``com.companyname.Foo`` - for applications and packages developed and
|
||||
maintained by a third party controlling the "companyname.com" domain name
|
||||
|
||||
* ``io.murano.Foo`` - for applications and packages developed and maintained by
|
||||
the core murano team as part of the murano project. So, io.murano is a
|
||||
preferred alias for longer ``org.openstack.murano`` FQN prefix.
|
||||
|
||||
In the following example the ``com.example.apache.`` part is the "package" part
|
||||
of the name, while ``ApacheHttpServer`` stands for the "class" part of the name:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
FullName: com.example.apache.ApacheHttpServer
|
||||
|
||||
|
||||
Name
|
||||
----
|
||||
|
||||
Stands for the display name of the application. You will be able to reset a display name
|
||||
when you upload ApacheHTTPServer package to Murano:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Name: Apache HTTP Server
|
||||
|
||||
Description
|
||||
-----------
|
||||
|
||||
Contains the application description rendered under the application title:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Description: |
|
||||
The Apache HTTP Server Project is an effort to develop and maintain an
|
||||
open-source HTTP server for modern operating systems including UNIX and
|
||||
Windows NT. The goal of this project is to provide a secure, efficient and
|
||||
extensible server that provides HTTP services in sync with the current HTTP
|
||||
standards.
|
||||
Apache httpd has been the most popular web server on the Internet since
|
||||
April 1996, and celebrated its 17th birthday as a project this February.
|
||||
|
||||
Let's take a closer look at the syntax:
|
||||
|
||||
The vertical line ``|`` symbol comes from YAML syntax.
|
||||
The ``>`` symbol can be used interchangeably.
|
||||
These are the `YAML block style indicators <http://yaml.org/spec/current.html#id2537921>`_,
|
||||
which mean that all the leading indents and new line symbols should be preserved.
|
||||
This is very useful for long, multi-line descriptions, because this affects how
|
||||
they are displayed on the UI.
|
||||
|
||||
.. warning::
|
||||
Avoid tab symbols inside YAML files. If YAML contains the tab symbol, it will not
|
||||
be parsed correctly. The error message may be cryptic or misleading.
|
||||
We recommend that you check the YAML syntax before composing the application
|
||||
package using any of the available online tools.
|
||||
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
Contains the name of the author of an application, it is only displayed
|
||||
in the application details and does not affect anything.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Author: Mirantis, Inc
|
||||
|
||||
.. note::
|
||||
Single quotes usage is optional here: ``Author: 'Mirantis, Inc'``,
|
||||
thus they are omitted in the code extract below.
|
||||
|
||||
|
||||
Tags
|
||||
----
|
||||
|
||||
Is an array of tags. You can search an application by its tag.
|
||||
You may want to specify several tags for one application:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Tags: [HTTP, Server, WebServer, HTML, Apache]
|
||||
|
||||
Besides, YAML allows tag specification using another syntax,
|
||||
which is an equivalent to the one given above:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Tags:
|
||||
- HTTP
|
||||
- Server
|
||||
- WebServer
|
||||
- HTML
|
||||
- Apache
|
||||
|
||||
Classes
|
||||
-------
|
||||
|
||||
Is a mapping between all classes present in ApacheHttpServer application
|
||||
and the file names where these classes are defined in. This is one-to-one relationship,
|
||||
which means that there is one and the only class per a single file.
|
||||
|
||||
The line ``com.example.apache.ApacheHttpServer: ApacheHttpServer.yaml`` says that the class ``com.example.apache.ApacheHttpServer`` is defined in the file ``ApacheHttpServer.yaml``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Classes:
|
||||
com.example.apache.ApacheHttpServer: ApacheHttpServer.yaml
|
||||
|
||||
|
||||
Step 3. Create the execution plan template
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The execution plan template contains the instructions understandable to the murano
|
||||
agent on what should be executed to deploy an application. It is the file with the
|
||||
``.template`` extension located in the ``/APP_NAME/Resources`` directory.
|
||||
|
||||
The ApacheHTTPServer's DeployApache.template:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
FormatVersion: 2.0.0
|
||||
Version: 1.0.0
|
||||
Name: Deploy Apache
|
||||
|
||||
Parameters:
|
||||
enablePHP: $enablePHP
|
||||
|
||||
Body: |
|
||||
return apacheDeploy('{0}'.format(args.enablePHP)).stdout
|
||||
|
||||
Scripts:
|
||||
apacheDeploy:
|
||||
Type: Application
|
||||
Version: 1.0.0
|
||||
EntryPoint: runApacheDeploy.sh
|
||||
Files: []
|
||||
Options:
|
||||
captureStdout: true
|
||||
captureStderr: true
|
||||
|
||||
As it can be viewed from the source code, besides specifying versions of different items,
|
||||
ApacheHTTPServer execution plan accepts the ``enablePHP`` parameter. This parameter is
|
||||
an input parameter to the ``apacheDeploy.sh`` script. This script initiates ``runApacheDeploy.sh``
|
||||
execution, which is also located at the ``Resources`` directory and installs apache app and php
|
||||
if selected.
|
||||
|
||||
For the detailed information regarding the execution plan template, its sections and syntax,
|
||||
please refer to the :ref:`Execution plan template <exec_plan>`.
|
||||
|
||||
|
||||
Step 4. Create the dynamic UI form definition
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
ApacheHTTPServer's ui.yaml source code:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Version: 2
|
||||
|
||||
Application:
|
||||
?:
|
||||
type: com.example.apache.ApacheHttpServer
|
||||
name: $.appConfiguration.name
|
||||
enablePHP: $.appConfiguration.enablePHP
|
||||
instance:
|
||||
?:
|
||||
type: io.murano.resources.LinuxMuranoInstance
|
||||
name: generateHostname($.instanceConfiguration.unitNamingPattern, 1)
|
||||
flavor: $.instanceConfiguration.flavor
|
||||
image: $.instanceConfiguration.osImage
|
||||
keyname: $.instanceConfiguration.keyPair
|
||||
availabilityZone: $.instanceConfiguration.availabilityZone
|
||||
assignFloatingIp: $.appConfiguration.assignFloatingIP
|
||||
|
||||
Forms:
|
||||
- appConfiguration:
|
||||
fields:
|
||||
- name: license
|
||||
type: string
|
||||
description: Apache License, Version 2.0
|
||||
hidden: true
|
||||
required: false
|
||||
- name: name
|
||||
type: string
|
||||
label: Application Name
|
||||
initial: 'ApacheHttpServer'
|
||||
description: >-
|
||||
Enter a desired name for the application. Just A-Z, a-z, 0-9, dash and
|
||||
underline are allowed
|
||||
- name: enablePHP
|
||||
label: Enable PHP
|
||||
type: boolean
|
||||
description: >-
|
||||
Add php support to the Apache WebServer
|
||||
initial: false
|
||||
required: false
|
||||
widgetMedia:
|
||||
css: {all: ['muranodashboard/css/checkbox.css']}
|
||||
- name: assignFloatingIP
|
||||
type: boolean
|
||||
label: Assign Floating IP
|
||||
description: >-
|
||||
Select to true to assign floating IP automatically
|
||||
initial: false
|
||||
required: false
|
||||
widgetMedia:
|
||||
css: {all: ['muranodashboard/css/checkbox.css']}
|
||||
- name: dcInstances
|
||||
type: integer
|
||||
hidden: true
|
||||
initial: 1
|
||||
|
||||
...
|
||||
|
||||
Now, let's inspect it line-by-line.
|
||||
|
||||
Application
|
||||
-----------
|
||||
|
||||
Defines the object model by which engine deploys the ApacheHTTPServer
|
||||
application, and includes YAQL expressions.
|
||||
|
||||
The section contains the reference to the Apache class,
|
||||
the one that is provided in the manifest, named with the `?`
|
||||
symbol. This indicates system information:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Application:
|
||||
?:
|
||||
type: com.example.apache.ApacheHttpServer
|
||||
|
||||
For ApacheHTTPServer application it is defined that the user should input the application name,
|
||||
some instance parameters and decide whether PHP should be enabled or not:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
enablePHP: $.appConfiguration.enablePHP
|
||||
|
||||
The `instance` section assumes that the value, entered by the user in the first form
|
||||
named ``appConfiguration`` is stored in an application object module. The same applies
|
||||
for the instance parameter. Providing the question mark with the defined type
|
||||
``io.murano.resources.LinuxMuranoInstance`` indicates an instance of MuranoPl object.
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
instance:
|
||||
?:
|
||||
type: io.murano.resources.LinuxMuranoInstance
|
||||
|
||||
.. note::
|
||||
This parameter is named ``instance`` here because its class definition
|
||||
property has the ``instance`` name. You can specify any name in
|
||||
the :ref:`class definition file <step_5_class_definition>`,
|
||||
and then use it in the UI form definition.
|
||||
|
||||
Forms
|
||||
-----
|
||||
|
||||
Contains UI forms prototypes that are merged to the application
|
||||
creation wizard.
|
||||
|
||||
Each form field will be translated to the Django field and most of
|
||||
the parameters correspond to parameters in the Django form field.
|
||||
All fields are required by default. Hidden fields are used to print
|
||||
extra information in the form description.
|
||||
|
||||
After the upload, the section content will be browsed on the left
|
||||
side of the form and its description on the right.
|
||||
|
||||
Please take a look at the :guilabel:`Configure Application: Apache HTTP Server` dialog:
|
||||
|
||||
.. image:: ../figures/step_1.png
|
||||
|
||||
.. note::
|
||||
The :guilabel:`assignFloatingIP` and :guilabel:`enablePHP`
|
||||
boolean fields are shown as checkboxes.
|
||||
|
||||
Here is how the second dialog looks like:
|
||||
|
||||
.. image:: ../figures/step_2.png
|
||||
|
||||
For more information about Dynamic UI, please refer to
|
||||
:ref:`the main reference <DynamicUISpec>`.
|
||||
|
||||
.. _step_5_class_definition:
|
||||
|
||||
Step 5: Define MuranoPL class definitions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
All application classes are located in the ``Classes`` folder. As ApacheHttpServer
|
||||
uses only one class, just one file can be found in this directory.
|
||||
|
||||
Here is how it looks like:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Namespaces:
|
||||
=: com.example.apache
|
||||
std: io.murano
|
||||
res: io.murano.resources
|
||||
sys: io.murano.system
|
||||
|
||||
Name: ApacheHttpServer
|
||||
|
||||
Extends: std:Application
|
||||
|
||||
Properties:
|
||||
name:
|
||||
Contract: $.string().notNull()
|
||||
|
||||
enablePHP:
|
||||
Contract: $.bool()
|
||||
Default: false
|
||||
|
||||
instance:
|
||||
Contract: $.class(res:Instance).notNull()
|
||||
|
||||
Methods:
|
||||
initialize:
|
||||
Body:
|
||||
- $._environment: $.find(std:Environment).require()
|
||||
|
||||
deploy:
|
||||
Body:
|
||||
- If: not $.getAttr(deployed, false)
|
||||
Then:
|
||||
- $._environment.reporter.report($this, 'Creating VM for Apache Server.')
|
||||
- $securityGroupIngress:
|
||||
...
|
||||
- $._environment.securityGroupManager.addGroupIngress($securityGroupIngress)
|
||||
- $.instance.deploy()
|
||||
- $resources: new(sys:Resources)
|
||||
- $template: $resources.yaml('DeployApache.template').bind(dict(enablePHP => $.enablePHP))
|
||||
- $._environment.reporter.report($this, 'Instance is created. Deploying Apache')
|
||||
- $.instance.agent.call($template, $resources)
|
||||
- $._environment.reporter.report($this, 'Apache is installed.')
|
||||
- $.setAttr(deployed, true)
|
||||
|
||||
Now, let's inspect it line-by-line.
|
||||
|
||||
Namespaces
|
||||
----------
|
||||
|
||||
Can be named *shortcuts* since this is an additional section
|
||||
which enables short names instead of the long ones:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
Namespaces:
|
||||
=: com.example.apache
|
||||
std: io.murano
|
||||
res: io.murano.resources
|
||||
sys: io.murano.system
|
||||
|
||||
.. note::
|
||||
``=:`` refers to the *current* namespace
|
||||
|
||||
Name
|
||||
----
|
||||
|
||||
Contains the class name that is defined in this file.
|
||||
So full class name will be current namespace and name, provided by corresponding key: ``com.example.apache.ApacheHttpServer``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Name: ApacheHttpServer
|
||||
|
||||
.. note::
|
||||
One .yaml file should contain only one class definition.
|
||||
|
||||
Extends
|
||||
-------
|
||||
|
||||
Determines inheritance, and ``io.murano.Application`` should be a parent
|
||||
for all the murano applications.
|
||||
|
||||
This class has defined deploy method and only instances of that class can be used in Environment class.
|
||||
Environment class, in its turn, is responsible for the deployment configurations. Definition of both
|
||||
classes are located at meta/io.murano folder of murano repository.
|
||||
|
||||
Thus, if you want to have some modifications of ApacheHttpServer, you can set com.example.apache.ApacheHttpServer
|
||||
in the Extends section of a new Application class:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
Extends: std:Application
|
||||
|
||||
Properties
|
||||
----------
|
||||
|
||||
Defines the dictionary.
|
||||
Apache HTTP Server application has three properties: ``name``, ``enablePHP`` and ``instance``.
|
||||
For each of them certain ``Contract`` is defined.
|
||||
|
||||
Only ``enablePHP`` is optional, and its default value equals to ``false``.
|
||||
|
||||
``Instance`` is the required parameter and should be an instance of the predefined in core library ``io.murano.resources.Instance`` class.
|
||||
|
||||
Methods
|
||||
-------
|
||||
|
||||
The ``initialize`` method is like ``__init__`` in Python, and executes together with properties
|
||||
initialization.
|
||||
|
||||
It accesses the environment, which the application belongs to, and is used only for
|
||||
sending reports about the deployment state.
|
||||
|
||||
Private variable ``_environment`` is defined as follows:
|
||||
|
||||
.. code-block:: yaml
|
||||
:linenos:
|
||||
|
||||
initialize:
|
||||
Body:
|
||||
- $._environment: $.find(std:Environment).require()
|
||||
|
||||
The ``deploy`` method sets up instance spawning and configuration. This method should be executed
|
||||
only once. So in the first order deployed variable is checked to be false in the
|
||||
current scope.
|
||||
|
||||
It performs the following actions:
|
||||
|
||||
* configures securityGroups;
|
||||
* initiates new virtual machine spawning: ``$.instance.deploy()``
|
||||
* loads the execution plan template, located in the ``Resources`` directory to the
|
||||
instance of resources class: ``$resources.yaml('DeployApache.template')``
|
||||
* updates the plan with parameters taken from the user: ``bind(dict(enablePHP => $.enablePHP))``
|
||||
* sends ready-to-execute-plan to murano agent: ``$.instance.agent.call($template, $resources)``
|
||||
|
||||
|
||||
Step 6. Add the application logo (optional)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Download or create your own ``.png`` image associated with your application.
|
||||
|
||||
The recommended size is 70x70 px, and the square shape is preferable.
|
||||
There are no limits regarding the image filename. In Apache HTTP Server we
|
||||
use the default name ``logo.png``:
|
||||
|
||||
.. image:: ../figures/logo.png
|
||||
|
||||
Step 7. Compose a zip archive
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Select all the files prepared for the package and create an archive in zip format.
|
||||
If the command is executed from the console, do not forget to add the ``-r`` option
|
||||
to include all the attachments.
|
||||
|
||||
.. note::
|
||||
The manifest file should not contain the root folder. In other words, the manifest
|
||||
should be located in the archive root directory.
|
||||
|
||||
|
|
||||
|
||||
**Congratulations! Your application is ready to be uploaded to the application catalog.**
|
||||
|
||||
.. TODO: add links to "How to upload a package" (OG)
|
||||
part1
|
||||
part2
|
||||
part3
|
||||
part4
|
||||
|
||||
.. #. Creating your first Application Package
|
||||
.. #. Adding User Interface to your package and other improvements
|
||||
.. #. Modifying the application to do something useful
|
||||
.. #. Adding scalability scenarios
|
||||
.. #. Learning some advanced stuff for production-grade deployments
|
||||
|
||||
Before you proceed, please ensure that you have an OpenStack cloud
|
||||
(devstack-based will work just fine) and a latest version of Murano deployed.
|
||||
This guide assumes that the reader has a basic knowledge of some programming
|
||||
languages and object-oriented design and is a bit familiar with the scripting
|
||||
languages used to configure Linux servers. Also it would be benficial to be
|
||||
familiar with YAML format: lots of software configuration tools nowadays use
|
||||
YAML, and Murano is no difference.
|
||||
|
||||
|