← Back to team overview

cloud-init-dev team mailing list archive

[Merge] lp:~juergh/cloud-init/add-sles-support into lp:cloud-init

 

You have been requested to review the proposed merge of lp:~juergh/cloud-init/add-sles-support into lp:cloud-init.

For more details, see:
https://code.launchpad.net/~juergh/cloud-init/add-sles-support/+merge/171223

This branch adds support for SLES plus some basic unittests. Some code from the RHEL handler was moved to a new file so that it can be used by the SLES handler.

-- 
https://code.launchpad.net/~juergh/cloud-init/add-sles-support/+merge/171223
Your team cloud init development team is requested to review the proposed merge of lp:~juergh/cloud-init/add-sles-support into lp:cloud-init.
=== modified file 'cloudinit/config/cc_resolv_conf.py'
--- cloudinit/config/cc_resolv_conf.py	2013-01-28 16:32:45 +0000
+++ cloudinit/config/cc_resolv_conf.py	2013-06-25 07:03:26 +0000
@@ -1,8 +1,12 @@
 # vi: ts=4 expandtab
 #
 #    Copyright (C) 2013 Craig Tracey
+#    Copyright (C) 2013 SUSE LLC
+#    Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
 #
 #    Author: Craig Tracey <craigtracey@xxxxxxxxx>
+#    Author: Robert Schweikert <rjschwei@xxxxxxxx>
+#    Author: Juerg Haefliger <juerg.haefliger@xxxxxx>
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License version 3, as
@@ -53,7 +57,7 @@
 
 frequency = PER_INSTANCE
 
-distros = ['fedora', 'rhel']
+distros = ['fedora', 'rhel', 'sles']
 
 
 def generate_resolv_conf(cloud, log, params):

=== modified file 'cloudinit/distros/__init__.py'
--- cloudinit/distros/__init__.py	2013-06-19 06:44:00 +0000
+++ cloudinit/distros/__init__.py	2013-06-25 07:03:26 +0000
@@ -3,11 +3,13 @@
 #    Copyright (C) 2012 Canonical Ltd.
 #    Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
 #    Copyright (C) 2012 Yahoo! Inc.
+#    Copyright (C) 2013 SUSE LLC
 #
 #    Author: Scott Moser <scott.moser@xxxxxxxxxxxxx>
 #    Author: Juerg Haefliger <juerg.haefliger@xxxxxx>
 #    Author: Joshua Harlow <harlowja@xxxxxxxxxxxxx>
 #    Author: Ben Howard <ben.howard@xxxxxxxxxxxxx>
+#    Author: Robert Schweikert <rjschwei@xxxxxxxx>
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License version 3, as
@@ -38,7 +40,8 @@
 
 OSFAMILIES = {
     'debian': ['debian', 'ubuntu'],
-    'redhat': ['fedora', 'rhel']
+    'redhat': ['fedora', 'rhel'],
+    'suse': ['sles']
 }
 
 LOG = logging.getLogger(__name__)
@@ -281,15 +284,16 @@
     def get_default_user(self):
         return self.get_option('default_user')
 
-    def create_user(self, name, **kwargs):
-        """
-            Creates users for the system using the GNU passwd tools. This
-            will work on an GNU system. This should be overriden on
-            distros where useradd is not desirable or not available.
-        """
+    def add_user(self, name, **kwargs):
+        """
+        Add a user to the system using standard GNU tools
+        """
+        if util.is_user(name):
+            LOG.info("User %s already exists, skipping." % name)
+            return
 
         adduser_cmd = ['useradd', name]
-        x_adduser_cmd = ['useradd', name]
+        log_adduser_cmd = ['useradd', name]
 
         # Since we are creating users, we want to carefully validate the
         # inputs. If something goes wrong, we can end up with a system
@@ -306,63 +310,65 @@
             "selinux_user": '--selinux-user',
         }
 
-        adduser_opts_flags = {
+        adduser_flags = {
             "no_user_group": '--no-user-group',
             "system": '--system',
             "no_log_init": '--no-log-init',
-            "no_create_home": "-M",
         }
 
-        redact_fields = ['passwd']
-
-        # Now check the value and create the command
-        for option in kwargs:
-            value = kwargs[option]
-            if option in adduser_opts and value \
-                and isinstance(value, str):
-                adduser_cmd.extend([adduser_opts[option], value])
-                # Redact certain fields from the logs
-                if option in redact_fields:
-                    x_adduser_cmd.extend([adduser_opts[option], 'REDACTED'])
-                else:
-                    x_adduser_cmd.extend([adduser_opts[option], value])
-            elif option in adduser_opts_flags and value:
-                adduser_cmd.append(adduser_opts_flags[option])
-                # Redact certain fields from the logs
-                if option in redact_fields:
-                    x_adduser_cmd.append('REDACTED')
-                else:
-                    x_adduser_cmd.append(adduser_opts_flags[option])
-
-        # Default to creating home directory unless otherwise directed
-        #  Also, we do not create home directories for system users.
-        if "no_create_home" not in kwargs and "system" not in kwargs:
+        redact_opts = ['passwd']
+
+        # Check the values and create the command
+        for key, val in kwargs.iteritems():
+
+            if key in adduser_opts and val and isinstance(val, str):
+                adduser_cmd.extend([adduser_opts[key], val])
+
+                # Redact certain fields from the logs
+                if key in redact_opts:
+                    log_adduser_cmd.extend([adduser_opts[key], 'REDACTED'])
+                else:
+                    log_adduser_cmd.extend([adduser_opts[key], val])
+
+            elif key in adduser_flags and val:
+                adduser_cmd.append(adduser_flags[key])
+                log_adduser_cmd.append(adduser_flags[key])
+
+        # Don't create the home directory if directed so or if the user is a
+        # system user
+        if 'no_create_home' in kwargs or 'system' in kwargs:
+            adduser_cmd.append('-M')
+            log_adduser_cmd.append('-M')
+        else:
             adduser_cmd.append('-m')
-
-        # Create the user
-        if util.is_user(name):
-            LOG.warn("User %s already exists, skipping." % name)
-        else:
-            LOG.debug("Adding user named %s", name)
-            try:
-                util.subp(adduser_cmd, logstring=x_adduser_cmd)
-            except Exception as e:
-                util.logexc(LOG, "Failed to create user %s", name)
-                raise e
+            log_adduser_cmd.append('-m')
+
+        # Run the command
+        LOG.debug("Adding user %s", name)
+        try:
+            util.subp(adduser_cmd, logstring=log_adduser_cmd)
+        except Exception as e:
+            util.logexc(LOG, "Failed to create user %s", name)
+            raise e
+
+    def create_user(self, name, **kwargs):
+        """
+        Creates users for the system using the GNU passwd tools. This
+        will work on an GNU system. This should be overriden on
+        distros where useradd is not desirable or not available.
+        """
+
+        # Add the user
+        self.add_user(name, **kwargs)
 
         # Set password if plain-text password provided
-        if 'plain_text_passwd' in kwargs and kwargs['plain_text_passwd']:
+        if 'plain_text_passwd' in kwargs:
             self.set_passwd(name, kwargs['plain_text_passwd'])
 
         # Default locking down the account.  'lock_passwd' defaults to True.
         # lock account unless lock_password is False.
         if kwargs.get('lock_passwd', True):
-            try:
-                util.subp(['passwd', '--lock', name])
-            except Exception as e:
-                util.logexc(LOG, "Failed to disable password logins for "
-                            "user %s", name)
-                raise e
+            self.lock_passwd(name)
 
         # Configure sudo access
         if 'sudo' in kwargs:
@@ -375,6 +381,16 @@
 
         return True
 
+    def lock_passwd(self, name):
+        """
+        Lock the password of a user, i.e., disable password logins
+        """
+        try:
+            util.subp(['passwd', '--lock', name])
+        except Exception as e:
+            util.logexc(LOG, 'Failed to disable password for user %s', name)
+            raise e
+
     def set_passwd(self, user, passwd, hashed=False):
         pass_string = '%s:%s' % (user, passwd)
         cmd = ['chpasswd']

=== modified file 'cloudinit/distros/rhel.py'
--- cloudinit/distros/rhel.py	2013-06-19 06:44:00 +0000
+++ cloudinit/distros/rhel.py	2013-06-25 07:03:26 +0000
@@ -3,10 +3,12 @@
 #    Copyright (C) 2012 Canonical Ltd.
 #    Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
 #    Copyright (C) 2012 Yahoo! Inc.
+#    Copyright (C) 2013 SUSE LLC
 #
 #    Author: Scott Moser <scott.moser@xxxxxxxxxxxxx>
 #    Author: Juerg Haefliger <juerg.haefliger@xxxxxx>
 #    Author: Joshua Harlow <harlowja@xxxxxxxxxxxxx>
+#    Author: Robert Schweikert <rjschwei@xxxxxxxx>
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU General Public License version 3, as
@@ -23,14 +25,11 @@
 import os
 
 from cloudinit import distros
-
-from cloudinit.distros.parsers.resolv_conf import ResolvConf
-from cloudinit.distros.parsers.sys_conf import SysConf
-
 from cloudinit import helpers
 from cloudinit import log as logging
 from cloudinit import util
 
+from cloudinit.distros import rhel_util
 from cloudinit.settings import PER_INSTANCE
 
 LOG = logging.getLogger(__name__)
@@ -67,32 +66,9 @@
     def install_packages(self, pkglist):
         self.package_command('install', pkgs=pkglist)
 
-    def _adjust_resolve(self, dns_servers, search_servers):
-        try:
-            r_conf = ResolvConf(util.load_file(self.resolve_conf_fn))
-            r_conf.parse()
-        except IOError:
-            util.logexc(LOG, "Failed at parsing %s reverting to an empty "
-                        "instance", self.resolve_conf_fn)
-            r_conf = ResolvConf('')
-            r_conf.parse()
-        if dns_servers:
-            for s in dns_servers:
-                try:
-                    r_conf.add_nameserver(s)
-                except ValueError:
-                    util.logexc(LOG, "Failed at adding nameserver %s", s)
-        if search_servers:
-            for s in search_servers:
-                try:
-                    r_conf.add_search_domain(s)
-                except ValueError:
-                    util.logexc(LOG, "Failed at adding search domain %s", s)
-        util.write_file(self.resolve_conf_fn, str(r_conf), 0644)
-
     def _write_network(self, settings):
         # TODO(harlowja) fix this... since this is the ubuntu format
-        entries = translate_network(settings)
+        entries = rhel_util.translate_network(settings)
         LOG.debug("Translated ubuntu style network settings %s into %s",
                   settings, entries)
         # Make the intermediate format as the rhel format...
@@ -111,41 +87,21 @@
                 'MACADDR': info.get('hwaddress'),
                 'ONBOOT': _make_sysconfig_bool(info.get('auto')),
             }
-            self._update_sysconfig_file(net_fn, net_cfg)
+            rhel_util.update_sysconfig_file(net_fn, net_cfg)
             if 'dns-nameservers' in info:
                 nameservers.extend(info['dns-nameservers'])
             if 'dns-search' in info:
                 searchservers.extend(info['dns-search'])
         if nameservers or searchservers:
-            self._adjust_resolve(nameservers, searchservers)
+            rhel_util.update_resolve_conf_file(self.resolve_conf_fn,
+                                               nameservers, searchservers)
         if dev_names:
             net_cfg = {
                 'NETWORKING': _make_sysconfig_bool(True),
             }
-            self._update_sysconfig_file(self.network_conf_fn, net_cfg)
+            rhel_util.update_sysconfig_file(self.network_conf_fn, net_cfg)
         return dev_names
 
-    def _update_sysconfig_file(self, fn, adjustments, allow_empty=False):
-        if not adjustments:
-            return
-        (exists, contents) = self._read_conf(fn)
-        updated_am = 0
-        for (k, v) in adjustments.items():
-            if v is None:
-                continue
-            v = str(v)
-            if len(v) == 0 and not allow_empty:
-                continue
-            contents[k] = v
-            updated_am += 1
-        if updated_am:
-            lines = [
-                str(contents),
-            ]
-            if not exists:
-                lines.insert(0, util.make_header())
-            util.write_file(fn, "\n".join(lines) + "\n", 0644)
-
     def _dist_uses_systemd(self):
         # Fedora 18 and RHEL 7 were the first adopters in their series
         (dist, vers) = util.system_info()['dist'][:2]
@@ -164,7 +120,7 @@
         locale_cfg = {
             'LANG': locale,
         }
-        self._update_sysconfig_file(out_fn, locale_cfg)
+        rhel_util.update_sysconfig_file(out_fn, locale_cfg)
 
     def _write_hostname(self, hostname, out_fn):
         if self._dist_uses_systemd():
@@ -173,7 +129,7 @@
             host_cfg = {
                 'HOSTNAME': hostname,
             }
-            self._update_sysconfig_file(out_fn, host_cfg)
+            rhel_util.update_sysconfig_file(out_fn, host_cfg)
 
     def _select_hostname(self, hostname, fqdn):
         # See: http://bit.ly/TwitgL
@@ -197,22 +153,12 @@
             else:
                 return default
         else:
-            (_exists, contents) = self._read_conf(filename)
+            (_exists, contents) = rhel_util.read_sysconfig_file(filename)
             if 'HOSTNAME' in contents:
                 return contents['HOSTNAME']
             else:
                 return default
 
-    def _read_conf(self, fn):
-        exists = False
-        try:
-            contents = util.load_file(fn).splitlines()
-            exists = True
-        except IOError:
-            contents = []
-        return (exists,
-                SysConf(contents))
-
     def _bring_up_interfaces(self, device_names):
         if device_names and 'all' in device_names:
             raise RuntimeError(('Distro %s can not translate '
@@ -236,7 +182,7 @@
             clock_cfg = {
                 'ZONE': str(tz),
             }
-            self._update_sysconfig_file(self.clock_conf_fn, clock_cfg)
+            rhel_util.update_sysconfig_file(self.clock_conf_fn, clock_cfg)
             # This ensures that the correct tz will be used for the system
             util.copy(tz_file, self.tz_local_fn)
 
@@ -271,90 +217,3 @@
     def update_package_sources(self):
         self._runner.run("update-sources", self.package_command,
                          ["makecache"], freq=PER_INSTANCE)
-
-
-# This is a util function to translate a ubuntu /etc/network/interfaces 'blob'
-# to a rhel equiv. that can then be written to /etc/sysconfig/network-scripts/
-# TODO(harlowja) remove when we have python-netcf active...
-def translate_network(settings):
-    # Get the standard cmd, args from the ubuntu format
-    entries = []
-    for line in settings.splitlines():
-        line = line.strip()
-        if not line or line.startswith("#"):
-            continue
-        split_up = line.split(None, 1)
-        if len(split_up) <= 1:
-            continue
-        entries.append(split_up)
-    # Figure out where each iface section is
-    ifaces = []
-    consume = {}
-    for (cmd, args) in entries:
-        if cmd == 'iface':
-            if consume:
-                ifaces.append(consume)
-                consume = {}
-            consume[cmd] = args
-        else:
-            consume[cmd] = args
-    # Check if anything left over to consume
-    absorb = False
-    for (cmd, args) in consume.iteritems():
-        if cmd == 'iface':
-            absorb = True
-    if absorb:
-        ifaces.append(consume)
-    # Now translate
-    real_ifaces = {}
-    for info in ifaces:
-        if 'iface' not in info:
-            continue
-        iface_details = info['iface'].split(None)
-        dev_name = None
-        if len(iface_details) >= 1:
-            dev = iface_details[0].strip().lower()
-            if dev:
-                dev_name = dev
-        if not dev_name:
-            continue
-        iface_info = {}
-        if len(iface_details) >= 3:
-            proto_type = iface_details[2].strip().lower()
-            # Seems like this can be 'loopback' which we don't
-            # really care about
-            if proto_type in ['dhcp', 'static']:
-                iface_info['bootproto'] = proto_type
-        # These can just be copied over
-        for k in ['netmask', 'address', 'gateway', 'broadcast']:
-            if k in info:
-                val = info[k].strip().lower()
-                if val:
-                    iface_info[k] = val
-        # Name server info provided??
-        if 'dns-nameservers' in info:
-            iface_info['dns-nameservers'] = info['dns-nameservers'].split()
-        # Name server search info provided??
-        if 'dns-search' in info:
-            iface_info['dns-search'] = info['dns-search'].split()
-        # Is any mac address spoofing going on??
-        if 'hwaddress' in info:
-            hw_info = info['hwaddress'].lower().strip()
-            hw_split = hw_info.split(None, 1)
-            if len(hw_split) == 2 and hw_split[0].startswith('ether'):
-                hw_addr = hw_split[1]
-                if hw_addr:
-                    iface_info['hwaddress'] = hw_addr
-        real_ifaces[dev_name] = iface_info
-    # Check for those that should be started on boot via 'auto'
-    for (cmd, args) in entries:
-        if cmd == 'auto':
-            # Seems like auto can be like 'auto eth0 eth0:1' so just get the
-            # first part out as the device name
-            args = args.split(None)
-            if not args:
-                continue
-            dev_name = args[0].strip().lower()
-            if dev_name in real_ifaces:
-                real_ifaces[dev_name]['auto'] = True
-    return real_ifaces

=== added file 'cloudinit/distros/rhel_util.py'
--- cloudinit/distros/rhel_util.py	1970-01-01 00:00:00 +0000
+++ cloudinit/distros/rhel_util.py	2013-06-25 07:03:26 +0000
@@ -0,0 +1,179 @@
+# vi: ts=4 expandtab
+#
+#    Copyright (C) 2012 Canonical Ltd.
+#    Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
+#    Copyright (C) 2012 Yahoo! Inc.
+#    Copyright (C) 2013 SUSE LLC
+#
+#    Author: Scott Moser <scott.moser@xxxxxxxxxxxxx>
+#    Author: Juerg Haefliger <juerg.haefliger@xxxxxx>
+#    Author: Joshua Harlow <harlowja@xxxxxxxxxxxxx>
+#    Author: Robert Schweikert <rjschwei@xxxxxxxx>
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    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 cloudinit.distros.parsers.resolv_conf import ResolvConf
+from cloudinit.distros.parsers.sys_conf import SysConf
+
+from cloudinit import log as logging
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+
+
+# This is a util function to translate Debian based distro interface blobs as
+# given in /etc/network/interfaces to an equivalent format for distributions
+# that use ifcfg-* style (Red Hat and SUSE).
+# TODO(harlowja) remove when we have python-netcf active...
+def translate_network(settings):
+    # Get the standard cmd, args from the ubuntu format
+    entries = []
+    for line in settings.splitlines():
+        line = line.strip()
+        if not line or line.startswith("#"):
+            continue
+        split_up = line.split(None, 1)
+        if len(split_up) <= 1:
+            continue
+        entries.append(split_up)
+    # Figure out where each iface section is
+    ifaces = []
+    consume = {}
+    for (cmd, args) in entries:
+        if cmd == 'iface':
+            if consume:
+                ifaces.append(consume)
+                consume = {}
+            consume[cmd] = args
+        else:
+            consume[cmd] = args
+    # Check if anything left over to consume
+    absorb = False
+    for (cmd, args) in consume.iteritems():
+        if cmd == 'iface':
+            absorb = True
+    if absorb:
+        ifaces.append(consume)
+    # Now translate
+    real_ifaces = {}
+    for info in ifaces:
+        if 'iface' not in info:
+            continue
+        iface_details = info['iface'].split(None)
+        dev_name = None
+        if len(iface_details) >= 1:
+            dev = iface_details[0].strip().lower()
+            if dev:
+                dev_name = dev
+        if not dev_name:
+            continue
+        iface_info = {}
+        if len(iface_details) >= 3:
+            proto_type = iface_details[2].strip().lower()
+            # Seems like this can be 'loopback' which we don't
+            # really care about
+            if proto_type in ['dhcp', 'static']:
+                iface_info['bootproto'] = proto_type
+        # These can just be copied over
+        for k in ['netmask', 'address', 'gateway', 'broadcast']:
+            if k in info:
+                val = info[k].strip().lower()
+                if val:
+                    iface_info[k] = val
+        # Name server info provided??
+        if 'dns-nameservers' in info:
+            iface_info['dns-nameservers'] = info['dns-nameservers'].split()
+        # Name server search info provided??
+        if 'dns-search' in info:
+            iface_info['dns-search'] = info['dns-search'].split()
+        # Is any mac address spoofing going on??
+        if 'hwaddress' in info:
+            hw_info = info['hwaddress'].lower().strip()
+            hw_split = hw_info.split(None, 1)
+            if len(hw_split) == 2 and hw_split[0].startswith('ether'):
+                hw_addr = hw_split[1]
+                if hw_addr:
+                    iface_info['hwaddress'] = hw_addr
+        real_ifaces[dev_name] = iface_info
+    # Check for those that should be started on boot via 'auto'
+    for (cmd, args) in entries:
+        if cmd == 'auto':
+            # Seems like auto can be like 'auto eth0 eth0:1' so just get the
+            # first part out as the device name
+            args = args.split(None)
+            if not args:
+                continue
+            dev_name = args[0].strip().lower()
+            if dev_name in real_ifaces:
+                real_ifaces[dev_name]['auto'] = True
+    return real_ifaces
+
+
+# Helper function to update a RHEL/SUSE /etc/sysconfig/* file
+def update_sysconfig_file(fn, adjustments, allow_empty=False):
+    if not adjustments:
+        return
+    (exists, contents) = read_sysconfig_file(fn)
+    updated_am = 0
+    for (k, v) in adjustments.items():
+        if v is None:
+            continue
+        v = str(v)
+        if len(v) == 0 and not allow_empty:
+            continue
+        contents[k] = v
+        updated_am += 1
+    if updated_am:
+        lines = [
+            str(contents),
+        ]
+        if not exists:
+            lines.insert(0, util.make_header())
+        util.write_file(fn, "\n".join(lines) + "\n", 0644)
+
+
+# Helper function to read a RHEL/SUSE /etc/sysconfig/* file
+def read_sysconfig_file(fn):
+    exists = False
+    try:
+        contents = util.load_file(fn).splitlines()
+        exists = True
+    except IOError:
+        contents = []
+    return (exists, SysConf(contents))
+
+
+# Helper function to update RHEL/SUSE /etc/resolv.conf
+def update_resolve_conf_file(fn, dns_servers, search_servers):
+    try:
+        r_conf = ResolvConf(util.load_file(fn))
+        r_conf.parse()
+    except IOError:
+        util.logexc(LOG, "Failed at parsing %s reverting to an empty "
+                    "instance", fn)
+        r_conf = ResolvConf('')
+        r_conf.parse()
+    if dns_servers:
+        for s in dns_servers:
+            try:
+                r_conf.add_nameserver(s)
+            except ValueError:
+                util.logexc(LOG, "Failed at adding nameserver %s", s)
+    if search_servers:
+        for s in search_servers:
+            try:
+                r_conf.add_search_domain(s)
+            except ValueError:
+                util.logexc(LOG, "Failed at adding search domain %s", s)
+    util.write_file(fn, str(r_conf), 0644)

=== added file 'cloudinit/distros/sles.py'
--- cloudinit/distros/sles.py	1970-01-01 00:00:00 +0000
+++ cloudinit/distros/sles.py	2013-06-25 07:03:26 +0000
@@ -0,0 +1,226 @@
+# vi: ts=4 expandtab
+#
+#    Copyright (C) 2013 SUSE LLC
+#    Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
+#
+#    Author: Robert Schweikert <rjschwei@xxxxxxxx>
+#    Author: Juerg Haefliger <juerg.haefliger@xxxxxx>
+#
+#    Leaning very heavily on the RHEL and Debian implementation
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    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 os
+
+from cloudinit import distros
+
+from cloudinit.distros.parsers.hostname import HostnameConf
+
+from cloudinit import helpers
+from cloudinit import log as logging
+from cloudinit import util
+
+from cloudinit.distros import rhel_util
+from cloudinit.settings import PER_INSTANCE
+
+LOG = logging.getLogger(__name__)
+
+
+class Distro(distros.Distro):
+    clock_conf_fn = '/etc/sysconfig/clock'
+    locale_conf_fn = '/etc/sysconfig/language'
+    network_conf_fn = '/etc/sysconfig/network'
+    hostname_conf_fn = '/etc/HOSTNAME'
+    network_script_tpl = '/etc/sysconfig/network/ifcfg-%s'
+    resolve_conf_fn = '/etc/resolv.conf'
+    tz_local_fn = '/etc/localtime'
+    tz_zone_dir = '/usr/share/zoneinfo'
+
+    def __init__(self, name, cfg, paths):
+        distros.Distro.__init__(self, name, cfg, paths)
+        # This will be used to restrict certain
+        # calls from repeatly happening (when they
+        # should only happen say once per instance...)
+        self._runner = helpers.Runners(paths)
+        self.osfamily = 'suse'
+
+    def install_packages(self, pkglist):
+        self.package_command('install', args='-l', pkgs=pkglist)
+
+    def _write_network(self, settings):
+        # Convert debian settings to ifcfg format
+        entries = rhel_util.translate_network(settings)
+        LOG.debug("Translated ubuntu style network settings %s into %s",
+                  settings, entries)
+        # Make the intermediate format as the suse format...
+        nameservers = []
+        searchservers = []
+        dev_names = entries.keys()
+        for (dev, info) in entries.iteritems():
+            net_fn = self.network_script_tpl % (dev)
+            mode = info.get('auto')
+            if mode and mode.lower() == 'true':
+                mode = 'auto'
+            else:
+                mode = 'manual'
+            net_cfg = {
+                'BOOTPROTO': info.get('bootproto'),
+                'BROADCAST': info.get('broadcast'),
+                'GATEWAY': info.get('gateway'),
+                'IPADDR': info.get('address'),
+                'LLADDR': info.get('hwaddress'),
+                'NETMASK': info.get('netmask'),
+                'STARTMODE': mode,
+                'USERCONTROL': 'no'
+            }
+            if dev != 'lo':
+                net_cfg['ETHERDEVICE'] = dev
+                net_cfg['ETHTOOL_OPTIONS'] = ''
+            else:
+                net_cfg['FIREWALL'] = 'no'
+            rhel_util.update_sysconfig_file(net_fn, net_cfg, True)
+            if 'dns-nameservers' in info:
+                nameservers.extend(info['dns-nameservers'])
+            if 'dns-search' in info:
+                searchservers.extend(info['dns-search'])
+        if nameservers or searchservers:
+            rhel_util.update_resolve_conf_file(self.resolve_conf_fn,
+                                               nameservers, searchservers)
+        return dev_names
+
+    def apply_locale(self, locale, out_fn=None):
+        if not out_fn:
+            out_fn = self.locale_conf_fn
+        locale_cfg = {
+            'RC_LANG': locale,
+        }
+        rhel_util.update_sysconfig_file(out_fn, locale_cfg)
+
+    def _write_hostname(self, hostname, out_fn):
+        conf = None
+        try:
+            # Try to update the previous one
+            # so lets see if we can read it first.
+            conf = self._read_hostname_conf(out_fn)
+        except IOError:
+            pass
+        if not conf:
+            conf = HostnameConf('')
+        conf.set_hostname(hostname)
+        util.write_file(out_fn, str(conf), 0644)
+
+    def _select_hostname(self, hostname, fqdn):
+        # Prefer the short hostname over the long
+        # fully qualified domain name
+        if not hostname:
+            return fqdn
+        return hostname
+
+    def _read_system_hostname(self):
+        host_fn = self.hostname_conf_fn
+        return (host_fn, self._read_hostname(host_fn))
+
+    def _read_hostname_conf(self, filename):
+        conf = HostnameConf(util.load_file(filename))
+        conf.parse()
+        return conf
+
+    def _read_hostname(self, filename, default=None):
+        hostname = None
+        try:
+            conf = self._read_hostname_conf(filename)
+            hostname = conf.hostname
+        except IOError:
+            pass
+        if not hostname:
+            return default
+        return hostname
+
+    def _bring_up_interfaces(self, device_names):
+        if device_names and 'all' in device_names:
+            raise RuntimeError(('Distro %s can not translate '
+                                'the device name "all"') % (self.name))
+        return distros.Distro._bring_up_interfaces(self, device_names)
+
+    def set_timezone(self, tz):
+        # TODO(harlowja): move this code into
+        # the parent distro...
+        tz_file = os.path.join(self.tz_zone_dir, str(tz))
+        if not os.path.isfile(tz_file):
+            raise RuntimeError(("Invalid timezone %s,"
+                                " no file found at %s") % (tz, tz_file))
+        # Adjust the sysconfig clock zone setting
+        clock_cfg = {
+            'TIMEZONE': str(tz),
+        }
+        rhel_util.update_sysconfig_file(self.clock_conf_fn, clock_cfg)
+        # This ensures that the correct tz will be used for the system
+        util.copy(tz_file, self.tz_local_fn)
+
+    def package_command(self, command, args=None, pkgs=None):
+        if pkgs is None:
+            pkgs = []
+
+        cmd = ['zypper']
+        # No user interaction possible, enable non-interactive mode
+        cmd.append('-t')
+        # Do not check the keys, we assume that the initial repos configured
+        # in the image can be trusted
+        cmd.append('--no-gpg-checks')
+
+        # Comand is the operation, such as install
+        cmd.append(command)
+
+        # args are the arguments to the command, not global options
+        if args and isinstance(args, str):
+            cmd.append(args)
+        elif args and isinstance(args, list):
+            cmd.extend(args)
+
+        pkglist = util.expand_package_list('%s-%s', pkgs)
+        cmd.extend(pkglist)
+
+        # Allow the output of this to flow outwards (ie not be captured)
+        util.subp(cmd, capture=False)
+
+    def update_package_sources(self):
+        self._runner.run("update-sources", self.package_command,
+                         ['refresh'], freq=PER_INSTANCE)
+
+    # Copied from parent class and modified to use short option names since
+    # the SLES command doesn't support long names (yet). This method can be
+    # removed when SLES finally catches up.
+    def lock_passwd(self, name):
+        """
+        Lock the password of a user, i.e., disable password logins
+        """
+        try:
+            util.subp(['passwd', '-l', name])
+        except Exception as e:
+            util.logexc(LOG, 'Failed to disable password for user %s', name)
+            raise e
+
+    # Copied from parent class and modified to use short option names since
+    # the SLES command doesn't support long names (yet). This method can be
+    # removed when SLES finally catches up.
+    def set_passwd(self, user, passwd, hashed=False):
+        pass_string = '%s:%s' % (user, passwd)
+        cmd = ['chpasswd']
+        if hashed:
+            cmd.append('-e')
+        try:
+            util.subp(cmd, pass_string, logstring="chpasswd for %s" % user)
+        except Exception as e:
+            util.logexc(LOG, "Failed to set password for %s", user)
+            raise e
+        return True

=== added file 'templates/hosts.suse.tmpl'
--- templates/hosts.suse.tmpl	1970-01-01 00:00:00 +0000
+++ templates/hosts.suse.tmpl	2013-06-25 07:03:26 +0000
@@ -0,0 +1,24 @@
+#*
+    This file /etc/cloud/templates/hosts.suse.tmpl is only utilized
+    if enabled in cloud-config.  Specifically, in order to enable it
+    you need to add the following to config:
+      manage_etc_hosts: True
+*#
+# Your system has configured 'manage_etc_hosts' as True.
+# As a result, if you wish for changes to this file to persist
+# then you will need to either
+# a.) make changes to the master file in /etc/cloud/templates/hosts.suse.tmpl
+# b.) change or remove the value of 'manage_etc_hosts' in
+#     /etc/cloud/cloud.cfg or cloud-config from user-data
+#
+# The following lines are desirable for IPv4 capable hosts
+127.0.0.1 localhost
+
+# The following lines are desirable for IPv6 capable hosts
+::1 localhost ipv6-localhost ipv6-loopback
+fe00::0 ipv6-localnet
+
+ff00::0 ipv6-mcastprefix
+ff02::1 ipv6-allnodes
+ff02::2 ipv6-allrouters
+ff02::3 ipv6-allhosts

=== modified file 'tests/unittests/helpers.py'
--- tests/unittests/helpers.py	2013-03-08 23:57:05 +0000
+++ tests/unittests/helpers.py	2013-06-25 07:03:26 +0000
@@ -146,7 +146,8 @@
                    ('chmod', 1),
                    ('delete_dir_contents', 1),
                    ('del_file', 1),
-                   ('sym_link', -1)],
+                   ('sym_link', -1),
+                   ('copy', -1)],
         }
         for (mod, funcs) in patch_funcs.items():
             for (f, am) in funcs:

=== added file 'tests/unittests/test_handler/test_handler_locale.py'
--- tests/unittests/test_handler/test_handler_locale.py	1970-01-01 00:00:00 +0000
+++ tests/unittests/test_handler/test_handler_locale.py	2013-06-25 07:03:26 +0000
@@ -0,0 +1,64 @@
+#    Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
+#
+#    Author: Juerg Haefliger <juerg.haefliger@xxxxxx>
+#
+#    Based on test_handler_set_hostname.py
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    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 cloudinit.config import cc_locale
+
+from cloudinit import cloud
+from cloudinit import distros
+from cloudinit import helpers
+from cloudinit import util
+
+from cloudinit.sources import DataSourceNoCloud
+
+from tests.unittests import helpers as t_help
+
+from configobj import ConfigObj
+
+from StringIO import StringIO
+
+import logging
+
+LOG = logging.getLogger(__name__)
+
+
+class TestLocale(t_help.FilesystemMockingTestCase):
+    def setUp(self):
+        super(TestLocale, self).setUp()
+        self.new_root = self.makeDir(prefix="unittest_")
+
+    def _get_cloud(self, distro):
+        self.patchUtils(self.new_root)
+        paths = helpers.Paths({})
+
+        cls = distros.fetch(distro)
+        d = cls(distro, {}, paths)
+        ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths)
+        cc = cloud.Cloud(ds, paths, {}, d, None)
+        return cc
+
+    def test_set_locale_sles(self):
+
+        cfg = {
+            'locale': 'My.Locale',
+        }
+        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))
+        self.assertEquals({'RC_LANG': cfg['locale']}, dict(n_cfg))

=== modified file 'tests/unittests/test_handler/test_handler_set_hostname.py'
--- tests/unittests/test_handler/test_handler_set_hostname.py	2013-05-13 22:12:30 +0000
+++ tests/unittests/test_handler/test_handler_set_hostname.py	2013-06-25 07:03:26 +0000
@@ -55,3 +55,16 @@
                                cfg, cc, LOG, [])
         contents = util.load_file("/etc/hostname")
         self.assertEquals('blah', contents.strip())
+
+    def test_write_hostname_sles(self):
+        cfg = {
+            'hostname': 'blah.blah.blah.suse.com',
+        }
+        distro = self._fetch_distro('sles')
+        paths = helpers.Paths({})
+        ds = None
+        cc = cloud.Cloud(ds, paths, {}, distro, None)
+        self.patchUtils(self.tmp)
+        cc_set_hostname.handle('cc_set_hostname', cfg, cc, LOG, [])
+        contents = util.load_file("/etc/HOSTNAME")
+        self.assertEquals('blah', contents.strip())

=== added file 'tests/unittests/test_handler/test_handler_timezone.py'
--- tests/unittests/test_handler/test_handler_timezone.py	1970-01-01 00:00:00 +0000
+++ tests/unittests/test_handler/test_handler_timezone.py	2013-06-25 07:03:26 +0000
@@ -0,0 +1,75 @@
+#    Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
+#
+#    Author: Juerg Haefliger <juerg.haefliger@xxxxxx>
+#
+#    Based on test_handler_set_hostname.py
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License version 3, as
+#    published by the Free Software Foundation.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    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 cloudinit.config import cc_timezone
+
+from cloudinit import cloud
+from cloudinit import distros
+from cloudinit import helpers
+from cloudinit import util
+
+from cloudinit.sources import DataSourceNoCloud
+
+from tests.unittests import helpers as t_help
+
+from configobj import ConfigObj
+
+from StringIO import StringIO
+
+import logging
+
+LOG = logging.getLogger(__name__)
+
+
+class TestTimezone(t_help.FilesystemMockingTestCase):
+    def setUp(self):
+        super(TestTimezone, self).setUp()
+        self.new_root = self.makeDir(prefix="unittest_")
+
+    def _get_cloud(self, distro):
+        self.patchUtils(self.new_root)
+        self.patchOS(self.new_root)
+
+        paths = helpers.Paths({})
+
+        cls = distros.fetch(distro)
+        d = cls(distro, {}, paths)
+        ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths)
+        cc = cloud.Cloud(ds, paths, {}, d, None)
+        return cc
+
+    def test_set_timezone_sles(self):
+
+        cfg = {
+            'timezone': 'Tatooine/Bestine',
+        }
+        cc = self._get_cloud('sles')
+
+        # Create a dummy timezone file
+        dummy_contents = '0123456789abcdefgh'
+        util.write_file('/usr/share/zoneinfo/%s' % cfg['timezone'],
+                        dummy_contents)
+
+        cc_timezone.handle('cc_timezone', cfg, cc, LOG, [])
+
+        contents = util.load_file('/etc/sysconfig/clock')
+        n_cfg = ConfigObj(StringIO(contents))
+        self.assertEquals({'TIMEZONE': cfg['timezone']}, dict(n_cfg))
+
+        contents = util.load_file('/etc/localtime')
+        self.assertEquals(dummy_contents, contents.strip())