# 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.debug("Erasing old filesystems") utils.execute('/usr/bin/erase-oldfs.sh') 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) if __name__ == "__main__": sys.exit(main())