Extend ironic formula

This patch updates ironicng salt module to be able of:

 * list vifs for a given node
 * attach vif for a given node
 * detach vif from a given node
 * deploy user image to node
 * generate configdrive

Fix some default values when enrolling nodes automatically.

Allow to download images from http to conductor http_root.

Change-Id: Id99ad955c8c7256ae10ece7a173242044692e713
diff --git a/_modules/configdrive.py b/_modules/configdrive.py
new file mode 100644
index 0000000..0a45b02
--- /dev/null
+++ b/_modules/configdrive.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+
+
+import gzip
+import json
+import logging
+import os
+import StringIO
+import shutil
+import six
+import tempfile
+import yaml
+
+
+HAS_LIBS = False
+try:
+  from oslo_utils import uuidutils
+  from oslo_utils import fileutils
+  from oslo_concurrency import processutils
+  from oslo_serialization import base64
+  HAS_LIBS = True
+except ImportError:
+    pass
+
+LOG = logging.getLogger(__name__)
+
+
+def __virtual__():
+    '''
+    Only load this module if mkisofs is installed on this minion.
+    '''
+    if not HAS_LIBS:
+        return False
+
+    for path in os.environ["PATH"].split(os.pathsep):
+        if os.access(os.path.join(path, 'mkisofs'), os.X_OK):
+            return True
+
+    return False
+
+class ConfigDriveBuilder(object):
+    """Build config drives, optionally as a context manager."""
+
+    def __init__(self, image_file):
+        self.image_file = image_file
+        self.mdfiles=[] # List with (path, data)
+
+    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:
+            # the given data can be either text or bytes. we can only write
+            # bytes into files.
+            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, public_keys=None,
+             user_data=None, network_data=None, ironic_format=False):
+    ''' 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 public_keys: dict of public keys.
+    :param user_data: custom user data dictionary.
+    :param network_data: custom network info dictionary.
+    :param ironic_format: create base64 of gzipped ISO format
+
+    CLI Example:
+    .. code-block:: bash
+        salt '*' configdrive.generate dst=/tmp/my_cfgdrive.iso hostname=host1
+    '''
+    instance_md = {}
+    public_keys = public_keys or {}
+
+    instance_md['uuid'] = instance_id or uuidutils.generate_uuid()
+    instance_md['hostname'] = '%s.%s' % (hostname, domainname)
+    instance_md['name'] = hostname
+    instance_md['public_keys'] = public_keys
+
+    data = json.dumps(instance_md)
+
+    if user_data:
+        user_data = '#cloud-config\n\n' + yaml.dump(user_data, default_flow_style=False)
+
+    LOG.debug('Generating config drive for %s' % hostname)
+
+    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',
+                               json.dumps(network_data))
+        cfgdrive.add_file('openstack/latest/vendor_data.json', '{}')
+        cfgdrive.add_file('openstack/latest/vendor_data2.json', '{}')
+
+    b64_gzip = None
+    if ironic_format:
+        with open(dst) as f:
+            with tempfile.NamedTemporaryFile() as tmpzipfile:
+                g = gzip.GzipFile(fileobj=tmpzipfile, mode='wb')
+                shutil.copyfileobj(f, g)
+                g.close()
+                tmpzipfile.seek(0)
+                b64_gzip = base64.encode_as_bytes(tmpzipfile.read())
+        with open(dst, 'w') as f:
+            f.write(b64_gzip)
+
+    LOG.debug('Config drive was built %s' % dst)
+    res = {}
+    res['meta-data'] = data
+    if user_data:
+        res['user-data'] = user_data
+    if b64_gzip:
+        res['base64_gzip'] = b64_gzip
+    return res