Skip to content

Commit 2d345e0

Browse files
committed
Add API Key authentication
Starting with IBM Spectrum Scale version 5.1.1 any client querying the performance data from the IBM Spectrum Scale cluster needs the API key authentication. - The default API key name for grafana_bridge set to scale_grafana - The API key name and value could be stored in the config.ini file or passed as input parameters(--apiKeyName, --apiKeyValue) at the grafana-bridge start. - The default ServerPort changed from 9084 to 9980
1 parent 99fd12f commit 2d345e0

12 files changed

+261
-131
lines changed

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ github3.py
77
flake8
88
six
99
cherrypy
10+
requests
11+
urllib3

source/Dockerfile

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ FROM registry.access.redhat.com/ubi8/python-36
33
RUN pip3 install --upgrade pip
44
RUN pip3 install setuptools
55
RUN pip3 install cherrypy
6+
RUN pip3 install urllib3
7+
RUN pip3 install requests
68

79
CMD python3 -V
810
CMD pip3 list
@@ -23,6 +25,10 @@ ARG HTTPPORT=4242
2325
ENV PORT=$HTTPPORT
2426
RUN echo "the HTTP/S port is set to $PORT"
2527

28+
ARG PERFMONPORT=9981
29+
ENV SERVERPORT=$PERFMONPORT
30+
RUN echo "the PERFMONPORT port is set to $SERVERPORT"
31+
2632
ARG CERTPATH=None
2733
ENV TLSKEYPATH=$CERTPATH
2834

@@ -32,6 +38,12 @@ ENV TLSKEYFILE=$KEYFILE
3238
ARG CERTFILE=None
3339
ENV TLSCERTFILE=$CERTFILE
3440

41+
ARG KEYNAME=None
42+
ENV APIKEYNAME=$KEYNAME
43+
44+
ARG KEYVALUE=None
45+
ENV APIKEYVALUE=$KEYVALUE
46+
3547
RUN if [ -z "$TLSKEYPATH" ] || [ -z "$TLSCERTFILE" ] || [ -z "$TLSKEYFILE" ] && [ "$PORT" -eq 8443 ]; then echo "TLSKEYPATH FOR SSL CONNECTION NOT SET - ERROR"; exit 1; else echo "PASS"; fi
3648
RUN echo "the ssl certificates path is set to $TLSKEYPATH"
3749

@@ -42,7 +54,7 @@ RUN echo "the pmcollector server ip is set to $SERVER"
4254

4355
WORKDIR /opt/IBM/bridge
4456

45-
ARG DEFAULTLOGPATH='/var/log/ibm_bridge_for_grafanalogs/install.log'
57+
ARG DEFAULTLOGPATH='/var/log/ibm_bridge_for_grafana/install.log'
4658
ENV LOGPATH=$DEFAULTLOGPATH
4759
RUN mkdir -p $(dirname $LOGPATH)
4860
RUN echo "the log will use $(dirname $LOGPATH)"
@@ -54,7 +66,7 @@ RUN echo "pmcollector_server: $SERVER" >> $LOGPATH
5466
RUN echo "ssl certificates location: $TLSKEYPATH" >> $LOGPATH
5567
RUN echo "HTTP/S port: $PORT" >> $LOGPATH
5668

57-
CMD ["sh", "-c", "python3 zimonGrafanaIntf.py -c 10 -s $SERVER -p $PORT -t $TLSKEYPATH --tlsKeyFile $TLSKEYFILE --tlsCertFile $TLSCERTFILE"]
69+
CMD ["sh", "-c", "python3 zimonGrafanaIntf.py -c 10 -s $SERVER -p $PORT -P $SERVERPORT -t $TLSKEYPATH --tlsKeyFile $TLSKEYFILE --tlsCertFile $TLSCERTFILE --apiKeyName $APIKEYNAME --apiKeyValue $APIKEYVALUE"]
5870

5971
EXPOSE 4242 8443
6072

source/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@
2020
@author: HWASSMAN
2121
'''
2222

23-
__version__ = '6.1.2'
23+
__version__ = '7.0'

source/confParser.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import os
2525
from messages import MSG
2626
import configparser
27+
import getpass
2728

2829

2930
def checkFileExists(path, filename):
@@ -43,6 +44,11 @@ def checkTLSsettings(args):
4344
return False, MSG['CertError']
4445
return True, ''
4546

47+
def checkAPIsettings(args):
48+
if not args.get('apiKeyName') or not args.get('apiKeyValue'):
49+
return False, MSG['MissingParm']
50+
return True, ''
51+
4652

4753
def getSettings(argv):
4854
settings = {}
@@ -55,11 +61,15 @@ def getSettings(argv):
5561
settings = args
5662
else:
5763
return None, msg
64+
# check API key settings
65+
valid, msg = checkAPIsettings(settings)
66+
if not valid:
67+
return None, msg
5868
# check TLS settings
5969
valid, msg = checkTLSsettings(settings)
60-
if valid:
61-
return settings, ''
62-
return None, msg
70+
if not valid:
71+
return None, msg
72+
return settings, ''
6373

6474

6575
def merge_defaults_and_args(defaults, args):
@@ -139,18 +149,25 @@ def parse_defaults(self):
139149
return defaults
140150

141151

152+
class Password(argparse.Action):
153+
defaults = ConfigManager().defaults
154+
155+
def __call__(self, parser, namespace, values, option_string):
156+
if values is None and self.defaults.get('apiKeyValue', None) == None:
157+
print('no valid apiKeyValue found in the config.ini')
158+
values = getpass.getpass()
159+
160+
setattr(namespace, self.dest, values)
161+
162+
142163
def parse_cmd_args(argv):
143164
'''parse input parameters'''
144165

145166
parser = argparse.ArgumentParser('python zimonGrafanaIntf.py')
146167
parser.add_argument('-s', '--server', action="store", default=None,
147-
help='Host name or ip address of the ZIMon collector (Default from config.ini: 127.0.0.1) \
148-
NOTE: Per default ZIMon does not accept queries from remote machines. \
149-
To run the bridge from outside of the ZIMon collector, you need to modify ZIMon queryinterface settings (\'ZIMonCollector.cfg\')')
150-
parser.add_argument('-P', '--serverPort', action="store", type=int, choices=[9084, 9094], default=None,
151-
help='ZIMon collector port number (Default from config.ini: 9084) \
152-
NOTE: In some environments, for better bridge performance the usage of the multi-threaded port 9094 could be helpful.\
153-
In this case make sure the \'query2port = \"9094\"\' is enabled in the ZIMon queryinterface settings (\'ZIMonCollector.cfg\')')
168+
help='Host name or ip address of the ZIMon collector (Default from config.ini: 127.0.0.1)')
169+
parser.add_argument('-P', '--serverPort', action="store", type=int, choices=[9980, 9981], default=None,
170+
help='ZIMon collector port number (Default from config.ini: 9980)')
154171
parser.add_argument('-l', '--logPath', action="store", default=None, help='location path of the log file (Default from config.ini: \'/var/log/ibm_bridge_for_grafana\')')
155172
parser.add_argument('-f', '--logFile', action="store", default=None, help='Name of the log file (Default from config.ini: zserver.log')
156173
parser.add_argument('-c', '--logLevel', action="store", type=int, default=None,
@@ -159,6 +176,8 @@ def parse_cmd_args(argv):
159176
parser.add_argument('-t', '--tlsKeyPath', action="store", default=None, help='Directory path of tls privkey.pem and cert.pem file location (Required only for HTTPS port 8443)')
160177
parser.add_argument('-k', '--tlsKeyFile', action="store", default=None, help='Name of TLS key file, f.e.: privkey.pem (Required only for HTTPS port 8443)')
161178
parser.add_argument('-m', '--tlsCertFile', action="store", default=None, help='Name of TLS certificate file, f.e.: cert.pem (Required only for HTTPS port 8443)')
179+
parser.add_argument('-n', '--apiKeyName', action="store", default=None, help='Name of api key file (Default from config.ini: \'scale_grafana\')')
180+
parser.add_argument('-v', '--apiKeyValue', action=Password, nargs='?', dest='apiKeyValue', default=None, help='Enter your apiKey value:')
162181

163182
args = parser.parse_args(argv)
164183
return args, ''

source/config.ini

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@ port = 4242
2222
# The ip address to bind to, empty will bind to all interfaces
2323
server = localhost
2424

25-
# The http port to use
26-
serverPort = 9084
25+
# The https port to use
26+
serverPort = 9980
27+
28+
# The name of REST HTTPS API key name
29+
apiKeyName = scale_grafana
30+
31+
# The REST HTTPS API key value, f.e:
32+
# apiKeyValue = e40960c9-de0a-4c75-bc71-0bcae6db23b2
2733

2834
#################################### Logging ##################################
2935
[logging]

source/messages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
'MissingParm': 'Missing mandatory parameters, quitting',
3131
'KeyPathError': 'KeyPath directory not found, quitting',
3232
'CertError': 'Missing certificates in tht specified keyPath directory, quitting',
33-
'CollectorErr': 'Failed to initialize connection to pmcollector, quitting',
33+
'CollectorErr': 'Failed to initialize connection to pmcollector: {}, quitting',
3434
'MetaError': 'Metadata could not be retrieved. Check log file for more details, quitting',
3535
'MetaSuccess': 'Successfully retrieved MetaData',
3636
'QueryError': 'Query request could not be proceed. Reason: {}',
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'''
2+
##############################################################################
3+
# Copyright 2021 IBM Corp.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
##############################################################################
17+
18+
Created on Jan 28, 2021
19+
20+
@author: HWASSMAN
21+
'''
22+
23+
# catch import failure on AIX since we will not be shipping our third-party libraries on AIX
24+
try:
25+
import requests
26+
import urllib3
27+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
28+
except Exception:
29+
pass
30+
31+
32+
33+
DEFAULT_HEADERS = {
34+
"Accept": "application/json",
35+
"Content-type": "application/json"
36+
}
37+
38+
39+
def getAuthHandler(keyName, keyValue):
40+
if not isinstance(keyName, bytes):
41+
keyName = bytes(keyName, 'utf-8')
42+
if not isinstance(keyValue, bytes):
43+
keyValue = bytes(keyValue, 'utf-8')
44+
return requests.auth.HTTPBasicAuth(keyName,keyValue)
45+
46+
47+
def createRequestDataObj(logger, method, endpoint, host, port, auth, headers=None, files=None, json=None, params=None, data=None, cookies=None, hooks=None):
48+
'''This method is used to prepare an instance of the class: <requests.Request>, which will be sent to the server. '''
49+
50+
if method.upper() not in ['GET', 'DELETE']:
51+
logger.error('createRequestDataObj __ request METHOD {} not allowed'.format(method))
52+
return None
53+
if not host or not port or not endpoint or not auth:
54+
logger.error('createRequestDataObj __ missing mandatory parameters')
55+
return None
56+
url = f'https://{host}:{port}/sysmon/v1/{endpoint}'
57+
# Create the Request.
58+
req = requests.Request(method=method.upper(),
59+
url=url,
60+
headers=headers or DEFAULT_HEADERS,
61+
files=files,
62+
data=data or {},
63+
json=json,
64+
params=params or {},
65+
auth=auth,
66+
cookies=cookies,
67+
hooks=hooks,
68+
)
69+
logger.debug('createRequestDataObj __ created request')
70+
return req
71+
72+
73+
class perfHTTPrequestHelper(object):
74+
"""
75+
REST communication handler / dispatcher for the QueryHandler
76+
1. does send a prepared Request object to a server
77+
2. waits until the server response, finally forwards a result to the QueryHandler
78+
"""
79+
80+
def __init__(self, logger, reqdata=None, session=None):
81+
self.session = session or requests.Session()
82+
self.requestData = reqdata
83+
self.logger = logger
84+
85+
def doRequest(self):
86+
if self.requestData and isinstance(self.requestData, requests.Request):
87+
_prepRequest = self.session.prepare_request(self.requestData)
88+
try:
89+
res = self.session.send(_prepRequest)
90+
return res
91+
except requests.exceptions.ConnectionError:
92+
res = requests.Response()
93+
res.status_code = 503
94+
res.reason = "Connection refused from server"
95+
res.content = None
96+
return res
97+
except requests.exceptions.RequestException as e:
98+
self.logger.debug('doRequest __ RequestException. Request data: {}, Response data: {}'.format(e.request, e.response))
99+
res = requests.Response()
100+
res.status_code = 404
101+
res.reason = "The request could not be processed from server"
102+
res.content = None
103+
return res
104+
else:
105+
raise TypeError('doRequest __ Error: request data wrong format')

0 commit comments

Comments
 (0)