Docker is a containerization platform that gained immense popularity in the IT world as a tool that can contain an application and help to deploy it to multiple environments.

History

Before Docker, there were different approaches whose goal was to obtain a reliable development and deployment process, which means testing and putting the application into production as a reproducible unit where the same result is always achieved.

The leading way to isolate and organize applications along with their their dependencies was to place each and every application in its own virtual machine. Physical hardware runs multiple such environments and the whole process is called virtualization.

Containerization

Containerization is a type of virtualization which brings virtualization to the operating system level. Containerization brings abstraction to the operating system, while virtualization brings abstraction to the hardware.

  • Containers are lightweight and typically faster than Virtual Machines
  • Containers share relevant libraries and resources
  • The boot up process is very fast since the applications run on the host kernel

Docker

Docker is a tool used to easily package, distribute and deploy applications with all its dependencies into a standardized unit called a Docker image. It provides the ability to create predictable environments which can be used for development, testing and deploying the application in multiple environments, including production.

Some Docker concepts:

  • Image: an executable package that includes everything needed to run the application
  • Container: a runtime instance of an image

A Docker container can be connected with other containers that provide functionality to the application like a database or a load balancer.

In Zato we create Docker images of two kinds:

The idea behind the Zato quickstart image is to very quickly provision a fully working all-in-one Zato cluster that can be used to test a new service locally or explore Zato but, because this is a real Zato cluster, it can also be used for production purposes.

The image used for cloud environments provides more flexibility and can be used to deploy an instance of a Zato component like server, scheduler or web admin into the Zato cluster, each under its own container. That is, whereas Zato quickstart contains all the Zato components in a single image, the cloud one splits the components into separate Docker containers.

Wrapping up

This is it for now - we will be talking about these two types of Zato images and how to use them in upcoming articles.

Zato startup callable objects are a means through which arbitrary Python functions or classes can be invoked when a server is booting up in order to influence its configuration or setup, even before any service is invoked.

This technique offers customisation possibilities in addition to the standard configuration options set before a server starts - read on to learn details.

Startup lifecycle

When a Zato server is starting, it goes through several stages. Most of them are internal details but two of them are exposed to users. Their names are FS_CONFIG_ONLY and AFTER_STARTED.

  • FS_CONFIG_ONLY - one of the earliest stages during which only server configuration files exist.

    No connections to any resources are opened yet, no services are deployed, the server does even have a connection to its configuration databases established - the only piece of configuration in existence are server config files.

    During this stage it is possible to make broad, low-level changes to configuration, such as reading configuration options from external endpoints, as in the first example below.

  • AFTER_STARTED - one of the latest stages in the process during which all of the configuration is already read in.

    All services are deployed and most HTTP-based connections are started. Notably, connections started in connectors, e.g. Odoo or WebSockets may be not started yet.

    This stage is useful if access to services is needed, as in the second example which invokes an internal service to create a new cache definition.

How to configure startup callable objects

A startup callable is a regular Python function or a Python class placed on a server's PYTHONPATH. For instance, if Zato is installed to /opt/zato/current, placing a Python module my_startup.py in, depending on one's Zato version, /opt/zato/current/extlib or /opt/zato/current/zato_extra_paths will make it appear on PYTHONPATH.

Next, add a dotted name of the callable to server.conf, under the [misc] stanza. For instance, if a function called "update_fs_config" is in a module named "my_startup.py", the configuration will look like below:

[misc]
...
startup_callable=my_startup.create_cache
...

Note that key "startup_callable" can be a comma-separated list of callable objects to invoke, such as:

[misc]
...
startup_callable=my_startup.update_fs_config, my_startup.create_cache
...

Each callable will be invoked in the order defined in the key and each will need to decide whether it is interested in a particular phase or not.

Python code

The only part left now is to create a callable - this can be anything in Python that can be invoked and the example below uses two functions, already registered in server.conf above.

The code consists of the usual imports, including ones for type completion hints, followed by actual callable functions.

Note that each callable is given a PhaseCtx object on input - this is an object that describes the current stage, the current phase the server is going through.

A callable will filter out stages it is not interested, in this case, each callable is interested in a single one only but there is nothing preventing a callable to handle more than one.

Note that there are no limits to what a callable can do - it has access to all the Python libraries that a server has on PYTHONPATH, and, depending on the stage, to all the services already deployed, all of which means that there are virtually no limits to customisation choices of the startup process.

# -*- coding: utf-8 -*-

# Requests
import requests

# Zato
from zato.common import CACHE, SERVER_STARTUP
from zato.common.util import as_bool

# Type completion imports
if 0:
    from zato.server.base.parallel import ParallelServer
    from zato.server.startup_callable import PhaseCtx

    ParallelServer = ParallelServer
    PhaseCtx = PhaseCtx

def update_fs_config(ctx):
    """ This callable has access to only to file-system based configuration files.
    """
    # type: (PhaseCtx)

    if ctx.phase == SERVER_STARTUP.PHASE.FS_CONFIG_ONLY:

        # server.conf
        server_config = ctx.kwargs['server_config']

        # pickup.conf
        pickup_config = ctx.kwargs['pickup_config']

        # simple-io.conf
        sio_config = ctx.kwargs['sio_config']

        # sso.conf
        sso_config = ctx.kwargs['sso_config']

        # Base directory the server was installed to
        base_dir = ctx.kwargs['base_dir']

        # Consult a remote resource and check
        # if Cassandra connections should be enabled
        response = requests.get('https://example.com/')

        # Convert to a bool object
        is_enabled = as_bool(response.text)

        # Update the server configuration in place
        server_config.component_enabled.cassandara = is_enabled

def create_cache(ctx):
    """ This is the callable that is invoked when the server is started.
    """
    # type: (PhaseCtx)

    if ctx.phase == SERVER_STARTUP.PHASE.AFTER_STARTED:

        # Server object where all our services are deployed to already
        server = ctx.kwargs['server'] # type: ParallelServer

        # Service to invoke
        service = 'zato.cache.builtin.create'

        # Input data to the service
        request = {
            'cluster_id': server.cluster_id,
            'name': 'my.cache',
            'is_active': True,
            'is_default': False,
            'max_size': 10000,
            'max_item_size': 10000,
            'extend_expiry_on_get': True,
            'extend_expiry_on_set': True,
            'sync_method': CACHE.SYNC_METHOD.IN_BACKGROUND.id,
            'persistent_storage': CACHE.PERSISTENT_STORAGE.NO_PERSISTENT_STORAGE.id,
            'cache_type': CACHE.TYPE.BUILTIN,
        }

        # Create a cache object by invoke the service
        server.invoke(service, request)

Starting with version 3.2, Zato will use a new, dark theme for its auto-generated API documentation and specifications. Here is its preview.

An API service and its documentation

Suppose we have a Zato service with I/O as defined below ..

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class Login(Service):
    """ Logs a user in.

    - Sets session to expire in 24 hours if unused

    - Metadata stored for each session:
      - Creation time in UTC
      - Remote IP and fully-qualified name
    """
    name = 'my.api.login'

    class SimpleIO:
        """
        * user_name - User name to log in as
        * password - User password

        * token - Session token
        * user_id - User's ID
        * user_display_name - User's display name, e.g. first and last name
        * user_email - User's email
        * preferred_language - User's preferred language in the system
        """
        input_required = 'user_name', 'password'
        output_required = 'token', 'user_id', 'user_display_name', 'user_email'
        output_optional = 'preferred_language'

    def handle(self):
        # Skip actual implementation
        pass

.. here is how its documentation will look like with the dark theme as generated by zato apispec.

Zato API specification index

Zato API specification service

Note that, as previously, the quick access WSDL and OpenAPI links are in the downloads section, to the left hand side.

More links

To check it out live, here is a link to the actual documentation from the screenshot above. Also, Zato has its own API documentation whose specification you can find here.

Coming up next

Yet, there is more. The new theme is but a part of a series of works focused on API documentation and specifications. Coming up next are:

  • A service invoker to execute services directly from their documentation
  • A mobile version
  • Searchable API specications

Stay tuned for more news.

This article presents a procedure for backing up all of Zato Single Sign-On (SSO) data and restoring it later on.

A single Zato server with SQLite is used for simplicity reasons but the same principles hold regardless of the size of one's environment or the SQL database used.

Overview

Zato Single-Sign On overview

There are two data sources that SSO uses:

  • Run-time information, such as users, groups, and all the other objects , are stored in an SQL database in tables prefixed with zato_sso_, e.g. zato_sso_user or zato_sso_group.

  • Encryption keys are kept in a file called secrets.conf - the same file is shared by all servers in a cluster

Thus, to make a backup:

  • Connect to an existing server via SSH

  • Dump the SQL contents of SSO tables and related objects such as indexes

  • Make a copy of secrets.conf

  • Save everything in a safe place

Conversely, to restore a backup:

  • Load the backup from the safe place

  • Connect to a new Zato server via SSH

  • Move the contents of the SQL dump to the database

  • Replace the server's secrets.conf with the one copied over earlier during backup

Backing up SQL data

Assuming that a Zato server is in a directory called /home/zato/sso1/server, here is how to back up an SQLite database:

$ cd /home/zato/sso1/server
$ sqlite3 zato.db ".dump zato_sso_%" > zato-sso-backup.sql

This will create a file called zato-sso-backup.sql the contents of which is the schema and rows of all the SSO objects.

To make it easier to restore it later, open the file and add the following commands right after "BEGIN TRANSACTION;"

DROP TABLE IF EXISTS zato_sso_group;
DROP TABLE IF EXISTS zato_sso_user;
DROP TABLE IF EXISTS zato_sso_user_group;
DROP TABLE IF EXISTS zato_sso_session;
DROP TABLE IF EXISTS zato_sso_attr;
DROP TABLE IF EXISTS zato_sso_linked_auth;

The idea with the DROP statements is that when you are restoring SSO from a backup, these tables, albeit empty, will already exist, so we can just drop them to silence out any SQLite warnings.

Backing up secrets.conf

Again, if the server is in /home/zato/sso1/server, the full path to secrets.conf is /home/zato/sso1/server/config/repo/secrets.conf - simply copy the whole file to a location of choice.

Just to confirm it, the contents should be akin to this:

[secret_keys]
key1=P8ViJZs8hM...

[zato]
well_known_data=gAAAAABe0LcDT...
server_conf.kvdb.password=gAAAAA...
server_conf.main.token=gAAAAABe0LcDPy...
server_conf.misc.jwt_secret=gAAAAABe0Lc...
server_conf.odb.password=gAAAAABe0LcD2MqLa...

Creating a new server

We work under assumption that a new server will be created in a directory named /home/zato/sso2/server.

Note that it should be a completely new instance in a new cluster. Do not start the server yet.

Restoring SQL data

Move the zato-sso-backup.sql file to /home/zato/sso2/server and run the commands below:

$ cd /home/zato/sso2/server
$ sqlite3 zato.db < zato-sso-backup.sql
$ echo $?
$ 0

Exit code 0 should be returned on output, indicating a successful operation.

Restoring secrets.conf

The file backed up previously needs to be saved to /home/zato/sso2/server/config/repo/secrets.conf, as below:

$ cd /home/zato/sso2/server/config/repo
$ mv ./secrets.conf ./secrets.conf.bak # Just in case
$ cp /path/to/backup/secrets.conf .

Confirming it all

Now, the server can be started and we can confirm that the SSO data can be accessed by logging it to the system as one of its users, as below - output was reformatted for clarity:

$ zato sso login /home/zato/sso2/server my.user
User logged in {
  'username': 'my.user',
  'user_id': 'zusr6htg...',
  'ust': 'gAAAAABe0M_Pf8cdBa6bimnjfVUt5CF...',
  'creation_time': '2020-05-29T09:03:11.459337',
  'expiration_time': '2020-05-29T10:03:11.459337',
  'has_w_about_to_exp': False
}
$ 

That concludes the process - the SSO data is now restored and the server can be fully used, just like the original one.

One of the exciting additions of the upcoming Zato 3.2 release is the ability to invoke services through OpenAPI endpoints without a need for creation of REST channels explicitly - read more for details.

Python code

Supposing we have a service such as below - note that it is uses SimpleIO and that each of its input/output attributes is documented in the docstring - we would like to invoke it from an external application while delegating to Zato as much effort involved in it as possible.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

class GetUserAccount(Service):
    """ Returns user account details based on user ID and type.
    """
    name = 'api.get.user.account'

    class SimpleIO:
        """
        * account_id - ID of the user account to return
        * user_type - Type of the user the account belongs to

        * user_name - Name of the user to whom account_id belongs
        * needs_review - A flag indicating whether the account needs to be reviewed
        """
        input_required = 'account_id', 'user_type'
        output_required = 'account_name', 'needs_review'

    def handle(self):

        # Local aliases
        account_id = self.request.input.account_id
        user_type  = self.request.input.user_type

        # In reality, additional work would be carried out here first ..
        self.logger.info('Returning data for `%s` (%s)', account_id, user_type )

        # .. produce the output now.
        self.response.payload.user_name = 'my.user'
        self.response.payload.needs_review = True

REST channels

We will not pursue this path in this article but, in other circumstances, we could just create a REST channel for the service. This is great and it works perfectly fine, always letting one adjust many options, such as caching or rate-limiting.

Creating a REST channel

However, today, we will auto-create API definitions using OpenAPI.

Creating OpenAPI specifications

Documentation, including OpenAPI specifications, can be created from the command line, as below:

$ zato apispec /path/to/server \
--dir /my/output/directory     \
--include api.*                \
--tags public                  \
--verbose

What we will find in the output directory is a Sphinx project with human-readable documentation, which is nice in itself, along with formal specifications, including OpenAPI.

Sphinx documentation

Sphinx documentation

OpenAPI download

Inspecting the OpenAPI specification

Upon downloading the openapi.yaml file we can confirm that it is a formal specification of the APIs exposed - including request and response schemas as well as endpoint addresses.

Note that, because we are using SimpleIO, the specification includes description of parameters and the parameters have expected data types, e.g. account_id is an integer while needs_review is a boolean.

components:
  schemas:
    request_api_get_user_account:
      properties:
        account_id:
          description: ID of the user account to return
          format: int32
          type: integer
        user_type:
          description: Type of the user the account belongs to
          format: string
          type: string
      required:
      - account_id
      - user_type
      title: Request object for api.get.user.account
      type: object
    response_api_get_user_account:
      properties:
        needs_review:
          description: A flag indicating whether the account needs to be reviewed
          format: boolean
          type: boolean
        user_name:
          description: Name of the user to whom account_id belongs
          format: string
          type: string
      required:
      - user_name
      - needs_review
      title: Response object for api.get.user.account
      type: object
paths:
  /zato/api/invoke/api.get.user.account:
    post:
      consumes:
      - application/json
      operationId: post__zato_api_invoke_api_get_user_account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/request_api_get_user_account'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/response_api_get_user_account'
info:
  title: API spec
  version: '1.0'
openapi: 3.0.2
servers:
- url: http://localhost:11223

Using OpenAPI

With the specification downloaded, we can start to invoke our service immediately - in this case, we will use Postman. Essentially, import a definition, choose the desired service, provide input data and click the "Send" button.

Afterwards, we can also confirm in Zato server logs that the request was processed correctly.

Naturally, Postman is just a sample client utilising the OpenAPI specification - clients in other languages can be easily used as well, including Python, Java, .NET, JavaScript and more.

Postman - import OpenAPI

Postman - select an endpoint

Postman - invoke an endpoint

Zato logs

Even more protocols

The above suffices for external clients to invoke your services purely based on their Python definitions with endpoints, documentation and specifications auto-generated by Zato.

But there is more. Have you noticed that nothing in the service used is specific to REST, OpenAPI or HTTP for that matter?

That is one of the benefits of using Zato - such a service is completely reusable across a range of other protocols, which means that it can exposed as-is through other channels, e.g. IBM MQ, AMQP, SOAP, WebSockets, Publish/Subscribe topics, Scheduler, File transfer and many more.

Be sure to check the Zato documentation for more details!