Skip to content

Commit 8fdd6a8

Browse files
[api] OPSAPS-31699 Add SSL context argument to ApiResource
Starting in Python 2.7, SSL certificate validation is turned on by default (yay!), but means that if CM is using an SSL certificate not signed by a public CA, Python API calls will fail. To allow specifying a local CA certificate, we need to allow API users to pass in a custom SSL context. This change adds the SSL context argument to ApiResource. Because HTTPSHandler only supports the context argument in Python 2.7.9 and above, this feature is only available in those versions. Testing: - Added an example script which connects to CM using a custom CA cert - Tested against my cluster which has HTTPS turned on
1 parent 21eb5f1 commit 8fdd6a8

File tree

3 files changed

+108
-8
lines changed

3 files changed

+108
-8
lines changed

python/examples/tls.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env python
2+
# Licensed to Cloudera, Inc. under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. Cloudera, Inc. licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
18+
"""
19+
Lists all hosts managed by CM. Demonstrates connecting to CM via HTTPS using a
20+
custom CA certificate in PEM format. Requires Python 2.7.9 or higher.
21+
22+
Usage: %s hostname username password ca_cert_file [options]
23+
24+
Options:
25+
-h Displays usage
26+
"""
27+
28+
import getopt
29+
import inspect
30+
import logging
31+
import ssl
32+
import sys
33+
import textwrap
34+
35+
from cm_api.api_client import ApiResource
36+
37+
def list_hosts(host, username, password, cafile):
38+
context = ssl.create_default_context(cafile=cafile)
39+
40+
api = ApiResource(host, username=username, password=password, use_tls=True,
41+
ssl_context=context)
42+
43+
for h in api.get_all_hosts():
44+
print h.hostname
45+
46+
def usage():
47+
doc = inspect.getmodule(usage).__doc__
48+
print >>sys.stderr, textwrap.dedent(doc % (sys.argv[0],))
49+
50+
def setup_logging(level):
51+
logging.basicConfig()
52+
logging.getLogger().setLevel(level)
53+
54+
def main(argv):
55+
setup_logging(logging.INFO)
56+
57+
# Argument parsing
58+
try:
59+
opts, args = getopt.getopt(argv[1:], "h")
60+
except getopt.GetoptError, err:
61+
print >>sys.stderr, err
62+
usage()
63+
return -1
64+
65+
for option, val in opts:
66+
if option == '-h':
67+
usage()
68+
return -1
69+
else:
70+
print >>sys.stderr, "Unknown flag:", option
71+
usage()
72+
return -1
73+
74+
if len(args) < 4:
75+
print >>sys.stderr, "Invalid number of arguments"
76+
usage()
77+
return -1
78+
79+
# Do work
80+
list_hosts(*args)
81+
return 0
82+
83+
84+
#
85+
# The "main" entry
86+
#
87+
if __name__ == '__main__':
88+
sys.exit(main(sys.argv))

python/src/cm_api/api_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ class ApiResource(Resource):
5555

5656
def __init__(self, server_host, server_port=None,
5757
username="admin", password="admin",
58-
use_tls=False, version=API_CURRENT_VERSION):
58+
use_tls=False, version=API_CURRENT_VERSION,
59+
ssl_context=None):
5960
"""
6061
Creates a Resource object that provides API endpoints.
6162
@@ -66,6 +67,7 @@ def __init__(self, server_host, server_port=None,
6667
@param password: Login password.
6768
@param use_tls: Whether to use tls (https).
6869
@param version: API version.
70+
@param ssl_context: A custom SSL context to use for HTTPS (Python 2.7.9+)
6971
@return: Resource object referring to the root.
7072
"""
7173
self._version = version
@@ -75,7 +77,8 @@ def __init__(self, server_host, server_port=None,
7577
base_url = "%s://%s:%s/api/v%s" % \
7678
(protocol, server_host, server_port, version)
7779

78-
client = HttpClient(base_url, exc_class=ApiException)
80+
client = HttpClient(base_url, exc_class=ApiException,
81+
ssl_context=ssl_context)
7982
client.set_basic_auth(username, password, API_AUTH_REALM)
8083
client.set_headers( { "Content-Type" : "application/json" } )
8184
Resource.__init__(self, client)

python/src/cm_api/http_client.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,11 @@ class HttpClient(object):
7878
"""
7979
Basic HTTP client tailored for rest APIs.
8080
"""
81-
def __init__(self, base_url, exc_class=None, logger=None):
81+
def __init__(self, base_url, exc_class=None, logger=None, ssl_context=None):
8282
"""
8383
@param base_url: The base url to the API.
8484
@param exc_class: An exception class to handle non-200 results.
85+
@param ssl_context: A custom SSL context to use for HTTPS (Python 2.7.9+)
8586
8687
Creates an HTTP(S) client to connect to the Cloudera Manager API.
8788
"""
@@ -97,11 +98,19 @@ def __init__(self, base_url, exc_class=None, logger=None):
9798
# Make a cookie processor
9899
cookiejar = cookielib.CookieJar()
99100

100-
self._opener = urllib2.build_opener(
101-
HTTPErrorProcessor(),
102-
urllib2.HTTPCookieProcessor(cookiejar),
103-
authhandler)
104-
101+
# Python 2.6's HTTPSHandler does not support the context argument, so only
102+
# instantiate it if non-None context is given
103+
if (ssl_context is not None):
104+
self._opener = urllib2.build_opener(
105+
urllib2.HTTPSHandler(context=ssl_context),
106+
HTTPErrorProcessor(),
107+
urllib2.HTTPCookieProcessor(cookiejar),
108+
authhandler)
109+
else:
110+
self._opener = urllib2.build_opener(
111+
HTTPErrorProcessor(),
112+
urllib2.HTTPCookieProcessor(cookiejar),
113+
authhandler)
105114

106115
def set_basic_auth(self, username, password, realm):
107116
"""

0 commit comments

Comments
 (0)