← Back to team overview

cloud-init-dev team mailing list archive

[Merge] lp:~daniel-thewatkins/cloud-init/shim_fixes into lp:cloud-init

 

Dan Watkins has proposed merging lp:~daniel-thewatkins/cloud-init/shim_fixes into lp:cloud-init.

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

For more details, see:
https://code.launchpad.net/~daniel-thewatkins/cloud-init/shim_fixes/+merge/269199
-- 
Your team cloud init development team is requested to review the proposed merge of lp:~daniel-thewatkins/cloud-init/shim_fixes into lp:cloud-init.
=== modified file 'ChangeLog'
--- ChangeLog	2015-08-07 19:45:01 +0000
+++ ChangeLog	2015-08-26 12:18:02 +0000
@@ -39,6 +39,7 @@
    [Surojit Pathak]
  - Azure: do not re-set hostname if user has changed it (LP: #1375252)
  - Fix exception when running with no arguments on Python 3. [Daniel Watkins]
+<<<<<<< TREE
  - Centos: detect/expect use of systemd on centos 7. [Brian Rak]
  - Azure: remove dependency on walinux-agent [Daniel Watkins]
  - EC2: know about eu-central-1 availability-zone (LP: #1456684)
@@ -60,6 +61,19 @@
  - rsyslog: add additional configuration mode (LP: #1478103)
  - status_wrapper in main: fix use of print_exc when handling exception
  - reporting: add reporting module for web hook or logging of events.
+=======
+ - Centos: detect/expect use of systemd on centos 7. [Brian Rak]
+ - Azure: remove dependency on walinux-agent [Daniel Watkins]
+ - EC2: know about eu-central-1 availability-zone (LP: #1456684)
+ - Azure: remove password from on-disk ovf-env.xml (LP: #1443311) [Ben Howard]
+ - Doc: include information on user-data in OpenStack [Daniel Watkins]
+ - Systemd: check for systemd using sd_booted symantics (LP: #1461201)
+   [Lars Kellogg-Stedman]
+ - Add an rh_subscription module to handle registration of Red Hat instances.
+   [Brent Baude]
+ - cc_apt_configure: fix importing keys under python3 (LP: #1463373)
+ - cc_growpart: fix specification of 'devices' list (LP: #1465436)
+>>>>>>> MERGE-SOURCE
 0.7.6:
  - open 0.7.6
  - Enable vendordata on CloudSigma datasource (LP: #1303986)

=== added file 'cloudinit/config/cc_rh_subscription.py'
--- cloudinit/config/cc_rh_subscription.py	1970-01-01 00:00:00 +0000
+++ cloudinit/config/cc_rh_subscription.py	2015-08-26 12:18:02 +0000
@@ -0,0 +1,404 @@
+# vi: ts=4 expandtab
+#
+#    Copyright (C) 2015 Red Hat, Inc.
+#
+#    Author: Brent Baude <bbaude@xxxxxxxxxx>
+#
+#    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 import util
+
+
+def handle(_name, cfg, _cloud, log, _args):
+    sm = SubscriptionManager(cfg)
+    sm.log = log
+    if not sm.is_registered:
+        try:
+            verify, verify_msg = sm._verify_keys()
+            if verify is not True:
+                raise SubscriptionError(verify_msg)
+            cont = sm.rhn_register()
+            if not cont:
+                raise SubscriptionError("Registration failed or did not "
+                                        "run completely")
+
+            # Splitting up the registration, auto-attach, and servicelevel
+            # commands because the error codes, messages from subman are not
+            # specific enough.
+
+            # Attempt to change the service level
+            if sm.auto_attach and sm.servicelevel is not None:
+                if not sm._set_service_level():
+                    raise SubscriptionError("Setting of service-level "
+                                            "failed")
+                else:
+                    sm.log.debug("Completed auto-attach with service level")
+            elif sm.auto_attach:
+                if not sm._set_auto_attach():
+                    raise SubscriptionError("Setting auto-attach failed")
+                else:
+                    sm.log.debug("Completed auto-attach")
+
+            if sm.pools is not None:
+                if not isinstance(sm.pools, list):
+                    pool_fail = "Pools must in the format of a list"
+                    raise SubscriptionError(pool_fail)
+
+                return_stat = sm.addPool(sm.pools)
+                if not return_stat:
+                    raise SubscriptionError("Unable to attach pools {0}"
+                                            .format(sm.pools))
+            if (sm.enable_repo is not None) or (sm.disable_repo is not None):
+                return_stat = sm.update_repos(sm.enable_repo, sm.disable_repo)
+                if not return_stat:
+                    raise SubscriptionError("Unable to add or remove repos")
+            sm.log_success("rh_subscription plugin completed successfully")
+        except SubscriptionError as e:
+            sm.log_warn(str(e))
+            sm.log_warn("rh_subscription plugin did not complete successfully")
+    else:
+        sm.log_success("System is already registered")
+
+
+class SubscriptionError(Exception):
+    pass
+
+
+class SubscriptionManager(object):
+    valid_rh_keys = ['org', 'activation-key', 'username', 'password',
+                     'disable-repo', 'enable-repo', 'add-pool',
+                     'rhsm-baseurl', 'server-hostname',
+                     'auto-attach', 'service-level']
+
+    def __init__(self, cfg):
+        self.cfg = cfg
+        self.rhel_cfg = self.cfg.get('rh_subscription', {})
+        self.rhsm_baseurl = self.rhel_cfg.get('rhsm-baseurl')
+        self.server_hostname = self.rhel_cfg.get('server-hostname')
+        self.pools = self.rhel_cfg.get('add-pool')
+        self.activation_key = self.rhel_cfg.get('activation-key')
+        self.org = self.rhel_cfg.get('org')
+        self.userid = self.rhel_cfg.get('username')
+        self.password = self.rhel_cfg.get('password')
+        self.auto_attach = self.rhel_cfg.get('auto-attach')
+        self.enable_repo = self.rhel_cfg.get('enable-repo')
+        self.disable_repo = self.rhel_cfg.get('disable-repo')
+        self.servicelevel = self.rhel_cfg.get('service-level')
+        self.subman = ['subscription-manager']
+        self.is_registered = self._is_registered()
+
+    def log_success(self, msg):
+        '''Simple wrapper for logging info messages. Useful for unittests'''
+        self.log.info(msg)
+
+    def log_warn(self, msg):
+        '''Simple wrapper for logging warning messages. Useful for unittests'''
+        self.log.warn(msg)
+
+    def _verify_keys(self):
+        '''
+        Checks that the keys in the rh_subscription dict from the user-data
+        are what we expect.
+        '''
+
+        for k in self.rhel_cfg:
+            if k not in self.valid_rh_keys:
+                bad_key = "{0} is not a valid key for rh_subscription. "\
+                          "Valid keys are: "\
+                          "{1}".format(k, ', '.join(self.valid_rh_keys))
+                return False, bad_key
+
+        # Check for bad auto-attach value
+        if (self.auto_attach is not None) and \
+                not (util.is_true(self.auto_attach) or
+                     util.is_false(self.auto_attach)):
+            not_bool = "The key auto-attach must be a boolean value "\
+                       "(True/False "
+            return False, not_bool
+
+        if (self.servicelevel is not None) and \
+                ((not self.auto_attach)
+                 or (util.is_false(str(self.auto_attach)))):
+
+            no_auto = "The service-level key must be used in conjunction with "\
+                      "the auto-attach key.  Please re-run with auto-attach: "\
+                      "True"
+            return False, no_auto
+        return True, None
+
+    def _is_registered(self):
+        '''
+        Checks if the system is already registered and returns
+        True if so, else False
+        '''
+        cmd = ['identity']
+
+        try:
+            self._sub_man_cli(cmd)
+        except util.ProcessExecutionError:
+            return False
+
+        return True
+
+    def _sub_man_cli(self, cmd, logstring_val=False):
+        '''
+        Uses the prefered cloud-init subprocess def of util.subp
+        and runs subscription-manager.  Breaking this to a
+        separate function for later use in mocking and unittests
+        '''
+        cmd = self.subman + cmd
+        return util.subp(cmd, logstring=logstring_val)
+
+    def rhn_register(self):
+        '''
+        Registers the system by userid and password or activation key
+        and org.  Returns True when successful False when not.
+        '''
+
+        if (self.activation_key is not None) and (self.org is not None):
+            # register by activation key
+            cmd = ['register', '--activationkey={0}'.
+                   format(self.activation_key), '--org={0}'.format(self.org)]
+
+            # If the baseurl and/or server url are passed in, we register
+            # with them.
+
+            if self.rhsm_baseurl is not None:
+                cmd.append("--baseurl={0}".format(self.rhsm_baseurl))
+
+            if self.server_hostname is not None:
+                cmd.append("--serverurl={0}".format(self.server_hostname))
+
+            try:
+                return_out, return_err = self._sub_man_cli(cmd,
+                                                           logstring_val=True)
+            except util.ProcessExecutionError as e:
+                if e.stdout == "":
+                    self.log_warn("Registration failed due "
+                                  "to: {0}".format(e.stderr))
+                return False
+
+        elif (self.userid is not None) and (self.password is not None):
+            # register by username and password
+            cmd = ['register', '--username={0}'.format(self.userid),
+                   '--password={0}'.format(self.password)]
+
+            # If the baseurl and/or server url are passed in, we register
+            # with them.
+
+            if self.rhsm_baseurl is not None:
+                cmd.append("--baseurl={0}".format(self.rhsm_baseurl))
+
+            if self.server_hostname is not None:
+                cmd.append("--serverurl={0}".format(self.server_hostname))
+
+            # Attempting to register the system only
+            try:
+                return_out, return_err = self._sub_man_cli(cmd,
+                                                           logstring_val=True)
+            except util.ProcessExecutionError as e:
+                if e.stdout == "":
+                    self.log_warn("Registration failed due "
+                                  "to: {0}".format(e.stderr))
+                return False
+
+        else:
+            self.log_warn("Unable to register system due to incomplete "
+                          "information.")
+            self.log_warn("Use either activationkey and org *or* userid "
+                          "and password")
+            return False
+
+        reg_id = return_out.split("ID: ")[1].rstrip()
+        self.log.debug("Registered successfully with ID {0}".format(reg_id))
+        return True
+
+    def _set_service_level(self):
+        cmd = ['attach', '--auto', '--servicelevel={0}'
+               .format(self.servicelevel)]
+
+        try:
+            return_out, return_err = self._sub_man_cli(cmd)
+        except util.ProcessExecutionError as e:
+            if e.stdout.rstrip() != '':
+                for line in e.stdout.split("\n"):
+                    if line is not '':
+                        self.log_warn(line)
+            else:
+                self.log_warn("Setting the service level failed with: "
+                              "{0}".format(e.stderr.strip()))
+            return False
+        for line in return_out.split("\n"):
+            if line is not "":
+                self.log.debug(line)
+        return True
+
+    def _set_auto_attach(self):
+        cmd = ['attach', '--auto']
+        try:
+            return_out, return_err = self._sub_man_cli(cmd)
+        except util.ProcessExecutionError:
+            self.log_warn("Auto-attach failed with: "
+                          "{0}]".format(return_err.strip()))
+            return False
+        for line in return_out.split("\n"):
+            if line is not "":
+                self.log.debug(line)
+        return True
+
+    def _getPools(self):
+        '''
+        Gets the list pools for the active subscription and returns them
+        in list form.
+        '''
+        available = []
+        consumed = []
+
+        # Get all available pools
+        cmd = ['list', '--available', '--pool-only']
+        results, errors = self._sub_man_cli(cmd)
+        available = (results.rstrip()).split("\n")
+
+        # Get all consumed pools
+        cmd = ['list', '--consumed', '--pool-only']
+        results, errors = self._sub_man_cli(cmd)
+        consumed = (results.rstrip()).split("\n")
+
+        return available, consumed
+
+    def _getRepos(self):
+        '''
+        Obtains the current list of active yum repositories and returns
+        them in list form.
+        '''
+
+        cmd = ['repos', '--list-enabled']
+        return_out, return_err = self._sub_man_cli(cmd)
+        active_repos = []
+        for repo in return_out.split("\n"):
+            if "Repo ID:" in repo:
+                active_repos.append((repo.split(':')[1]).strip())
+
+        cmd = ['repos', '--list-disabled']
+        return_out, return_err = self._sub_man_cli(cmd)
+
+        inactive_repos = []
+        for repo in return_out.split("\n"):
+            if "Repo ID:" in repo:
+                inactive_repos.append((repo.split(':')[1]).strip())
+        return active_repos, inactive_repos
+
+    def addPool(self, pools):
+        '''
+        Takes a list of subscription pools and "attaches" them to the
+        current subscription
+        '''
+
+        # An empty list was passed
+        if len(pools) == 0:
+            self.log.debug("No pools to attach")
+            return True
+
+        pool_available, pool_consumed = self._getPools()
+        pool_list = []
+        cmd = ['attach']
+        for pool in pools:
+            if (pool not in pool_consumed) and (pool in pool_available):
+                pool_list.append('--pool={0}'.format(pool))
+            else:
+                self.log_warn("Pool {0} is not available".format(pool))
+        if len(pool_list) > 0:
+            cmd.extend(pool_list)
+            try:
+                self._sub_man_cli(cmd)
+                self.log.debug("Attached the following pools to your "
+                               "system: %s" % (", ".join(pool_list))
+                               .replace('--pool=', ''))
+                return True
+            except util.ProcessExecutionError as e:
+                self.log_warn("Unable to attach pool {0} "
+                              "due to {1}".format(pool, e))
+                return False
+
+    def update_repos(self, erepos, drepos):
+        '''
+        Takes a list of yum repo ids that need to be disabled or enabled; then
+        it verifies if they are already enabled or disabled and finally
+        executes the action to disable or enable
+        '''
+
+        if (erepos is not None) and (not isinstance(erepos, list)):
+            self.log_warn("Repo IDs must in the format of a list.")
+            return False
+
+        if (drepos is not None) and (not isinstance(drepos, list)):
+            self.log_warn("Repo IDs must in the format of a list.")
+            return False
+
+        # Bail if both lists are not populated
+        if (len(erepos) == 0) and (len(drepos) == 0):
+            self.log.debug("No repo IDs to enable or disable")
+            return True
+
+        active_repos, inactive_repos = self._getRepos()
+        # Creating a list of repoids to be enabled
+        enable_list = []
+        enable_list_fail = []
+        for repoid in erepos:
+            if (repoid in inactive_repos):
+                enable_list.append("--enable={0}".format(repoid))
+            else:
+                enable_list_fail.append(repoid)
+
+        # Creating a list of repoids to be disabled
+        disable_list = []
+        disable_list_fail = []
+        for repoid in drepos:
+            if repoid in active_repos:
+                disable_list.append("--disable={0}".format(repoid))
+            else:
+                disable_list_fail.append(repoid)
+
+        # Logging any repos that are already enabled or disabled
+        if len(enable_list_fail) > 0:
+            for fail in enable_list_fail:
+                # Check if the repo exists or not
+                if fail in active_repos:
+                    self.log.debug("Repo {0} is already enabled".format(fail))
+                else:
+                    self.log_warn("Repo {0} does not appear to "
+                                  "exist".format(fail))
+        if len(disable_list_fail) > 0:
+            for fail in disable_list_fail:
+                self.log.debug("Repo {0} not disabled "
+                               "because it is not enabled".format(fail))
+
+        cmd = ['repos']
+        if len(enable_list) > 0:
+            cmd.extend(enable_list)
+        if len(disable_list) > 0:
+            cmd.extend(disable_list)
+
+        try:
+            self._sub_man_cli(cmd)
+        except util.ProcessExecutionError as e:
+            self.log_warn("Unable to alter repos due to {0}".format(e))
+            return False
+
+        if len(enable_list) > 0:
+            self.log.debug("Enabled the following repos: %s" %
+                           (", ".join(enable_list)).replace('--enable=', ''))
+        if len(disable_list) > 0:
+            self.log.debug("Disabled the following repos: %s" %
+                           (", ".join(disable_list)).replace('--disable=', ''))
+        return True

=== renamed file 'cloudinit/config/cc_rh_subscription.py' => 'cloudinit/config/cc_rh_subscription.py.moved'
=== modified file 'cloudinit/distros/__init__.py'
=== modified file 'cloudinit/sources/DataSourceAzure.py'
--- cloudinit/sources/DataSourceAzure.py	2015-07-22 19:17:10 +0000
+++ cloudinit/sources/DataSourceAzure.py	2015-08-26 12:18:02 +0000
@@ -244,6 +244,7 @@
         if self.ds_cfg['agent_command'] == '__builtin__':
             metadata_func = get_metadata_from_fabric
         else:
+<<<<<<< TREE
             metadata_func = self.get_metadata_from_agent
         try:
             fabric_data = metadata_func()
@@ -255,6 +256,19 @@
         self.metadata.update(fabric_data)
 
         found_ephemeral = find_fabric_formatted_ephemeral_disk()
+=======
+            metadata_func = self.get_metadata_from_agent
+        try:
+            fabric_data = metadata_func()
+        except Exception as exc:
+            LOG.info("Error communicating with Azure fabric; assume we aren't"
+                     " on Azure.", exc_info=True)
+            return False
+
+        self.metadata.update(fabric_data)
+
+        found_ephemeral = find_ephemeral_disk()
+>>>>>>> MERGE-SOURCE
         if found_ephemeral:
             self.ds_cfg['disk_aliases']['ephemeral0'] = found_ephemeral
             LOG.debug("using detected ephemeral0 of %s", found_ephemeral)
@@ -423,6 +437,7 @@
 
 
 def write_files(datadir, files, dirmode=None):
+<<<<<<< TREE
 
     def _redact_password(cnt, fname):
         """Azure provides the UserPassword in plain text. So we redact it"""
@@ -437,6 +452,22 @@
             LOG.critical("failed to redact userpassword in {}".format(fname))
             return cnt
 
+=======
+
+    def _redact_password(cnt, fname):
+        """Azure provides the UserPassword in plain text. So we redact it"""
+        try:
+            root = ET.fromstring(cnt)
+            for elem in root.iter():
+                if ('UserPassword' in elem.tag and
+                   elem.text != DEF_PASSWD_REDACTION):
+                    elem.text = DEF_PASSWD_REDACTION
+            return ET.tostring(root)
+        except Exception as e:
+            LOG.critical("failed to redact userpassword in {}".format(fname))
+            return cnt
+
+>>>>>>> MERGE-SOURCE
     if not datadir:
         return
     if not files:

=== modified file 'cloudinit/util.py'
=== added file 'doc/examples/cloud-config-rh_subscription.txt'
--- doc/examples/cloud-config-rh_subscription.txt	1970-01-01 00:00:00 +0000
+++ doc/examples/cloud-config-rh_subscription.txt	2015-08-26 12:18:02 +0000
@@ -0,0 +1,49 @@
+#cloud-config
+
+# register your Red Hat Enterprise Linux based operating system
+#
+# this cloud-init plugin is capable of registering by username 
+# and password *or* activation and org.  Following a successfully
+# registration you can:
+#   - auto-attach subscriptions
+#   - set the service level
+#   - add subscriptions based on its pool ID
+#   - enable yum repositories based on its repo id
+#   - disable yum repositories based on its repo id
+#   - alter the rhsm_baseurl and server-hostname in the
+#     /etc/rhsm/rhs.conf file
+
+rh_subscription:
+    username: joe@xxxxxxx
+
+    ## Quote your password if it has symbols to be safe
+    password: '1234abcd'
+
+    ## If you prefer, you can use the activation key and 
+    ## org instead of username and password. Be sure to
+    ## comment out username and password
+
+    #activation-key: foobar
+    #org: 12345
+
+    ## Uncomment to auto-attach subscriptions to your system 
+    #auto-attach: True
+
+    ## Uncomment to set the service level for your 
+    ##   subscriptions
+    #service-level: self-support
+
+    ## Uncomment to add pools (needs to be a list of IDs)
+    #add-pool: []
+
+    ## Uncomment to add or remove yum repos
+    ##   (needs to be a list of repo IDs)
+    #enable-repo: []
+    #disable-repo: []
+
+    ## Uncomment to alter the baseurl in /etc/rhsm/rhsm.conf
+    #rhsm-baseurl: http://url
+
+    ## Uncomment to alter the server hostname in 
+    ##  /etc/rhsm/rhsm.conf
+    #server-hostname: foo.bar.com

=== renamed file 'doc/examples/cloud-config-rh_subscription.txt' => 'doc/examples/cloud-config-rh_subscription.txt.moved'
=== modified file 'packages/debian/control.in'
=== modified file 'tests/unittests/test_datasource/test_azure.py'
--- tests/unittests/test_datasource/test_azure.py	2015-07-22 19:17:10 +0000
+++ tests/unittests/test_datasource/test_azure.py	2015-08-26 12:18:02 +0000
@@ -145,6 +145,7 @@
 
         return dsrc
 
+<<<<<<< TREE
     def xml_equals(self, oxml, nxml):
         """Compare two sets of XML to make sure they are equal"""
 
@@ -178,6 +179,41 @@
             return
         raise AssertionError("XML is the same")
 
+=======
+    def xml_equals(self, oxml, nxml):
+        """Compare two sets of XML to make sure they are equal"""
+
+        def create_tag_index(xml):
+            et = ET.fromstring(xml)
+            ret = {}
+            for x in et.iter():
+                ret[x.tag] = x
+            return ret
+
+        def tags_exists(x, y):
+            for tag in x.keys():
+                self.assertIn(tag, y)
+            for tag in y.keys():
+                self.assertIn(tag, x)
+
+        def tags_equal(x, y):
+            for x_tag, x_val in x.items():
+                y_val = y.get(x_val.tag)
+                self.assertEquals(x_val.text, y_val.text)
+
+        old_cnt = create_tag_index(oxml)
+        new_cnt = create_tag_index(nxml)
+        tags_exists(old_cnt, new_cnt)
+        tags_equal(old_cnt, new_cnt)
+
+    def xml_notequals(self, oxml, nxml):
+        try:
+            self.xml_equals(oxml, nxml)
+        except AssertionError as e:
+            return
+        raise AssertionError("XML is the same")
+
+>>>>>>> MERGE-SOURCE
     def test_basic_seed_dir(self):
         odata = {'HostName': "myhost", 'UserName': "myuser"}
         data = {'ovfcontent': construct_valid_ovf_env(data=odata),
@@ -475,6 +511,7 @@
             mock.patch.object(DataSourceAzure, 'list_possible_azure_ds_devs',
                               mock.MagicMock(return_value=[])))
         self.patches.enter_context(
+<<<<<<< TREE
             mock.patch.object(DataSourceAzure,
                               'find_fabric_formatted_ephemeral_disk',
                               mock.MagicMock(return_value=None)))
@@ -485,6 +522,16 @@
         self.patches.enter_context(
             mock.patch.object(DataSourceAzure, 'get_metadata_from_fabric',
                               mock.MagicMock(return_value={})))
+=======
+            mock.patch.object(DataSourceAzure, 'find_ephemeral_disk',
+                              mock.MagicMock(return_value=None)))
+        self.patches.enter_context(
+            mock.patch.object(DataSourceAzure, 'find_ephemeral_part',
+                              mock.MagicMock(return_value=None)))
+        self.patches.enter_context(
+            mock.patch.object(DataSourceAzure, 'get_metadata_from_fabric',
+                              mock.MagicMock(return_value={})))
+>>>>>>> MERGE-SOURCE
 
     def setUp(self):
         super(TestAzureBounce, self).setUp()

=== modified file 'tests/unittests/test_distros/test_generic.py'
=== added file 'tests/unittests/test_rh_subscription.py'
--- tests/unittests/test_rh_subscription.py	1970-01-01 00:00:00 +0000
+++ tests/unittests/test_rh_subscription.py	2015-08-26 12:18:02 +0000
@@ -0,0 +1,208 @@
+from cloudinit import util
+from cloudinit.config import cc_rh_subscription
+import logging
+import mock
+import unittest
+
+
+class GoodTests(unittest.TestCase):
+    def setUp(self):
+        super(GoodTests, self).setUp()
+        self.name = "cc_rh_subscription"
+        self.cloud_init = None
+        self.log = logging.getLogger("good_tests")
+        self.args = []
+        self.handle = cc_rh_subscription.handle
+        self.SM = cc_rh_subscription.SubscriptionManager
+
+        self.config = {'rh_subscription':
+                       {'username': 'scooby@xxxxxx',
+                        'password': 'scooby-snacks'
+                        }}
+        self.config_full = {'rh_subscription':
+                            {'username': 'scooby@xxxxxx',
+                             'password': 'scooby-snacks',
+                             'auto-attach': True,
+                             'service-level': 'self-support',
+                             'add-pool': ['pool1', 'pool2', 'pool3'],
+                             'enable-repo': ['repo1', 'repo2', 'repo3'],
+                             'disable-repo': ['repo4', 'repo5']
+                             }}
+
+    def test_already_registered(self):
+        '''
+        Emulates a system that is already registered. Ensure it gets
+        a non-ProcessExecution error from is_registered()
+        '''
+        with mock.patch.object(cc_rh_subscription.SubscriptionManager,
+                               '_sub_man_cli') as mockobj:
+            self.SM.log_success = mock.MagicMock()
+            self.handle(self.name, self.config, self.cloud_init,
+                        self.log, self.args)
+            self.assertEqual(self.SM.log_success.call_count, 1)
+            self.assertEqual(mockobj.call_count, 1)
+
+    def test_simple_registration(self):
+        '''
+        Simple registration with username and password
+        '''
+        self.SM.log_success = mock.MagicMock()
+        reg = "The system has been registered with ID:" \
+              " 12345678-abde-abcde-1234-1234567890abc"
+        self.SM._sub_man_cli = mock.MagicMock(
+            side_effect=[util.ProcessExecutionError, (reg, 'bar')])
+        self.handle(self.name, self.config, self.cloud_init,
+                    self.log, self.args)
+        self.assertIn(mock.call(['identity']),
+                      self.SM._sub_man_cli.call_args_list)
+        self.assertIn(mock.call(['register', '--username=scooby@xxxxxx',
+                                 '--password=scooby-snacks'],
+                                logstring_val=True),
+                      self.SM._sub_man_cli.call_args_list)
+
+        self.assertEqual(self.SM.log_success.call_count, 1)
+        self.assertEqual(self.SM._sub_man_cli.call_count, 2)
+
+    def test_full_registration(self):
+        '''
+        Registration with auto-attach, service-level, adding pools,
+        and enabling and disabling yum repos
+        '''
+        call_lists = []
+        call_lists.append(['attach', '--pool=pool1', '--pool=pool3'])
+        call_lists.append(['repos', '--enable=repo2', '--enable=repo3',
+                           '--disable=repo5'])
+        call_lists.append(['attach', '--auto', '--servicelevel=self-support'])
+        self.SM.log_success = mock.MagicMock()
+        reg = "The system has been registered with ID:" \
+              " 12345678-abde-abcde-1234-1234567890abc"
+        self.SM._sub_man_cli = mock.MagicMock(
+            side_effect=[util.ProcessExecutionError, (reg, 'bar'),
+                         ('Service level set to: self-support', ''),
+                         ('pool1\npool3\n', ''), ('pool2\n', ''), ('', ''),
+                         ('Repo ID: repo1\nRepo ID: repo5\n', ''),
+                         ('Repo ID: repo2\nRepo ID: repo3\nRepo ID: '
+                          'repo4', ''),
+                         ('', '')])
+        self.handle(self.name, self.config_full, self.cloud_init,
+                    self.log, self.args)
+        for call in call_lists:
+            self.assertIn(mock.call(call), self.SM._sub_man_cli.call_args_list)
+        self.assertEqual(self.SM.log_success.call_count, 1)
+        self.assertEqual(self.SM._sub_man_cli.call_count, 9)
+
+
+class TestBadInput(unittest.TestCase):
+    name = "cc_rh_subscription"
+    cloud_init = None
+    log = logging.getLogger("bad_tests")
+    args = []
+    SM = cc_rh_subscription.SubscriptionManager
+    reg = "The system has been registered with ID:" \
+          " 12345678-abde-abcde-1234-1234567890abc"
+
+    config_no_password = {'rh_subscription':
+                          {'username': 'scooby@xxxxxx'
+                           }}
+
+    config_no_key = {'rh_subscription':
+                     {'activation-key': '1234abcde',
+                      }}
+
+    config_service = {'rh_subscription':
+                      {'username': 'scooby@xxxxxx',
+                       'password': 'scooby-snacks',
+                       'service-level': 'self-support'
+                       }}
+
+    config_badpool = {'rh_subscription':
+                      {'username': 'scooby@xxxxxx',
+                       'password': 'scooby-snacks',
+                       'add-pool': 'not_a_list'
+                       }}
+    config_badrepo = {'rh_subscription':
+                      {'username': 'scooby@xxxxxx',
+                       'password': 'scooby-snacks',
+                       'enable-repo': 'not_a_list'
+                       }}
+    config_badkey = {'rh_subscription':
+                     {'activation_key': 'abcdef1234',
+                      'org': '123',
+                      }}
+
+    def setUp(self):
+        super(TestBadInput, self).setUp()
+        self.handle = cc_rh_subscription.handle
+
+    def test_no_password(self):
+        '''
+        Attempt to register without the password key/value
+        '''
+        self.input_is_missing_data(self.config_no_password)
+
+    def test_no_org(self):
+        '''
+        Attempt to register without the org key/value
+        '''
+        self.input_is_missing_data(self.config_no_key)
+
+    def test_service_level_without_auto(self):
+        '''
+        Attempt to register using service-level without the auto-attach key
+        '''
+        self.SM.log_warn = mock.MagicMock()
+        self.SM._sub_man_cli = mock.MagicMock(
+            side_effect=[util.ProcessExecutionError, (self.reg, 'bar')])
+        self.handle(self.name, self.config_service, self.cloud_init,
+                    self.log, self.args)
+        self.assertEqual(self.SM._sub_man_cli.call_count, 1)
+        self.assertEqual(self.SM.log_warn.call_count, 2)
+
+    def test_pool_not_a_list(self):
+        '''
+        Register with pools that are not in the format of a list
+        '''
+        self.SM.log_warn = mock.MagicMock()
+        self.SM._sub_man_cli = mock.MagicMock(
+            side_effect=[util.ProcessExecutionError, (self.reg, 'bar')])
+        self.handle(self.name, self.config_badpool, self.cloud_init,
+                    self.log, self.args)
+        self.assertEqual(self.SM._sub_man_cli.call_count, 2)
+        self.assertEqual(self.SM.log_warn.call_count, 2)
+
+    def test_repo_not_a_list(self):
+        '''
+        Register with repos that are not in the format of a list
+        '''
+        self.SM.log_warn = mock.MagicMock()
+        self.SM._sub_man_cli = mock.MagicMock(
+            side_effect=[util.ProcessExecutionError, (self.reg, 'bar')])
+        self.handle(self.name, self.config_badrepo, self.cloud_init,
+                    self.log, self.args)
+        self.assertEqual(self.SM.log_warn.call_count, 3)
+        self.assertEqual(self.SM._sub_man_cli.call_count, 2)
+
+    def test_bad_key_value(self):
+        '''
+        Attempt to register with a key that we don't know
+        '''
+        self.SM.log_warn = mock.MagicMock()
+        self.SM._sub_man_cli = mock.MagicMock(
+            side_effect=[util.ProcessExecutionError, (self.reg, 'bar')])
+        self.handle(self.name, self.config_badkey, self.cloud_init,
+                    self.log, self.args)
+        self.assertEqual(self.SM.log_warn.call_count, 2)
+        self.assertEqual(self.SM._sub_man_cli.call_count, 1)
+
+    def input_is_missing_data(self, config):
+        '''
+        Helper def for tests that having missing information
+        '''
+        self.SM.log_warn = mock.MagicMock()
+        self.SM._sub_man_cli = mock.MagicMock(
+            side_effect=[util.ProcessExecutionError])
+        self.handle(self.name, config, self.cloud_init,
+                    self.log, self.args)
+        self.SM._sub_man_cli.assert_called_with(['identity'])
+        self.assertEqual(self.SM.log_warn.call_count, 4)
+        self.assertEqual(self.SM._sub_man_cli.call_count, 1)

=== renamed file 'tests/unittests/test_rh_subscription.py' => 'tests/unittests/test_rh_subscription.py.moved'

Follow ups