← Back to team overview

cloud-init-dev team mailing list archive

[Merge] lp:~ifeoktistov/cloud-init/remotedisk-setup into lp:cloud-init

 

ifeoktistov has proposed merging lp:~ifeoktistov/cloud-init/remotedisk-setup into lp:cloud-init.

Requested reviews:
  cloud init development team (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~ifeoktistov/cloud-init/remotedisk-setup/+merge/294055

Added remotedisk-setup config module which provides a simple and uniform way to handle remote disks such as:
       - iSCSI LUN's:
           - configures Open iSCSI initiator;
           - configures device multipath;
           - enables necessary services;
           - attaches iSCSI LUN;
           - discovers multipath device;
           - creates logical volume;
           - creates filesystem;
           - mounts filesystem;
           - configures /etc/fstab
       - Hypervisor disks (OpenStack Cinder volumes, AWS EBS, etc):
           - creates logical volume;
           - creates filesystem;
           - mounts filesystem;
           - configures /etc/fstab
       - NFS shares:
           - mounts NFS share;
           - configures /etc/fstab

The module was extensively tested on RHEL/CentOS 6.7, RHEL/CentOS 7.2, and Ubuntu 14.04 in OpenStack/KVM and AWS.
iSCSI is tested against NetApp cDOT storage.

-- 
Your team cloud init development team is requested to review the proposed merge of lp:~ifeoktistov/cloud-init/remotedisk-setup into lp:cloud-init.
=== added file 'cloudinit/config/cc_remotedisk_setup.py'
--- cloudinit/config/cc_remotedisk_setup.py	1970-01-01 00:00:00 +0000
+++ cloudinit/config/cc_remotedisk_setup.py	2016-05-06 21:33:46 +0000
@@ -0,0 +1,678 @@
+# vi: ts=4 expandtab
+#
+#    Copyright (C) 2009-2010 Canonical Ltd.
+#    Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
+#
+#    Author: Igor Feoktistov <Igor.Feoktistov@xxxxxxxxxx>
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+import os
+import time
+import shlex
+import fnmatch
+import subprocess
+import re
+from string import whitespace
+
+from cloudinit.settings import PER_INSTANCE
+from cloudinit import type_utils
+from cloudinit import util
+from cloudinit import templater
+
+frequency = PER_INSTANCE
+
+WAIT_4_BLOCKDEV_MAPPING_ITER = 60
+WAIT_4_BLOCKDEV_MAPPING_SLEEP = 5
+WAIT_4_BLOCKDEV_DEVICE_ITER = 12
+WAIT_4_BLOCKDEV_DEVICE_SLEEP = 5
+
+LVM_CMD = util.which("lvm")
+ISCSIADM_CMD = util.which("iscsiadm")
+MULTIPATH_CMD = util.which("multipath")
+SYSTEMCTL_CMD = util.which("systemctl")
+CHKCONFIG_CMD = util.which("chkconfig")
+SERVICE_CMD = util.which("service")
+FSTAB_PATH = "/etc/fstab"
+ISCSI_INITIATOR_PATH = "/etc/iscsi/initiatorname.iscsi"
+
+
+def handle(_name, cfg, cloud, log, _args):
+    if "remotedisk_setup" not in cfg:
+        log.debug("Skipping module named %s, no configuration found" % _name)
+        return
+    remotedisk_setup = cfg.get("remotedisk_setup")
+    log.debug("setting up remote disk: %s", str(remotedisk_setup))
+    for definition in remotedisk_setup:
+        try:
+            device = definition.get("device")
+            if device:
+                if device.startswith("iscsi"):
+                    handle_iscsi(cfg, cloud, log, definition)
+                elif device.startswith("nfs"):
+                    handle_nfs(cfg, cloud, log, definition)
+                elif device.startswith("ebs"):
+                    handle_ebs(cfg, cloud, log, definition)
+                elif device.startswith("ephemeral"):
+                    handle_ebs(cfg, cloud, log, definition)
+                else:
+                    if "fs_type" in definition:
+                        fs_type = definition.get("fs_type")
+                        if fs_type == "nfs":
+                            handle_nfs(cfg, cloud, log, definition)
+                        else:
+                            handle_ebs(cfg, cloud, log, definition)
+                    else:
+                        util.logexc(log, "Expexted \"fs_type\" parameter")
+            else:
+                util.logexc(log, "Expexted \"device\" parameter")
+        except Exception as e:
+            util.logexc(log, "Failed during remote disk operation\n"
+                             "Exception: %s" % e)
+
+
+def handle_iscsi(cfg, cloud, log, definition):
+    # Handle iSCSI LUN
+    device = definition.get("device")
+    try:
+        (iscsi_host,
+         iscsi_proto,
+         iscsi_port,
+         iscsi_lun,
+         iscsi_target) = device.split(":", 5)[1:]
+    except Exception as e:
+        util.logexc(log,
+                    "handle_iscsi: "
+                    "expected \"device\" attribute in the format: "
+                    "\"iscsi:<iSCSI host>:<protocol>:<port>:<LUN>:"
+                    "<iSCSI target name>\": %s" % e)
+        return
+    (hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)
+    if "initiator_name" in definition:
+        initiator_name = definition.get("initiator_name")
+    else:
+        initiator_name = "iqn.2005-02.com.open-iscsi:%s" % hostname
+    util.write_file(ISCSI_INITIATOR_PATH, "InitiatorName=%s" % initiator_name)
+    multipath_tmpl_fn = cloud.get_template_filename("multipath.conf")
+    if not multipath_tmpl_fn:
+        util.logexc(log, "handle_iscsi: template multipath.conf not found")
+        return
+    templater.render_to_file(multipath_tmpl_fn, "/etc/multipath.conf", {})
+    if cloud.distro.osfamily == "redhat":
+        iscsi_services = ["iscsi", "iscsid"]
+        multipath_services = ["multipathd"]
+    elif cloud.distro.osfamily == 'debian':
+        iscsi_services = ["open-iscsi"]
+        multipath_services = ["multipath-tools"]
+    else:
+        util.logexc(log,
+                    "handle_iscsi: "
+                    "unsupported osfamily \"%s\"" % cloud.distro.osfamily)
+        return
+    for service in iscsi_services:
+        _service_wrapper(cloud, log, service, "enable")
+        _service_wrapper(cloud, log, service, "restart")
+    for service in multipath_services:
+        _service_wrapper(cloud, log, service, "enable")
+        _service_wrapper(cloud, log, service, "restart")
+    blockdev = _iscsi_lun_discover(log,
+                                   iscsi_host,
+                                   iscsi_port,
+                                   iscsi_lun,
+                                   iscsi_target)
+    if blockdev:
+        lvm_group = definition.get("lvm_group")
+        lvm_volume = definition.get("lvm_volume")
+        fs_type = definition.get("fs_type")
+        fs_opts = definition.get("fs_opts")
+        mount_point = definition.get("mount_point")
+        mount_opts = definition.get("mount_opts")
+        if not mount_opts:
+            mount_opts = 'defaults,_netdev'
+        else:
+            if mount_opts.find("_netdev") == -1:
+                mount_opts = "%s,_netdev" % (mount_opts)
+        fs_freq = definition.get("fs_freq")
+        if not fs_freq:
+            fs_freq = "1"
+        fs_passno = definition.get("fs_passno")
+        if not fs_passno:
+            fs_passno = "2"
+        if lvm_group and lvm_volume:
+            for vg_name in _list_vg_names():
+                if vg_name == lvm_group:
+                    util.logexc(log,
+                                "handle_iscsi: "
+                                "logical volume group '%s' exists already"
+                                % lvm_group)
+                    return
+            for lv_name in _list_lv_names():
+                if lv_name == lvm_volume:
+                    util.logexc(log,
+                                "handle_iscsi: "
+                                "logical volume '%s' exists already"
+                                % lvm_volume)
+                    return
+            blockdev = _create_lv(log, blockdev, lvm_group, lvm_volume)
+        if blockdev:
+            if mount_point and fs_type:
+                _create_fs(log, blockdev, fs_type, fs_opts)
+                _add_fstab_entry(log,
+                                 blockdev,
+                                 mount_point,
+                                 fs_type,
+                                 mount_opts,
+                                 fs_freq,
+                                 fs_passno)
+                _mount_fs(log, mount_point)
+            else:
+                util.logexc(log,
+                            "handle_iscsi: "
+                            "expexted \"mount_point\" "
+                            "and \"fs_type\" parameters")
+
+
+def handle_nfs(cfg, cloud, log, definition):
+    # Handle NFS share mounts
+    device = definition.get("device")
+    if device.startswith("nfs"):
+        (proto, share_path) = device.split(":", 1)
+    else:
+        share_path = device
+    fs_type = definition.get("fs_type")
+    mount_point = definition.get("mount_point")
+    mount_opts = definition.get("mount_opts")
+    if not mount_opts:
+        mount_opts = "defaults"
+    fs_freq = definition.get("fs_freq")
+    if not fs_freq:
+        fs_freq = "0"
+    fs_passno = definition.get("fs_passno")
+    if not fs_passno:
+        fs_passno = "0"
+    if mount_point and fs_type:
+        _add_fstab_entry(log,
+                         share_path,
+                         mount_point,
+                         fs_type,
+                         mount_opts,
+                         fs_freq,
+                         fs_passno)
+        _mount_fs(log, mount_point)
+    else:
+        util.logexc(log,
+                    "handle_nfs: "
+                    "expexted \"mount_point\" and \"fs_type\" parameters")
+
+
+def handle_ebs(cfg, cloud, log, definition):
+    # Handle block device either explicitly provided via device path or
+    # via device name mapping (Amazon/OpenStack)
+    device = definition.get("device")
+    blockdev = _cloud_device_2_os_device(cloud, log, device)
+    if blockdev:
+        lvm_group = definition.get("lvm_group")
+        lvm_volume = definition.get("lvm_volume")
+        fs_type = definition.get("fs_type")
+        fs_opts = definition.get("fs_opts")
+        mount_point = definition.get("mount_point")
+        mount_opts = definition.get("mount_opts")
+        if not mount_opts:
+            mount_opts = "defaults"
+        fs_freq = definition.get("fs_freq")
+        if not fs_freq:
+            fs_freq = "1"
+        fs_passno = definition.get("fs_passno")
+        if not fs_passno:
+            fs_passno = "2"
+        if lvm_group and lvm_volume:
+            for vg_name in _list_vg_names():
+                if vg_name == lvm_group:
+                    util.logexc(log,
+                                "handle_ebs: "
+                                "logical volume group '%s' exists already"
+                                % lvm_group)
+                    return
+            for lv_name in _list_lv_names():
+                if lv_name == lvm_volume:
+                    util.logexc(log,
+                                "handle_ebs: "
+                                "logical volume '%s' exists already"
+                                % lvm_volume)
+                    return
+            blockdev = _create_lv(log, blockdev, lvm_group, lvm_volume)
+        if blockdev:
+            if mount_point and fs_type:
+                _create_fs(log, blockdev, fs_type, fs_opts)
+                _add_fstab_entry(log,
+                                 blockdev,
+                                 mount_point,
+                                 fs_type,
+                                 mount_opts,
+                                 fs_freq,
+                                 fs_passno)
+                _mount_fs(log, mount_point)
+            else:
+                util.logexc(log,
+                            "handle_ebs: "
+                            "expexted \"mount_point\" and "
+                            "\"fs_type\" parameters")
+
+
+def _cloud_device_2_os_device(cloud, log, name):
+    # Translate cloud device (ebs# and ephemaral#) to OS block device path
+    blockdev = None
+    for i in range(WAIT_4_BLOCKDEV_MAPPING_ITER):
+        if (cloud.datasource.metadata and
+                "block-device-mapping" in cloud.datasource.metadata):
+            metadata = cloud.datasource.metadata
+        else:
+            if (cloud.datasource.ec2_metadata and
+                    "block-device-mapping" in cloud.datasource.ec2_metadata):
+                metadata = cloud.datasource.ec2_metadata
+            else:
+                util.logexc(log,
+                            "_cloud_device_2_os_device: "
+                            "metadata item block-device-mapping not found")
+                return None
+        blockdev_items = metadata["block-device-mapping"].iteritems()
+        for (map_name, device) in blockdev_items:
+            if map_name == name:
+                blockdev = device
+                break
+        if blockdev is None:
+            cloud.datasource.get_data()
+            time.sleep(WAIT_4_BLOCKDEV_MAPPING_SLEEP)
+            continue
+    if blockdev is None:
+        util.logexc(log,
+                    "_cloud_device_2_os_device: "
+                    "unable to convert %s to a device" % name)
+        return None
+    if not blockdev.startswith("/"):
+        blockdev_path = "/dev/%s" % blockdev
+    else:
+        blockdev_path = blockdev
+    for i in range(WAIT_4_BLOCKDEV_DEVICE_ITER):
+        if os.path.exists(blockdev_path):
+            return blockdev_path
+        time.sleep(WAIT_4_BLOCKDEV_DEVICE_SLEEP)
+    util.logexc(log,
+                "_cloud_device_2_os_device: "
+                "device %s does not exist" % blockdev_path)
+    return None
+
+
+def _list_vg_names():
+    # List all LVM volume groups
+    p = subprocess.Popen([LVM_CMD, "vgs", "-o", "vg_name"],
+                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    err = p.wait()
+    if err:
+        return []
+    output = p.communicate()[0]
+    output = output.split("\n")
+    if not output:
+        return []
+    header = output[0].strip()
+    if header != "VG":
+        return []
+    names = []
+    for name in output[1:]:
+        if not name:
+            break
+        names.append(name.strip())
+    return names
+
+
+def _list_lv_names():
+    # List all LVM logical volumes
+    p = subprocess.Popen([LVM_CMD, "lvs", "-o", "lv_name"],
+                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    err = p.wait()
+    if err:
+        return []
+    output = p.communicate()[0]
+    output = output.split("\n")
+    if not output:
+        return []
+    header = output[0].strip()
+    if header != "LV":
+        return []
+    names = []
+    for name in output[1:]:
+        if not name:
+            break
+        names.append(name.strip())
+    return names
+
+
+def _create_lv(log, device, vg_name, lv_name):
+    # Create volume group
+    pvcreate_cmd = [LVM_CMD, "pvcreate", device]
+    vgcreate_cmd = [LVM_CMD, "vgcreate", vg_name, device]
+    lvcreate_cmd = [LVM_CMD,
+                    "lvcreate", "-l", "100%FREE", "--name", lv_name, vg_name]
+    try:
+        util.subp(pvcreate_cmd)
+        util.subp(vgcreate_cmd)
+        util.subp(lvcreate_cmd)
+        return "/dev/mapper/%s-%s" % (vg_name, lv_name)
+    except Exception as e:
+        util.logexc(log,
+                    "_create_lv: "
+                    "failed to create LVM volume '%s' for device '%s': %s"
+                    % (lv_name, device, e))
+        return None
+
+
+def _create_fs(log, device, fs_type, fs_opts=None):
+    # Create filesystem
+    mkfs_cmd = util.which("mkfs.%s" % fs_type)
+    if not mkfs_cmd:
+        mkfs_cmd = util.which("mk%s" % fs_type)
+    if not mkfs_cmd:
+        util.logexc(log,
+                    "_create_fs: "
+                    "cannot create filesystem type '%s': "
+                    "failed to find mkfs.%s command" % (fs_type, fs_type))
+        return
+    try:
+        if fs_opts:
+            util.subp([mkfs_cmd, fs_opts, device])
+        else:
+            util.subp([mkfs_cmd, device])
+    except Exception as e:
+        util.logexc(log,
+                    "_create_fs: "
+                    "failed to create filesystem type '%s': %s" % (fs_type, e))
+
+
+def _add_fstab_entry(log,
+                     device,
+                     mount_point,
+                     fs_type,
+                     mount_opts,
+                     fs_freq,
+                     fs_passno):
+    # Create fstab entry
+    fstab_lines = []
+    for line in util.load_file(FSTAB_PATH).splitlines():
+        try:
+            toks = re.compile("[%s]+" % (whitespace)).split(line)
+        except:
+            pass
+        if len(toks) > 0 and toks[0] == device:
+            util.logexc(log,
+                        "_add_fstab_entry: "
+                        "file %s has device %s already" % (FSTAB_PATH, device))
+            return
+        if len(toks) > 1 and toks[1] == mount_point:
+            util.logexc(log,
+                        "_add_fstab_entry: "
+                        "file %s has mount point %s already"
+                        % (FSTAB_PATH, mount_point))
+            return
+        fstab_lines.append(line)
+    fstab_lines.extend(["%s\t%s\t%s\t%s\t%s\t%s" %
+                       (device,
+                        mount_point,
+                        fs_type,
+                        mount_opts,
+                        fs_freq,
+                        fs_passno)])
+    contents = "%s\n" % ('\n'.join(fstab_lines))
+    util.write_file(FSTAB_PATH, contents)
+
+
+def _mount_fs(log, mount_point):
+    # Mount filesystem according to fstab entry
+    try:
+        util.ensure_dir(mount_point)
+    except Exception as e:
+        util.logexc(log,
+                    "_mount_fs: "
+                    "failed to make '%s' mount point directory: %s"
+                    % (mount_point, e))
+        return
+    try:
+        util.subp(["mount", mount_point])
+    except Exception as e:
+        util.logexc(log,
+                    "_mount_fs: "
+                    "activating mounts via 'mount %s' failed: %s"
+                    % (mount_point, e))
+
+
+def _service_wrapper(cloud, log, service, command):
+    # Wrapper for service related commands
+    if cloud.distro.osfamily == "redhat":
+        if SYSTEMCTL_CMD:
+            svc_cmd = [SYSTEMCTL_CMD, command, service]
+        else:
+            if command == "enable" or command == "disable":
+                if CHKCONFIG_CMD:
+                    if command == "enable":
+                        svc_cmd = [CHKCONFIG_CMD, service, "on"]
+                    else:
+                        svc_cmd = [CHKCONFIG_CMD, service, "off"]
+                else:
+                    util.logexc(log,
+                                "_handle_service: "
+                                "service config command \"chkconfig\" "
+                                "not found")
+                    return
+            else:
+                svc_cmd = [SERVICE_CMD, service, command]
+    elif cloud.distro.osfamily == "debian":
+        if SYSTEMCTL_CMD:
+            svc_cmd = [SYSTEMCTL_CMD, command, service]
+        else:
+            if command == 'enable' or command == "disable":
+                if os.path.exists('/usr/sbin/update-rc.d'):
+                    svc_cmd = ['/usr/sbin/update-rc.d', service, "defaults"]
+                else:
+                    util.logexc(log,
+                                "_handle_service: "
+                                "command \"/usr/sbin/update-rc.d\" not found")
+                    return
+            else:
+                svc_cmd = [SERVICE_CMD, service, command]
+    else:
+        util.logexc(log,
+                    "_handle_service: "
+                    "unsupported osfamily \"%s\"" % cloud.distro.osfamily)
+        return
+    try:
+        util.subp(svc_cmd, capture=False)
+    except Exception as e:
+        util.logexc(log,
+                    "_handle_service: "
+                    "failure to \"%s\" \"%s\": %s" % (command, service, e))
+
+
+def _iscsi_lun_discover(log, iscsi_host, iscsi_port, iscsi_lun, iscsi_target):
+    # Discover iSCSI target and map LUN ID to multipath device path
+    blockdev = None
+    for i in range(WAIT_4_BLOCKDEV_MAPPING_ITER):
+        try:
+            util.subp([ISCSIADM_CMD,
+                       "--mode",
+                       "discoverydb",
+                       "--type",
+                       "sendtargets",
+                       "--portal",
+                       "%s:%s" % (iscsi_host, iscsi_port),
+                       "--discover",
+                       "--login",
+                       "all"],
+                      capture=False)
+        except Exception as e:
+            util.logexc(log,
+                        "_iscsi_lun_discover: "
+                        "failure in attempt to discover iSCSI LUN for target "
+                        "\"%s\": %s" % (iscsi_target, e))
+            return None
+        p = subprocess.Popen([ISCSIADM_CMD, "-m", "node"],
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.PIPE)
+        err = p.wait()
+        if err:
+            util.logexc(log,
+                        "_iscsi_lun_discover: "
+                        "failure from \"%s -m node\" command" % ISCSIADM_CMD)
+            return None
+        output = p.communicate()[0]
+        output = output.split("\n")
+        if not output:
+            util.logexc(log,
+                        "_iscsi_lun_discover: "
+                        "no iSCSI nodes discovered for target \"%s\""
+                        % iscsi_target)
+            time.sleep(WAIT_4_BLOCKDEV_MAPPING_SLEEP)
+            continue
+        for node in output:
+            iscsi_portal = node.split(",", 1)[0]
+            if iscsi_portal:
+                try:
+                    util.subp([ISCSIADM_CMD,
+                               "-m",
+                               "node",
+                               "-T",
+                               iscsi_target,
+                               "-p",
+                               iscsi_portal,
+                               "--op",
+                               "update",
+                               "-n",
+                               "node.startup",
+                               "-v",
+                               "automatic"],
+                              capture=False)
+                except Exception as e:
+                    util.logexc(log,
+                                "_iscsi_lun_discover: "
+                                "failure in attempt to set automatic binding "
+                                "for target portal \"%s\": %s"
+                                % (iscsi_portal, e))
+                    return None
+        p = subprocess.Popen([ISCSIADM_CMD, "-m", "session", "-P3"],
+                             stdout=subprocess.PIPE,
+                             stderr=subprocess.PIPE)
+        err = p.wait()
+        if err:
+            util.logexc(log,
+                        "_iscsi_lun_discover: "
+                        "failure from \"%s -m session -P3\" command"
+                        % ISCSIADM_CMD)
+            return None
+        output = p.communicate()[0]
+        output = output.split("\n")
+        if not output:
+            util.logexc(log,
+                        "_iscsi_lun_discover: "
+                        "no iSCSI sessions discovered for target \"%s\""
+                        % iscsi_target)
+        else:
+            current_iscsi_target = None
+            current_iscsi_sid = None
+            current_iscsi_lun = None
+            for line in output:
+                m = re.search("^Target: ([a-z0-9\.:-]*)", line)
+                if m:
+                    current_iscsi_target = m.group(1)
+                    continue
+                else:
+                    if (current_iscsi_target and
+                            current_iscsi_target == iscsi_target):
+                        m = re.search("SID: ([0-9]*)", line)
+                        if m:
+                            if current_iscsi_sid and not current_iscsi_lun:
+                                try:
+                                    util.subp([ISCSIADM_CMD,
+                                               "-m",
+                                               "session",
+                                               "-r",
+                                               current_iscsi_sid,
+                                               "-u"],
+                                              capture=False)
+                                except:
+                                    pass
+                            current_iscsi_sid = m.group(1)
+                            current_iscsi_lun = None
+                            continue
+                        m = re.search("scsi[0-9]* Channel [0-9]* "
+                                      "Id [0-9]* Lun: ([0-9]*)", line)
+                        if m:
+                            current_iscsi_lun = m.group(1)
+                            continue
+                        if (current_iscsi_lun and
+                                current_iscsi_lun == iscsi_lun):
+                            m = re.search("Attached scsi disk (sd[a-z]*)",
+                                          line)
+                            if m:
+                                attached_scsi_disk = m.group(1)
+                                p = subprocess.Popen(["/lib/udev/scsi_id",
+                                                      "-g", "-u", "-d",
+                                                      "/dev/%s"
+                                                      % attached_scsi_disk],
+                                                     stdout=subprocess.PIPE,
+                                                     stderr=subprocess.PIPE)
+                                err = p.wait()
+                                if err:
+                                    util.logexc(log,
+                                                "_iscsi_lun_discover: "
+                                                "failure from "
+                                                "\"/lib/udev/scsi_id\" "
+                                                "command")
+                                    return None
+                                output2 = p.communicate()[0]
+                                output2 = output2.split('\n')
+                                if not output2:
+                                    util.logexc(log,
+                                                "_iscsi_lun_discover: "
+                                                "no wwid returned for device "
+                                                "\"/dev/%s\""
+                                                % attached_scsi_disk)
+                                else:
+                                    blockdev = "/dev/mapper/%s" % output2[0]
+            if current_iscsi_sid and not current_iscsi_lun:
+                try:
+                    util.subp([ISCSIADM_CMD,
+                               "-m",
+                               "session",
+                               "-r",
+                               current_iscsi_sid,
+                               "-u"],
+                              capture=False)
+                except:
+                    pass
+        if blockdev:
+            break
+        else:
+            time.sleep(WAIT_4_BLOCKDEV_MAPPING_SLEEP)
+    if blockdev:
+        for i in range(WAIT_4_BLOCKDEV_DEVICE_ITER):
+            if os.path.exists(blockdev):
+                return blockdev
+            try:
+                util.subp([MULTIPATH_CMD], capture=False)
+            except Exception as e:
+                util.logexc(log,
+                            "_iscsi_lun_discover: "
+                            "failure to run \"%s\": %s" % (MULTIPATH_CMD, e))
+                return None
+            time.sleep(WAIT_4_BLOCKDEV_DEVICE_SLEEP)
+    else:
+        return None

=== modified file 'config/cloud.cfg'
--- config/cloud.cfg	2016-03-09 22:34:11 +0000
+++ config/cloud.cfg	2016-05-06 21:33:46 +0000
@@ -44,6 +44,7 @@
 # this can be used by upstart jobs for 'start on cloud-config'.
  - emit_upstart
  - disk_setup
+ - remotedisk_setup
  - mounts
  - ssh-import-id
  - locale

=== added file 'doc/examples/cloud-config-remotedisk-setup.txt'
--- doc/examples/cloud-config-remotedisk-setup.txt	1970-01-01 00:00:00 +0000
+++ doc/examples/cloud-config-remotedisk-setup.txt	2016-05-06 21:33:46 +0000
@@ -0,0 +1,95 @@
+#cloud-config
+#
+# The module remotedisk_setup provides a simple and uniform way
+# to handle remote disks such as:
+#	- iSCSI LUN's:
+#	    - configures Open iSCSI initiator;
+#	    - configures device multipath;
+#	    - enables necessary services;
+#	    - attaches iSCSI LUN;
+#	    - discovers multipath device;
+#	    - creates logical volume;
+#	    - creates filesystem;
+#	    - mounts filesystem;
+#	    - configures /etc/fstab
+#	- Hypervisor disks (OpenStack Cinder volumes, AWS EBS, etc):
+#	    - creates logical volume;
+#	    - creates filesystem;
+#	    - mounts filesystem;
+#	    - configures /etc/fstab
+#	- NFS shares:
+#	    - mounts NFS share;
+#	    - configures /etc/fstab
+#
+remotedisk_setup:
+
+############################################	    
+# Example configuration to handle iSCSI LUN:
+############################################
+   - device: 'iscsi:192.168.1.1:6:3260:1:iqn.1992-08.com.netapp:sn.62546b567fbf11e4811590e2ba6cc3b4:vs.10'
+     lvm_group: 'vg_data1'
+     lvm_volume: 'lv_data1'
+     fs_type: 'ext4'
+     mount_point: '/apps/data1'
+
+############################################
+# Parameters:
+#    mandatory:
+#       device: 'iscsi:<iSCSI target host/LIF>:<transport protocol>:<port>:<LUN ID>:<iSCSI target name>'
+#       fs_type: '<filesystem type>'
+#       mount_point: '<mount point dir path>'
+#    optional:
+#       initiator_name: '<iSCSI initiator name, default is iqn.2005-02.com.open-iscsi:<hostname>>'
+#       mount_opts: '<filesystem mount options, default is "defaults,_netdev">'
+#       lvm_group: '<LVM logical group name>'
+#       lvm_volume: '<LVM logical volume name>'
+#       fs_opts: '<filesystem create options specific to mkfs.fs_type>'
+#       fs_freq: '<fstab fs freq, default is "1">'
+#       fs_passno: '<fstab fs passno, default is "2">'
+#    notes:
+#       missing lvm_group and lvm_volume will cause filesystem creation on top of multipath device
+#
+
+##########################################################
+# Example configuration to handle OpenStack Cinder volume:
+##########################################################
+   - device: 'ebs0'
+     lvm_group: 'vg_data1'
+     lvm_volume: 'lv_data1'
+     fs_type: 'ext4'
+     mount_point: '/apps/data1'
+
+##########################################################
+# Parameters:
+#    mandatory:
+#       device: 'ebs<0-9> or block device path /dev/vd<b-z>'
+#       fs_type: '<filesystem type>'
+#       mount_point: '<mount point dir path>'
+#    optional:
+#       mount_opts: '<filesystem mount options, default is "defaults">'
+#       lvm_group: '<LVM logical group name>'
+#       lvm_volume: '<LVM logical volume name>'
+#       fs_opts: '<filesystem create options specific to mkfs.fs_type>'
+#       fs_freq: '<fstab fs freq, default is "1">'
+#       fs_passno: '<fstab fs passno, default is "2">'
+#    notes:
+#       missing lvm_group and lvm_volume will cause filesystem creation on top of block device
+#       
+
+#############################################
+# Example configuration to handle NFS shares:
+#############################################
+   - device: 'nfs:192.168.1.1:/myshare'
+     mount_point: '/apps/data1'
+     mount_opts: 'tcp,rw,rsize=65536,wsize=65536'
+
+#############################################
+# Parameters:
+#    mandatory:
+#       device: 'nfs:<NFS host>:<NFS share path>'
+#       mount_point: '<mount point dir path>'
+#    optional:
+#       mount_opts: '<NFS share mount options, default is "defaults">'
+#       fs_type: 'nfs'
+#       fs_freq: '<fstab fs freq, default is "0">'
+#       fs_passno: '<fstab fs passno, default is "0">'

=== added file 'templates/multipath.conf.tmpl'
--- templates/multipath.conf.tmpl	1970-01-01 00:00:00 +0000
+++ templates/multipath.conf.tmpl	2016-05-06 21:33:46 +0000
@@ -0,0 +1,29 @@
+defaults {
+    find_multipaths		yes
+    user_friendly_names		no
+    no_path_retry		queue
+    queue_without_daemon	no
+    flush_on_last_del		yes
+    max_fds			max
+}
+blacklist {
+    devnode	"^hd[a-z]"
+    devnode	"^vd[a-z]"
+    devnode	"^(ram|raw|loop|fd|md|dm-|sr|scd|st)[0-9]*"
+    devnode	"^cciss.*"
+}
+devices {
+    device {
+	vendor			"NETAPP"
+	product			"LUN"
+	path_grouping_policy	group_by_prio
+	features		"3 queue_if_no_path pg_init_retries 50"
+	prio			"alua"
+	path_checker		tur
+	failback		immediate
+	path_selector		"round-robin 0"
+	hardware_handler	"1 alua"
+	rr_weight		uniform
+	rr_min_io		128
+    }
+}


Follow ups