Adding ability to manualy configure network inside VM before boot.

This patchset obsoletes switching from "dhcp" to "static" during
deployment.

Example pillars:
salt:
  control:
    enabled: true
    virt_enabled: true
    size:
      small:
        cpu: 1
        ram: 1
    cluster:
      infra:
        domain: example.com
        engine: virt
        config:
          engine: salt
          host: master.domain.com
        cloud_init:
          network_data:
            links:
            - id: ens2
              name: ens2
              type: phy
          user_data:
            disable_ec2_metadata: true
            resize_rootfs: True
            timezone: UTC
            ssh_deletekeys: True
            ssh_genkeytypes: ['rsa', 'dsa', 'ecdsa']
            ssh_svcname: ssh
            locale: en_US.UTF-8
            disable_root: true
            apt_preserve_sources_list: false
            apt:
              sources_list: ""
              sources:
                ubuntu.list:
                  source: ${linux:system:repo:ubuntu:source}
                mcp_saltstack.list:
                  source: ${linux:system:repo:mcp_saltstack:source}
        node:
          ctl01:
            provider: kvm01.example.com
            image: ubuntu-16.04.qcow2
            size: small
            cloud_init:
              network_data:
                networks:
                - id: private-ipv4
                  ip_address: 192.168.0.161
                  link: ens2
                  netmask: 255.255.255.0
                  routes:
                  - gateway: 192.168.0.1
                    netmask: 0.0.0.0
                    network: 0.0.0.0
                  type: ipv4

Change-Id: I087518404c61e0bab2303e40957bacca94692102
diff --git a/README.rst b/README.rst
index fd15b19..33d356a 100644
--- a/README.rst
+++ b/README.rst
@@ -394,6 +394,16 @@
 
 .. code-block:: yaml
 
+    _param:
+      private-ipv4: &private-ipv4
+      - id: private-ipv4
+        type: ipv4
+        link: ens2
+        netmask: 255.255.255.0
+        routes:
+        - gateway: 192.168.0.1
+          netmask: 0.0.0.0
+          network: 0.0.0.0
     virt:
       disk:
         three_disks:
@@ -440,6 +450,24 @@
             engine: virt
             #Option to set rng globaly
             rng: false
+            cloud_init:
+              user_data:
+                disable_ec2_metadata: true
+                resize_rootfs: True
+                timezone: UTC
+                ssh_deletekeys: True
+                ssh_genkeytypes: ['rsa', 'dsa', 'ecdsa']
+                ssh_svcname: ssh
+                locale: en_US.UTF-8
+                disable_root: true
+                apt_preserve_sources_list: false
+                apt:
+                  sources_list: ""
+                  sources:
+                    ubuntu.list:
+                      source: ${linux:system:repo:ubuntu:source}
+                    mcp_saltstack.list:
+                      source: ${linux:system:repo:mcp_saltstack:source}
             node:
               ubuntu1:
                 provider: node01.domain.com
@@ -456,6 +484,13 @@
                 mac:
                   nic01: AC:DE:48:AA:AA:AA
                   nic02: AC:DE:48:AA:AA:BB
+                # netconfig affects: hostname during boot
+                # manual interfaces configuration
+                cloud_init:
+                  network_data:
+                    networks:
+                    - <<: *private-ipv4
+                      ip_address: 192.168.0.161
 
 To enable Redis plugin for the Salt caching subsystem, use the
 below pillar structure:
diff --git a/_modules/cfgdrive.py b/_modules/cfgdrive.py
new file mode 100644
index 0000000..bc76b77
--- /dev/null
+++ b/_modules/cfgdrive.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+
+import json
+import logging
+import os
+import shutil
+import six
+import tempfile
+import yaml
+
+from oslo_utils import uuidutils
+from oslo_utils import fileutils
+from oslo_concurrency import processutils
+
+class ConfigDriveBuilder(object):
+    """Build config drives, optionally as a context manager."""
+
+    def __init__(self, image_file):
+        self.image_file = image_file
+        self.mdfiles=[]
+
+    def __enter__(self):
+        fileutils.delete_if_exists(self.image_file)
+        return self
+
+    def __exit__(self, exctype, excval, exctb):
+        self.make_drive()
+
+    def add_file(self, path, data):
+        self.mdfiles.append((path, data))
+
+    def _add_file(self, basedir, path, data):
+        filepath = os.path.join(basedir, path)
+        dirname = os.path.dirname(filepath)
+        fileutils.ensure_tree(dirname)
+        with open(filepath, 'wb') as f:
+            if isinstance(data, six.text_type):
+                data = data.encode('utf-8')
+            f.write(data)
+
+    def _write_md_files(self, basedir):
+        for data in self.mdfiles:
+            self._add_file(basedir, data[0], data[1])
+
+    def _make_iso9660(self, path, tmpdir):
+
+        processutils.execute('mkisofs',
+            '-o', path,
+            '-ldots',
+            '-allow-lowercase',
+            '-allow-multidot',
+            '-l',
+            '-V', 'config-2',
+            '-r',
+            '-J',
+            '-quiet',
+            tmpdir,
+            attempts=1,
+            run_as_root=False)
+
+    def make_drive(self):
+        """Make the config drive.
+        :raises ProcessExecuteError if a helper process has failed.
+        """
+        try:
+            tmpdir = tempfile.mkdtemp()
+            self._write_md_files(tmpdir)
+            self._make_iso9660(self.image_file, tmpdir)
+        finally:
+            shutil.rmtree(tmpdir)
+
+
+def generate(
+               dst,
+               hostname,
+               domainname,
+               instance_id=None,
+               user_data=None,
+               network_data=None,
+               saltconfig=None
+            ):
+
+    ''' Generate config drive
+
+    :param dst: destination file to place config drive.
+    :param hostname: hostname of Instance.
+    :param domainname: instance domain.
+    :param instance_id: UUID of the instance.
+    :param user_data: custom user data dictionary. type: json
+    :param network_data: custom network info dictionary. type: json
+    :param saltconfig: salt minion configuration. type: json
+
+    '''
+
+    instance_md              = {}
+    instance_md['uuid']      = instance_id or uuidutils.generate_uuid()
+    instance_md['hostname']  = '%s.%s' % (hostname, domainname)
+    instance_md['name']      = hostname
+
+    if user_data:
+      user_data = '#cloud-config\n\n' + yaml.dump(yaml.load(user_data), default_flow_style=False)
+      if saltconfig:
+        user_data += yaml.dump(yaml.load(str(saltconfig)), default_flow_style=False)
+
+    data = json.dumps(instance_md)
+
+    with ConfigDriveBuilder(dst) as cfgdrive:
+      cfgdrive.add_file('openstack/latest/meta_data.json', data)
+      if user_data:
+        cfgdrive.add_file('openstack/latest/user_data', user_data)
+      if network_data:
+         cfgdrive.add_file('openstack/latest/network_data.json', network_data)
+      cfgdrive.add_file('openstack/latest/vendor_data.json', '{}')
+      cfgdrive.add_file('openstack/latest/vendor_data2.json', '{}')
diff --git a/_modules/seedng.py b/_modules/seedng.py
index 1d93c5d..4d67e27 100644
--- a/_modules/seedng.py
+++ b/_modules/seedng.py
@@ -91,8 +91,16 @@
         __salt__['mount.umount'](mpt, util='qemu_nbd')
 
 
-def apply_(path, id_=None, config=None, approve_key=True, install=True,
-           prep_install=False, pub_key=None, priv_key=None, mount_point=None):
+def apply_(
+             path, id_=None,
+             config=None,
+             approve_key=True,
+             install=True,
+             prep_install=False,
+             pub_key=None,
+             priv_key=None,
+             mount_point=None
+          ):
     '''
     Seed a location (disk image, directory, or block device) with the
     minion config, approve the minion's key, and/or install salt-minion.
diff --git a/_modules/virtng.py b/_modules/virtng.py
index ce09508..b9ef85d 100644
--- a/_modules/virtng.py
+++ b/_modules/virtng.py
@@ -20,6 +20,7 @@
 
 # Import third party libs
 import yaml
+import json
 import jinja2
 import jinja2.exceptions
 import salt.ext.six as six
@@ -558,6 +559,9 @@
             diskp[0][disk_name]['image'] = image
 
     # Create multiple disks, empty or from specified images.
+    cloud_init = None
+    cfg_drive  = None
+
     for disk in diskp:
         log.debug("Creating disk for VM [ {0} ]: {1}".format(name, disk))
 
@@ -618,13 +622,39 @@
                         raise CommandExecutionError('problem while copying image. {0} - {1}'.format(args['image'], e))
 
                     if kwargs.get('seed'):
-                        install = kwargs.get('install', True)
-                        seed_cmd = kwargs.get('seed_cmd', 'seedng.apply')
+                        seed_cmd   = kwargs.get('seed_cmd', 'seedng.apply')
+                        cloud_init = kwargs.get('cloud_init', None)
+                        master     = __salt__['config.option']('master')
+                        cfg_drive  = os.path.join(img_dir,'config-2.iso')
 
-                        __salt__[seed_cmd](img_dest,
-                                           id_=name,
-                                           config=kwargs.get('config'),
-                                           install=install)
+                        if cloud_init:
+                          _tmp         = name.split('.')
+
+                          try:
+                            user_data  = json.dumps(cloud_init["user_data"])
+                          except:
+                            user_data  = None
+
+                          try:
+                            network_data = json.dumps(cloud_init["network_data"])
+                          except:
+                            network_data = None
+
+                          __salt__["cfgdrive.generate"](
+                            dst          = cfg_drive,
+                            hostname     = _tmp.pop(0),
+                            domainname   = '.'.join(_tmp),
+                            user_data    = user_data,
+                            network_data = network_data,
+                            saltconfig   = { "salt_minion": { "conf": { "master": master, "id": name } } }
+                          )
+                        else:
+                          __salt__[seed_cmd](
+                            path      = img_dest,
+                            id_       = name,
+                            config    = kwargs.get('config'),
+                            install   = kwargs.get('install', True)
+                          )
                 else:
                     # Create empty disk
                     try:
@@ -649,6 +679,26 @@
 
     xml = _gen_xml(name, cpu, mem, diskp, nicp, hypervisor, **kwargs)
 
+    if cloud_init and cfg_drive:
+      xml_doc = minidom.parseString(xml)
+      iso_xml = xml_doc.createElement("disk")
+      iso_xml.setAttribute("type", "file")
+      iso_xml.setAttribute("device", "cdrom")
+      iso_xml.appendChild(xml_doc.createElement("readonly"))
+      driver = xml_doc.createElement("driver")
+      driver.setAttribute("name", "qemu")
+      driver.setAttribute("type", "raw")
+      target = xml_doc.createElement("target")
+      target.setAttribute("dev", "hdc")
+      target.setAttribute("bus", "ide")
+      source = xml_doc.createElement("source")
+      source.setAttribute("file", cfg_drive)
+      iso_xml.appendChild(driver)
+      iso_xml.appendChild(target)
+      iso_xml.appendChild(source)
+      xml_doc.getElementsByTagName("domain")[0].getElementsByTagName("devices")[0].appendChild(iso_xml)
+      xml = xml_doc.toxml()
+
     # TODO: Remove this code and refactor module, when salt-common would have updated libvirt_domain.jinja template
     for _nic in nicp:
         if _nic['virtualport']:
diff --git a/salt/control/virt.sls b/salt/control/virt.sls
index a2e56ff..5b6fee8 100644
--- a/salt/control/virt.sls
+++ b/salt/control/virt.sls
@@ -42,6 +42,9 @@
 {%- if node.provider == grains.id %}
 
 {%- set size = control.size.get(node.size) %}
+{%- set cluster_cloud_init = cluster.get('cloud_init', {}) %}
+{%- set node_cloud_init = node.get('cloud_init', {}) %}
+{%- set cloud_init = salt['grains.filter_by']({'default': cluster_cloud_init}, merge=node_cloud_init) %}
 
 salt_control_virt_{{ cluster_name }}_{{ node_name }}:
   module.run:
@@ -59,6 +62,9 @@
   - rng: {{ rng }}
   {%- endif %}
   - kwargs:
+      {%- if cloud_init is defined %}
+      cloud_init: {{ cloud_init }}
+      {%- endif %}
       seed: True
       serial_type: pty
       console: True
diff --git a/tests/pillar/control_virt_custom.sls b/tests/pillar/control_virt_custom.sls
index 71cf37f..061d47d 100644
--- a/tests/pillar/control_virt_custom.sls
+++ b/tests/pillar/control_virt_custom.sls
@@ -1,3 +1,13 @@
+_param:
+  private-ipv4: &private-ipv4
+  - id: private-ipv4
+    type: ipv4
+    link: ens2
+    netmask: 255.255.255.0
+    routes:
+    - gateway: 192.168.0.1
+      netmask: 0.0.0.0
+      network: 0.0.0.0
 virt:
   disk:
     three_disks:
@@ -57,6 +67,29 @@
         config:
           engine: salt
           host: master.domain.com
+        cloud_init:
+          user_data:
+            disable_ec2_metadata: true
+            resize_rootfs: True
+            timezone: UTC
+            ssh_deletekeys: True
+            ssh_genkeytypes: ['rsa', 'dsa', 'ecdsa']
+            ssh_svcname: ssh
+            locale: en_US.UTF-8
+            disable_root: true
+            apt_preserve_sources_list: false
+            apt:
+              sources_list: ""
+              sources:
+                ubuntu.list:
+                  source: ${linux:system:repo:ubuntu:source}
+                mcp_saltstack.list:
+                  source: ${linux:system:repo:mcp_saltstack:source}
+          network_data:
+            links:
+            - id: ens2
+              type: phy
+              name: ens2
         node:
           ubuntu1:
             provider: node01.domain.com
@@ -72,6 +105,11 @@
             provider: node03.domain.com
             image: meowbuntu.qcom2
             size: medium_three_disks
+            cloud_init:
+              network_data:
+                networks:
+                - <<: *private-ipv4
+                  ip_address: 192.168.0.161
             rng:
               backend: /dev/urandom
               model: random