Blob Blame History Raw
From ae514b04a95cadb3ac1819a9097dbee694f4596b Mon Sep 17 00:00:00 2001
From: Ondrej Mular <omular@redhat.com>
Date: Tue, 21 Jun 2016 15:23:07 +0200
Subject: [PATCH] bz1315371-01-add support for pacemaker alerts

---
 .pylintrc                            |   2 +-
 pcs/alert.py                         | 237 +++++++++
 pcs/app.py                           |   6 +
 pcs/cli/common/env.py                |   1 +
 pcs/cli/common/lib_wrapper.py        |  28 +-
 pcs/cli/common/middleware.py         |   2 +-
 pcs/common/report_codes.py           |   6 +
 pcs/config.py                        |   4 +
 pcs/lib/cib/alert.py                 | 281 +++++++++++
 pcs/lib/cib/nvpair.py                |  90 ++++
 pcs/lib/cib/test/test_alert.py       | 931 +++++++++++++++++++++++++++++++++++
 pcs/lib/cib/test/test_nvpair.py      | 206 ++++++++
 pcs/lib/cib/tools.py                 | 127 +++++
 pcs/lib/commands/alert.py            | 169 +++++++
 pcs/lib/commands/test/test_alert.py  | 639 ++++++++++++++++++++++++
 pcs/lib/commands/test/test_ticket.py |   2 +-
 pcs/lib/env.py                       |  32 +-
 pcs/lib/pacemaker.py                 |  17 +-
 pcs/lib/reports.py                   |  91 ++++
 pcs/pcs.8                            |  25 +
 pcs/test/resources/cib-empty-2.5.xml |  10 +
 pcs/test/test_alert.py               | 363 ++++++++++++++
 pcs/test/test_lib_cib_tools.py       | 181 ++++++-
 pcs/test/test_lib_env.py             | 140 +++++-
 pcs/test/test_lib_pacemaker.py       |  24 +-
 pcs/test/test_resource.py            |   6 +
 pcs/test/test_stonith.py             |   3 +
 pcs/test/tools/color_text_runner.py  |  10 +
 pcs/usage.py                         |  43 ++
 pcs/utils.py                         |   9 +-
 30 files changed, 3649 insertions(+), 36 deletions(-)
 create mode 100644 pcs/alert.py
 create mode 100644 pcs/lib/cib/alert.py
 create mode 100644 pcs/lib/cib/nvpair.py
 create mode 100644 pcs/lib/cib/test/test_alert.py
 create mode 100644 pcs/lib/cib/test/test_nvpair.py
 create mode 100644 pcs/lib/commands/alert.py
 create mode 100644 pcs/lib/commands/test/test_alert.py
 create mode 100644 pcs/test/resources/cib-empty-2.5.xml
 create mode 100644 pcs/test/test_alert.py

diff --git a/.pylintrc b/.pylintrc
index 661f3d2..e378e6a 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -92,7 +92,7 @@ dummy-variables-rgx=_$|dummy
 
 [FORMAT]
 # Maximum number of lines in a module
-max-module-lines=4571
+max-module-lines=4577
 # Maximum number of characters on a single line.
 max-line-length=1291
 
diff --git a/pcs/alert.py b/pcs/alert.py
new file mode 100644
index 0000000..d3a6e28
--- /dev/null
+++ b/pcs/alert.py
@@ -0,0 +1,237 @@
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
+import sys
+
+from pcs import (
+    usage,
+    utils,
+)
+from pcs.cli.common.errors import CmdLineInputError
+from pcs.cli.common.parse_args import prepare_options
+from pcs.cli.common.console_report import indent
+from pcs.lib.errors import LibraryError
+
+
+def alert_cmd(*args):
+    argv = args[1]
+    if not argv:
+        sub_cmd = "config"
+    else:
+        sub_cmd = argv.pop(0)
+    try:
+        if sub_cmd == "help":
+            usage.alert(argv)
+        elif sub_cmd == "create":
+            alert_add(*args)
+        elif sub_cmd == "update":
+            alert_update(*args)
+        elif sub_cmd == "remove":
+            alert_remove(*args)
+        elif sub_cmd == "config" or sub_cmd == "show":
+            print_alert_config(*args)
+        elif sub_cmd == "recipient":
+            recipient_cmd(*args)
+        else:
+            raise CmdLineInputError()
+    except LibraryError as e:
+        utils.process_library_reports(e.args)
+    except CmdLineInputError as e:
+        utils.exit_on_cmdline_input_errror(e, "alert", sub_cmd)
+
+
+def recipient_cmd(*args):
+    argv = args[1]
+
+    if not argv:
+        usage.alert(["recipient"])
+        sys.exit(1)
+
+    sub_cmd = argv.pop(0)
+    try:
+        if sub_cmd == "help":
+            usage.alert(["recipient"])
+        elif sub_cmd == "add":
+            recipient_add(*args)
+        elif sub_cmd == "update":
+            recipient_update(*args)
+        elif sub_cmd == "remove":
+            recipient_remove(*args)
+    except CmdLineInputError as e:
+        utils.exit_on_cmdline_input_errror(
+            e, "alert", "recipient {0}".format(sub_cmd)
+        )
+
+
+def parse_cmd_sections(arg_list, section_list):
+    output = dict([(section, []) for section in section_list + ["main"]])
+    cur_section = "main"
+    for arg in arg_list:
+        if arg in section_list:
+            cur_section = arg
+            continue
+        output[cur_section].append(arg)
+
+    return output
+
+
+def ensure_only_allowed_options(parameter_dict, allowed_list):
+    for arg, value in parameter_dict.items():
+        if arg not in allowed_list:
+            raise CmdLineInputError(
+                "Unexpected parameter '{0}={1}'".format(arg, value)
+            )
+
+
+def alert_add(lib, argv, modifiers):
+    if not argv:
+        raise CmdLineInputError()
+
+    sections = parse_cmd_sections(argv, ["options", "meta"])
+    main_args = prepare_options(sections["main"])
+    ensure_only_allowed_options(main_args, ["id", "description", "path"])
+
+    lib.alert.create_alert(
+        main_args.get("id", None),
+        main_args.get("path", None),
+        prepare_options(sections["options"]),
+        prepare_options(sections["meta"]),
+        main_args.get("description", None)
+    )
+
+
+def alert_update(lib, argv, modifiers):
+    if not argv:
+        raise CmdLineInputError()
+
+    alert_id = argv[0]
+
+    sections = parse_cmd_sections(argv[1:], ["options", "meta"])
+    main_args = prepare_options(sections["main"])
+    ensure_only_allowed_options(main_args, ["description", "path"])
+
+    lib.alert.update_alert(
+        alert_id,
+        main_args.get("path", None),
+        prepare_options(sections["options"]),
+        prepare_options(sections["meta"]),
+        main_args.get("description", None)
+    )
+
+
+def alert_remove(lib, argv, modifiers):
+    if len(argv) != 1:
+        raise CmdLineInputError()
+
+    lib.alert.remove_alert(argv[0])
+
+
+def recipient_add(lib, argv, modifiers):
+    if len(argv) < 2:
+        raise CmdLineInputError()
+
+    alert_id = argv[0]
+    recipient_value = argv[1]
+
+    sections = parse_cmd_sections(argv[2:], ["options", "meta"])
+    main_args = prepare_options(sections["main"])
+    ensure_only_allowed_options(main_args, ["description"])
+
+    lib.alert.add_recipient(
+        alert_id,
+        recipient_value,
+        prepare_options(sections["options"]),
+        prepare_options(sections["meta"]),
+        main_args.get("description", None)
+    )
+
+
+def recipient_update(lib, argv, modifiers):
+    if len(argv) < 2:
+        raise CmdLineInputError()
+
+    alert_id = argv[0]
+    recipient_value = argv[1]
+
+    sections = parse_cmd_sections(argv[2:], ["options", "meta"])
+    main_args = prepare_options(sections["main"])
+    ensure_only_allowed_options(main_args, ["description"])
+
+    lib.alert.update_recipient(
+        alert_id,
+        recipient_value,
+        prepare_options(sections["options"]),
+        prepare_options(sections["meta"]),
+        main_args.get("description", None)
+    )
+
+
+def recipient_remove(lib, argv, modifiers):
+    if len(argv) != 2:
+        raise CmdLineInputError()
+
+    lib.alert.remove_recipient(argv[0], argv[1])
+
+
+def _nvset_to_str(nvset_obj):
+    output = []
+    for nvpair_obj in nvset_obj:
+        output.append("{key}={value}".format(
+            key=nvpair_obj["name"], value=nvpair_obj["value"]
+        ))
+    return " ".join(output)
+
+
+def __description_attributes_to_str(obj):
+    output = []
+    if obj.get("description"):
+        output.append("Description: {desc}".format(desc=obj["description"]))
+    if obj.get("instance_attributes"):
+        output.append("Options: {attributes}".format(
+            attributes=_nvset_to_str(obj["instance_attributes"])
+        ))
+    if obj.get("meta_attributes"):
+        output.append("Meta options: {attributes}".format(
+            attributes=_nvset_to_str(obj["meta_attributes"])
+        ))
+    return output
+
+
+def _alert_to_str(alert):
+    content = []
+    content.extend(__description_attributes_to_str(alert))
+
+    recipients = []
+    for recipient in alert.get("recipient_list", []):
+        recipients.extend( _recipient_to_str(recipient))
+
+    if recipients:
+        content.append("Recipients:")
+        content.extend(indent(recipients, 1))
+
+    return ["Alert: {alert_id} (path={path})".format(
+        alert_id=alert["id"], path=alert["path"]
+    )] + indent(content, 1)
+
+
+def _recipient_to_str(recipient):
+    return ["Recipient: {value}".format(value=recipient["value"])] + indent(
+        __description_attributes_to_str(recipient), 1
+    )
+
+
+def print_alert_config(lib, argv, modifiers):
+    if argv:
+        raise CmdLineInputError()
+
+    print("Alerts:")
+    alert_list = lib.alert.get_all_alerts()
+    if alert_list:
+        for alert in alert_list:
+            print("\n".join(indent(_alert_to_str(alert), 1)))
+    else:
+        print(" No alerts defined")
diff --git a/pcs/app.py b/pcs/app.py
index 3c4865f..3758ee4 100644
--- a/pcs/app.py
+++ b/pcs/app.py
@@ -27,6 +27,7 @@ from pcs import (
     stonith,
     usage,
     utils,
+    alert,
 )
 
 from pcs.cli.common import completion
@@ -193,6 +194,11 @@ def main(argv=None):
             argv,
             utils.get_modificators()
         ),
+        "alert": lambda args: alert.alert_cmd(
+            utils.get_library_wrapper(),
+            args,
+            utils.get_modificators()
+        ),
     }
     if command not in cmd_map:
         usage.main()
diff --git a/pcs/cli/common/env.py b/pcs/cli/common/env.py
index f407981..2ba4f70 100644
--- a/pcs/cli/common/env.py
+++ b/pcs/cli/common/env.py
@@ -8,6 +8,7 @@ from __future__ import (
 class Env(object):
     def __init__(self):
         self.cib_data = None
+        self.cib_upgraded = False
         self.user = None
         self.groups = None
         self.corosync_conf_data = None
diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py
index 909b435..2ba5602 100644
--- a/pcs/cli/common/lib_wrapper.py
+++ b/pcs/cli/common/lib_wrapper.py
@@ -19,6 +19,7 @@ from pcs.lib.commands import (
     quorum,
     qdevice,
     sbd,
+    alert,
 )
 from pcs.cli.common.reports import (
     LibraryReportProcessorToConsole as LibraryReportProcessorToConsole,
@@ -42,6 +43,14 @@ def cli_env_to_lib_env(cli_env):
         cli_env.auth_tokens_getter,
     )
 
+def lib_env_to_cli_env(lib_env, cli_env):
+    if not lib_env.is_cib_live:
+        cli_env.cib_data = lib_env._get_cib_xml()
+        cli_env.cib_upgraded = lib_env.cib_upgraded
+    if not lib_env.is_corosync_conf_live:
+        cli_env.corosync_conf_data = lib_env.get_corosync_conf_data()
+    return cli_env
+
 def bind(cli_env, run_with_middleware, run_library_command):
     def run(cli_env, *args, **kwargs):
         lib_env = cli_env_to_lib_env(cli_env)
@@ -50,10 +59,7 @@ def bind(cli_env, run_with_middleware, run_library_command):
 
         #midlewares needs finish its work and they see only cli_env
         #so we need reflect some changes to cli_env
-        if not lib_env.is_cib_live:
-            cli_env.cib_data = lib_env.get_cib_xml()
-        if not lib_env.is_corosync_conf_live:
-            cli_env.corosync_conf_data = lib_env.get_corosync_conf_data()
+        lib_env_to_cli_env(lib_env, cli_env)
 
         return lib_call_result
     return partial(run_with_middleware, run, cli_env)
@@ -140,6 +146,20 @@ def load_module(env, middleware_factory, name):
                 "get_local_sbd_config": sbd.get_local_sbd_config,
             }
         )
+    if name == "alert":
+        return bind_all(
+            env,
+            middleware.build(middleware_factory.cib),
+            {
+                "create_alert": alert.create_alert,
+                "update_alert": alert.update_alert,
+                "remove_alert": alert.remove_alert,
+                "add_recipient": alert.add_recipient,
+                "update_recipient": alert.update_recipient,
+                "remove_recipient": alert.remove_recipient,
+                "get_all_alerts": alert.get_all_alerts,
+            }
+        )
 
     raise Exception("No library part '{0}'".format(name))
 
diff --git a/pcs/cli/common/middleware.py b/pcs/cli/common/middleware.py
index 16618e1..e53e138 100644
--- a/pcs/cli/common/middleware.py
+++ b/pcs/cli/common/middleware.py
@@ -34,7 +34,7 @@ def cib(use_local_cib, load_cib_content, write_cib):
         result_of_next = next_in_line(env, *args, **kwargs)
 
         if use_local_cib:
-            write_cib(env.cib_data)
+            write_cib(env.cib_data, env.cib_upgraded)
 
         return result_of_next
     return apply
diff --git a/pcs/common/report_codes.py b/pcs/common/report_codes.py
index 927df35..bda982a 100644
--- a/pcs/common/report_codes.py
+++ b/pcs/common/report_codes.py
@@ -20,11 +20,17 @@ SKIP_OFFLINE_NODES = "SKIP_OFFLINE_NODES"
 AGENT_GENERAL_ERROR = "AGENT_GENERAL_ERROR"
 AGENT_NOT_FOUND = "AGENT_NOT_FOUND"
 BAD_CLUSTER_STATE_FORMAT = 'BAD_CLUSTER_STATE_FORMAT'
+CIB_ALERT_NOT_FOUND = "CIB_ALERT_NOT_FOUND"
+CIB_ALERT_RECIPIENT_ALREADY_EXISTS = "CIB_ALERT_RECIPIENT_ALREADY_EXISTS"
+CIB_ALERT_RECIPIENT_NOT_FOUND = "CIB_ALERT_RECIPIENT_NOT_FOUND"
 CIB_CANNOT_FIND_MANDATORY_SECTION = "CIB_CANNOT_FIND_MANDATORY_SECTION"
 CIB_LOAD_ERROR_BAD_FORMAT = "CIB_LOAD_ERROR_BAD_FORMAT"
 CIB_LOAD_ERROR = "CIB_LOAD_ERROR"
 CIB_LOAD_ERROR_SCOPE_MISSING = "CIB_LOAD_ERROR_SCOPE_MISSING"
 CIB_PUSH_ERROR = "CIB_PUSH_ERROR"
+CIB_UPGRADE_FAILED = "CIB_UPGRADE_FAILED"
+CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION = "CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION"
+CIB_UPGRADE_SUCCESSFUL = "CIB_UPGRADE_SUCCESSFUL"
 CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES = "CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES"
 CMAN_BROADCAST_ALL_RINGS = 'CMAN_BROADCAST_ALL_RINGS'
 CMAN_UDPU_RESTART_REQUIRED = 'CMAN_UDPU_RESTART_REQUIRED'
diff --git a/pcs/config.py b/pcs/config.py
index 51de822..4659c5b 100644
--- a/pcs/config.py
+++ b/pcs/config.py
@@ -38,6 +38,7 @@ from pcs import (
     stonith,
     usage,
     utils,
+    alert,
 )
 from pcs.lib.errors import LibraryError
 from pcs.lib.commands import quorum as lib_quorum
@@ -123,6 +124,9 @@ def config_show_cib():
     ticket_command.show(lib, [], modificators)
 
     print()
+    alert.print_alert_config(lib, [], modificators)
+
+    print()
     del utils.pcs_options["--all"]
     print("Resources Defaults:")
     resource.show_defaults("rsc_defaults", indent=" ")
diff --git a/pcs/lib/cib/alert.py b/pcs/lib/cib/alert.py
new file mode 100644
index 0000000..6b72996
--- /dev/null
+++ b/pcs/lib/cib/alert.py
@@ -0,0 +1,281 @@
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
+from lxml import etree
+
+from pcs.lib import reports
+from pcs.lib.errors import LibraryError
+from pcs.lib.cib.nvpair import update_nvset, get_nvset
+from pcs.lib.cib.tools import (
+    check_new_id_applicable,
+    get_sub_element,
+    find_unique_id,
+    get_alerts,
+)
+
+
+def update_instance_attributes(tree, element, attribute_dict):
+    """
+    Updates instance attributes of element. Returns updated instance
+    attributes element.
+
+    tree -- cib etree node
+    element -- parent element of instance attributes
+    attribute_dict -- dictionary of nvpairs
+    """
+    return update_nvset("instance_attributes", tree, element, attribute_dict)
+
+
+def update_meta_attributes(tree, element, attribute_dict):
+    """
+    Updates meta attributes of element. Returns updated meta attributes element.
+
+    tree -- cib etree node
+    element -- parent element of meta attributes
+    attribute_dict -- dictionary of nvpairs
+    """
+    return update_nvset("meta_attributes", tree, element, attribute_dict)
+
+
+def _update_optional_attribute(element, attribute, value):
+    """
+    Update optional attribute of element. Remove existing element if value
+    is empty.
+
+    element -- parent element of specified attribute
+    attribute -- attribute to be updated
+    value -- new value
+    """
+    if value is None:
+        return
+    if value:
+        element.set(attribute, value)
+    elif attribute in element.attrib:
+        del element.attrib[attribute]
+
+
+def get_alert_by_id(tree, alert_id):
+    """
+    Returns alert element with specified id.
+    Raises AlertNotFound if alert with specified id doesn't exist.
+
+    tree -- cib etree node
+    alert_id -- id of alert
+    """
+    alert = get_alerts(tree).find("./alert[@id='{0}']".format(alert_id))
+    if alert is None:
+        raise LibraryError(reports.cib_alert_not_found(alert_id))
+    return alert
+
+
+def get_recipient(alert, recipient_value):
+    """
+    Returns recipient element with value recipient_value which belong to
+    specified alert.
+    Raises RecipientNotFound if recipient doesn't exist.
+
+    alert -- parent element of required recipient
+    recipient_value -- value of recipient
+    """
+    recipient = alert.find(
+        "./recipient[@value='{0}']".format(recipient_value)
+    )
+    if recipient is None:
+        raise LibraryError(reports.cib_alert_recipient_not_found(
+            alert.get("id"), recipient_value
+        ))
+    return recipient
+
+
+def create_alert(tree, alert_id, path, description=""):
+    """
+    Create new alert element. Returns newly created element.
+    Raises LibraryError if element with specified id already exists.
+
+    tree -- cib etree node
+    alert_id -- id of new alert, it will be generated if it is None
+    path -- path to script
+    description -- description
+    """
+    if alert_id:
+        check_new_id_applicable(tree, "alert-id", alert_id)
+    else:
+        alert_id = find_unique_id(tree, "alert")
+
+    alert = etree.SubElement(get_alerts(tree), "alert", id=alert_id, path=path)
+    if description:
+        alert.set("description", description)
+
+    return alert
+
+
+def update_alert(tree, alert_id, path, description=None):
+    """
+    Update existing alert. Return updated alert element.
+    Raises AlertNotFound if alert with specified id doesn't exist.
+
+    tree -- cib etree node
+    alert_id -- id of alert to be updated
+    path -- new value of path, stay unchanged if None
+    description -- new value of description, stay unchanged if None, remove
+        if empty
+    """
+    alert = get_alert_by_id(tree, alert_id)
+    if path:
+        alert.set("path", path)
+    _update_optional_attribute(alert, "description", description)
+    return alert
+
+
+def remove_alert(tree, alert_id):
+    """
+    Remove alert with specified id.
+    Raises AlertNotFound if alert with specified id doesn't exist.
+
+    tree -- cib etree node
+    alert_id -- id of alert which should be removed
+    """
+    alert = get_alert_by_id(tree, alert_id)
+    alert.getparent().remove(alert)
+
+
+def add_recipient(
+    tree,
+    alert_id,
+    recipient_value,
+    description=""
+):
+    """
+    Add recipient to alert with specified id. Returns added recipient element.
+    Raises AlertNotFound if alert with specified id doesn't exist.
+    Raises LibraryError if recipient already exists.
+
+    tree -- cib etree node
+    alert_id -- id of alert which should be parent of new recipient
+    recipient_value -- value of recipient
+    description -- description of recipient
+    """
+    alert = get_alert_by_id(tree, alert_id)
+
+    recipient = alert.find(
+        "./recipient[@value='{0}']".format(recipient_value)
+    )
+    if recipient is not None:
+        raise LibraryError(reports.cib_alert_recipient_already_exists(
+            alert_id, recipient_value
+        ))
+
+    recipient = etree.SubElement(
+        alert,
+        "recipient",
+        id=find_unique_id(tree, "{0}-recipient".format(alert_id)),
+        value=recipient_value
+    )
+
+    if description:
+        recipient.set("description", description)
+
+    return recipient
+
+
+def update_recipient(tree, alert_id, recipient_value, description):
+    """
+    Update specified recipient. Returns updated recipient element.
+    Raises AlertNotFound if alert with specified id doesn't exist.
+    Raises RecipientNotFound if recipient doesn't exist.
+
+    tree -- cib etree node
+    alert_id -- id of alert, parent element of recipient
+    recipient_value -- recipient value
+    description -- description, if empty it will be removed, stay unchanged
+        if None
+    """
+    recipient = get_recipient(
+        get_alert_by_id(tree, alert_id), recipient_value
+    )
+    _update_optional_attribute(recipient, "description", description)
+    return recipient
+
+
+def remove_recipient(tree, alert_id, recipient_value):
+    """
+    Remove specified recipient.
+    Raises AlertNotFound if alert with specified id doesn't exist.
+    Raises RecipientNotFound if recipient doesn't exist.
+
+    tree -- cib etree node
+    alert_id -- id of alert, parent element of recipient
+    recipient_value -- recipient value
+    """
+    recipient = get_recipient(
+        get_alert_by_id(tree, alert_id), recipient_value
+    )
+    recipient.getparent().remove(recipient)
+
+
+def get_all_recipients(alert):
+    """
+    Returns list of all recipient of specified alert. Format:
+    [
+        {
+            "id": <id of recipient>,
+            "value": <value of recipient>,
+            "description": <recipient description>,
+            "instance_attributes": <list of nvpairs>,
+            "meta_attributes": <list of nvpairs>
+        }
+    ]
+
+    alert -- parent element of recipients to return
+    """
+    recipient_list = []
+    for recipient in alert.findall("./recipient"):
+        recipient_list.append({
+            "id": recipient.get("id"),
+            "value": recipient.get("value"),
+            "description": recipient.get("description", ""),
+            "instance_attributes": get_nvset(
+                get_sub_element(recipient, "instance_attributes")
+            ),
+            "meta_attributes": get_nvset(
+                get_sub_element(recipient, "meta_attributes")
+            )
+        })
+    return recipient_list
+
+
+def get_all_alerts(tree):
+    """
+    Returns list of all alerts specified in tree. Format:
+    [
+        {
+            "id": <id of alert>,
+            "path": <path to script>,
+            "description": <alert description>,
+            "instance_attributes": <list of nvpairs>,
+            "meta_attributes": <list of nvpairs>,
+            "recipients_list": <list of alert's recipients>
+        }
+    ]
+
+    tree -- cib etree node
+    """
+    alert_list = []
+    for alert in get_alerts(tree).findall("./alert"):
+        alert_list.append({
+            "id": alert.get("id"),
+            "path": alert.get("path"),
+            "description": alert.get("description", ""),
+            "instance_attributes": get_nvset(
+                get_sub_element(alert, "instance_attributes")
+            ),
+            "meta_attributes": get_nvset(
+                get_sub_element(alert, "meta_attributes")
+            ),
+            "recipient_list": get_all_recipients(alert)
+        })
+    return alert_list
diff --git a/pcs/lib/cib/nvpair.py b/pcs/lib/cib/nvpair.py
new file mode 100644
index 0000000..d1a0cae
--- /dev/null
+++ b/pcs/lib/cib/nvpair.py
@@ -0,0 +1,90 @@
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
+from lxml import etree
+
+from pcs.lib.cib.tools import (
+    get_sub_element,
+    find_unique_id,
+)
+
+
+def update_nvpair(tree, element, name, value):
+    """
+    Update nvpair, create new if it doesn't yet exist or remove existing
+    nvpair if value is empty. Returns created/updated/removed nvpair element.
+
+    tree -- cib etree node
+    element -- element in which nvpair should be added/updated/removed
+    name -- name of nvpair
+    value -- value of nvpair
+    """
+    nvpair = element.find("./nvpair[@name='{0}']".format(name))
+    if nvpair is None:
+        if not value:
+            return None
+        nvpair_id = find_unique_id(
+            tree, "{0}-{1}".format(element.get("id"), name)
+        )
+        nvpair = etree.SubElement(
+            element, "nvpair", id=nvpair_id, name=name, value=value
+        )
+    else:
+        if value:
+            nvpair.set("value", value)
+        else:
+            # remove nvpair if value is empty
+            element.remove(nvpair)
+    return nvpair
+
+
+def update_nvset(tag_name, tree, element, attribute_dict):
+    """
+    This method updates nvset specified by tag_name. If specified nvset
+    doesn't exist it will be created. Returns updated nvset element or None if
+    attribute_dict is empty.
+
+    tag_name -- tag name of nvset element
+    tree -- cib etree node
+    element -- parent element of nvset
+    attribute_dict -- dictionary of nvpairs
+    """
+    if not attribute_dict:
+        return None
+
+    attributes = get_sub_element(element, tag_name, find_unique_id(
+        tree, "{0}-{1}".format(element.get("id"), tag_name)
+    ), 0)
+
+    for name, value in sorted(attribute_dict.items()):
+        update_nvpair(tree, attributes, name, value)
+
+    return attributes
+
+
+def get_nvset(nvset):
+    """
+    Returns nvset element as list of nvpairs with format:
+    [
+        {
+            "id": <id of nvpair>,
+            "name": <name of nvpair>,
+            "value": <value of nvpair>
+        },
+        ...
+    ]
+
+    nvset -- nvset element
+    """
+    nvpair_list = []
+    for nvpair in nvset.findall("./nvpair"):
+        nvpair_list.append({
+            "id": nvpair.get("id"),
+            "name": nvpair.get("name"),
+            "value": nvpair.get("value", "")
+        })
+    return nvpair_list
diff --git a/pcs/lib/cib/test/test_alert.py b/pcs/lib/cib/test/test_alert.py
new file mode 100644
index 0000000..c387aaf
--- /dev/null
+++ b/pcs/lib/cib/test/test_alert.py
@@ -0,0 +1,931 @@
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
+from unittest import TestCase
+
+from lxml import etree
+
+from pcs.common import report_codes
+from pcs.lib.cib import alert
+from pcs.lib.errors import ReportItemSeverity as severities
+from pcs.test.tools.assertions import(
+    assert_raise_library_error,
+    assert_xml_equal,
+)
+from pcs.test.tools.pcs_mock import mock
+
+
+@mock.patch("pcs.lib.cib.alert.update_nvset")
+class UpdateInstanceAttributesTest(TestCase):
+    def test_success(self, mock_update_nvset):
+        ret_val = etree.Element("nvset")
+        tree = etree.Element("tree")
+        element = etree.Element("element")
+        attributes = {"a": 1}
+        mock_update_nvset.return_value = ret_val
+        self.assertEqual(
+            alert.update_instance_attributes(tree, element, attributes),
+            ret_val
+        )
+        mock_update_nvset.assert_called_once_with(
+            "instance_attributes", tree, element, attributes
+        )
+
+
+@mock.patch("pcs.lib.cib.alert.update_nvset")
+class UpdateMetaAttributesTest(TestCase):
+    def test_success(self, mock_update_nvset):
+        ret_val = etree.Element("nvset")
+        tree = etree.Element("tree")
+        element = etree.Element("element")
+        attributes = {"a": 1}
+        mock_update_nvset.return_value = ret_val
+        self.assertEqual(
+            alert.update_meta_attributes(tree, element, attributes),
+            ret_val
+        )
+        mock_update_nvset.assert_called_once_with(
+            "meta_attributes", tree, element, attributes
+        )
+
+
+class UpdateOptionalAttributeTest(TestCase):
+    def test_add(self):
+        element = etree.Element("element")
+        alert._update_optional_attribute(element, "attr", "value1")
+        self.assertEqual(element.get("attr"), "value1")
+
+    def test_update(self):
+        element = etree.Element("element", attr="value")
+        alert._update_optional_attribute(element, "attr", "value1")
+        self.assertEqual(element.get("attr"), "value1")
+
+    def test_remove(self):
+        element = etree.Element("element", attr="value")
+        alert._update_optional_attribute(element, "attr", "")
+        self.assertTrue(element.get("attr") is None)
+
+
+class GetAlertByIdTest(TestCase):
+    def test_found(self):
+        xml = """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert-1"/>
+                        <alert id="alert-2"/>
+                    </alerts>
+                </configuration>
+            </cib>
+        """
+        assert_xml_equal(
+            '<alert id="alert-2"/>',
+            etree.tostring(
+                alert.get_alert_by_id(etree.XML(xml), "alert-2")
+            ).decode()
+        )
+
+    def test_different_place(self):
+        xml = """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert-1"/>
+                    </alerts>
+                    <alert id="alert-2"/>
+                </configuration>
+            </cib>
+        """
+        assert_raise_library_error(
+            lambda: alert.get_alert_by_id(etree.XML(xml), "alert-2"),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "alert-2"}
+            )
+        )
+
+    def test_not_exist(self):
+        xml = """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert-1"/>
+                    </alerts>
+                </configuration>
+            </cib>
+        """
+        assert_raise_library_error(
+            lambda: alert.get_alert_by_id(etree.XML(xml), "alert-2"),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "alert-2"}
+            )
+        )
+
+
+class GetRecipientTest(TestCase):
+    def setUp(self):
+        self.xml = etree.XML(
+            """
+                <alert id="alert-1">
+                    <recipient id="rec-1" value="value1"/>
+                    <recipient id="rec-2" value="value2"/>
+                    <not_recipient value="value3"/>
+                    <recipients>
+                        <recipient id="rec-4" value="value4"/>
+                    </recipients>
+                </alert>
+            """
+        )
+
+    def test_exist(self):
+        assert_xml_equal(
+            '<recipient id="rec-2" value="value2"/>',
+            etree.tostring(alert.get_recipient(self.xml, "value2")).decode()
+        )
+
+    def test_different_place(self):
+        assert_raise_library_error(
+            lambda: alert.get_recipient(self.xml, "value4"),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND,
+                {
+                    "alert": "alert-1",
+                    "recipient": "value4"
+                }
+            )
+        )
+
+    def test_not_recipient(self):
+        assert_raise_library_error(
+            lambda: alert.get_recipient(self.xml, "value3"),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND,
+                {
+                    "alert": "alert-1",
+                    "recipient": "value3"
+                }
+            )
+        )
+
+
+class CreateAlertTest(TestCase):
+    def setUp(self):
+        self.tree = etree.XML(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """
+        )
+
+    def test_no_alerts(self):
+        tree = etree.XML(
+            """
+            <cib>
+                <configuration/>
+            </cib>
+            """
+        )
+        assert_xml_equal(
+            '<alert id="my-alert" path="/test/path"/>',
+            etree.tostring(
+                alert.create_alert(tree, "my-alert", "/test/path")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="my-alert" path="/test/path"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(tree).decode()
+        )
+
+    def test_alerts_exists(self):
+        assert_xml_equal(
+            '<alert id="my-alert" path="/test/path"/>',
+            etree.tostring(
+                alert.create_alert(self.tree, "my-alert", "/test/path")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert"/>
+                        <alert id="my-alert" path="/test/path"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_alerts_exists_with_description(self):
+        assert_xml_equal(
+            '<alert id="my-alert" path="/test/path" description="nothing"/>',
+            etree.tostring(alert.create_alert(
+                self.tree, "my-alert", "/test/path", "nothing"
+            )).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert"/>
+                        <alert
+                            id="my-alert"
+                            path="/test/path"
+                            description="nothing"
+                        />
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_invalid_id(self):
+        assert_raise_library_error(
+            lambda: alert.create_alert(self.tree, "1alert", "/path"),
+            (
+                severities.ERROR,
+                report_codes.INVALID_ID,
+                {
+                    "id": "1alert",
+                    "id_description": "alert-id",
+                    "invalid_character": "1",
+                    "reason": "invalid first character"
+                }
+            )
+        )
+
+    def test_id_exists(self):
+        assert_raise_library_error(
+            lambda: alert.create_alert(self.tree, "alert", "/path"),
+            (
+                severities.ERROR,
+                report_codes.ID_ALREADY_EXISTS,
+                {"id": "alert"}
+            )
+        )
+
+    def test_no_id(self):
+        assert_xml_equal(
+            '<alert id="alert-1" path="/test/path"/>',
+            etree.tostring(
+                alert.create_alert(self.tree, None, "/test/path")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert"/>
+                        <alert id="alert-1" path="/test/path"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+
+class UpdateAlertTest(TestCase):
+    def setUp(self):
+        self.tree = etree.XML(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path"/>
+                        <alert id="alert1" path="/path1" description="nothing"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """
+        )
+
+    def test_update_path(self):
+        assert_xml_equal(
+            '<alert id="alert" path="/test/path"/>',
+            etree.tostring(
+                alert.update_alert(self.tree, "alert", "/test/path")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/test/path"/>
+                        <alert id="alert1" path="/path1" description="nothing"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_remove_path(self):
+        assert_xml_equal(
+            '<alert id="alert" path="/path"/>',
+            etree.tostring(alert.update_alert(self.tree, "alert", "")).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path"/>
+                        <alert id="alert1" path="/path1" description="nothing"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_update_description(self):
+        assert_xml_equal(
+            '<alert id="alert" path="/path" description="desc"/>',
+            etree.tostring(
+                alert.update_alert(self.tree, "alert", None, "desc")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path" description="desc"/>
+                        <alert id="alert1" path="/path1" description="nothing"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_remove_description(self):
+        assert_xml_equal(
+            '<alert id="alert1" path="/path1"/>',
+            etree.tostring(
+                alert.update_alert(self.tree, "alert1", None, "")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path"/>
+                        <alert id="alert1" path="/path1"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_id_not_exists(self):
+        assert_raise_library_error(
+            lambda: alert.update_alert(self.tree, "alert0", "/test"),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "alert0"}
+            )
+        )
+
+
+class RemoveAlertTest(TestCase):
+    def setUp(self):
+        self.tree = etree.XML(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path"/>
+                        <alert id="alert-1" path="/next"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """
+        )
+
+    def test_success(self):
+        alert.remove_alert(self.tree, "alert")
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert-1" path="/next"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_not_existing_id(self):
+        assert_raise_library_error(
+            lambda: alert.remove_alert(self.tree, "not-existing-id"),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "not-existing-id"}
+            )
+        )
+
+
+class AddRecipientTest(TestCase):
+    def setUp(self):
+        self.tree = etree.XML(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path">
+                            <recipient id="alert-recipient" value="test_val"/>
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+            """
+        )
+
+    def test_success(self):
+        assert_xml_equal(
+            '<recipient id="alert-recipient-1" value="value1"/>',
+            etree.tostring(
+                alert.add_recipient(self.tree, "alert", "value1")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path">
+                            <recipient id="alert-recipient" value="test_val"/>
+                            <recipient id="alert-recipient-1" value="value1"/>
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_recipient_exist(self):
+        assert_raise_library_error(
+            lambda: alert.add_recipient(self.tree, "alert", "test_val"),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS,
+                {
+                    "recipient": "test_val",
+                    "alert": "alert"
+                }
+            )
+        )
+
+    def test_alert_not_exist(self):
+        assert_raise_library_error(
+            lambda: alert.add_recipient(self.tree, "alert1", "test_val"),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "alert1"}
+            )
+        )
+
+    def test_with_description(self):
+        assert_xml_equal(
+            """
+            <recipient
+                id="alert-recipient-1"
+                value="value1"
+                description="desc"
+            />
+            """,
+            etree.tostring(alert.add_recipient(
+                self.tree, "alert", "value1", "desc"
+            )).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path">
+                            <recipient id="alert-recipient" value="test_val"/>
+                            <recipient
+                                id="alert-recipient-1"
+                                value="value1"
+                                description="desc"
+                            />
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+
+class UpdateRecipientTest(TestCase):
+    def setUp(self):
+        self.tree = etree.XML(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path">
+                            <recipient id="alert-recipient" value="test_val"/>
+                            <recipient
+                                id="alert-recipient-1"
+                                value="value1"
+                                description="desc"
+                            />
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+            """
+        )
+
+    def test_add_description(self):
+        assert_xml_equal(
+            """
+            <recipient
+                id="alert-recipient" value="test_val" description="description"
+            />
+            """,
+            etree.tostring(alert.update_recipient(
+                self.tree, "alert", "test_val", "description"
+            )).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path">
+                            <recipient
+                                id="alert-recipient"
+                                value="test_val"
+                                description="description"
+                            />
+                            <recipient
+                                id="alert-recipient-1"
+                                value="value1"
+                                description="desc"
+                            />
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_update_description(self):
+        assert_xml_equal(
+            """
+            <recipient
+                id="alert-recipient-1" value="value1" description="description"
+            />
+            """,
+            etree.tostring(alert.update_recipient(
+                self.tree, "alert", "value1", "description"
+            )).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path">
+                            <recipient id="alert-recipient" value="test_val"/>
+                            <recipient
+                                id="alert-recipient-1"
+                                value="value1"
+                                description="description"
+                            />
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_remove_description(self):
+        assert_xml_equal(
+            """
+                <recipient id="alert-recipient-1" value="value1"/>
+            """,
+            etree.tostring(
+               alert.update_recipient(self.tree, "alert", "value1", "")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path">
+                            <recipient id="alert-recipient" value="test_val"/>
+                            <recipient id="alert-recipient-1" value="value1"/>
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_alert_not_exists(self):
+        assert_raise_library_error(
+            lambda: alert.update_recipient(self.tree, "alert1", "test_val", ""),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "alert1"}
+            )
+        )
+
+    def test_recipient_not_exists(self):
+        assert_raise_library_error(
+            lambda: alert.update_recipient(self.tree, "alert", "unknown", ""),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND,
+                {
+                    "alert": "alert",
+                    "recipient": "unknown"
+                }
+            )
+        )
+
+
+class RemoveRecipientTest(TestCase):
+    def setUp(self):
+        self.tree = etree.XML(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path">
+                            <recipient id="alert-recipient" value="test_val"/>
+                            <recipient id="alert-recipient-2" value="val"/>
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+            """
+        )
+
+    def test_success(self):
+        alert.remove_recipient(self.tree, "alert", "val")
+        assert_xml_equal(
+            """
+            <cib>
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="/path">
+                            <recipient id="alert-recipient" value="test_val"/>
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            etree.tostring(self.tree).decode()
+        )
+
+    def test_alert_not_exists(self):
+        assert_raise_library_error(
+            lambda: alert.remove_recipient(self.tree, "alert1", "test_val"),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "alert1"}
+            )
+        )
+
+    def test_recipient_not_exists(self):
+        assert_raise_library_error(
+            lambda: alert.remove_recipient(self.tree, "alert", "unknown"),
+            (
+                severities.ERROR,
+                report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND,
+                {
+                    "alert": "alert",
+                    "recipient": "unknown"
+                }
+            )
+        )
+
+
+class GetAllRecipientsTest(TestCase):
+    def test_success(self):
+        alert_obj = etree.XML(
+            """
+            <alert id="alert" path="/path">
+                <recipient id="alert-recipient" value="test_val">
+                    <instance_attributes>
+                        <nvpair
+                            id="nvset-name1-value1" name="name1" value="value1"
+                        />
+                        <nvpair
+                            id="nvset-name2-value2" name="name2" value="value2"
+                        />
+                    </instance_attributes>
+                    <meta_attributes>
+                        <nvpair id="nvset-name3" name="name3"/>
+                    </meta_attributes>
+                </recipient>
+                <recipient
+                    id="alert-recipient-1" value="value1" description="desc"
+                />
+            </alert>
+            """
+        )
+        self.assertEqual(
+            [
+                {
+                    "id": "alert-recipient",
+                    "value": "test_val",
+                    "description": "",
+                    "instance_attributes": [
+                        {
+                            "id": "nvset-name1-value1",
+                            "name": "name1",
+                            "value": "value1"
+                        },
+                        {
+                            "id": "nvset-name2-value2",
+                            "name": "name2",
+                            "value": "value2"
+                        }
+                    ],
+                    "meta_attributes": [
+                        {
+                            "id": "nvset-name3",
+                            "name": "name3",
+                            "value": ""
+                        }
+                    ]
+                },
+                {
+                    "id": "alert-recipient-1",
+                    "value": "value1",
+                    "description": "desc",
+                    "instance_attributes": [],
+                    "meta_attributes": []
+                }
+            ],
+            alert.get_all_recipients(alert_obj)
+        )
+
+
+class GetAllAlertsTest(TestCase):
+    def test_success(self):
+        alerts = etree.XML(
+            """
+<cib>
+    <configuration>
+        <alerts>
+            <alert id="alert" path="/path">
+                <recipient id="alert-recipient" value="test_val">
+                    <instance_attributes>
+                        <nvpair
+                            id="instance_attributes-name1-value1"
+                            name="name1"
+                            value="value1"
+                        />
+                        <nvpair
+                            id="instance_attributes-name2-value2"
+                            name="name2"
+                            value="value2"
+                        />
+                    </instance_attributes>
+                    <meta_attributes>
+                        <nvpair id="meta_attributes-name3" name="name3"/>
+                    </meta_attributes>
+                </recipient>
+                <recipient
+                    id="alert-recipient-1" value="value1" description="desc"
+                />
+            </alert>
+            <alert id="alert1" path="/test/path" description="desc">
+                <instance_attributes>
+                    <nvpair
+                        id="alert1-name1-value1" name="name1" value="value1"
+                    />
+                    <nvpair
+                        id="alert1-name2-value2" name="name2" value="value2"
+                    />
+                </instance_attributes>
+                <meta_attributes>
+                    <nvpair id="alert1-name3" name="name3"/>
+                </meta_attributes>
+            </alert>
+        </alerts>
+    </configuration>
+</cib>
+            """
+        )
+        self.assertEqual(
+            [
+                {
+                    "id": "alert",
+                    "path": "/path",
+                    "description": "",
+                    "instance_attributes": [],
+                    "meta_attributes": [],
+                    "recipient_list": [
+                        {
+                            "id": "alert-recipient",
+                            "value": "test_val",
+                            "description": "",
+                            "instance_attributes": [
+                                {
+                                    "id": "instance_attributes-name1-value1",
+                                    "name": "name1",
+                                    "value": "value1"
+                                },
+                                {
+                                    "id": "instance_attributes-name2-value2",
+                                    "name": "name2",
+                                    "value": "value2"
+                                }
+                            ],
+                            "meta_attributes": [
+                                {
+                                    "id": "meta_attributes-name3",
+                                    "name": "name3",
+                                    "value": ""
+                                }
+                            ]
+                        },
+                        {
+                            "id": "alert-recipient-1",
+                            "value": "value1",
+                            "description": "desc",
+                            "instance_attributes": [],
+                            "meta_attributes": []
+                        }
+                    ]
+                },
+                {
+                    "id": "alert1",
+                    "path": "/test/path",
+                    "description": "desc",
+                    "instance_attributes": [
+                        {
+                            "id": "alert1-name1-value1",
+                            "name": "name1",
+                            "value": "value1"
+                        },
+                        {
+                            "id": "alert1-name2-value2",
+                            "name": "name2",
+                            "value": "value2"
+                        }
+                    ],
+                    "meta_attributes": [
+                        {
+                            "id": "alert1-name3",
+                            "name": "name3",
+                            "value": ""
+                        }
+                    ],
+                    "recipient_list": []
+                }
+            ],
+            alert.get_all_alerts(alerts)
+        )
diff --git a/pcs/lib/cib/test/test_nvpair.py b/pcs/lib/cib/test/test_nvpair.py
new file mode 100644
index 0000000..6907f25
--- /dev/null
+++ b/pcs/lib/cib/test/test_nvpair.py
@@ -0,0 +1,206 @@
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
+from unittest import TestCase
+
+from lxml import etree
+
+from pcs.lib.cib import nvpair
+from pcs.test.tools.assertions import assert_xml_equal
+
+
+class UpdateNvpairTest(TestCase):
+    def setUp(self):
+        self.nvset = etree.Element("nvset", id="nvset")
+        etree.SubElement(
+            self.nvset, "nvpair", id="nvset-attr", name="attr", value="1"
+        )
+        etree.SubElement(
+            self.nvset, "nvpair", id="nvset-attr2", name="attr2", value="2"
+        )
+        etree.SubElement(
+            self.nvset, "notnvpair", id="nvset-test", name="test", value="0"
+        )
+
+    def test_update(self):
+        assert_xml_equal(
+            "<nvpair id='nvset-attr' name='attr' value='10'/>",
+            etree.tostring(
+                nvpair.update_nvpair(self.nvset, self.nvset, "attr", "10")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <nvset id="nvset">
+                <nvpair id="nvset-attr" name="attr" value="10"/>
+                <nvpair id="nvset-attr2" name="attr2" value="2"/>
+                <notnvpair id="nvset-test" name="test" value="0"/>
+            </nvset>
+            """,
+            etree.tostring(self.nvset).decode()
+        )
+
+    def test_add(self):
+        assert_xml_equal(
+            "<nvpair id='nvset-test-1' name='test' value='0'/>",
+            etree.tostring(
+                nvpair.update_nvpair(self.nvset, self.nvset, "test", "0")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <nvset id="nvset">
+                <nvpair id="nvset-attr" name="attr" value="1"/>
+                <nvpair id="nvset-attr2" name="attr2" value="2"/>
+                <notnvpair id="nvset-test" name="test" value="0"/>
+                <nvpair id="nvset-test-1" name="test" value="0"/>
+            </nvset>
+            """,
+            etree.tostring(self.nvset).decode()
+        )
+
+    def test_remove(self):
+        assert_xml_equal(
+            "<nvpair id='nvset-attr2' name='attr2' value='2'/>",
+            etree.tostring(
+                nvpair.update_nvpair(self.nvset, self.nvset, "attr2", "")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <nvset id="nvset">
+                <nvpair id="nvset-attr" name="attr" value="1"/>
+                <notnvpair id="nvset-test" name="test" value="0"/>
+            </nvset>
+            """,
+            etree.tostring(self.nvset).decode()
+        )
+
+    def test_remove_not_existing(self):
+        self.assertTrue(
+            nvpair.update_nvpair(self.nvset, self.nvset, "attr3", "") is None
+        )
+        assert_xml_equal(
+            """
+            <nvset id="nvset">
+                <nvpair id="nvset-attr" name="attr" value="1"/>
+                <nvpair id="nvset-attr2" name="attr2" value="2"/>
+                <notnvpair id="nvset-test" name="test" value="0"/>
+            </nvset>
+            """,
+            etree.tostring(self.nvset).decode()
+        )
+
+
+class UpdateNvsetTest(TestCase):
+    def setUp(self):
+        self.root = etree.Element("root", id="root")
+        self.nvset = etree.SubElement(self.root, "nvset", id="nvset")
+        etree.SubElement(
+            self.nvset, "nvpair", id="nvset-attr", name="attr", value="1"
+        )
+        etree.SubElement(
+            self.nvset, "nvpair", id="nvset-attr2", name="attr2", value="2"
+        )
+        etree.SubElement(
+            self.nvset, "notnvpair", id="nvset-test", name="test", value="0"
+        )
+
+    def test_None(self):
+        self.assertTrue(
+            nvpair.update_nvset("nvset", self.root, self.root, None) is None
+        )
+
+    def test_empty(self):
+        self.assertTrue(
+            nvpair.update_nvset("nvset", self.root, self.root, {}) is None
+        )
+
+    def test_existing(self):
+        self.assertEqual(
+            self.nvset,
+            nvpair.update_nvset("nvset", self.root, self.root, {
+                "attr": "10",
+                "new_one": "20",
+                "test": "0",
+                "attr2": ""
+            })
+        )
+        assert_xml_equal(
+            """
+            <nvset id="nvset">
+                <nvpair id="nvset-attr" name="attr" value="10"/>
+                <notnvpair id="nvset-test" name="test" value="0"/>
+                <nvpair id="nvset-new_one" name="new_one" value="20"/>
+                <nvpair id="nvset-test-1" name="test" value="0"/>
+            </nvset>
+            """,
+            etree.tostring(self.nvset).decode()
+        )
+
+    def test_new(self):
+        root = etree.Element("root", id="root")
+        assert_xml_equal(
+            """
+            <nvset id="root-nvset">
+                <nvpair id="root-nvset-attr" name="attr" value="10"/>
+                <nvpair id="root-nvset-new_one" name="new_one" value="20"/>
+                <nvpair id="root-nvset-test" name="test" value="0"/>
+            </nvset>
+            """,
+            etree.tostring(nvpair.update_nvset("nvset", root, root, {
+                "attr": "10",
+                "new_one": "20",
+                "test": "0",
+                "attr2": ""
+            })).decode()
+        )
+        assert_xml_equal(
+            """
+            <root id="root">
+                <nvset id="root-nvset">
+                    <nvpair id="root-nvset-attr" name="attr" value="10"/>
+                    <nvpair id="root-nvset-new_one" name="new_one" value="20"/>
+                    <nvpair id="root-nvset-test" name="test" value="0"/>
+                </nvset>
+            </root>
+            """,
+            etree.tostring(root).decode()
+        )
+
+
+class GetNvsetTest(TestCase):
+    def test_success(self):
+        nvset = etree.XML(
+            """
+            <nvset>
+                <nvpair id="nvset-name1" name="name1" value="value1"/>
+                <nvpair id="nvset-name2" name="name2" value="value2"/>
+                <nvpair id="nvset-name3" name="name3"/>
+            </nvset>
+            """
+        )
+        self.assertEqual(
+            [
+                {
+                    "id": "nvset-name1",
+                    "name": "name1",
+                    "value": "value1"
+                },
+                {
+                    "id": "nvset-name2",
+                    "name": "name2",
+                    "value": "value2"
+                },
+                {
+                    "id": "nvset-name3",
+                    "name": "name3",
+                    "value": ""
+                }
+            ],
+            nvpair.get_nvset(nvset)
+        )
diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py
index dfe31fc..b59d50d 100644
--- a/pcs/lib/cib/tools.py
+++ b/pcs/lib/cib/tools.py
@@ -5,8 +5,12 @@ from __future__ import (
     unicode_literals,
 )
 
+import os
+import re
+import tempfile
 from lxml import etree
 
+from pcs import settings
 from pcs.lib import reports
 from pcs.lib.errors import LibraryError
 from pcs.lib.pacemaker_values import validate_id
@@ -71,6 +75,15 @@ def get_acls(tree):
         acls = etree.SubElement(get_configuration(tree), "acls")
     return acls
 
+
+def get_alerts(tree):
+    """
+    Return 'alerts' element from tree, create a new one if missing
+    tree -- cib etree node
+    """
+    return get_sub_element(get_configuration(tree), "alerts")
+
+
 def get_constraints(tree):
     """
     Return 'constraint' element from tree
@@ -87,3 +100,117 @@ def find_parent(element, tag_names):
 
 def export_attributes(element):
     return  dict((key, value) for key, value in element.attrib.items())
+
+
+def get_sub_element(element, sub_element_tag, new_id=None, new_index=None):
+    """
+    Returns sub-element sub_element_tag of element. It will create new
+    element if such doesn't exist yet. Id of new element will be new_if if
+    it's not None. new_index specify where will be new element added, if None
+    it will be appended.
+
+    element -- parent element
+    sub_element_tag -- tag of wanted element
+    new_id -- id of new element
+    new_index -- index for new element
+    """
+    sub_element = element.find("./{0}".format(sub_element_tag))
+    if sub_element is None:
+        sub_element = etree.Element(sub_element_tag)
+        if new_id:
+            sub_element.set("id", new_id)
+        if new_index is None:
+            element.append(sub_element)
+        else:
+            element.insert(new_index, sub_element)
+    return sub_element
+
+
+def get_pacemaker_version_by_which_cib_was_validated(cib):
+    """
+    Return version of pacemaker which validated specified cib as tree.
+    Version is returned as tuple of integers: (<major>, <minor>, <revision>).
+    Raises LibraryError on any failure.
+
+    cib -- cib etree
+    """
+    version = cib.get("validate-with")
+    if version is None:
+        raise LibraryError(reports.cib_load_error_invalid_format())
+
+    regexp = re.compile(
+        r"pacemaker-(?P<major>\d+)\.(?P<minor>\d+)(\.(?P<rev>\d+))?"
+    )
+    match = regexp.match(version)
+    if not match:
+        raise LibraryError(reports.cib_load_error_invalid_format())
+    return (
+        int(match.group("major")),
+        int(match.group("minor")),
+        int(match.group("rev") or 0)
+    )
+
+
+def upgrade_cib(cib, runner):
+    """
+    Upgrade CIB to the latest schema of installed pacemaker. Returns upgraded
+    CIB as string.
+    Raises LibraryError on any failure.
+
+    cib -- cib etree
+    runner -- CommandRunner
+    """
+    temp_file = tempfile.NamedTemporaryFile("w+", suffix=".pcs")
+    temp_file.write(etree.tostring(cib).decode())
+    temp_file.flush()
+    output, retval = runner.run(
+        [
+            os.path.join(settings.pacemaker_binaries, "cibadmin"),
+            "--upgrade",
+            "--force"
+        ],
+        env_extend={"CIB_file": temp_file.name}
+    )
+
+    if retval != 0:
+        temp_file.close()
+        LibraryError(reports.cib_upgrade_failed(output))
+
+    try:
+        temp_file.seek(0)
+        return etree.fromstring(temp_file.read())
+    except (EnvironmentError, etree.XMLSyntaxError, etree.DocumentInvalid) as e:
+        LibraryError(reports.cib_upgrade_failed(str(e)))
+    finally:
+        temp_file.close()
+
+
+def ensure_cib_version(runner, cib, version):
+    """
+    This method ensures that specified cib is verified by pacemaker with
+    version 'version' or newer. If cib doesn't correspond to this version,
+    method will try to upgrade cib.
+    Returns cib which was verified by pacemaker version 'version' or later.
+    Raises LibraryError on any failure.
+
+    runner -- CommandRunner
+    cib -- cib tree
+    version -- tuple of integers (<major>, <minor>, <revision>)
+    """
+    current_version = get_pacemaker_version_by_which_cib_was_validated(
+        cib
+    )
+    if current_version >= version:
+        return None
+
+    upgraded_cib = upgrade_cib(cib, runner)
+    current_version = get_pacemaker_version_by_which_cib_was_validated(
+        upgraded_cib
+    )
+
+    if current_version >= version:
+        return upgraded_cib
+
+    raise LibraryError(reports.unable_to_upgrade_cib_to_required_version(
+        current_version, version
+    ))
diff --git a/pcs/lib/commands/alert.py b/pcs/lib/commands/alert.py
new file mode 100644
index 0000000..7371fbc
--- /dev/null
+++ b/pcs/lib/commands/alert.py
@@ -0,0 +1,169 @@
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
+from pcs.lib import reports
+from pcs.lib.cib import alert
+from pcs.lib.errors import LibraryError
+
+
+REQUIRED_CIB_VERSION = (2, 5, 0)
+
+
+def create_alert(
+    lib_env,
+    alert_id,
+    path,
+    instance_attribute_dict,
+    meta_attribute_dict,
+    description=None
+):
+    """
+    Create new alert.
+    Raises LibraryError if path is not specified, or any other failure.
+
+    lib_env -- LibraryEnvironment
+    alert_id -- id of alert to be created, if None it will be generated
+    path -- path to script for alert
+    instance_attribute_dict -- dictionary of instance attributes
+    meta_attribute_dict -- dictionary of meta attributes
+    description -- alert description description
+    """
+    if not path:
+        raise LibraryError(reports.required_option_is_missing("path"))
+
+    cib = lib_env.get_cib(REQUIRED_CIB_VERSION)
+
+    alert_el = alert.create_alert(cib, alert_id, path, description)
+    alert.update_instance_attributes(cib, alert_el, instance_attribute_dict)
+    alert.update_meta_attributes(cib, alert_el, meta_attribute_dict)
+
+    lib_env.push_cib(cib)
+
+
+def update_alert(
+    lib_env,
+    alert_id,
+    path,
+    instance_attribute_dict,
+    meta_attribute_dict,
+    description=None
+):
+    """
+    Update existing alert with specified id.
+
+    lib_env -- LibraryEnvironment
+    alert_id -- id of alert to be updated
+    path -- new path, if None old value will stay unchanged
+    instance_attribute_dict -- dictionary of instance attributes to update
+    meta_attribute_dict -- dictionary of meta attributes to update
+    description -- new description, if empty string, old description will be
+        deleted, if None old value will stay unchanged
+    """
+    cib = lib_env.get_cib(REQUIRED_CIB_VERSION)
+
+    alert_el = alert.update_alert(cib, alert_id, path, description)
+    alert.update_instance_attributes(cib, alert_el, instance_attribute_dict)
+    alert.update_meta_attributes(cib, alert_el, meta_attribute_dict)
+
+    lib_env.push_cib(cib)
+
+
+def remove_alert(lib_env, alert_id):
+    """
+    Remove alert with specified id.
+
+    lib_env -- LibraryEnvironment
+    alert_id -- id of alert which should be removed
+    """
+    cib = lib_env.get_cib(REQUIRED_CIB_VERSION)
+    alert.remove_alert(cib, alert_id)
+    lib_env.push_cib(cib)
+
+
+def add_recipient(
+    lib_env,
+    alert_id,
+    recipient_value,
+    instance_attribute_dict,
+    meta_attribute_dict,
+    description=None
+):
+    """
+    Add new recipient to alert witch id alert_id.
+
+    lib_env -- LibraryEnvironment
+    alert_id -- id of alert to which new recipient should be added
+    recipient_value -- value of new recipient
+    instance_attribute_dict -- dictionary of instance attributes to update
+    meta_attribute_dict -- dictionary of meta attributes to update
+    description -- recipient description
+    """
+    if not recipient_value:
+        raise LibraryError(
+            reports.required_option_is_missing("value")
+        )
+
+    cib = lib_env.get_cib(REQUIRED_CIB_VERSION)
+    recipient = alert.add_recipient(
+        cib, alert_id, recipient_value, description
+    )
+    alert.update_instance_attributes(cib, recipient, instance_attribute_dict)
+    alert.update_meta_attributes(cib, recipient, meta_attribute_dict)
+
+    lib_env.push_cib(cib)
+
+
+def update_recipient(
+    lib_env,
+    alert_id,
+    recipient_value,
+    instance_attribute_dict,
+    meta_attribute_dict,
+    description=None
+):
+    """
+    Update existing recipient.
+
+    lib_env -- LibraryEnvironment
+    alert_id -- id of alert to which recipient belong
+    recipient_value -- recipient to be updated
+    instance_attribute_dict -- dictionary of instance attributes to update
+    meta_attribute_dict -- dictionary of meta attributes to update
+    description -- new description, if empty string, old description will be
+        deleted, if None old value will stay unchanged
+    """
+    cib = lib_env.get_cib(REQUIRED_CIB_VERSION)
+    recipient = alert.update_recipient(
+        cib, alert_id, recipient_value, description
+    )
+    alert.update_instance_attributes(cib, recipient, instance_attribute_dict)
+    alert.update_meta_attributes(cib, recipient, meta_attribute_dict)
+
+    lib_env.push_cib(cib)
+
+
+def remove_recipient(lib_env, alert_id, recipient_value):
+    """
+    Remove existing recipient.
+
+    lib_env -- LibraryEnvironment
+    alert_id -- id of alert to which recipient belong
+    recipient_value -- recipient to be removed
+    """
+    cib = lib_env.get_cib(REQUIRED_CIB_VERSION)
+    alert.remove_recipient(cib, alert_id, recipient_value)
+    lib_env.push_cib(cib)
+
+
+def get_all_alerts(lib_env):
+    """
+    Returns list of all alerts. See docs of pcs.lib.cib.alert.get_all_alerts for
+    description of data format.
+
+    lib_env -- LibraryEnvironment
+    """
+    return alert.get_all_alerts(lib_env.get_cib())
diff --git a/pcs/lib/commands/test/test_alert.py b/pcs/lib/commands/test/test_alert.py
new file mode 100644
index 0000000..34813df
--- /dev/null
+++ b/pcs/lib/commands/test/test_alert.py
@@ -0,0 +1,639 @@
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
+import logging
+from lxml import etree
+
+from unittest import TestCase
+
+from pcs.test.tools.pcs_mock import mock
+from pcs.test.tools.assertions import (
+    assert_raise_library_error,
+    assert_xml_equal,
+)
+from pcs.test.tools.custom_mock import MockLibraryReportProcessor
+
+from pcs.common import report_codes
+from pcs.lib.errors import ReportItemSeverity as Severities
+from pcs.lib.env import LibraryEnvironment
+from pcs.lib.external import CommandRunner
+
+import pcs.lib.commands.alert as cmd_alert
+
+
+@mock.patch("pcs.lib.cib.tools.upgrade_cib")
+class CreateAlertTest(TestCase):
+    def setUp(self):
+        self.mock_log = mock.MagicMock(spec_set=logging.Logger)
+        self.mock_run = mock.MagicMock(spec_set=CommandRunner)
+        self.mock_rep = MockLibraryReportProcessor()
+        self.mock_env = LibraryEnvironment(
+            self.mock_log, self.mock_rep, cib_data="<cib/>"
+        )
+
+    def test_no_path(self, mock_upgrade_cib):
+        assert_raise_library_error(
+            lambda: cmd_alert.create_alert(
+                self.mock_env, None, None, None, None
+            ),
+            (
+                Severities.ERROR,
+                report_codes.REQUIRED_OPTION_IS_MISSING,
+                {"option_name": "path"}
+            )
+        )
+        self.assertEqual(0, mock_upgrade_cib.call_count)
+
+    def test_upgrade_needed(self, mock_upgrade_cib):
+        self.mock_env._push_cib_xml(
+            """
+            <cib validate-with="pacemaker-2.4.1">
+                <configuration>
+                </configuration>
+            </cib>
+            """
+        )
+        mock_upgrade_cib.return_value = etree.XML(
+            """
+            <cib validate-with="pacemaker-2.5.0">
+                <configuration>
+                </configuration>
+            </cib>
+            """
+        )
+        cmd_alert.create_alert(
+            self.mock_env,
+            "my-alert",
+            "/my/path",
+            {
+                "instance": "value",
+                "another": "val"
+            },
+            {"meta1": "val1"},
+            "my description"
+        )
+        assert_xml_equal(
+            """
+<cib validate-with="pacemaker-2.5.0">
+    <configuration>
+        <alerts>
+            <alert id="my-alert" path="/my/path" description="my description">
+                <meta_attributes id="my-alert-meta_attributes">
+                    <nvpair
+                        id="my-alert-meta_attributes-meta1"
+                        name="meta1"
+                        value="val1"
+                    />
+                </meta_attributes>
+                <instance_attributes id="my-alert-instance_attributes">
+                    <nvpair
+                        id="my-alert-instance_attributes-another"
+                        name="another"
+                        value="val"
+                    />
+                    <nvpair
+                        id="my-alert-instance_attributes-instance"
+                        name="instance"
+                        value="value"
+                    />
+                </instance_attributes>
+            </alert>
+        </alerts>
+    </configuration>
+</cib>
+            """,
+            self.mock_env._get_cib_xml()
+        )
+        self.assertEqual(1, mock_upgrade_cib.call_count)
+
+
+class UpdateAlertTest(TestCase):
+    def setUp(self):
+        self.mock_log = mock.MagicMock(spec_set=logging.Logger)
+        self.mock_run = mock.MagicMock(spec_set=CommandRunner)
+        self.mock_rep = MockLibraryReportProcessor()
+        self.mock_env = LibraryEnvironment(
+            self.mock_log, self.mock_rep, cib_data="<cib/>"
+        )
+
+    def test_update_all(self):
+        self.mock_env._push_cib_xml(
+            """
+<cib validate-with="pacemaker-2.5">
+    <configuration>
+        <alerts>
+            <alert id="my-alert" path="/my/path" description="my description">
+                <instance_attributes id="my-alert-instance_attributes">
+                    <nvpair
+                        id="my-alert-instance_attributes-instance"
+                        name="instance"
+                        value="value"
+                    />
+                    <nvpair
+                        id="my-alert-instance_attributes-another"
+                        name="another"
+                        value="val"
+                    />
+                </instance_attributes>
+                <meta_attributes id="my-alert-meta_attributes">
+                    <nvpair
+                        id="my-alert-meta_attributes-meta1"
+                        name="meta1"
+                        value="val1"
+                    />
+                </meta_attributes>
+            </alert>
+        </alerts>
+    </configuration>
+</cib>
+            """
+        )
+        cmd_alert.update_alert(
+            self.mock_env,
+            "my-alert",
+            "/another/one",
+            {
+                "instance": "",
+                "my-attr": "its_val"
+            },
+            {"meta1": "val2"},
+            ""
+        )
+        assert_xml_equal(
+            """
+<cib validate-with="pacemaker-2.5">
+    <configuration>
+        <alerts>
+            <alert id="my-alert" path="/another/one">
+                <instance_attributes id="my-alert-instance_attributes">
+                    <nvpair
+                        id="my-alert-instance_attributes-another"
+                        name="another"
+                        value="val"
+                    />
+                    <nvpair
+                        id="my-alert-instance_attributes-my-attr"
+                        name="my-attr"
+                        value="its_val"
+                    />
+                </instance_attributes>
+                <meta_attributes id="my-alert-meta_attributes">
+                    <nvpair
+                        id="my-alert-meta_attributes-meta1"
+                        name="meta1"
+                        value="val2"
+                    />
+                </meta_attributes>
+            </alert>
+        </alerts>
+    </configuration>
+</cib>
+            """,
+            self.mock_env._get_cib_xml()
+        )
+
+    def test_update_instance_attribute(self):
+        self.mock_env._push_cib_xml(
+            """
+<cib validate-with="pacemaker-2.5">
+    <configuration>
+        <alerts>
+            <alert id="my-alert" path="/my/path" description="my description">
+                <instance_attributes id="my-alert-instance_attributes">
+                    <nvpair
+                        id="my-alert-instance_attributes-instance"
+                        name="instance"
+                        value="value"
+                    />
+                </instance_attributes>
+            </alert>
+        </alerts>
+    </configuration>
+</cib>
+            """
+        )
+        cmd_alert.update_alert(
+            self.mock_env,
+            "my-alert",
+            None,
+            {"instance": "new_val"},
+            {},
+            None
+        )
+        assert_xml_equal(
+            """
+<cib validate-with="pacemaker-2.5">
+    <configuration>
+        <alerts>
+            <alert id="my-alert" path="/my/path" description="my description">
+                <instance_attributes id="my-alert-instance_attributes">
+                    <nvpair
+                        id="my-alert-instance_attributes-instance"
+                        name="instance"
+                        value="new_val"
+                    />
+                </instance_attributes>
+            </alert>
+        </alerts>
+    </configuration>
+</cib>
+            """,
+            self.mock_env._get_cib_xml()
+        )
+
+    def test_alert_doesnt_exist(self):
+        self.mock_env._push_cib_xml(
+            """
+            <cib validate-with="pacemaker-2.5">
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="path"/>
+                    </alerts>
+                </configuration>
+            </cib>
+            """
+        )
+        assert_raise_library_error(
+            lambda: cmd_alert.update_alert(
+                self.mock_env, "unknown", "test", {}, {}, None
+            ),
+            (
+                Severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "unknown"}
+            )
+        )
+
+
+class RemoveAlertTest(TestCase):
+    def setUp(self):
+        self.mock_log = mock.MagicMock(spec_set=logging.Logger)
+        self.mock_run = mock.MagicMock(spec_set=CommandRunner)
+        self.mock_rep = MockLibraryReportProcessor()
+        cib = """
+            <cib validate-with="pacemaker-2.5">
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="path"/>
+                        <alert id="alert-1" path="/path"/>
+                    </alerts>
+                </configuration>
+            </cib>
+        """
+        self.mock_env = LibraryEnvironment(
+            self.mock_log, self.mock_rep, cib_data=cib
+        )
+
+    def test_success(self):
+        cmd_alert.remove_alert(self.mock_env, "alert")
+        assert_xml_equal(
+            """
+                <cib validate-with="pacemaker-2.5">
+                    <configuration>
+                        <alerts>
+                            <alert id="alert-1" path="/path"/>
+                        </alerts>
+                    </configuration>
+                </cib>
+            """,
+            self.mock_env._get_cib_xml()
+        )
+
+    def test_not_existing_alert(self):
+        assert_raise_library_error(
+            lambda: cmd_alert.remove_alert(self.mock_env, "unknown"),
+            (
+                Severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "unknown"}
+            )
+        )
+
+
+class AddRecipientTest(TestCase):
+    def setUp(self):
+        self.mock_log = mock.MagicMock(spec_set=logging.Logger)
+        self.mock_run = mock.MagicMock(spec_set=CommandRunner)
+        self.mock_rep = MockLibraryReportProcessor()
+        cib = """
+            <cib validate-with="pacemaker-2.5">
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="path">
+                            <recipient id="alert-recipient" value="value1"/>
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+        """
+        self.mock_env = LibraryEnvironment(
+            self.mock_log, self.mock_rep, cib_data=cib
+        )
+
+    def test_alert_not_found(self):
+        assert_raise_library_error(
+            lambda: cmd_alert.add_recipient(
+                self.mock_env, "unknown", "recipient", {}, {}
+            ),
+            (
+                Severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "unknown"}
+            )
+        )
+
+    def test_value_not_defined(self):
+        assert_raise_library_error(
+            lambda: cmd_alert.add_recipient(
+                self.mock_env, "unknown", "", {}, {}
+            ),
+            (
+                Severities.ERROR,
+                report_codes.REQUIRED_OPTION_IS_MISSING,
+                {"option_name": "value"}
+            )
+        )
+
+    def test_recipient_already_exists(self):
+        assert_raise_library_error(
+            lambda: cmd_alert.add_recipient(
+                self.mock_env, "alert", "value1", {}, {}
+            ),
+            (
+                Severities.ERROR,
+                report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS,
+                {
+                    "recipient": "value1",
+                    "alert": "alert"
+                }
+            )
+        )
+
+    def test_success(self):
+        cmd_alert.add_recipient(
+            self.mock_env,
+            "alert",
+            "value",
+            {"attr1": "val1"},
+            {
+                "attr2": "val2",
+                "attr1": "val1"
+            }
+        )
+        assert_xml_equal(
+            """
+<cib validate-with="pacemaker-2.5">
+    <configuration>
+        <alerts>
+            <alert id="alert" path="path">
+                <recipient id="alert-recipient" value="value1"/>
+                <recipient id="alert-recipient-1" value="value">
+                    <meta_attributes
+                        id="alert-recipient-1-meta_attributes"
+                    >
+                        <nvpair
+                            id="alert-recipient-1-meta_attributes-attr1"
+                            name="attr1"
+                            value="val1"
+                        />
+                        <nvpair
+                            id="alert-recipient-1-meta_attributes-attr2"
+                            name="attr2"
+                            value="val2"
+                        />
+                    </meta_attributes>
+                    <instance_attributes
+                        id="alert-recipient-1-instance_attributes"
+                    >
+                        <nvpair
+                            id="alert-recipient-1-instance_attributes-attr1"
+                            name="attr1"
+                            value="val1"
+                        />
+                    </instance_attributes>
+                </recipient>
+            </alert>
+        </alerts>
+    </configuration>
+</cib>
+            """,
+            self.mock_env._get_cib_xml()
+        )
+
+
+class UpdateRecipientTest(TestCase):
+    def setUp(self):
+        self.mock_log = mock.MagicMock(spec_set=logging.Logger)
+        self.mock_run = mock.MagicMock(spec_set=CommandRunner)
+        self.mock_rep = MockLibraryReportProcessor()
+        cib = """
+<cib validate-with="pacemaker-2.5">
+    <configuration>
+        <alerts>
+            <alert id="alert" path="path">
+                <recipient id="alert-recipient" value="value1"/>
+                <recipient id="alert-recipient-1" value="value" description="d">
+                    <meta_attributes
+                        id="alert-recipient-1-meta_attributes"
+                    >
+                        <nvpair
+                            id="alert-recipient-1-meta_attributes-attr1"
+                            name="attr1"
+                            value="val1"
+                        />
+                        <nvpair
+                            id="alert-recipient-1-meta_attributes-attr2"
+                            name="attr2"
+                            value="val2"
+                        />
+                    </meta_attributes>
+                    <instance_attributes
+                        id="alert-recipient-1-instance_attributes"
+                    >
+                        <nvpair
+                            id="alert-recipient-1-instance_attributes-attr1"
+                            name="attr1"
+                            value="val1"
+                        />
+                    </instance_attributes>
+                </recipient>
+            </alert>
+        </alerts>
+    </configuration>
+</cib>
+        """
+        self.mock_env = LibraryEnvironment(
+            self.mock_log, self.mock_rep, cib_data=cib
+        )
+
+    def test_alert_not_found(self):
+        assert_raise_library_error(
+            lambda: cmd_alert.update_recipient(
+                self.mock_env, "unknown", "recipient", {}, {}
+            ),
+            (
+                Severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "unknown"}
+            )
+        )
+
+    def test_recipient_not_found(self):
+        assert_raise_library_error(
+            lambda: cmd_alert.update_recipient(
+                self.mock_env, "alert", "recipient", {}, {}
+            ),
+            (
+                Severities.ERROR,
+                report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND,
+                {
+                    "recipient": "recipient",
+                    "alert": "alert"
+                }
+            )
+        )
+
+    def test_update_all(self):
+        cmd_alert.update_recipient(
+            self.mock_env,
+            "alert",
+            "value",
+            {"attr1": "value"},
+            {
+                "attr1": "",
+                "attr3": "new_val"
+            },
+            "desc"
+        )
+        assert_xml_equal(
+            """
+<cib validate-with="pacemaker-2.5">
+    <configuration>
+        <alerts>
+            <alert id="alert" path="path">
+                <recipient id="alert-recipient" value="value1"/>
+                <recipient
+                    id="alert-recipient-1"
+                    value="value"
+                    description="desc"
+                >
+                    <meta_attributes
+                        id="alert-recipient-1-meta_attributes"
+                    >
+                        <nvpair
+                            id="alert-recipient-1-meta_attributes-attr2"
+                            name="attr2"
+                            value="val2"
+                        />
+                        <nvpair
+                            id="alert-recipient-1-meta_attributes-attr3"
+                            name="attr3"
+                            value="new_val"
+                        />
+                    </meta_attributes>
+                    <instance_attributes
+                        id="alert-recipient-1-instance_attributes"
+                    >
+                        <nvpair
+                            id="alert-recipient-1-instance_attributes-attr1"
+                            name="attr1"
+                            value="value"
+                        />
+                    </instance_attributes>
+                </recipient>
+            </alert>
+        </alerts>
+    </configuration>
+</cib>
+            """,
+            self.mock_env._get_cib_xml()
+        )
+
+
+class RemoveRecipientTest(TestCase):
+    def setUp(self):
+        self.mock_log = mock.MagicMock(spec_set=logging.Logger)
+        self.mock_run = mock.MagicMock(spec_set=CommandRunner)
+        self.mock_rep = MockLibraryReportProcessor()
+        cib = """
+            <cib validate-with="pacemaker-2.5">
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="path">
+                            <recipient id="alert-recipient" value="value1"/>
+                            <recipient id="alert-recipient-1" value="value"/>
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+        """
+        self.mock_env = LibraryEnvironment(
+            self.mock_log, self.mock_rep, cib_data=cib
+        )
+
+    def test_alert_not_found(self):
+        assert_raise_library_error(
+            lambda: cmd_alert.remove_recipient(
+                self.mock_env, "unknown", "recipient"
+            ),
+            (
+                Severities.ERROR,
+                report_codes.CIB_ALERT_NOT_FOUND,
+                {"alert": "unknown"}
+            )
+        )
+
+    def test_recipient_not_found(self):
+        assert_raise_library_error(
+            lambda: cmd_alert.remove_recipient(
+                self.mock_env, "alert", "recipient"
+            ),
+            (
+                Severities.ERROR,
+                report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND,
+                {
+                    "recipient": "recipient",
+                    "alert": "alert"
+                }
+            )
+        )
+
+    def test_success(self):
+        cmd_alert.remove_recipient(self.mock_env, "alert", "value1")
+        assert_xml_equal(
+            """
+            <cib validate-with="pacemaker-2.5">
+                <configuration>
+                    <alerts>
+                        <alert id="alert" path="path">
+                            <recipient id="alert-recipient-1" value="value"/>
+                        </alert>
+                    </alerts>
+                </configuration>
+            </cib>
+            """,
+            self.mock_env._get_cib_xml()
+        )
+
+
+@mock.patch("pcs.lib.cib.alert.get_all_alerts")
+class GetAllAlertsTest(TestCase):
+    def setUp(self):
+        self.mock_log = mock.MagicMock(spec_set=logging.Logger)
+        self.mock_run = mock.MagicMock(spec_set=CommandRunner)
+        self.mock_rep = MockLibraryReportProcessor()
+        self.mock_env = LibraryEnvironment(
+            self.mock_log, self.mock_rep, cib_data='<cib/>'
+        )
+
+    def test_success(self, mock_alerts):
+        mock_alerts.return_value = [{"id": "alert"}]
+        self.assertEqual(
+            [{"id": "alert"}],
+            cmd_alert.get_all_alerts(self.mock_env)
+        )
+        self.assertEqual(1, mock_alerts.call_count)
diff --git a/pcs/lib/commands/test/test_ticket.py b/pcs/lib/commands/test/test_ticket.py
index a22a014..751001b 100644
--- a/pcs/lib/commands/test/test_ticket.py
+++ b/pcs/lib/commands/test/test_ticket.py
@@ -44,7 +44,7 @@ class CreateTest(TestCase):
         })
 
         assert_xml_equal(
-            env.get_cib_xml(),
+            env._get_cib_xml(),
             str(cib.append_to_first_tag_name(
                 'constraints', """
                     <rsc_ticket
diff --git a/pcs/lib/env.py b/pcs/lib/env.py
index 99e3397..1151891 100644
--- a/pcs/lib/env.py
+++ b/pcs/lib/env.py
@@ -27,6 +27,7 @@ from pcs.lib.pacemaker import (
     get_cib_xml,
     replace_cib_configuration_xml,
 )
+from pcs.lib.cib.tools import ensure_cib_version
 
 
 class LibraryEnvironment(object):
@@ -54,6 +55,7 @@ class LibraryEnvironment(object):
         # related code currently - it's in pcsd
         self._auth_tokens_getter = auth_tokens_getter
         self._auth_tokens = None
+        self._cib_upgraded = False
 
     @property
     def logger(self):
@@ -77,27 +79,45 @@ class LibraryEnvironment(object):
             self._is_cman_cluster = is_cman_cluster(self.cmd_runner())
         return self._is_cman_cluster
 
-    def get_cib_xml(self):
+    @property
+    def cib_upgraded(self):
+        return self._cib_upgraded
+
+    def _get_cib_xml(self):
         if self.is_cib_live:
             return get_cib_xml(self.cmd_runner())
         else:
             return self._cib_data
 
-    def get_cib(self):
-        return get_cib(self.get_cib_xml())
+    def get_cib(self, minimal_version=None):
+        cib = get_cib(self._get_cib_xml())
+        if minimal_version is not None:
+            upgraded_cib = ensure_cib_version(
+                self.cmd_runner(), cib, minimal_version
+            )
+            if upgraded_cib is not None:
+                cib = upgraded_cib
+                self._cib_upgraded = True
+        return cib
 
-    def push_cib_xml(self, cib_data):
+    def _push_cib_xml(self, cib_data):
         if self.is_cib_live:
-            replace_cib_configuration_xml(self.cmd_runner(), cib_data)
+            replace_cib_configuration_xml(
+                self.cmd_runner(), cib_data, self._cib_upgraded
+            )
+            if self._cib_upgraded:
+                self._cib_upgraded = False
+                self.report_processor.process(reports.cib_upgrade_successful())
         else:
             self._cib_data = cib_data
 
+
     def push_cib(self, cib):
         #etree returns bytes: b'xml'
         #python 3 removed .encode() from bytes
         #run(...) calls subprocess.Popen.communicate which calls encode...
         #so here is bytes to str conversion
-        self.push_cib_xml(etree.tostring(cib).decode())
+        self._push_cib_xml(etree.tostring(cib).decode())
 
     @property
     def is_cib_live(self):
diff --git a/pcs/lib/pacemaker.py b/pcs/lib/pacemaker.py
index 14745c5..fd6f97b 100644
--- a/pcs/lib/pacemaker.py
+++ b/pcs/lib/pacemaker.py
@@ -55,24 +55,21 @@ def get_cib(xml):
     except (etree.XMLSyntaxError, etree.DocumentInvalid):
         raise LibraryError(reports.cib_load_error_invalid_format())
 
-def replace_cib_configuration_xml(runner, xml):
-    output, retval = runner.run(
-        [
-            __exec("cibadmin"),
-            "--replace", "--scope", "configuration", "--verbose", "--xml-pipe"
-        ],
-        stdin_string=xml
-    )
+def replace_cib_configuration_xml(runner, xml, cib_upgraded=False):
+    cmd = [__exec("cibadmin"), "--replace",  "--verbose", "--xml-pipe"]
+    if not cib_upgraded:
+        cmd += ["--scope", "configuration"]
+    output, retval = runner.run(cmd, stdin_string=xml)
     if retval != 0:
         raise LibraryError(reports.cib_push_error(retval, output))
 
-def replace_cib_configuration(runner, tree):
+def replace_cib_configuration(runner, tree, cib_upgraded=False):
     #etree returns bytes: b'xml'
     #python 3 removed .encode() from bytes
     #run(...) calls subprocess.Popen.communicate which calls encode...
     #so here is bytes to str conversion
     xml = etree.tostring(tree).decode()
-    return replace_cib_configuration_xml(runner, xml)
+    return replace_cib_configuration_xml(runner, xml, cib_upgraded)
 
 def get_local_node_status(runner):
     try:
diff --git a/pcs/lib/reports.py b/pcs/lib/reports.py
index 4f4f580..490b4ff 100644
--- a/pcs/lib/reports.py
+++ b/pcs/lib/reports.py
@@ -1436,3 +1436,94 @@ def cluster_restart_required_to_apply_changes():
         report_codes.CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES,
         "Cluster restart is required in order to apply these changes."
     )
+
+
+def cib_alert_recipient_already_exists(alert_id, recipient_value):
+    """
+    Error that recipient already exists.
+
+    alert_id -- id of alert to which recipient belongs
+    recipient_value -- value of recipient
+    """
+    return ReportItem.error(
+        report_codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS,
+        "Recipient '{recipient}' in alert '{alert}' already exists.",
+        info={
+            "recipient": recipient_value,
+            "alert": alert_id
+        }
+    )
+
+
+def cib_alert_recipient_not_found(alert_id, recipient_value):
+    """
+    Specified recipient not found.
+
+    alert_id -- id of alert to which recipient should belong
+    recipient_value -- recipient value
+    """
+    return ReportItem.error(
+        report_codes.CIB_ALERT_RECIPIENT_NOT_FOUND,
+        "Recipient '{recipient}' not found in alert '{alert}'.",
+        info={
+            "recipient": recipient_value,
+            "alert": alert_id
+        }
+    )
+
+
+def cib_alert_not_found(alert_id):
+    """
+    Alert with specified id doesn't exist.
+
+    alert_id -- id of alert
+    """
+    return ReportItem.error(
+        report_codes.CIB_ALERT_NOT_FOUND,
+        "Alert '{alert}' not found.",
+        info={"alert": alert_id}
+    )
+
+
+def cib_upgrade_successful():
+    """
+    Upgrade of CIB schema was successful.
+    """
+    return ReportItem.info(
+        report_codes.CIB_UPGRADE_SUCCESSFUL,
+        "CIB has been upgraded to the latest schema version."
+    )
+
+
+def cib_upgrade_failed(reason):
+    """
+    Upgrade of CIB schema failed.
+
+    reason -- reason of failure
+    """
+    return ReportItem.error(
+        report_codes.CIB_UPGRADE_FAILED,
+        "Upgrading of CIB to the latest schema failed: {reason}",
+        info={"reason": reason}
+    )
+
+
+def unable_to_upgrade_cib_to_required_version(
+    current_version, required_version
+):
+    """
+    Unable to upgrade CIB to minimal required schema version.
+
+    current_version -- current version of CIB schema
+    required_version -- required version of CIB schema
+    """
+    return ReportItem.error(
+        report_codes.CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION,
+        "Unable to upgrade CIB to required schema version {required_version} "
+        "or higher. Current version is {current_version}. Newer version of "
+        "pacemaker is needed.",
+        info={
+            "required_version": "{0}.{1}.{2}".format(*required_version),
+            "current_version": "{0}.{1}.{2}".format(*current_version)
+        }
+    )
diff --git a/pcs/pcs.8 b/pcs/pcs.8
index 0e230b7..425b613 100644
--- a/pcs/pcs.8
+++ b/pcs/pcs.8
@@ -56,6 +56,9 @@ Manage pcs daemon.
 .TP
 node
 Manage cluster nodes.
+.TP
+alert
+Manage pacemaker alerts.
 .SS "resource"
 .TP
 [show [resource id]] [\fB\-\-full\fR] [\fB\-\-groups\fR]
@@ -635,6 +638,28 @@ Remove node from standby mode (the node specified will now be able to host resou
 .TP
 utilization [<node> [<name>=<value> ...]]
 Add specified utilization options to specified node. If node is not specified, shows utilization of all nodes. If utilization options are not specified, shows utilization of specified node. Utilization option should be in format name=value, value has to be integer. Options may be removed by setting an option without a value. Example: pcs node utilization node1 cpu=4 ram=
+.SS "alert"
+.TP
+[config|show]
+Show all configured alerts.
+.TP
+create path=<path> [id=<alert\-id>] [description=<description>] [options [<option>=<value>]...] [meta [<meta\-option>=<value>]...]
+Create new alert with specified path. Id will be automatically generated if it is not specified.
+.TP
+update <alert\-id> [path=<path>] [description=<description>] [options [<option>=<value>]...] [meta [<meta\-option>=<value>]...]
+Update existing alert with specified id.
+.TP
+remove <alert\-id>
+Remove alert with specified id.
+.TP
+recipient add <alert\-id> <recipient\-value> [description=<description>] [options [<option>=<value>]...] [meta [<meta\-option>=<value>]...]
+Add new recipient to specified alert.
+.TP
+recipient update <alert\-id> <recipient\-value> [description=<description>] [options [<option>=<value>]...] [meta [<meta\-option>=<value>]...]
+Update existing recipient identified by alert and it's value.
+.TP
+recipient remove <alert\-id> <recipient\-value>
+Remove specified recipient.
 .SH EXAMPLES
 .TP
 Show all resources
diff --git a/pcs/test/resources/cib-empty-2.5.xml b/pcs/test/resources/cib-empty-2.5.xml
new file mode 100644
index 0000000..1b4fb0a
--- /dev/null
+++ b/pcs/test/resources/cib-empty-2.5.xml
@@ -0,0 +1,10 @@
+<cib epoch="557" num_updates="122" admin_epoch="0" validate-with="pacemaker-2.5" crm_feature_set="3.0.9" update-origin="rh7-3" update-client="crmd" cib-last-written="Thu Aug 23 16:49:17 2012" have-quorum="0" dc-uuid="2">
+  <configuration>
+    <crm_config/>
+    <nodes>
+    </nodes>
+    <resources/>
+    <constraints/>
+  </configuration>
+  <status/>
+</cib>
diff --git a/pcs/test/test_alert.py b/pcs/test/test_alert.py
new file mode 100644
index 0000000..905dc9f
--- /dev/null
+++ b/pcs/test/test_alert.py
@@ -0,0 +1,363 @@
+
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
+import shutil
+import sys
+
+from pcs.test.tools.misc import (
+    get_test_resource as rc,
+    is_minimum_pacemaker_version,
+)
+from pcs.test.tools.assertions import AssertPcsMixin
+from pcs.test.tools.pcs_runner import PcsRunner
+
+major, minor = sys.version_info[:2]
+if major == 2 and minor == 6:
+    import unittest2 as unittest
+else:
+    import unittest
+
+
+old_cib = rc("cib-empty.xml")
+empty_cib = rc("cib-empty-2.5.xml")
+temp_cib = rc("temp-cib.xml")
+
+
+ALERTS_SUPPORTED = is_minimum_pacemaker_version(1, 1, 15)
+ALERTS_NOT_SUPPORTED_MSG = "Pacemaker version is too old (must be >= 1.1.15)" +\
+    " to test alerts"
+
+
+class PcsAlertTest(unittest.TestCase, AssertPcsMixin):
+    def setUp(self):
+        shutil.copy(empty_cib, temp_cib)
+        self.pcs_runner = PcsRunner(temp_cib)
+
+
+@unittest.skipUnless(ALERTS_SUPPORTED, ALERTS_NOT_SUPPORTED_MSG)
+class AlertCibUpgradeTest(unittest.TestCase, AssertPcsMixin):
+    def setUp(self):
+        shutil.copy(old_cib, temp_cib)
+        self.pcs_runner = PcsRunner(temp_cib)
+
+    def test_cib_upgrade(self):
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ No alerts defined
+"""
+        )
+
+        self.assert_pcs_success(
+            "alert create path=test",
+            "CIB has been upgraded to the latest schema version.\n"
+        )
+
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert (path=test)
+"""
+        )
+
+
+@unittest.skipUnless(ALERTS_SUPPORTED, ALERTS_NOT_SUPPORTED_MSG)
+class CreateAlertTest(PcsAlertTest):
+    def test_create_multiple_without_id(self):
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ No alerts defined
+"""
+        )
+
+        self.assert_pcs_success("alert create path=test")
+        self.assert_pcs_success("alert create path=test")
+        self.assert_pcs_success("alert create path=test2")
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert (path=test)
+ Alert: alert-1 (path=test)
+ Alert: alert-2 (path=test2)
+"""
+        )
+
+    def test_create_multiple_with_id(self):
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ No alerts defined
+"""
+        )
+        self.assert_pcs_success("alert create id=alert1 path=test")
+        self.assert_pcs_success(
+            "alert create id=alert2 description=desc path=test"
+        )
+        self.assert_pcs_success(
+            "alert create description=desc2 path=test2 id=alert3"
+        )
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert1 (path=test)
+ Alert: alert2 (path=test)
+  Description: desc
+ Alert: alert3 (path=test2)
+  Description: desc2
+"""
+        )
+
+    def test_create_with_options(self):
+        self.assert_pcs_success(
+            "alert create id=alert1 description=desc path=test "
+            "options opt1=val1 opt2=val2 meta m1=v1 m2=v2"
+        )
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert1 (path=test)
+  Description: desc
+  Options: opt1=val1 opt2=val2
+  Meta options: m1=v1 m2=v2
+"""
+        )
+
+    def test_already_exists(self):
+        self.assert_pcs_success("alert create id=alert1 path=test")
+        self.assert_pcs_fail(
+            "alert create id=alert1 path=test",
+            "Error: 'alert1' already exists\n"
+        )
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert1 (path=test)
+"""
+        )
+
+    def test_path_is_required(self):
+        self.assert_pcs_fail(
+            "alert create id=alert1",
+            "Error: required option 'path' is missing\n"
+        )
+
+
+@unittest.skipUnless(ALERTS_SUPPORTED, ALERTS_NOT_SUPPORTED_MSG)
+class UpdateAlertTest(PcsAlertTest):
+    def test_update_everything(self):
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ No alerts defined
+"""
+        )
+        self.assert_pcs_success(
+            "alert create id=alert1 description=desc path=test "
+            "options opt1=val1 opt2=val2 meta m1=v1 m2=v2"
+        )
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert1 (path=test)
+  Description: desc
+  Options: opt1=val1 opt2=val2
+  Meta options: m1=v1 m2=v2
+"""
+        )
+        self.assert_pcs_success(
+            "alert update alert1 description=new_desc path=/new/path "
+            "options opt1= opt2=test opt3=1 meta m1= m2=v m3=3"
+        )
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert1 (path=/new/path)
+  Description: new_desc
+  Options: opt2=test opt3=1
+  Meta options: m2=v m3=3
+"""
+        )
+
+    def test_not_existing_alert(self):
+        self.assert_pcs_fail(
+            "alert update alert1", "Error: Alert 'alert1' not found.\n"
+        )
+
+
+@unittest.skipUnless(ALERTS_SUPPORTED, ALERTS_NOT_SUPPORTED_MSG)
+class RemoveAlertTest(PcsAlertTest):
+    def test_not_existing_alert(self):
+        self.assert_pcs_fail(
+            "alert remove alert1", "Error: Alert 'alert1' not found.\n"
+        )
+
+    def test_success(self):
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ No alerts defined
+"""
+        )
+
+        self.assert_pcs_success("alert create path=test")
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert (path=test)
+"""
+        )
+        self.assert_pcs_success("alert remove alert")
+
+
+@unittest.skipUnless(ALERTS_SUPPORTED, ALERTS_NOT_SUPPORTED_MSG)
+class AddRecipientTest(PcsAlertTest):
+    def test_success(self):
+        self.assert_pcs_success("alert create path=test")
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert (path=test)
+"""
+        )
+        self.assert_pcs_success("alert recipient add alert rec_value")
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert (path=test)
+  Recipients:
+   Recipient: rec_value
+"""
+        )
+        self.assert_pcs_success(
+            "alert recipient add alert rec_value2 description=description "
+            "options o1=1 o2=2 meta m1=v1 m2=v2"
+        )
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert (path=test)
+  Recipients:
+   Recipient: rec_value
+   Recipient: rec_value2
+    Description: description
+    Options: o1=1 o2=2
+    Meta options: m1=v1 m2=v2
+"""
+        )
+
+    def test_no_alert(self):
+        self.assert_pcs_fail(
+            "alert recipient add alert rec_value",
+            "Error: Alert 'alert' not found.\n"
+        )
+
+    def test_already_exists(self):
+        self.assert_pcs_success("alert create path=test")
+        self.assert_pcs_success("alert recipient add alert rec_value")
+        self.assert_pcs_fail(
+            "alert recipient add alert rec_value",
+            "Error: Recipient 'rec_value' in alert 'alert' already exists.\n"
+        )
+
+
+@unittest.skipUnless(ALERTS_SUPPORTED, ALERTS_NOT_SUPPORTED_MSG)
+class UpdateRecipientAlert(PcsAlertTest):
+    def test_success(self):
+        self.assert_pcs_success("alert create path=test")
+        self.assert_pcs_success(
+            "alert recipient add alert rec_value description=description "
+            "options o1=1 o2=2 meta m1=v1 m2=v2"
+        )
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert (path=test)
+  Recipients:
+   Recipient: rec_value
+    Description: description
+    Options: o1=1 o2=2
+    Meta options: m1=v1 m2=v2
+"""
+        )
+        self.assert_pcs_success(
+            "alert recipient update alert rec_value description=desc "
+            "options o1= o2=v2 o3=3 meta m1= m2=2 m3=3"
+        )
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert (path=test)
+  Recipients:
+   Recipient: rec_value
+    Description: desc
+    Options: o2=v2 o3=3
+    Meta options: m2=2 m3=3
+"""
+        )
+
+    def test_no_alert(self):
+        self.assert_pcs_fail(
+            "alert recipient update alert rec_value description=desc",
+            "Error: Alert 'alert' not found.\n"
+        )
+
+    def test_no_recipient(self):
+        self.assert_pcs_success("alert create path=test")
+        self.assert_pcs_fail(
+            "alert recipient update alert rec_value description=desc",
+            "Error: Recipient 'rec_value' not found in alert 'alert'.\n"
+        )
+
+
+@unittest.skipUnless(ALERTS_SUPPORTED, ALERTS_NOT_SUPPORTED_MSG)
+class RemoveRecipientTest(PcsAlertTest):
+    def test_success(self):
+        self.assert_pcs_success("alert create path=test")
+        self.assert_pcs_success("alert recipient add alert rec_value")
+        self.assert_pcs_success(
+            "alert config",
+            """\
+Alerts:
+ Alert: alert (path=test)
+  Recipients:
+   Recipient: rec_value
+"""
+        )
+        self.assert_pcs_success("alert recipient remove alert rec_value")
+
+    def test_no_alert(self):
+        self.assert_pcs_fail(
+            "alert recipient remove alert rec_value",
+            "Error: Alert 'alert' not found.\n"
+        )
+
+    def test_no_recipient(self):
+        self.assert_pcs_success("alert create path=test")
+        self.assert_pcs_fail(
+            "alert recipient remove alert rec_value",
+            "Error: Recipient 'rec_value' not found in alert 'alert'.\n"
+        )
diff --git a/pcs/test/test_lib_cib_tools.py b/pcs/test/test_lib_cib_tools.py
index 405a270..1149a3f 100644
--- a/pcs/test/test_lib_cib_tools.py
+++ b/pcs/test/test_lib_cib_tools.py
@@ -7,12 +7,18 @@ from __future__ import (
 
 from unittest import TestCase
 
-from pcs.test.tools.assertions import assert_raise_library_error
+from lxml import etree
+
+from pcs.test.tools.assertions import (
+    assert_raise_library_error,
+    assert_xml_equal,
+)
 from pcs.test.tools.misc import get_test_resource as rc
 from pcs.test.tools.pcs_mock import mock
 from pcs.test.tools.xml import get_xml_manipulation_creator_from_file
 
 from pcs.common import report_codes
+from pcs.lib.external import CommandRunner
 from pcs.lib.errors import ReportItemSeverity as severities
 
 from pcs.lib.cib import tools as lib
@@ -145,3 +151,176 @@ class ValidateIdDoesNotExistsTest(TestCase):
             ),
         )
         does_id_exists.assert_called_once_with("tree", "some-id")
+
+
+class GetSubElementTest(TestCase):
+    def setUp(self):
+        self.root = etree.Element("root")
+        self.sub = etree.SubElement(self.root, "sub_element")
+
+    def test_sub_element_exists(self):
+        self.assertEqual(
+            self.sub, lib.get_sub_element(self.root, "sub_element")
+        )
+
+    def test_new_no_id(self):
+        assert_xml_equal(
+            '<new_element/>',
+            etree.tostring(
+                lib.get_sub_element(self.root, "new_element")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <root>
+                <sub_element/>
+                <new_element/>
+            </root>
+            """,
+            etree.tostring(self.root).decode()
+        )
+
+    def test_new_with_id(self):
+        assert_xml_equal(
+            '<new_element id="new_id"/>',
+            etree.tostring(
+                lib.get_sub_element(self.root, "new_element", "new_id")
+            ).decode()
+        )
+        assert_xml_equal(
+            """
+            <root>
+                <sub_element/>
+                <new_element id="new_id"/>
+            </root>
+            """,
+            etree.tostring(self.root).decode()
+        )
+
+    def test_new_first(self):
+        lib.get_sub_element(self.root, "new_element", "new_id", 0)
+        assert_xml_equal(
+            """
+            <root>
+                <new_element id="new_id"/>
+                <sub_element/>
+            </root>
+            """,
+            etree.tostring(self.root).decode()
+        )
+
+    def test_new_last(self):
+        lib.get_sub_element(self.root, "new_element", "new_id", None)
+        assert_xml_equal(
+            """
+            <root>
+                <sub_element/>
+                <new_element id="new_id"/>
+            </root>
+            """,
+            etree.tostring(self.root).decode()
+        )
+
+
+class GetPacemakerVersionByWhichCibWasValidatedTest(TestCase):
+    def test_missing_attribute(self):
+        assert_raise_library_error(
+            lambda: lib.get_pacemaker_version_by_which_cib_was_validated(
+                etree.XML("<cib/>")
+            ),
+            (
+                severities.ERROR,
+                report_codes.CIB_LOAD_ERROR_BAD_FORMAT,
+                {}
+            )
+        )
+
+    def test_invalid_version(self):
+        assert_raise_library_error(
+            lambda: lib.get_pacemaker_version_by_which_cib_was_validated(
+                etree.XML('<cib validate-with="something-1.2.3"/>')
+            ),
+            (
+                severities.ERROR,
+                report_codes.CIB_LOAD_ERROR_BAD_FORMAT,
+                {}
+            )
+        )
+
+    def test_no_revision(self):
+        self.assertEqual(
+            (1, 2, 0),
+            lib.get_pacemaker_version_by_which_cib_was_validated(
+                etree.XML('<cib validate-with="pacemaker-1.2"/>')
+            )
+        )
+
+    def test_with_revision(self):
+        self.assertEqual(
+            (1, 2, 3),
+            lib.get_pacemaker_version_by_which_cib_was_validated(
+                etree.XML('<cib validate-with="pacemaker-1.2.3"/>')
+            )
+        )
+
+
+@mock.patch("pcs.lib.cib.tools.upgrade_cib")
+class EnsureCibVersionTest(TestCase):
+    def setUp(self):
+        self.mock_runner = mock.MagicMock(spec_set=CommandRunner)
+        self.cib = etree.XML('<cib validate-with="pacemaker-2.3.4"/>')
+
+    def test_same_version(self, mock_upgrade_cib):
+        self.assertTrue(
+            lib.ensure_cib_version(
+                self.mock_runner, self.cib, (2, 3, 4)
+            ) is None
+        )
+        self.assertEqual(0, mock_upgrade_cib.run.call_count)
+
+    def test_higher_version(self, mock_upgrade_cib):
+        self.assertTrue(
+            lib.ensure_cib_version(
+                self.mock_runner, self.cib, (2, 3, 3)
+            ) is None
+        )
+        self.assertEqual(0, mock_upgrade_cib.call_count)
+
+    def test_upgraded_same_version(self, mock_upgrade_cib):
+        upgraded_cib = etree.XML('<cib validate-with="pacemaker-2.3.5"/>')
+        mock_upgrade_cib.return_value = upgraded_cib
+        self.assertEqual(
+            upgraded_cib,
+            lib.ensure_cib_version(
+                self.mock_runner, self.cib, (2, 3, 5)
+            )
+        )
+        mock_upgrade_cib.assert_called_once_with(self.cib, self.mock_runner)
+
+    def test_upgraded_higher_version(self, mock_upgrade_cib):
+        upgraded_cib = etree.XML('<cib validate-with="pacemaker-2.3.6"/>')
+        mock_upgrade_cib.return_value = upgraded_cib
+        self.assertEqual(
+            upgraded_cib,
+            lib.ensure_cib_version(
+                self.mock_runner, self.cib, (2, 3, 5)
+            )
+        )
+        mock_upgrade_cib.assert_called_once_with(self.cib, self.mock_runner)
+
+    def test_upgraded_lower_version(self, mock_upgrade_cib):
+        mock_upgrade_cib.return_value = self.cib
+        assert_raise_library_error(
+            lambda: lib.ensure_cib_version(
+                self.mock_runner, self.cib, (2, 3, 5)
+            ),
+            (
+                severities.ERROR,
+                report_codes.CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION,
+                {
+                    "required_version": "2.3.5",
+                    "current_version": "2.3.4"
+                }
+            )
+        )
+        mock_upgrade_cib.assert_called_once_with(self.cib, self.mock_runner)
diff --git a/pcs/test/test_lib_env.py b/pcs/test/test_lib_env.py
index fbaac09..95f7a00 100644
--- a/pcs/test/test_lib_env.py
+++ b/pcs/test/test_lib_env.py
@@ -7,8 +7,13 @@ from __future__ import (
 
 from unittest import TestCase
 import logging
+from lxml import etree
 
-from pcs.test.tools.assertions import assert_raise_library_error
+from pcs.test.tools.assertions import (
+    assert_raise_library_error,
+    assert_xml_equal,
+    assert_report_item_list_equal,
+)
 from pcs.test.tools.custom_mock import MockLibraryReportProcessor
 from pcs.test.tools.misc import get_test_resource as rc
 from pcs.test.tools.pcs_mock import mock
@@ -82,13 +87,13 @@ class LibraryEnvironmentTest(TestCase):
 
         self.assertFalse(env.is_cib_live)
 
-        self.assertEqual(cib_data, env.get_cib_xml())
+        self.assertEqual(cib_data, env._get_cib_xml())
         self.assertEqual(0, mock_get_cib.call_count)
 
-        env.push_cib_xml(new_cib_data)
+        env._push_cib_xml(new_cib_data)
         self.assertEqual(0, mock_push_cib.call_count)
 
-        self.assertEqual(new_cib_data, env.get_cib_xml())
+        self.assertEqual(new_cib_data, env._get_cib_xml())
         self.assertEqual(0, mock_get_cib.call_count)
 
     @mock.patch("pcs.lib.env.replace_cib_configuration_xml")
@@ -101,12 +106,135 @@ class LibraryEnvironmentTest(TestCase):
 
         self.assertTrue(env.is_cib_live)
 
-        self.assertEqual(cib_data, env.get_cib_xml())
+        self.assertEqual(cib_data, env._get_cib_xml())
         self.assertEqual(1, mock_get_cib.call_count)
 
-        env.push_cib_xml(new_cib_data)
+        env._push_cib_xml(new_cib_data)
         self.assertEqual(1, mock_push_cib.call_count)
 
+    @mock.patch("pcs.lib.env.ensure_cib_version")
+    @mock.patch("pcs.lib.env.get_cib_xml")
+    def test_get_cib_no_version_live(
+            self, mock_get_cib_xml, mock_ensure_cib_version
+    ):
+        mock_get_cib_xml.return_value = '<cib/>'
+        env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+        assert_xml_equal('<cib/>', etree.tostring(env.get_cib()).decode())
+        self.assertEqual(1, mock_get_cib_xml.call_count)
+        self.assertEqual(0, mock_ensure_cib_version.call_count)
+        self.assertFalse(env.cib_upgraded)
+
+    @mock.patch("pcs.lib.env.ensure_cib_version")
+    @mock.patch("pcs.lib.env.get_cib_xml")
+    def test_get_cib_upgrade_live(
+        self, mock_get_cib_xml, mock_ensure_cib_version
+    ):
+        mock_get_cib_xml.return_value = '<cib/>'
+        mock_ensure_cib_version.return_value = etree.XML('<new_cib/>')
+        env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+        assert_xml_equal(
+            '<new_cib/>', etree.tostring(env.get_cib((1, 2, 3))).decode()
+        )
+        self.assertEqual(1, mock_get_cib_xml.call_count)
+        self.assertEqual(1, mock_ensure_cib_version.call_count)
+        self.assertTrue(env.cib_upgraded)
+
+    @mock.patch("pcs.lib.env.ensure_cib_version")
+    @mock.patch("pcs.lib.env.get_cib_xml")
+    def test_get_cib_no_upgrade_live(
+            self, mock_get_cib_xml, mock_ensure_cib_version
+    ):
+        mock_get_cib_xml.return_value = '<cib/>'
+        mock_ensure_cib_version.return_value = None
+        env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+        assert_xml_equal(
+            '<cib/>', etree.tostring(env.get_cib((1, 2, 3))).decode()
+        )
+        self.assertEqual(1, mock_get_cib_xml.call_count)
+        self.assertEqual(1, mock_ensure_cib_version.call_count)
+        self.assertFalse(env.cib_upgraded)
+
+    @mock.patch("pcs.lib.env.ensure_cib_version")
+    @mock.patch("pcs.lib.env.get_cib_xml")
+    def test_get_cib_no_version_file(
+            self, mock_get_cib_xml, mock_ensure_cib_version
+    ):
+        env = LibraryEnvironment(
+            self.mock_logger, self.mock_reporter, cib_data='<cib/>'
+        )
+        assert_xml_equal('<cib/>', etree.tostring(env.get_cib()).decode())
+        self.assertEqual(0, mock_get_cib_xml.call_count)
+        self.assertEqual(0, mock_ensure_cib_version.call_count)
+        self.assertFalse(env.cib_upgraded)
+
+    @mock.patch("pcs.lib.env.ensure_cib_version")
+    @mock.patch("pcs.lib.env.get_cib_xml")
+    def test_get_cib_upgrade_file(
+            self, mock_get_cib_xml, mock_ensure_cib_version
+    ):
+        mock_ensure_cib_version.return_value = etree.XML('<new_cib/>')
+        env = LibraryEnvironment(
+            self.mock_logger, self.mock_reporter, cib_data='<cib/>'
+        )
+        assert_xml_equal(
+            '<new_cib/>', etree.tostring(env.get_cib((1, 2, 3))).decode()
+        )
+        self.assertEqual(0, mock_get_cib_xml.call_count)
+        self.assertEqual(1, mock_ensure_cib_version.call_count)
+        self.assertTrue(env.cib_upgraded)
+
+    @mock.patch("pcs.lib.env.ensure_cib_version")
+    @mock.patch("pcs.lib.env.get_cib_xml")
+    def test_get_cib_no_upgrade_file(
+            self, mock_get_cib_xml, mock_ensure_cib_version
+    ):
+        mock_ensure_cib_version.return_value = None
+        env = LibraryEnvironment(
+            self.mock_logger, self.mock_reporter, cib_data='<cib/>'
+        )
+        assert_xml_equal(
+            '<cib/>', etree.tostring(env.get_cib((1, 2, 3))).decode()
+        )
+        self.assertEqual(0, mock_get_cib_xml.call_count)
+        self.assertEqual(1, mock_ensure_cib_version.call_count)
+        self.assertFalse(env.cib_upgraded)
+
+    @mock.patch("pcs.lib.env.replace_cib_configuration_xml")
+    @mock.patch.object(
+        LibraryEnvironment,
+        "cmd_runner",
+        lambda self: "mock cmd runner"
+    )
+    def test_push_cib_not_upgraded_live(self, mock_replace_cib):
+        env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+        env.push_cib(etree.XML('<cib/>'))
+        mock_replace_cib.assert_called_once_with(
+            "mock cmd runner", '<cib/>', False
+        )
+        self.assertEqual([], env.report_processor.report_item_list)
+
+    @mock.patch("pcs.lib.env.replace_cib_configuration_xml")
+    @mock.patch.object(
+        LibraryEnvironment,
+        "cmd_runner",
+        lambda self: "mock cmd runner"
+    )
+    def test_push_cib_upgraded_live(self, mock_replace_cib):
+        env = LibraryEnvironment(self.mock_logger, self.mock_reporter)
+        env._cib_upgraded = True
+        env.push_cib(etree.XML('<cib/>'))
+        mock_replace_cib.assert_called_once_with(
+            "mock cmd runner", '<cib/>', True
+        )
+        assert_report_item_list_equal(
+            env.report_processor.report_item_list,
+            [(
+                severity.INFO,
+                report_codes.CIB_UPGRADE_SUCCESSFUL,
+                {}
+            )]
+        )
+
     @mock.patch("pcs.lib.env.check_corosync_offline_on_nodes")
     @mock.patch("pcs.lib.env.reload_corosync_config")
     @mock.patch("pcs.lib.env.distribute_corosync_conf")
diff --git a/pcs/test/test_lib_pacemaker.py b/pcs/test/test_lib_pacemaker.py
index 85d2034..0edee5c 100644
--- a/pcs/test/test_lib_pacemaker.py
+++ b/pcs/test/test_lib_pacemaker.py
@@ -206,12 +206,28 @@ class ReplaceCibConfigurationTest(LibraryPacemakerTest):
 
         mock_runner.run.assert_called_once_with(
             [
-                self.path("cibadmin"), "--replace", "--scope", "configuration",
-                "--verbose", "--xml-pipe"
+                self.path("cibadmin"), "--replace", "--verbose", "--xml-pipe",
+                "--scope", "configuration"
             ],
             stdin_string=xml
         )
 
+    def test_cib_upgraded(self):
+        xml = "<xml/>"
+        expected_output = "expected output"
+        expected_retval = 0
+        mock_runner = mock.MagicMock(spec_set=CommandRunner)
+        mock_runner.run.return_value = (expected_output, expected_retval)
+
+        lib.replace_cib_configuration(
+            mock_runner, XmlManipulation.from_str(xml).tree, True
+        )
+
+        mock_runner.run.assert_called_once_with(
+            [self.path("cibadmin"), "--replace", "--verbose", "--xml-pipe"],
+            stdin_string=xml
+        )
+
     def test_error(self):
         xml = "<xml/>"
         expected_error = "expected error"
@@ -237,8 +253,8 @@ class ReplaceCibConfigurationTest(LibraryPacemakerTest):
 
         mock_runner.run.assert_called_once_with(
             [
-                self.path("cibadmin"), "--replace", "--scope", "configuration",
-                "--verbose", "--xml-pipe"
+                self.path("cibadmin"), "--replace", "--verbose", "--xml-pipe",
+                "--scope", "configuration"
             ],
             stdin_string=xml
         )
diff --git a/pcs/test/test_resource.py b/pcs/test/test_resource.py
index e8c0813..2fa5088 100644
--- a/pcs/test/test_resource.py
+++ b/pcs/test/test_resource.py
@@ -1541,6 +1541,9 @@ Ordering Constraints:
 Colocation Constraints:
 Ticket Constraints:
 
+Alerts:
+ No alerts defined
+
 Resources Defaults:
  No defaults set
 Operations Defaults:
@@ -1704,6 +1707,9 @@ Ordering Constraints:
 Colocation Constraints:
 Ticket Constraints:
 
+Alerts:
+ No alerts defined
+
 Resources Defaults:
  No defaults set
 Operations Defaults:
diff --git a/pcs/test/test_stonith.py b/pcs/test/test_stonith.py
index 479c8e9..a6ee2f5 100644
--- a/pcs/test/test_stonith.py
+++ b/pcs/test/test_stonith.py
@@ -149,6 +149,9 @@ Ordering Constraints:
 Colocation Constraints:
 Ticket Constraints:
 
+Alerts:
+ No alerts defined
+
 Resources Defaults:
  No defaults set
 Operations Defaults:
diff --git a/pcs/test/tools/color_text_runner.py b/pcs/test/tools/color_text_runner.py
index 305fe32..78a0787 100644
--- a/pcs/test/tools/color_text_runner.py
+++ b/pcs/test/tools/color_text_runner.py
@@ -64,6 +64,16 @@ class ColorTextTestResult(TextTestResult):
             self.stream.write(apply(["lightred", "bold"], 'F'))
             self.stream.flush()
 
+    def addSkip(self, test, reason):
+        super(TextTestResult, self).addSkip(test, reason)
+        if self.showAll:
+            self.stream.writeln(
+                apply(["blue", "bold"], "skipped {0!r}".format(reason))
+            )
+        elif self.dots:
+            self.stream.write(apply(["blue", "bold"], 's'))
+            self.stream.flush()
+
     def getDescription(self, test):
         doc_first_line = test.shortDescription()
         if self.descriptions and doc_first_line:
diff --git a/pcs/usage.py b/pcs/usage.py
index c4c417a..8ae6839 100644
--- a/pcs/usage.py
+++ b/pcs/usage.py
@@ -24,6 +24,7 @@ def full_usage():
     out += strip_extras(status([],False))
     out += strip_extras(config([],False))
     out += strip_extras(pcsd([],False))
+    out += strip_extras(alert([], False))
     print(out.strip())
     print("Examples:\n" + examples.replace(" \ ",""))
 
@@ -115,6 +116,7 @@ def generate_completion_tree_from_usage():
     tree["config"] = generate_tree(config([],False))
     tree["pcsd"] = generate_tree(pcsd([],False))
     tree["node"] = generate_tree(node([], False))
+    tree["alert"] = generate_tree(alert([], False))
     return tree
 
 def generate_tree(usage_txt):
@@ -169,6 +171,7 @@ Commands:
     config      View and manage cluster configuration.
     pcsd        Manage pcs daemon.
     node        Manage cluster nodes.
+    alert       Set pacemaker alerts.
 """
 # Advanced usage to possibly add later
 #  --corosync_conf=<corosync file> Specify alternative corosync.conf file
@@ -1347,9 +1350,49 @@ Commands:
     else:
         return output
 
+
+def alert(args=[], pout=True):
+    output = """
+Usage: pcs alert <command>
+Set pacemaker alerts.
+
+Commands:
+    [config|show]
+        Show all configured alerts.
+
+    create path=<path> [id=<alert-id>] [description=<description>]
+            [options [<option>=<value>]...] [meta [<meta-option>=<value>]...]
+        Create new alert with specified path. Id will be automatically
+        generated if it is not specified.
+
+    update <alert-id> [path=<path>] [description=<description>]
+            [options [<option>=<value>]...] [meta [<meta-option>=<value>]...]
+        Update existing alert with specified id.
+
+    remove <alert-id>
+        Remove alert with specified id.
+
+    recipient add <alert-id> <recipient-value> [description=<description>]
+            [options [<option>=<value>]...] [meta [<meta-option>=<value>]...]
+        Add new recipient to specified alert.
+
+    recipient update <alert-id> <recipient-value> [description=<description>]
+            [options [<option>=<value>]...] [meta [<meta-option>=<value>]...]
+        Update existing recipient identified by alert and it's value.
+
+    recipient remove <alert-id> <recipient-value>
+        Remove specified recipient.
+"""
+    if pout:
+        print(sub_usage(args, output))
+    else:
+        return output
+
+
 def show(main_usage_name, rest_usage_names):
     usage_map = {
         "acl": acl,
+        "alert": alert,
         "cluster": cluster,
         "config": config,
         "constraint": constraint,
diff --git a/pcs/utils.py b/pcs/utils.py
index 11bd4cf..f9cdb1c 100644
--- a/pcs/utils.py
+++ b/pcs/utils.py
@@ -1592,7 +1592,7 @@ def is_etree(var):
     )
 
 # Replace only configuration section of cib with dom passed
-def replace_cib_configuration(dom):
+def replace_cib_configuration(dom, cib_upgraded=False):
     if is_etree(dom):
         #etree returns string in bytes: b'xml'
         #python 3 removed .encode() from byte strings
@@ -1603,7 +1603,12 @@ def replace_cib_configuration(dom):
         new_dom = dom.toxml()
     else:
         new_dom = dom
-    output, retval = run(["cibadmin", "--replace", "-o", "configuration", "-V", "--xml-pipe"],False,new_dom)
+    cmd = ["cibadmin", "--replace", "-V", "--xml-pipe"]
+    if cib_upgraded:
+        print("CIB has been upgraded to the latest schema version.")
+    else:
+        cmd += ["-o", "configuration"]
+    output, retval = run(cmd, False, new_dom)
     if retval != 0:
         err("Unable to update cib\n"+output)
 
-- 
1.8.3.1