Developing Murano Packages 101

Change-Id: Ia227903d5efcbda8a955920e3e4123f22aa17aee
This commit is contained in:
Stan Lagun 2016-12-18 21:31:31 -08:00
parent 1c35bfc179
commit c9e1960305
19 changed files with 2023 additions and 599 deletions

View File

@ -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
----------

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View 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.

View 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.

View 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.

View 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!

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@ -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.