Seed code for ipa-deployer
[ta/ipa-deployer.git] / work / dib-ipa-element / virtmedia-netconf / ironic-virtmedia-netconfig / src / virtmedia_netconfig / main.py
1 # Copyright 2019 Nokia
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15
16
17 import tarfile
18 import sys
19 import os
20 import errno
21 import stat
22 import time
23 import tempfile
24 import shutil
25
26 import json
27 import logging
28 import subprocess
29
30 from oslo_config import cfg
31 from oslo_log import log
32 from ironic_python_agent import utils
33 from ironic_lib import utils as ironic_utils
34 from ironic_python_agent import errors
35 from oslo_concurrency import processutils
36
37 CONF = cfg.CONF
38 LOG = log.getLogger(__name__)
39 dhclient_physIfaces = []
40
41 def dhclient_path():
42     if os.path.exists("/usr/sbin/dhclient"):
43         return "/usr/sbin/dhclient"
44     elif os.path.exists("/sbin/dhclient"):
45         return "/sbin/dhclient"
46     else:
47         raise RuntimeError("Could not find dhclient")
48
49 def stop_dhclient_process(interface):
50     """Stop a DHCP process before running os-net-config.
51
52     :param interface: The interface on which to stop dhclient.
53     """
54     pid_file = '/var/run/dhclient-%s.pid' % (interface)
55     try:
56         dhclient = dhclient_path()
57     except RuntimeError as err:
58         LOG.info('Exception when stopping dhclient: %s' % err)
59         return
60
61     if os.path.exists(pid_file):
62         LOG.info('Stopping %s on interface %s' % (dhclient, interface))
63         utils.execute(dhclient, '-r', '-pf', pid_file, interface)
64         try:
65             os.unlink(pid_file)
66         except OSError as err:
67             LOG.error('Could not remove dhclient pid file \'%s\': %s' %
68                          (pid_file, err))
69
70 def _poll_interface(_ifacedata):
71     ifacedata = json.loads(_ifacedata)
72     global dhclient_physIfaces
73     
74     physIfaces = []
75     if "network_config" in ifacedata:
76         for netconfdata in ifacedata["network_config"]:
77             if "device" in netconfdata:
78                 if "bond" not in netconfdata["device"]:
79                     # Is (physical) interface
80                     LOG.debug('Physical device %s' % netconfdata["device"])
81                     physIfaces.append(netconfdata["device"])
82
83             elif "members" in netconfdata:
84                 # logical interface with member (f.ex bond)
85                 for _member in netconfdata["members"]:
86                     if "type" in _member:
87                         if _member["type"] == 'interface':
88                             if "name" in _member:
89                                 LOG.debug('Physical device %s' % _member["name"])
90                                 physIfaces.append(_member["name"])
91             elif "name" in netconfdata:
92                 if "type" in netconfdata and netconfdata["type"] == 'interface':
93                     LOG.debug('Physical device %s' % netconfdata["name"])
94                     physIfaces.append(netconfdata["name"])
95
96     LOG.info('Checking for physical device(s) "%s"' % ', '.join(physIfaces))
97     dhclient_physIfaces = list(physIfaces)
98     wait_secs = 5
99     max_wait_secs = 60
100
101     while len(physIfaces) > 0 and max_wait_secs >= 0:
102         missing_devices = []
103         max_wait_secs = max_wait_secs - wait_secs
104
105         for _device in physIfaces:
106             devicepath = "/sys/class/net/%s/device" % _device
107             LOG.debug('Check path "%s"' % devicepath )
108             if os.path.exists(devicepath):
109                 LOG.debug('Device "%s" in known by kernel' % _device)
110                 physIfaces.remove(_device)
111             else:
112                 LOG.debug('Device "%s" in not (yet) known by kernel' % _device)
113                 missing_devices.append(_device)
114
115         if len(physIfaces) > 0:
116             LOG.info('Device(s) not (yet?) known by kernel: "%s"' % ', '.join(missing_devices))
117             time.sleep(wait_secs)
118
119
120     if len(physIfaces) > 0:
121         msg = 'Timeout, Device(s) missing: "%s"' % ', '.join(physIfaces)
122         LOG.error(msg)
123         raise errors.VirtualMediaBootError(msg)
124     else:
125         LOG.info('All physical devices found.')
126
127     for _device in dhclient_physIfaces:
128         stop_dhclient_process(_device)
129
130 def _configure_static_net(os_net_config):
131     """Configures network using os-net-config utility"""
132     global dhclient_physIfaces
133     LOG.debug("Configuring static network with os-net-config: %s", os_net_config)
134     try:
135         os.makedirs('/etc/os-net-config/')
136     except OSError as exc:
137         if exc.errno != errno.EEXIST:
138             raise
139         pass
140     with open('/etc/os-net-config/config.yaml', 'w') as fp:
141         fp.write(os_net_config)
142
143     try:
144         _poll_interface(os_net_config)
145     except Exception as e:
146         LOG.info('Exception while checking for physical interfaces: %s' % str(e) )
147
148     try:
149         os.system('/usr/sbin/ip a > /tmp/ifaces_before_initial_netconfig')
150     except Exception as e:
151         LOG.info('Exception while logging runtime ifaces to /tmp/ifaces_before_initial_netconfig: %s' % str(e) )
152
153     LOG.info('Running os-net-config..')
154     
155     cmd = [ '/usr/bin/os-net-config', '--detailed-exit-codes', '-v', '-c', '/etc/os-net-config/config.yaml']
156     wait_secs = 5
157     retries = 3
158     while retries > 0:
159         retries = retries - 1
160         netconf_process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
161         while True:
162             output = netconf_process.stdout.readline()
163             if output == '' and netconf_process.poll() is not None:
164                 break
165             if output:
166                 LOG.info(output.strip())
167
168         rc = netconf_process.poll()
169         LOG.info('os-net-config exit with status %d' % rc)
170
171         # os-net-config returns:
172         # 0 when nothing changed,
173         # 1 on error,
174         # 2 when config was modified (assuming option "--detailed-exit-codes")
175         
176         if rc == 0 or rc == 1:
177             LOG.info('os-net-config modified nothing or execution error. Not what we want..')
178             LOG.info('Attempt removing physical interface ifcfg-files to force os-net-config to reconfigure')
179             
180             for iface in dhclient_physIfaces:
181                 ifcfg_file = '/etc/sysconfig/network-scripts/ifcfg-' + str(iface)
182                 try:
183                     LOG.info('Removing "%s"' % ifcfg_file)
184                     os.system('/usr/bin/rm -f ' + ifcfg_file)
185                 except Exception as e:
186                     LOG.info('Ignoring exception when removing "%s": %s' % (ifcfg_file, str(e)))
187                     pass
188             time.sleep(wait_secs)
189
190         elif rc == 2:
191             LOG.info('os-net-config done.')
192             break
193         else:
194             LOG.info('os-net-config unknown exit code??')
195             time.sleep(wait_secs)
196     
197     # Config should be in place assuming os-net-config above was successfull
198     # As additional step restart network.service
199     LOG.info('Restarting network.service')
200     try:
201         cmd = ['/usr/bin/systemctl', 'restart', 'network']
202         subprocess.check_call(cmd)
203     except Exception as e:
204         LOG.info('Igoring exception when restarting network service: %s' % str(e))
205         pass
206
207
208 def get_file_size(filename):
209     "Get the file size by seeking at end"
210     fd= os.open(filename, os.O_RDONLY)
211     try:
212         return os.lseek(fd, 0, os.SEEK_END)
213     finally:
214         os.close(fd)
215
216 def wait_for_cd_device():
217     """ This function waits for /dev/sr0 device to appear """
218     inputiso = '/dev/sr0'
219     wait_count = 30
220     while not os.path.exists(inputiso) and wait_count:
221         LOG.debug('Waiting for %s to appear. Time left = %d secs' %(inputiso,wait_count))
222         time.sleep(1)
223         wait_count -= 1
224
225     if not wait_count:
226         msg = "Unable to find device %s" %(inputiso)
227         raise errors.VirtualMediaBootError(msg)
228
229 def check_cd_config():
230     """ This function checks for any extended 64K block in CD.
231     If it is available it will extract the contents for the block.
232     Loop mount the image for reading configuration parameters.
233     """
234     inputiso = '/dev/sr0'
235     outputtgz = '/tmp/cdconf.tgz'
236     mode = os.stat(inputiso).st_mode
237     if stat.S_ISBLK(mode):
238         filesize = get_file_size(inputiso)
239         skip = filesize / 2048-32
240         ironic_utils.dd(inputiso, outputtgz, 'bs=2k', 'skip=%d'%skip)
241
242         # Check if tgz file is valid.
243         try:
244             utils.execute("/usr/bin/gzip", '-t', outputtgz)
245         except processutils.ProcessExecutionError as err:
246             if 'not in gzip format' in err.stderr:
247                 LOG.info('File is not gzip format skipping!!')
248                 sys.exit()
249
250         LOG.info('Configuration file in gzip format proceeding for extraction')
251         tar = tarfile.open(outputtgz)
252         tar.extractall('/tmp/floppy')
253         tar.close()
254
255         dir_list = os.listdir('/tmp/floppy')
256         for item in dir_list:
257             if item.find('.img') != -1:
258                 os.mkdir('/tmp/floppy/mnt')
259                 utils.execute("mount", '-o', 'loop', '/tmp/floppy/%s' %item, '/tmp/floppy/mnt')
260                 time.sleep(1)
261
262 def _get_vmedia_params():
263     """This method returns the parameters passed through virtual media floppy.
264
265     :returns: a partial dict of potential agent configuration parameters
266     :raises: VirtualMediaBootError when it cannot find the virtual media device
267     """
268     parameters_file = "parameters.txt"
269
270     vmedia_device_file_lower_case = "/dev/disk/by-label/ir-vfd-dev"
271     vmedia_device_file_upper_case = "/dev/disk/by-label/IR-VFD-DEV"
272     if os.path.exists(vmedia_device_file_lower_case):
273         vmedia_device_file = vmedia_device_file_lower_case
274     elif os.path.exists(vmedia_device_file_upper_case):
275         vmedia_device_file = vmedia_device_file_upper_case
276     else:
277
278         # TODO(rameshg87): This block of code is there only for compatibility
279         # reasons (so that newer agent can work with older Ironic). Remove
280         # this after Liberty release.
281         vmedia_device = utils._get_vmedia_device()
282         if not vmedia_device:
283             msg = "Unable to find virtual media device"
284             raise errors.VirtualMediaBootError(msg)
285
286         vmedia_device_file = os.path.join("/dev", vmedia_device)
287
288     vmedia_mount_point = tempfile.mkdtemp()
289     try:
290         try:
291             stdout, stderr = utils.execute("mount", vmedia_device_file,
292                                      vmedia_mount_point)
293         except processutils.ProcessExecutionError as e:
294             msg = ("Unable to mount virtual media device %(device)s: "
295                    "%(error)s" % {'device': vmedia_device_file, 'error': e})
296             raise errors.VirtualMediaBootError(msg)
297
298         parameters_file_path = os.path.join(vmedia_mount_point,
299                                             parameters_file)
300         params = _read_params_from_file(parameters_file_path, '\n')
301
302         try:
303             stdout, stderr = utils.execute("umount", vmedia_mount_point)
304         except processutils.ProcessExecutionError as e:
305             pass
306     finally:
307         try:
308             shutil.rmtree(vmedia_mount_point)
309         except Exception as e:
310             pass
311
312     return params
313
314 def _read_params_from_file(filepath, seperator=None):
315     """Extract key=value pairs from a file.
316
317     :param filepath: path to a file containing key=value pairs separated by
318                      whitespace or newlines.
319     :returns: a dictionary representing the content of the file
320     """
321     with open(filepath) as f:
322         cmdline = f.read()
323
324     options = cmdline.split(seperator)
325     params = {}
326     for option in options:
327         if '=' not in option:
328             continue
329         k, v = option.split('=', 1)
330         params[k] = v
331
332     return params
333
334 def main():
335     log.register_options(CONF)
336     CONF(args=sys.argv[1:])
337     log.setup(CONF, 'virtmedia-netconfig')
338     LOG.info("Starting virtmedia-netconfig!!")
339
340     params = _read_params_from_file('/proc/cmdline')
341     # If the node booted over virtual media, the parameters are passed
342     # in a text file within the virtual media floppy.
343
344     if params.get('boot_method') == 'vmedia':
345         LOG.info("This node is booted with vmedia. Checking for available virtual media!!")
346         wait_for_cd_device()
347         check_cd_config()
348         vmedia_params = _get_vmedia_params()
349         params.update(vmedia_params)
350         LOG.debug("vmedia parameters: %r", vmedia_params)
351         os_net_config = params.get('os_net_config')
352         LOG.info("virtmedia: os_net_config=%s" %os_net_config)
353         if os_net_config:
354             _configure_static_net(os_net_config)
355
356         LOG.debug("Erasing old filesystems")
357         utils.execute('/usr/bin/erase-oldfs.sh')
358
359
360 if __name__ == "__main__":
361     sys.exit(main())