Seed code for ipa-deployer
[ta/ipa-deployer.git] / work / dib-ipa-element / virtmedia-netconf / ironic-virtmedia-netconfig / src / virtmedia_netconfig / main.py
diff --git a/work/dib-ipa-element/virtmedia-netconf/ironic-virtmedia-netconfig/src/virtmedia_netconfig/main.py b/work/dib-ipa-element/virtmedia-netconf/ironic-virtmedia-netconfig/src/virtmedia_netconfig/main.py
new file mode 100644 (file)
index 0000000..06cfb04
--- /dev/null
@@ -0,0 +1,361 @@
+# Copyright 2019 Nokia
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+import tarfile
+import sys
+import os
+import errno
+import stat
+import time
+import tempfile
+import shutil
+
+import json
+import logging
+import subprocess
+
+from oslo_config import cfg
+from oslo_log import log
+from ironic_python_agent import utils
+from ironic_lib import utils as ironic_utils
+from ironic_python_agent import errors
+from oslo_concurrency import processutils
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+dhclient_physIfaces = []
+
+def dhclient_path():
+    if os.path.exists("/usr/sbin/dhclient"):
+        return "/usr/sbin/dhclient"
+    elif os.path.exists("/sbin/dhclient"):
+        return "/sbin/dhclient"
+    else:
+        raise RuntimeError("Could not find dhclient")
+
+def stop_dhclient_process(interface):
+    """Stop a DHCP process before running os-net-config.
+
+    :param interface: The interface on which to stop dhclient.
+    """
+    pid_file = '/var/run/dhclient-%s.pid' % (interface)
+    try:
+        dhclient = dhclient_path()
+    except RuntimeError as err:
+        LOG.info('Exception when stopping dhclient: %s' % err)
+        return
+
+    if os.path.exists(pid_file):
+        LOG.info('Stopping %s on interface %s' % (dhclient, interface))
+        utils.execute(dhclient, '-r', '-pf', pid_file, interface)
+        try:
+            os.unlink(pid_file)
+        except OSError as err:
+            LOG.error('Could not remove dhclient pid file \'%s\': %s' %
+                         (pid_file, err))
+
+def _poll_interface(_ifacedata):
+    ifacedata = json.loads(_ifacedata)
+    global dhclient_physIfaces
+    
+    physIfaces = []
+    if "network_config" in ifacedata:
+        for netconfdata in ifacedata["network_config"]:
+            if "device" in netconfdata:
+                if "bond" not in netconfdata["device"]:
+                    # Is (physical) interface
+                    LOG.debug('Physical device %s' % netconfdata["device"])
+                    physIfaces.append(netconfdata["device"])
+
+            elif "members" in netconfdata:
+                # logical interface with member (f.ex bond)
+                for _member in netconfdata["members"]:
+                    if "type" in _member:
+                        if _member["type"] == 'interface':
+                            if "name" in _member:
+                                LOG.debug('Physical device %s' % _member["name"])
+                                physIfaces.append(_member["name"])
+            elif "name" in netconfdata:
+                if "type" in netconfdata and netconfdata["type"] == 'interface':
+                    LOG.debug('Physical device %s' % netconfdata["name"])
+                    physIfaces.append(netconfdata["name"])
+
+    LOG.info('Checking for physical device(s) "%s"' % ', '.join(physIfaces))
+    dhclient_physIfaces = list(physIfaces)
+    wait_secs = 5
+    max_wait_secs = 60
+
+    while len(physIfaces) > 0 and max_wait_secs >= 0:
+        missing_devices = []
+        max_wait_secs = max_wait_secs - wait_secs
+
+        for _device in physIfaces:
+            devicepath = "/sys/class/net/%s/device" % _device
+            LOG.debug('Check path "%s"' % devicepath )
+            if os.path.exists(devicepath):
+                LOG.debug('Device "%s" in known by kernel' % _device)
+                physIfaces.remove(_device)
+            else:
+                LOG.debug('Device "%s" in not (yet) known by kernel' % _device)
+               missing_devices.append(_device)
+
+        if len(physIfaces) > 0:
+            LOG.info('Device(s) not (yet?) known by kernel: "%s"' % ', '.join(missing_devices))
+            time.sleep(wait_secs)
+
+
+    if len(physIfaces) > 0:
+        msg = 'Timeout, Device(s) missing: "%s"' % ', '.join(physIfaces)
+        LOG.error(msg)
+        raise errors.VirtualMediaBootError(msg)
+    else:
+        LOG.info('All physical devices found.')
+
+    for _device in dhclient_physIfaces:
+        stop_dhclient_process(_device)
+
+def _configure_static_net(os_net_config):
+    """Configures network using os-net-config utility"""
+    global dhclient_physIfaces
+    LOG.debug("Configuring static network with os-net-config: %s", os_net_config)
+    try:
+        os.makedirs('/etc/os-net-config/')
+    except OSError as exc:
+        if exc.errno != errno.EEXIST:
+            raise
+        pass
+    with open('/etc/os-net-config/config.yaml', 'w') as fp:
+        fp.write(os_net_config)
+
+    try:
+        _poll_interface(os_net_config)
+    except Exception as e:
+        LOG.info('Exception while checking for physical interfaces: %s' % str(e) )
+
+    try:
+        os.system('/usr/sbin/ip a > /tmp/ifaces_before_initial_netconfig')
+    except Exception as e:
+        LOG.info('Exception while logging runtime ifaces to /tmp/ifaces_before_initial_netconfig: %s' % str(e) )
+
+    LOG.info('Running os-net-config..')
+    
+    cmd = [ '/usr/bin/os-net-config', '--detailed-exit-codes', '-v', '-c', '/etc/os-net-config/config.yaml']
+    wait_secs = 5
+    retries = 3
+    while retries > 0:
+        retries = retries - 1
+        netconf_process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+        while True:
+            output = netconf_process.stdout.readline()
+            if output == '' and netconf_process.poll() is not None:
+                break
+            if output:
+                LOG.info(output.strip())
+
+        rc = netconf_process.poll()
+        LOG.info('os-net-config exit with status %d' % rc)
+
+        # os-net-config returns:
+        # 0 when nothing changed,
+        # 1 on error,
+        # 2 when config was modified (assuming option "--detailed-exit-codes")
+        
+        if rc == 0 or rc == 1:
+            LOG.info('os-net-config modified nothing or execution error. Not what we want..')
+            LOG.info('Attempt removing physical interface ifcfg-files to force os-net-config to reconfigure')
+            
+            for iface in dhclient_physIfaces:
+                ifcfg_file = '/etc/sysconfig/network-scripts/ifcfg-' + str(iface)
+                try:
+                    LOG.info('Removing "%s"' % ifcfg_file)
+                    os.system('/usr/bin/rm -f ' + ifcfg_file)
+                except Exception as e:
+                    LOG.info('Ignoring exception when removing "%s": %s' % (ifcfg_file, str(e)))
+                    pass
+            time.sleep(wait_secs)
+
+        elif rc == 2:
+            LOG.info('os-net-config done.')
+            break
+        else:
+            LOG.info('os-net-config unknown exit code??')
+            time.sleep(wait_secs)
+    
+    # Config should be in place assuming os-net-config above was successfull
+    # As additional step restart network.service
+    LOG.info('Restarting network.service')
+    try:
+        cmd = ['/usr/bin/systemctl', 'restart', 'network']
+        subprocess.check_call(cmd)
+    except Exception as e:
+        LOG.info('Igoring exception when restarting network service: %s' % str(e))
+        pass
+
+
+def get_file_size(filename):
+    "Get the file size by seeking at end"
+    fd= os.open(filename, os.O_RDONLY)
+    try:
+        return os.lseek(fd, 0, os.SEEK_END)
+    finally:
+        os.close(fd)
+
+def wait_for_cd_device():
+    """ This function waits for /dev/sr0 device to appear """
+    inputiso = '/dev/sr0'
+    wait_count = 30
+    while not os.path.exists(inputiso) and wait_count:
+        LOG.debug('Waiting for %s to appear. Time left = %d secs' %(inputiso,wait_count))
+        time.sleep(1)
+        wait_count -= 1
+
+    if not wait_count:
+        msg = "Unable to find device %s" %(inputiso)
+        raise errors.VirtualMediaBootError(msg)
+
+def check_cd_config():
+    """ This function checks for any extended 64K block in CD.
+    If it is available it will extract the contents for the block.
+    Loop mount the image for reading configuration parameters.
+    """
+    inputiso = '/dev/sr0'
+    outputtgz = '/tmp/cdconf.tgz'
+    mode = os.stat(inputiso).st_mode
+    if stat.S_ISBLK(mode):
+        filesize = get_file_size(inputiso)
+        skip = filesize / 2048-32
+        ironic_utils.dd(inputiso, outputtgz, 'bs=2k', 'skip=%d'%skip)
+
+        # Check if tgz file is valid.
+        try:
+            utils.execute("/usr/bin/gzip", '-t', outputtgz)
+        except processutils.ProcessExecutionError as err:
+            if 'not in gzip format' in err.stderr:
+                LOG.info('File is not gzip format skipping!!')
+                sys.exit()
+
+        LOG.info('Configuration file in gzip format proceeding for extraction')
+        tar = tarfile.open(outputtgz)
+        tar.extractall('/tmp/floppy')
+        tar.close()
+
+        dir_list = os.listdir('/tmp/floppy')
+        for item in dir_list:
+            if item.find('.img') != -1:
+                os.mkdir('/tmp/floppy/mnt')
+                utils.execute("mount", '-o', 'loop', '/tmp/floppy/%s' %item, '/tmp/floppy/mnt')
+                time.sleep(1)
+
+def _get_vmedia_params():
+    """This method returns the parameters passed through virtual media floppy.
+
+    :returns: a partial dict of potential agent configuration parameters
+    :raises: VirtualMediaBootError when it cannot find the virtual media device
+    """
+    parameters_file = "parameters.txt"
+
+    vmedia_device_file_lower_case = "/dev/disk/by-label/ir-vfd-dev"
+    vmedia_device_file_upper_case = "/dev/disk/by-label/IR-VFD-DEV"
+    if os.path.exists(vmedia_device_file_lower_case):
+        vmedia_device_file = vmedia_device_file_lower_case
+    elif os.path.exists(vmedia_device_file_upper_case):
+        vmedia_device_file = vmedia_device_file_upper_case
+    else:
+
+        # TODO(rameshg87): This block of code is there only for compatibility
+        # reasons (so that newer agent can work with older Ironic). Remove
+        # this after Liberty release.
+        vmedia_device = utils._get_vmedia_device()
+        if not vmedia_device:
+            msg = "Unable to find virtual media device"
+            raise errors.VirtualMediaBootError(msg)
+
+        vmedia_device_file = os.path.join("/dev", vmedia_device)
+
+    vmedia_mount_point = tempfile.mkdtemp()
+    try:
+        try:
+            stdout, stderr = utils.execute("mount", vmedia_device_file,
+                                     vmedia_mount_point)
+        except processutils.ProcessExecutionError as e:
+            msg = ("Unable to mount virtual media device %(device)s: "
+                   "%(error)s" % {'device': vmedia_device_file, 'error': e})
+            raise errors.VirtualMediaBootError(msg)
+
+        parameters_file_path = os.path.join(vmedia_mount_point,
+                                            parameters_file)
+        params = _read_params_from_file(parameters_file_path, '\n')
+
+        try:
+            stdout, stderr = utils.execute("umount", vmedia_mount_point)
+        except processutils.ProcessExecutionError as e:
+            pass
+    finally:
+        try:
+            shutil.rmtree(vmedia_mount_point)
+        except Exception as e:
+            pass
+
+    return params
+
+def _read_params_from_file(filepath, seperator=None):
+    """Extract key=value pairs from a file.
+
+    :param filepath: path to a file containing key=value pairs separated by
+                     whitespace or newlines.
+    :returns: a dictionary representing the content of the file
+    """
+    with open(filepath) as f:
+        cmdline = f.read()
+
+    options = cmdline.split(seperator)
+    params = {}
+    for option in options:
+        if '=' not in option:
+            continue
+        k, v = option.split('=', 1)
+        params[k] = v
+
+    return params
+
+def main():
+    log.register_options(CONF)
+    CONF(args=sys.argv[1:])
+    log.setup(CONF, 'virtmedia-netconfig')
+    LOG.info("Starting virtmedia-netconfig!!")
+
+    params = _read_params_from_file('/proc/cmdline')
+    # If the node booted over virtual media, the parameters are passed
+    # in a text file within the virtual media floppy.
+
+    if params.get('boot_method') == 'vmedia':
+        LOG.info("This node is booted with vmedia. Checking for available virtual media!!")
+        wait_for_cd_device()
+        check_cd_config()
+        vmedia_params = _get_vmedia_params()
+        params.update(vmedia_params)
+        LOG.debug("vmedia parameters: %r", vmedia_params)
+        os_net_config = params.get('os_net_config')
+        LOG.info("virtmedia: os_net_config=%s" %os_net_config)
+        if os_net_config:
+            _configure_static_net(os_net_config)
+
+        LOG.debug("Erasing old filesystems")
+        utils.execute('/usr/bin/erase-oldfs.sh')
+
+
+if __name__ == "__main__":
+    sys.exit(main())