cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #00858
[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