← Back to team overview

cloud-init-dev team mailing list archive

[Merge] lp:~harlowja/cloud-init/py2-3 into lp:cloud-init

 

Joshua Harlow has proposed merging lp:~harlowja/cloud-init/py2-3 into lp:cloud-init.

Requested reviews:
  cloud init development team (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~harlowja/cloud-init/py2-3/+merge/225240

Gets the basic integration of six usage going.

This change does the following:

- Moves to using the new locations of modules (using six as needed)
  - urlparse moved, configparser moved, stringio...
- Fixes the octal usage that we had previously (0o644 is the new way that works)
- The utils load_file() now decodes the files by default from binary -> utf-8 (unless decode=False, decode=False seems needed for the configobj module to correctly work)
- The utils write_file() now decodes to binary before writing (from unicode) as needed
- Adjust tests to work correctly using the new way of load/writing files
-- 
https://code.launchpad.net/~harlowja/cloud-init/py2-3/+merge/225240
Your team cloud init development team is requested to review the proposed merge of lp:~harlowja/cloud-init/py2-3 into lp:cloud-init.
=== modified file 'cloudinit/config/cc_debug.py'
--- cloudinit/config/cc_debug.py	2014-01-23 19:28:59 +0000
+++ cloudinit/config/cc_debug.py	2014-07-01 22:46:34 +0000
@@ -14,10 +14,12 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import copy
+
+from six import StringIO
+
 from cloudinit import type_utils
 from cloudinit import util
-import copy
-from StringIO import StringIO
 
 
 def _make_header(text):

=== modified file 'cloudinit/config/cc_landscape.py'
--- cloudinit/config/cc_landscape.py	2014-01-27 22:34:35 +0000
+++ cloudinit/config/cc_landscape.py	2014-07-01 22:46:34 +0000
@@ -20,7 +20,7 @@
 
 import os
 
-from StringIO import StringIO
+from six import StringIO
 
 from configobj import ConfigObj
 

=== modified file 'cloudinit/config/cc_mcollective.py'
--- cloudinit/config/cc_mcollective.py	2014-01-27 22:34:35 +0000
+++ cloudinit/config/cc_mcollective.py	2014-07-01 22:46:34 +0000
@@ -19,7 +19,7 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from StringIO import StringIO
+from six import StringIO
 
 # Used since this can maintain comments
 # and doesn't need a top level section
@@ -81,7 +81,7 @@
         contents = StringIO()
         mcollective_config.write(contents)
         contents = contents.getvalue()
-        util.write_file(SERVER_CFG, contents, mode=0644)
+        util.write_file(SERVER_CFG, contents, mode=0o644)
 
     # Start mcollective
     util.subp(['service', 'mcollective', 'start'], capture=False)

=== modified file 'cloudinit/config/cc_puppet.py'
--- cloudinit/config/cc_puppet.py	2014-02-05 15:36:47 +0000
+++ cloudinit/config/cc_puppet.py	2014-07-01 22:46:34 +0000
@@ -18,7 +18,7 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from StringIO import StringIO
+from six import StringIO
 
 import os
 import socket

=== modified file 'cloudinit/config/cc_seed_random.py'
--- cloudinit/config/cc_seed_random.py	2014-03-04 19:35:09 +0000
+++ cloudinit/config/cc_seed_random.py	2014-07-01 22:46:34 +0000
@@ -21,7 +21,8 @@
 
 import base64
 import os
-from StringIO import StringIO
+
+from six import StringIO
 
 from cloudinit.settings import PER_INSTANCE
 from cloudinit import log as logging

=== modified file 'cloudinit/distros/__init__.py'
--- cloudinit/distros/__init__.py	2014-02-12 19:56:55 +0000
+++ cloudinit/distros/__init__.py	2014-07-01 22:46:34 +0000
@@ -21,7 +21,7 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from StringIO import StringIO
+from six import StringIO
 
 import abc
 import itertools
@@ -268,7 +268,7 @@
             if header:
                 contents.write("%s\n" % (header))
             contents.write("%s\n" % (eh))
-            util.write_file(self.hosts_fn, contents.getvalue(), mode=0644)
+            util.write_file(self.hosts_fn, contents.getvalue(), mode=0o644)
 
     def _bring_up_interface(self, device_name):
         cmd = ['ifup', device_name]
@@ -452,7 +452,7 @@
                              util.make_header(base="added"),
                              "#includedir %s" % (path), '']
                     sudoers_contents = "\n".join(lines)
-                    util.write_file(sudo_base, sudoers_contents, 0440)
+                    util.write_file(sudo_base, sudoers_contents, 0o440)
                 else:
                     lines = ['', util.make_header(base="added"),
                              "#includedir %s" % (path), '']
@@ -462,7 +462,7 @@
             except IOError as e:
                 util.logexc(LOG, "Failed to write %s", sudo_base)
                 raise e
-        util.ensure_dir(path, 0750)
+        util.ensure_dir(path, 0o750)
 
     def write_sudo_rules(self, user, rules, sudo_file=None):
         if not sudo_file:
@@ -490,7 +490,7 @@
                 content,
             ]
             try:
-                util.write_file(sudo_file, "\n".join(contents), 0440)
+                util.write_file(sudo_file, "\n".join(contents), 0o440)
             except IOError as e:
                 util.logexc(LOG, "Failed to write sudoers file %s", sudo_file)
                 raise e

=== modified file 'cloudinit/distros/freebsd.py'
--- cloudinit/distros/freebsd.py	2014-02-28 21:40:08 +0000
+++ cloudinit/distros/freebsd.py	2014-07-01 22:46:34 +0000
@@ -16,10 +16,10 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from StringIO import StringIO
-
 import re
 
+from six import StringIO
+
 from cloudinit import distros
 from cloudinit import helpers
 from cloudinit import log as logging

=== modified file 'cloudinit/distros/parsers/hostname.py'
--- cloudinit/distros/parsers/hostname.py	2012-11-12 22:30:08 +0000
+++ cloudinit/distros/parsers/hostname.py	2014-07-01 22:46:34 +0000
@@ -16,7 +16,7 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from StringIO import StringIO
+from six import StringIO
 
 from cloudinit.distros.parsers import chop_comment
 

=== modified file 'cloudinit/distros/parsers/hosts.py'
--- cloudinit/distros/parsers/hosts.py	2012-11-13 06:14:31 +0000
+++ cloudinit/distros/parsers/hosts.py	2014-07-01 22:46:34 +0000
@@ -16,7 +16,7 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from StringIO import StringIO
+from six import StringIO
 
 from cloudinit.distros.parsers import chop_comment
 

=== modified file 'cloudinit/distros/parsers/resolv_conf.py'
--- cloudinit/distros/parsers/resolv_conf.py	2013-03-19 13:32:04 +0000
+++ cloudinit/distros/parsers/resolv_conf.py	2014-07-01 22:46:34 +0000
@@ -16,7 +16,7 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from StringIO import StringIO
+from six import StringIO
 
 from cloudinit import util
 

=== modified file 'cloudinit/distros/parsers/sys_conf.py'
--- cloudinit/distros/parsers/sys_conf.py	2012-11-12 22:30:08 +0000
+++ cloudinit/distros/parsers/sys_conf.py	2014-07-01 22:46:34 +0000
@@ -16,11 +16,11 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from StringIO import StringIO
-
 import pipes
 import re
 
+import six
+
 # This library is used to parse/write
 # out the various sysconfig files edited (best attempt effort)
 #
@@ -61,15 +61,15 @@
 
     def __str__(self):
         contents = self.write()
-        out_contents = StringIO()
+        out_contents = six.StringIO()
         if isinstance(contents, (list, tuple)):
             out_contents.write("\n".join(contents))
         else:
-            out_contents.write(str(contents))
+            out_contents.write(six.text_type(contents))
         return out_contents.getvalue()
 
     def _quote(self, value, multiline=False):
-        if not isinstance(value, (str, basestring)):
+        if not isinstance(value, six.string_types):
             raise ValueError('Value "%s" is not a string' % (value))
         if len(value) == 0:
             return ''

=== modified file 'cloudinit/ec2_utils.py'
--- cloudinit/ec2_utils.py	2014-02-08 20:20:33 +0000
+++ cloudinit/ec2_utils.py	2014-07-01 22:46:34 +0000
@@ -17,7 +17,6 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import functools
-import httplib
 import json
 
 from cloudinit import log as logging
@@ -25,7 +24,7 @@
 from cloudinit import util
 
 LOG = logging.getLogger(__name__)
-SKIP_USERDATA_CODES = frozenset([httplib.NOT_FOUND])
+SKIP_USERDATA_CODES = frozenset([url_helper.NOT_FOUND])
 
 
 def maybe_json_object(text):

=== modified file 'cloudinit/handlers/__init__.py'
--- cloudinit/handlers/__init__.py	2014-01-16 21:57:21 +0000
+++ cloudinit/handlers/__init__.py	2014-07-01 22:46:34 +0000
@@ -147,7 +147,7 @@
     if not modfname.endswith(".py"):
         modfname = "%s.py" % (modfname)
     # TODO(harlowja): Check if path exists??
-    util.write_file(modfname, payload, 0600)
+    util.write_file(modfname, payload, 0o600)
     handlers = pdata['handlers']
     try:
         mod = fixup_handler(importer.import_module(modname))

=== modified file 'cloudinit/handlers/boot_hook.py'
--- cloudinit/handlers/boot_hook.py	2013-07-21 16:34:26 +0000
+++ cloudinit/handlers/boot_hook.py	2014-07-01 22:46:34 +0000
@@ -50,7 +50,7 @@
         filepath = os.path.join(self.boothook_dir, filename)
         contents = util.strip_prefix_suffix(util.dos2unix(payload),
                                             prefix=BOOTHOOK_PREFIX)
-        util.write_file(filepath, contents.lstrip(), 0700)
+        util.write_file(filepath, contents.lstrip(), 0o700)
         return filepath
 
     def handle_part(self, _data, ctype, filename,  # pylint: disable=W0221

=== modified file 'cloudinit/handlers/cloud_config.py'
--- cloudinit/handlers/cloud_config.py	2014-01-09 00:16:24 +0000
+++ cloudinit/handlers/cloud_config.py	2014-07-01 22:46:34 +0000
@@ -95,7 +95,7 @@
             lines.append(util.yaml_dumps(self.cloud_buf))
         else:
             lines = []
-        util.write_file(self.cloud_fn, "\n".join(lines), 0600)
+        util.write_file(self.cloud_fn, "\n".join(lines), 0o600)
 
     def _extract_mergers(self, payload, headers):
         merge_header_headers = ''

=== modified file 'cloudinit/handlers/shell_script.py'
--- cloudinit/handlers/shell_script.py	2014-01-09 00:16:24 +0000
+++ cloudinit/handlers/shell_script.py	2014-07-01 22:46:34 +0000
@@ -53,4 +53,4 @@
         filename = util.clean_filename(filename)
         payload = util.dos2unix(payload)
         path = os.path.join(self.script_dir, filename)
-        util.write_file(path, payload, 0700)
+        util.write_file(path, payload, 0o700)

=== modified file 'cloudinit/handlers/upstart_job.py'
--- cloudinit/handlers/upstart_job.py	2013-07-21 16:26:44 +0000
+++ cloudinit/handlers/upstart_job.py	2014-07-01 22:46:34 +0000
@@ -66,7 +66,7 @@
 
         payload = util.dos2unix(payload)
         path = os.path.join(self.upstart_dir, filename)
-        util.write_file(path, payload, 0644)
+        util.write_file(path, payload, 0o644)
 
         if SUITABLE_UPSTART:
             util.subp(["initctl", "reload-configuration"], capture=False)

=== modified file 'cloudinit/helpers.py'
--- cloudinit/helpers.py	2014-01-17 20:12:31 +0000
+++ cloudinit/helpers.py	2014-07-01 22:46:34 +0000
@@ -26,7 +26,9 @@
 import io
 import os
 
-from ConfigParser import (NoSectionError, NoOptionError, RawConfigParser)
+from six.moves.configparser import (NoSectionError,
+                                    NoOptionError,
+                                    RawConfigParser)
 
 from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE,
                                 CFG_ENV_NAME)

=== modified file 'cloudinit/log.py'
--- cloudinit/log.py	2013-04-17 16:42:55 +0000
+++ cloudinit/log.py	2014-07-01 22:46:34 +0000
@@ -28,7 +28,7 @@
 import os
 import sys
 
-from StringIO import StringIO
+from six import StringIO
 
 # Logging levels for easy access
 CRITICAL = logging.CRITICAL

=== modified file 'cloudinit/signal_handler.py'
--- cloudinit/signal_handler.py	2012-09-19 20:33:56 +0000
+++ cloudinit/signal_handler.py	2014-07-01 22:46:34 +0000
@@ -22,7 +22,7 @@
 import signal
 import sys
 
-from StringIO import StringIO
+from six import StringIO
 
 from cloudinit import log as logging
 from cloudinit import util

=== modified file 'cloudinit/ssh_util.py'
--- cloudinit/ssh_util.py	2013-06-19 06:44:00 +0000
+++ cloudinit/ssh_util.py	2014-07-01 22:46:34 +0000
@@ -239,7 +239,7 @@
     # Make sure the users .ssh dir is setup accordingly
     (ssh_dir, pwent) = users_ssh_info(username)
     if not os.path.isdir(ssh_dir):
-        util.ensure_dir(ssh_dir, mode=0700)
+        util.ensure_dir(ssh_dir, mode=0o700)
         util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid)
 
     # Turn the 'update' keys given into actual entries
@@ -252,8 +252,8 @@
     (auth_key_fn, auth_key_entries) = extract_authorized_keys(username)
     with util.SeLinuxGuard(ssh_dir, recursive=True):
         content = update_authorized_keys(auth_key_entries, key_entries)
-        util.ensure_dir(os.path.dirname(auth_key_fn), mode=0700)
-        util.write_file(auth_key_fn, content, mode=0600)
+        util.ensure_dir(os.path.dirname(auth_key_fn), mode=0o700)
+        util.write_file(auth_key_fn, content, mode=0o600)
         util.chownbyid(auth_key_fn, pwent.pw_uid, pwent.pw_gid)
 
 

=== modified file 'cloudinit/stages.py'
--- cloudinit/stages.py	2014-02-13 18:53:08 +0000
+++ cloudinit/stages.py	2014-07-01 22:46:34 +0000
@@ -20,10 +20,9 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import cPickle as pickle
-
 import copy
 import os
+import pickle
 import sys
 
 from cloudinit.settings import (PER_INSTANCE, FREQUENCIES, CLOUD_CONFIG)
@@ -202,7 +201,7 @@
             util.logexc(LOG, "Failed pickling datasource %s", self.datasource)
             return False
         try:
-            util.write_file(pickled_fn, pk_contents, mode=0400)
+            util.write_file(pickled_fn, pk_contents, mode=0o400)
         except Exception:
             util.logexc(LOG, "Failed pickling datasource to %s", pickled_fn)
             return False
@@ -324,15 +323,15 @@
 
     def _store_userdata(self):
         raw_ud = "%s" % (self.datasource.get_userdata_raw())
-        util.write_file(self._get_ipath('userdata_raw'), raw_ud, 0600)
+        util.write_file(self._get_ipath('userdata_raw'), raw_ud, 0o600)
         processed_ud = "%s" % (self.datasource.get_userdata())
-        util.write_file(self._get_ipath('userdata'), processed_ud, 0600)
+        util.write_file(self._get_ipath('userdata'), processed_ud, 0o600)
 
     def _store_vendordata(self):
         raw_vd = "%s" % (self.datasource.get_vendordata_raw())
-        util.write_file(self._get_ipath('vendordata_raw'), raw_vd, 0600)
+        util.write_file(self._get_ipath('vendordata_raw'), raw_vd, 0o600)
         processed_vd = "%s" % (self.datasource.get_vendordata())
-        util.write_file(self._get_ipath('vendordata'), processed_vd, 0600)
+        util.write_file(self._get_ipath('vendordata'), processed_vd, 0o600)
 
     def _default_handlers(self, opts=None):
         if opts is None:

=== modified file 'cloudinit/url_helper.py'
--- cloudinit/url_helper.py	2014-02-13 17:13:42 +0000
+++ cloudinit/url_helper.py	2014-07-01 22:46:34 +0000
@@ -20,21 +20,28 @@
 #    You should have received a copy of the GNU General Public License
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import httplib
+import six
+
 import time
 import urllib
 
 import requests
 from requests import exceptions
 
-from urlparse import (urlparse, urlunparse)
+from six.moves.urllib.parse import (urlparse, urlunparse)
 
 from cloudinit import log as logging
 from cloudinit import version
 
 LOG = logging.getLogger(__name__)
 
-NOT_FOUND = httplib.NOT_FOUND
+if six.PY2:
+    import httplib
+    NOT_FOUND = httplib.NOT_FOUND
+else:
+    import http.client
+    NOT_FOUND = http.client.NOT_FOUND
+
 
 # Check if requests has ssl support (added in requests >= 0.8.8)
 SSL_ENABLED = False

=== modified file 'cloudinit/user_data.py'
--- cloudinit/user_data.py	2014-01-24 20:29:09 +0000
+++ cloudinit/user_data.py	2014-07-01 22:46:34 +0000
@@ -235,7 +235,7 @@
                 resp = util.read_file_or_url(include_url,
                                              ssl_details=self.ssl_details)
                 if include_once_on and resp.ok():
-                    util.write_file(include_once_fn, str(resp), mode=0600)
+                    util.write_file(include_once_fn, str(resp), mode=0o600)
                 if resp.ok():
                     content = str(resp)
                 else:

=== modified file 'cloudinit/util.py'
--- cloudinit/util.py	2014-02-24 22:20:12 +0000
+++ cloudinit/util.py	2014-07-01 22:46:34 +0000
@@ -22,8 +22,6 @@
 #
 # pylint: disable=C0302
 
-from StringIO import StringIO
-
 import contextlib
 import copy as obj_copy
 import ctypes
@@ -47,8 +45,10 @@
 import sys
 import tempfile
 import time
-import urlparse
-
+
+from six.moves.urllib import parse as urlparse
+
+import six
 import yaml
 
 from cloudinit import importer
@@ -71,8 +71,25 @@
 }
 FN_ALLOWED = ('_-.()' + string.digits + string.ascii_letters)
 
+TRUE_STRINGS = ('true', '1', 'on', 'yes')
+FALSE_STRINGS = ('off', '0', 'no', 'false')
+
 # Helper utils to see if running in a container
-CONTAINER_TESTS = ['running-in-container', 'lxc-is-container']
+CONTAINER_TESTS = ('running-in-container', 'lxc-is-container')
+
+
+def decode_binary(blob, encoding='utf-8'):
+    # Converts a binary type into a text type using given encoding.
+    if isinstance(blob, six.text_type):
+        return blob
+    return blob.decode(encoding)
+
+
+def encode_text(text, encoding='utf-8'):
+    # Converts a text string into a binary type using given encoding.
+    if isinstance(text, six.binary_type):
+        return text
+    return text.encode(encoding)
 
 
 class ProcessExecutionError(IOError):
@@ -149,7 +166,8 @@
         if self.selinux and self.selinux.is_selinux_enabled():
             path = os.path.realpath(os.path.expanduser(self.path))
             # path should be a string, not unicode
-            path = str(path)
+            if six.PY2:
+                path = str(path)
             do_restore = False
             try:
                 # See if even worth restoring??
@@ -211,10 +229,10 @@
 def is_true(val, addons=None):
     if isinstance(val, (bool)):
         return val is True
-    check_set = ['true', '1', 'on', 'yes']
+    check_set = TRUE_STRINGS
     if addons:
-        check_set = check_set + addons
-    if str(val).lower().strip() in check_set:
+        check_set = list(check_set) + addons
+    if six.text_type(val).lower().strip() in check_set:
         return True
     return False
 
@@ -222,10 +240,10 @@
 def is_false(val, addons=None):
     if isinstance(val, (bool)):
         return val is False
-    check_set = ['off', '0', 'no', 'false']
+    check_set = FALSE_STRINGS
     if addons:
-        check_set = check_set + addons
-    if str(val).lower().strip() in check_set:
+        check_set = list(check_set) + addons
+    if six.text_type(val).lower().strip() in check_set:
         return True
     return False
 
@@ -275,7 +293,7 @@
 def uniq_merge(*lists):
     combined_list = []
     for a_list in lists:
-        if isinstance(a_list, (str, basestring)):
+        if isinstance(a_list, six.string_types):
             a_list = a_list.strip().split(",")
             # Kickout the empty ones
             a_list = [a for a in a_list if len(a)]
@@ -298,14 +316,14 @@
 
 def decomp_gzip(data, quiet=True):
     try:
-        buf = StringIO(str(data))
+        buf = six.BytesIO(encode_text(data))
         with contextlib.closing(gzip.GzipFile(None, "rb", 1, buf)) as gh:
-            return gh.read()
+            return decode_binary(gh.read())
     except Exception as e:
         if quiet:
             return data
         else:
-            raise DecompressionError(str(e))
+            raise DecompressionError(six.text_type(e))
 
 
 def extract_usergroup(ug_pair):
@@ -364,7 +382,7 @@
 
 
 def load_json(text, root_types=(dict,)):
-    decoded = json.loads(text)
+    decoded = json.loads(decode_binary(text))
     if not isinstance(decoded, tuple(root_types)):
         expected_types = ", ".join([str(t) for t in root_types])
         raise TypeError("(%s) root types expected, got %s instead"
@@ -396,7 +414,7 @@
     if key not in yobj:
         return default
     val = yobj[key]
-    if not isinstance(val, (str, basestring)):
+    if not isinstance(val, six.string_types):
         val = str(val)
     return val
 
@@ -431,7 +449,7 @@
     if isinstance(val, (list)):
         cval = [v for v in val]
         return cval
-    if not isinstance(val, (basestring)):
+    if not isinstance(val, six.string_types):
         val = str(val)
     return [val]
 
@@ -706,11 +724,11 @@
 
 def load_yaml(blob, default=None, allowed=(dict,)):
     loaded = default
+    blob = decode_binary(blob)
     try:
-        blob = str(blob)
-        LOG.debug(("Attempting to load yaml from string "
-                 "of length %s with allowed root types %s"),
-                 len(blob), allowed)
+        LOG.debug("Attempting to load yaml from string "
+                  "of length %s with allowed root types %s",
+                  len(blob), allowed)
         converted = safeyaml.load(blob)
         if not isinstance(converted, allowed):
             # Yes this will just be caught, but thats ok for now...
@@ -782,7 +800,7 @@
     if "conf_d" in cfg:
         confd = cfg['conf_d']
         if confd:
-            if not isinstance(confd, (str, basestring)):
+            if not isinstance(confd, six.string_types):
                 raise TypeError(("Config file %s contains 'conf_d' "
                                  "with non-string type %s") %
                                  (cfgfile, type_utils.obj_name(confd)))
@@ -1074,9 +1092,9 @@
     return out_list
 
 
-def load_file(fname, read_cb=None, quiet=False):
+def load_file(fname, read_cb=None, quiet=False, decode=True):
     LOG.debug("Reading from %s (quiet=%s)", fname, quiet)
-    ofh = StringIO()
+    ofh = six.BytesIO()
     try:
         with open(fname, 'rb') as ifh:
             pipe_in_out(ifh, ofh, chunk_cb=read_cb)
@@ -1087,7 +1105,10 @@
             raise
     contents = ofh.getvalue()
     LOG.debug("Read %s bytes from %s", len(contents), fname)
-    return contents
+    if decode:
+        return decode_binary(contents)
+    else:
+        return contents
 
 
 def get_cmdline():
@@ -1217,7 +1238,7 @@
 
 def hash_blob(blob, routine, mlen=None):
     hasher = hashlib.new(routine)
-    hasher.update(blob)
+    hasher.update(encode_text(blob))
     digest = hasher.hexdigest()
     # Don't get to long now
     if mlen is not None:
@@ -1248,7 +1269,7 @@
     os.rename(src, dest)
 
 
-def ensure_dirs(dirlist, mode=0755):
+def ensure_dirs(dirlist, mode=0o755):
     for d in dirlist:
         ensure_dir(d, mode)
 
@@ -1262,7 +1283,7 @@
             return
         try:
             if key and content:
-                write_file(target_fn, content, mode=0600)
+                write_file(target_fn, content, mode=0o600)
                 LOG.debug(("Wrote to %s with contents of command line"
                           " url %s (len=%s)"), target_fn, url, len(content))
             elif key and not content:
@@ -1454,7 +1475,7 @@
     write_file(path, content, omode="ab", mode=None)
 
 
-def ensure_file(path, mode=0644):
+def ensure_file(path, mode=0o644):
     write_file(path, content='', omode="ab", mode=mode)
 
 
@@ -1472,7 +1493,7 @@
             os.chmod(path, real_mode)
 
 
-def write_file(filename, content, mode=0644, omode="wb"):
+def write_file(filename, content, mode=0o644, omode="wb"):
     """
     Writes a file with the given content and sets the file mode as specified.
     Resotres the SELinux context if possible.
@@ -1480,11 +1501,17 @@
     @param filename: The full path of the file to write.
     @param content: The content to write to the file.
     @param mode: The filesystem mode to set on the file.
-    @param omode: The open mode used when opening the file (r, rb, a, etc.)
+    @param omode: The open mode used when opening the file (w, wb, a, etc.)
     """
     ensure_dir(os.path.dirname(filename))
-    LOG.debug("Writing to %s - %s: [%s] %s bytes",
-               filename, omode, mode, len(content))
+    if 'b' in omode.lower():
+        content = encode_text(content)
+        write_type = 'bytes'
+    else:
+        content = decode_binary(content)
+        write_type = 'characters'
+    LOG.debug("Writing to %s - %s: [%s] %s %s",
+               filename, omode, mode, len(content), write_type)
     with SeLinuxGuard(path=filename):
         with open(filename, omode) as fh:
             fh.write(content)
@@ -1573,10 +1600,10 @@
         if isinstance(args, list):
             fixed = []
             for f in args:
-                fixed.append("'%s'" % (str(f).replace("'", escaped)))
+                fixed.append("'%s'" % (six.text_type(f).replace("'", escaped)))
             content = "%s%s\n" % (content, ' '.join(fixed))
             cmds_made += 1
-        elif isinstance(args, (str, basestring)):
+        elif isinstance(args, six.string_types):
             content = "%s%s\n" % (content, args)
             cmds_made += 1
         else:
@@ -1687,7 +1714,7 @@
 
     pkglist = []
     for pkg in pkgs:
-        if isinstance(pkg, basestring):
+        if isinstance(pkg, six.string_types):
             pkglist.append(pkg)
             continue
 

=== modified file 'packages/bddeb'
--- packages/bddeb	2014-01-17 22:08:58 +0000
+++ packages/bddeb	2014-07-01 22:46:34 +0000
@@ -37,6 +37,7 @@
     'pyserial': 'python-serial',
     'pyyaml': 'python-yaml',
     'requests': 'python-requests',
+    'six': 'python-six',
 }
 DEBUILD_ARGS = ["-us", "-S", "-uc", "-d"]
 

=== modified file 'packages/brpm'
--- packages/brpm	2014-01-17 22:08:58 +0000
+++ packages/brpm	2014-07-01 22:46:34 +0000
@@ -44,6 +44,7 @@
         'pyserial': 'pyserial',
         'pyyaml': 'PyYAML',
         'requests': 'python-requests',
+        'six': 'python-six',
     },
     'suse': {
         'argparse': 'python-argparse',
@@ -55,6 +56,7 @@
         'pyserial': 'python-pyserial',
         'pyyaml': 'python-yaml',
         'requests': 'python-requests',
+        'six': 'python-six',
     }
 }
 

=== modified file 'requirements.txt'
--- requirements.txt	2014-02-12 10:14:49 +0000
+++ requirements.txt	2014-07-01 22:46:34 +0000
@@ -31,3 +31,6 @@
 
 # For patching pieces of cloud-config together
 jsonpatch
+
+# For py2/3 simultaneous compatibility
+six

=== modified file 'tests/unittests/test_data.py'
--- tests/unittests/test_data.py	2014-01-17 15:27:09 +0000
+++ tests/unittests/test_data.py	2014-07-01 22:46:34 +0000
@@ -1,11 +1,12 @@
 """Tests for handling of userdata within cloud init."""
 
-import StringIO
-
 import gzip
 import logging
 import os
 
+from six import BytesIO
+from six import StringIO
+
 from email.mime.application import MIMEApplication
 from email.mime.base import MIMEBase
 from email.mime.multipart import MIMEMultipart
@@ -53,7 +54,7 @@
         self.patchUtils(root)
 
     def capture_log(self, lvl=logging.DEBUG):
-        log_file = StringIO.StringIO()
+        log_file = StringIO()
         self._log_handler = logging.StreamHandler(log_file)
         self._log_handler.setLevel(lvl)
         self._log = log.getLogger()
@@ -352,9 +353,9 @@
         """Tests that individual message gzip encoding works."""
 
         def gzip_part(text):
-            contents = StringIO.StringIO()
-            f = gzip.GzipFile(fileobj=contents, mode='w')
-            f.write(str(text))
+            contents = BytesIO()
+            f = gzip.GzipFile(fileobj=contents, mode='wb')
+            f.write(util.encode_text(text))
             f.flush()
             f.close()
             return MIMEApplication(contents.getvalue(), 'gzip')

=== modified file 'tests/unittests/test_datasource/test_nocloud.py'
--- tests/unittests/test_datasource/test_nocloud.py	2014-02-26 19:28:46 +0000
+++ tests/unittests/test_datasource/test_nocloud.py	2014-07-01 22:46:34 +0000
@@ -86,7 +86,7 @@
 
         data = {
             'fs_label': None,
-            'meta-data': {'instance-id': 'IID'},
+            'meta-data': yaml.safe_dump({'instance-id': 'IID'}),
             'user-data': "USER_DATA_RAW",
         }
 

=== modified file 'tests/unittests/test_datasource/test_openstack.py'
--- tests/unittests/test_datasource/test_openstack.py	2014-02-08 20:20:33 +0000
+++ tests/unittests/test_datasource/test_openstack.py	2014-07-01 22:46:34 +0000
@@ -20,9 +20,8 @@
 import json
 import re
 
-from StringIO import StringIO
-
-from urlparse import urlparse
+from six import StringIO
+from six.moves.urllib.parse import urlparse
 
 from tests.unittests import helpers as test_helpers
 

=== modified file 'tests/unittests/test_distros/test_netconfig.py'
--- tests/unittests/test_distros/test_netconfig.py	2012-10-11 19:49:45 +0000
+++ tests/unittests/test_distros/test_netconfig.py	2014-07-01 22:46:34 +0000
@@ -4,6 +4,8 @@
 
 import os
 
+from six import StringIO
+
 from cloudinit import distros
 from cloudinit import helpers
 from cloudinit import settings
@@ -11,9 +13,6 @@
 
 from cloudinit.distros.parsers.sys_conf import SysConf
 
-from StringIO import StringIO
-
-
 BASE_NET_CFG = '''
 auto lo
 iface lo inet loopback

=== modified file 'tests/unittests/test_handler/test_handler_locale.py'
--- tests/unittests/test_handler/test_handler_locale.py	2013-06-25 06:57:27 +0000
+++ tests/unittests/test_handler/test_handler_locale.py	2014-07-01 22:46:34 +0000
@@ -29,7 +29,7 @@
 
 from configobj import ConfigObj
 
-from StringIO import StringIO
+from six import BytesIO
 
 import logging
 
@@ -59,6 +59,6 @@
         cc = self._get_cloud('sles')
         cc_locale.handle('cc_locale', cfg, cc, LOG, [])
 
-        contents = util.load_file('/etc/sysconfig/language')
-        n_cfg = ConfigObj(StringIO(contents))
+        contents = util.load_file('/etc/sysconfig/language', decode=False)
+        n_cfg = ConfigObj(BytesIO(contents))
         self.assertEquals({'RC_LANG': cfg['locale']}, dict(n_cfg))

=== modified file 'tests/unittests/test_handler/test_handler_seed_random.py'
--- tests/unittests/test_handler/test_handler_seed_random.py	2014-03-04 19:35:09 +0000
+++ tests/unittests/test_handler/test_handler_seed_random.py	2014-07-01 22:46:34 +0000
@@ -22,7 +22,7 @@
 import gzip
 import tempfile
 
-from StringIO import StringIO
+from six import StringIO
 
 from cloudinit import cloud
 from cloudinit import distros

=== modified file 'tests/unittests/test_handler/test_handler_set_hostname.py'
--- tests/unittests/test_handler/test_handler_set_hostname.py	2013-06-25 06:57:27 +0000
+++ tests/unittests/test_handler/test_handler_set_hostname.py	2014-07-01 22:46:34 +0000
@@ -9,7 +9,7 @@
 
 import logging
 
-from StringIO import StringIO
+from six import BytesIO
 
 from configobj import ConfigObj
 
@@ -37,8 +37,8 @@
         self.patchUtils(self.tmp)
         cc_set_hostname.handle('cc_set_hostname',
                                cfg, cc, LOG, [])
-        contents = util.load_file("/etc/sysconfig/network")
-        n_cfg = ConfigObj(StringIO(contents))
+        contents = util.load_file("/etc/sysconfig/network", decode=False)
+        n_cfg = ConfigObj(BytesIO(contents))
         self.assertEquals({'HOSTNAME': 'blah.blah.blah.yahoo.com'},
                           dict(n_cfg))
 

=== modified file 'tests/unittests/test_handler/test_handler_timezone.py'
--- tests/unittests/test_handler/test_handler_timezone.py	2013-06-25 06:57:27 +0000
+++ tests/unittests/test_handler/test_handler_timezone.py	2014-07-01 22:46:34 +0000
@@ -29,7 +29,7 @@
 
 from configobj import ConfigObj
 
-from StringIO import StringIO
+from six import BytesIO
 
 import logging
 
@@ -67,8 +67,8 @@
 
         cc_timezone.handle('cc_timezone', cfg, cc, LOG, [])
 
-        contents = util.load_file('/etc/sysconfig/clock')
-        n_cfg = ConfigObj(StringIO(contents))
+        contents = util.load_file('/etc/sysconfig/clock', decode=False)
+        n_cfg = ConfigObj(BytesIO(contents))
         self.assertEquals({'TIMEZONE': cfg['timezone']}, dict(n_cfg))
 
         contents = util.load_file('/etc/localtime')

=== modified file 'tests/unittests/test_handler/test_handler_yum_add_repo.py'
--- tests/unittests/test_handler/test_handler_yum_add_repo.py	2014-04-01 18:20:57 +0000
+++ tests/unittests/test_handler/test_handler_yum_add_repo.py	2014-07-01 22:46:34 +0000
@@ -6,7 +6,7 @@
 
 import logging
 
-from StringIO import StringIO
+from six import BytesIO
 
 import configobj
 
@@ -52,8 +52,9 @@
         }
         self.patchUtils(self.tmp)
         cc_yum_add_repo.handle('yum_add_repo', cfg, None, LOG, [])
-        contents = util.load_file("/etc/yum.repos.d/epel_testing.repo")
-        contents = configobj.ConfigObj(StringIO(contents))
+        contents = util.load_file("/etc/yum.repos.d/epel_testing.repo",
+                                  decode=False)
+        contents = configobj.ConfigObj(BytesIO(contents))
         expected = {
             'epel_testing': {
                 'name': 'Extra Packages for Enterprise Linux 5 - Testing',


Follow ups