Blob Blame History Raw
diff --git a/python-rhsm.spec b/python-rhsm.spec
index 0318cc1..d2e00c1 100644
--- a/python-rhsm.spec
+++ b/python-rhsm.spec
@@ -13,7 +13,7 @@
 
 Name: python-rhsm
 Version: 1.15.4
-Release: 2%{?dist}
+Release: 3%{?dist}
 
 Summary: A Python library to communicate with a Red Hat Unified Entitlement Platform
 Group: Development/Libraries
@@ -74,6 +74,11 @@ rm -rf %{buildroot}
 %attr(644,root,root) %{_sysconfdir}/rhsm/ca/*.pem
 
 %changelog
+* Wed Aug 12 2015 Chris Rog <crog@redhat.com> 1.15.4-3
+- Add user-agent to rhsm requests. (alikins@redhat.com)
+- 1247890: KeyErrors are now caught when checking manager capabilities
+  (csnyder@redhat.com)
+
 * Thu Jul 23 2015 Chris Rog <crog@redhat.com> 1.15.4-2
 - Fedora 20 has been retired. (awood@redhat.com)
 
diff --git a/rel-eng/packages/python-rhsm b/rel-eng/packages/python-rhsm
index ace4dde..c370a05 100644
--- a/rel-eng/packages/python-rhsm
+++ b/rel-eng/packages/python-rhsm
@@ -1 +1 @@
-1.15.4-2 ./
+1.15.4-3 ./
diff --git a/rel-eng/tito.props b/rel-eng/tito.props
index fd057f6..6c86a16 100644
--- a/rel-eng/tito.props
+++ b/rel-eng/tito.props
@@ -1,4 +1,4 @@
-[buildconfig]
-builder = tito.builder.Builder
-tagger = tito.tagger.VersionTagger
+[globalconfig]
+default_builder = tito.distributionbuilder.DistributionBuilder
+default_tagger = tito.tagger.ReleaseTagger
 
diff --git a/src/rhsm/connection.py b/src/rhsm/connection.py
index 465405d..f5cbbdf 100644
--- a/src/rhsm/connection.py
+++ b/src/rhsm/connection.py
@@ -43,7 +43,7 @@ except ImportError:
     subman_version = "unknown"
 
 from rhsm import ourjson as json
-from rhsm.utils import get_env_proxy_info
+from rhsm import utils
 
 global_socket_timeout = 60
 timeout_altered = None
@@ -115,6 +115,7 @@ def drift_check(utc_time_string, hours=1):
     return drift
 
 
+
 class ConnectionException(Exception):
     pass
 
@@ -264,7 +265,6 @@ class ContentConnection(object):
         self.username = username
         self.password = password
         self.ssl_verify_depth = ssl_verify_depth
-
         self.timeout_altered = False
 
         # get the proxy information from the environment variable
@@ -275,13 +275,17 @@ class ContentConnection(object):
                    'proxy_port': '',
                    'proxy_password': ''}
         else:
-            info = get_env_proxy_info()
+            info = utils.get_env_proxy_info()
 
         self.proxy_hostname = proxy_hostname or config.get('server', 'proxy_hostname') or info['proxy_hostname']
         self.proxy_port = proxy_port or config.get('server', 'proxy_port') or info['proxy_port']
         self.proxy_user = proxy_user or config.get('server', 'proxy_user') or info['proxy_username']
         self.proxy_password = proxy_password or config.get('server', 'proxy_password') or info['proxy_password']
 
+    @property
+    def user_agent(self):
+        return "RHSM-content/1.0 (cmd=%s)" % utils.cmd_name(sys.argv)
+
     def _request(self, request_type, handler, body=None):
         # See note in Restlib._request
         context = SSL.Context("sslv23")
@@ -304,7 +308,11 @@ class ContentConnection(object):
 
         set_default_socket_timeout_if_python_2_3()
 
-        conn.request("GET", handler, body="", headers={"Host": "%s:%s" % (self.host, self.ssl_port), "Content-Length": "0"})
+        conn.request("GET", handler,
+                     body="",
+                     headers={"Host": "%s:%s" % (self.host, self.ssl_port),
+                              "Content-Length": "0",
+                              "User-Agent": self.user_agent})
         response = conn.getresponse()
         result = {
             "content": response.read(),
@@ -383,6 +391,9 @@ class Restlib(object):
         self.apihandler = apihandler
         lc = _get_locale()
 
+        # Default, updated by UepConnection
+        self.user_agent = "python-rhsm-user-agent"
+
         self.headers = {"Content-type": "application/json",
                         "Accept": "application/json",
                         "x-python-rhsm-version": python_rhsm_version,
@@ -492,6 +503,7 @@ class Restlib(object):
         else:
             conn = httpslib.HTTPSConnection(self.host, self.ssl_port, ssl_context=context)
 
+
         if info is not None:
             body = json.dumps(info, default=json.encode)
         else:
@@ -499,6 +511,9 @@ class Restlib(object):
 
         log.debug("Making request: %s %s" % (request_type, handler))
 
+        if self.user_agent:
+            self.headers['User-Agent'] = self.user_agent
+
         headers = self.headers
         if body is None:
             headers = dict(self.headers.items() +
@@ -665,7 +680,7 @@ class UEPConnection:
                    'proxy_port': '',
                    'proxy_password': ''}
         else:
-            info = get_env_proxy_info()
+            info = utils.get_env_proxy_info()
 
         self.proxy_hostname = proxy_hostname or config.get('server', 'proxy_hostname') or info['proxy_hostname']
         self.proxy_port = proxy_port or config.get('server', 'proxy_port') or info['proxy_port']
@@ -731,6 +746,8 @@ class UEPConnection:
                     ssl_verify_depth=self.ssl_verify_depth)
             auth_description = "auth=none"
 
+        self.conn.user_agent = "RHSM/1.0 (cmd=%s)" % utils.cmd_name(sys.argv)
+
         self.resources = None
         self.capabilities = None
         connection_description = ""
@@ -771,21 +788,27 @@ class UEPConnection:
     def _load_manager_capabilities(self):
         """
         Loads manager capabilities by doing a GET on the status
-        resource located at '/status/'
-        """
-        self.capabilities = {}
-        self.capabilities = self.conn.request_get("/status/")
-        self.capabilities = self.capabilities['managerCapabilities']
-        log.debug("Server has the following capabilities: %s",
-                  self.capabilities)
+        resource located at '/status'
+        """
+        status = self.getStatus()
+        capabilities = status.get('managerCapabilities')
+        if capabilities is None:
+            log.debug("The status retrieved did not \
+                      include key 'managerCapabilities'.\nStatus:'%s'" % status)
+            capabilities = []
+        elif isinstance(capabilities, list) and not capabilities:
+            log.debug("The managerCapabilities list \
+                      was empty\nStatus:'%s'" % status)
+        else:
+            log.debug("Server has the following capabilities: %s", capabilities)
+        return capabilities
 
     def has_capability(self, capability):
         """
         Check if the server we're connected to has a particular capability.
         """
         if self.capabilities is None:
-            self._load_manager_capabilities()
-
+            self.capabilities = self._load_manager_capabilities()
         return capability in self.capabilities
 
     def shutDown(self):
diff --git a/src/rhsm/utils.py b/src/rhsm/utils.py
index dd41161..f1a7f4e 100644
--- a/src/rhsm/utils.py
+++ b/src/rhsm/utils.py
@@ -17,6 +17,7 @@ import gettext
 import os
 import re
 from urlparse import urlparse
+
 from rhsm.config import DEFAULT_PROXY_PORT
 
 _ = lambda x: gettext.ldgettext("rhsm", x)
@@ -232,3 +233,24 @@ def get_env_proxy_info():
         else:
             the_proxy['proxy_port'] = int(info[3])
     return the_proxy
+
+
+def cmd_name(argv):
+    """Attempt to get a meaningful command name from argv.
+
+    This handles cases where argv[0] isn't helpful (for
+    example, '/usr/bin/python' or '__main__.py'.
+    """
+    argv0 = os.path.basename(argv[0])
+    argvdir = os.path.dirname(argv[0])
+    head, tail = os.path.split(argvdir)
+
+    cmd_name_string = argv0
+    # initial-setup is launched as 'python -m initial_setup', so
+    # sys.argv looks like
+    # ['/usr/lib/python2.7/site-packages/initial_setup/__main__.py'],
+    # so we look for initial_setup in the exe path.
+    if tail == "initial_setup":
+        cmd_name_string = "initial-setup"
+
+    return cmd_name_string
diff --git a/test/unit/connection-tests.py b/test/unit/connection-tests.py
index 6e449ad..8a0091d 100644
--- a/test/unit/connection-tests.py
+++ b/test/unit/connection-tests.py
@@ -37,6 +37,26 @@ class ConnectionTests(unittest.TestCase):
         self.cp = UEPConnection(username="dummy", password="dummy",
                 handler="/Test/", insecure=True)
 
+    def test_load_manager_capabilities(self):
+        expected_capabilities = ['hypervisors_async', 'cores']
+        proper_status = {'version':'1',
+                         'result':True,
+                         'managerCapabilities':expected_capabilities}
+        improper_status = dict.copy(proper_status)
+        # Remove the managerCapabilities key from the dict
+        del improper_status['managerCapabilities']
+        self.cp.conn = Mock()
+        # The first call will return the proper_status, the second, the improper
+        # status
+        original_getStatus = self.cp.getStatus
+        self.cp.getStatus = Mock(side_effect=[proper_status,
+                                                     improper_status])
+        actual_capabilities = self.cp._load_manager_capabilities()
+        self.assertEquals(sorted(actual_capabilities),
+                          sorted(expected_capabilities))
+        self.assertEquals([], self.cp._load_manager_capabilities())
+        self.cp.getStatus = original_getStatus
+
     def test_get_environment_by_name_requires_owner(self):
         self.assertRaises(Exception, self.cp.getEnvironment, None, {"name": "env name"})
 
diff --git a/test/unit/util-tests.py b/test/unit/util-tests.py
index c650d74..4d4a65b 100644
--- a/test/unit/util-tests.py
+++ b/test/unit/util-tests.py
@@ -6,7 +6,7 @@ from rhsm.utils import remove_scheme, get_env_proxy_info, \
     ServerUrlParseErrorEmpty, ServerUrlParseErrorNone, \
     ServerUrlParseErrorPort, ServerUrlParseErrorScheme, \
     ServerUrlParseErrorJustScheme, has_bad_scheme, has_good_scheme, \
-    parse_url
+    parse_url, cmd_name
 from rhsm.config import DEFAULT_PORT, DEFAULT_PREFIX, DEFAULT_HOSTNAME, \
     DEFAULT_CDN_HOSTNAME, DEFAULT_CDN_PORT, DEFAULT_CDN_PREFIX
 
@@ -379,3 +379,45 @@ class TestProxyInfo(unittest.TestCase):
             self.assertEquals(None, proxy_info["proxy_password"])
             self.assertEquals("host", proxy_info["proxy_hostname"])
             self.assertEquals(int("1111"), proxy_info["proxy_port"])
+
+
+class TestCmdName(unittest.TestCase):
+    def test_usr_sbin(self):
+        argv = ['/usr/sbin/subscription-manager', 'list']
+        self.assertEquals("subscription-manager", cmd_name(argv))
+
+    def test_bin(self):
+        argv = ['bin/subscription-manager', 'subscribe', '--auto']
+        self.assertEquals("subscription-manager", cmd_name(argv))
+
+    def test_sbin(self):
+        argv = ['/sbin/subscription-manager', 'list']
+        self.assertEquals("subscription-manager", cmd_name(argv))
+
+    def test_subscription_manager_gui(self):
+        argv = ['/sbin/subscription-manager-gui']
+        self.assertEquals("subscription-manager-gui", cmd_name(argv))
+
+    def test_initial_setup(self):
+        argv = ['/usr/lib/python2.7/site-packages/initial_setup/__main__.py']
+        self.assertEquals("initial-setup", cmd_name(argv))
+
+    def test_yum(self):
+        argv = ['/bin/yum', 'install', 'zsh']
+        self.assertEquals("yum", cmd_name(argv))
+
+    def test_rhsmcertd_worker(self):
+        argv = ['/usr/libexec/rhsmcertd-worker']
+        self.assertEquals("rhsmcertd-worker", cmd_name(argv))
+
+    def test_rhsm_debug(self):
+        argv = ['/bin/rhsm-debug']
+        self.assertEquals("rhsm-debug", cmd_name(argv))
+
+    def test_virt_who(self):
+        argv = ['/usr/share/virt-who/virtwho.py']
+        self.assertEquals("virtwho.py", cmd_name(argv))
+
+    def test_rhsmd(self):
+        argv = ['/usr/libexec/rhsmd', '-i', '-f', 'valid']
+        self.assertEquals("rhsmd", cmd_name(argv))