CLOUDSTACK-8272: Python based file-lock free password server implementation

- VRs are single CPU, so Threading based implementation favoured than Forking based
- Implements a Python based password server that does not use file based locks
- Saving password mechanism is provided by using secure token only to VR (localhost)
- Old serve_password implementation is removed
- Runs with Python 2.6+ with no external dependencies
- Locks used within threads for extra safety

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Rohit Yadav 2015-03-10 15:35:31 +05:30
parent cfd4573335
commit 4b45d25152
4 changed files with 208 additions and 146 deletions

View File

@ -20,13 +20,12 @@
addr=$1; addr=$1;
while [ "$ENABLED" == "1" ] while [ "$ENABLED" == "1" ]
do do
socat -lf /var/log/cloud.log TCP4-LISTEN:8080,reuseaddr,fork,crnl,bind=$addr SYSTEM:"/opt/cloud/bin/serve_password.sh \"\$SOCAT_PEERADDR\"" python /opt/cloud/bin/passwd_server_ip.py $addr >/dev/null 2>/dev/null
rc=$?
rc=$? if [ $rc -ne 0 ]
if [ $rc -ne 0 ] then
then logger -t cloud "Password server failed with error code $rc. Restarting it..."
logger -t cloud "Password server failed with error code $rc. Restarting socat..." sleep 3
sleep 3 fi
fi . /etc/default/cloud-passwd-srvr
. /etc/default/cloud-passwd-srvr
done done

View File

@ -0,0 +1,187 @@
#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# 'License'); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Client usage examples;
# Getting password:
# wget -q -t 3 -T 20 -O - --header 'DomU_Request: send_my_password' <routerIP>:8080
# Send ack:
# wget -t 3 -T 20 -O - --header 'DomU_Request: saved_password' localhost:8080
# Save password only from within router:
# /opt/cloud/bin/savepassword.sh -v <IP> -p <password>
# curl --header 'DomU_Request: save_password' http://localhost:8080/ -F ip=<IP> -F password=<passwd>
import binascii
import cgi
import os
import sys
import syslog
import threading
import urlparse
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from SocketServer import ThreadingMixIn #, ForkingMixIn
passMap = {}
secureToken = None
lock = threading.RLock()
def getTokenFile():
return '/tmp/passwdsrvrtoken'
def getPasswordFile():
return '/var/cache/cloud/passwords'
def initToken():
global secureToken
secureToken = binascii.hexlify(os.urandom(16))
with open(getTokenFile(), 'w') as f:
f.write(secureToken)
def checkToken(token):
return token == secureToken
def loadPasswordFile():
try:
with file(getPasswordFile()) as f:
for line in f:
if '=' not in line: continue
key, value = line.strip().split('=', 1)
passMap[key] = value
except IOError:
pass
def savePasswordFile():
with lock:
try:
with file(getPasswordFile(), 'w') as f:
for ip in passMap:
f.write('%s=%s\n' % (ip, passMap[ip]))
f.close()
except IOError, e:
syslog.syslog('serve_password: Unable to save to password file %s' % e)
def getPassword(ip):
return passMap.get(ip, None)
def setPassword(ip, password):
if not ip or not password:
return
with lock:
passMap[ip] = password
def removePassword(ip):
with lock:
if ip in passMap:
del passMap[ip]
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
pass
class PasswordRequestHandler(BaseHTTPRequestHandler):
server_version = 'CloudStack Password Server'
sys_version = '4.x'
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.send_header('Server', 'CloudStack Password Server')
self.end_headers()
requestType = self.headers.get('DomU_Request')
clientAddress = self.client_address[0]
if requestType == 'send_my_password':
password = getPassword(clientAddress)
if not password:
syslog.syslog('serve_password: requested password not found for %s' % clientAddress)
else:
self.wfile.write(password)
syslog.syslog('serve_password: password sent to %s' % clientAddress)
elif requestType == 'saved_password':
removePassword(clientAddress)
savePasswordFile()
self.wfile.write('saved_password')
syslog.syslog('serve_password: saved_password ack received from %s' % clientAddress)
else:
self.send_response(400)
self.wfile.write('bad_request')
syslog.syslog('serve_password: bad_request from IP %s' % clientAddress)
return
def do_POST(self):
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={'REQUEST_METHOD':'POST',
'CONTENT_TYPE':self.headers['Content-Type'],
})
self.send_response(200)
self.end_headers()
clientAddress = self.client_address[0]
if clientAddress not in ['localhost', '127.0.0.1']:
syslog.syslog('serve_password: non-localhost IP trying to save password: %s' % clientAddress)
self.send_response(403)
return
if 'ip' not in form or 'password' not in form or 'token' not in form or self.headers.get('DomU_Request') != 'save_password':
syslog.syslog('serve_password: request trying to save password does not contain both ip and password')
self.send_response(403)
return
token = form['token'].value
if not checkToken(token):
syslog.syslog('serve_password: invalid save_password token received from %s' % clientAddress)
self.send_response(403)
return
ip = form['ip'].value
password = form['password'].value
if not ip or not password:
syslog.syslog('serve_password: empty ip/password[%s/%s] received from savepassword' % (ip, password))
return
setPassword(ip, password)
savePasswordFile()
return
def log_message(self, format, *args):
return
def serve(HandlerClass = PasswordRequestHandler,
ServerClass = ThreadedHTTPServer):
listeningAddress = '127.0.0.1'
if len(sys.argv) > 1:
listeningAddress = sys.argv[1]
server_address = (listeningAddress, 8080)
passwordServer = ServerClass(server_address, HandlerClass)
passwordServer.allow_reuse_address = True
sa = passwordServer.socket.getsockname()
initToken()
loadPasswordFile()
syslog.syslog('serve_password running on %s:%s' % (sa[0], sa[1]))
try:
passwordServer.serve_forever()
except KeyboardInterrupt:
syslog.syslog('serve_password shutting down')
passwordServer.socket.close()
except Exception, e:
syslog.syslog('serve_password hit exception %s -- died' % e)
passwordServer.socket.close()
if __name__ == '__main__':
serve()

View File

@ -16,48 +16,27 @@
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
# Usage # Usage
# save_password -v <user VM IP> -p <password> # save_password -v <user VM IP> -p <password>
source /root/func.sh
lock="passwdlock"
#default timeout value is 30 mins as password reset command is not synchronized on agent side any more,
#and multiple commands can be sent to the same VR at a time
locked=$(getLockFile $lock 1800)
if [ "$locked" != "1" ]
then
exit 1
fi
PASSWD_FILE=/var/cache/cloud/passwords
while getopts 'v:p:' OPTION while getopts 'v:p:' OPTION
do do
case $OPTION in case $OPTION in
v) VM_IP="$OPTARG" v) VM_IP="$OPTARG"
;; ;;
p) PASSWORD="$OPTARG" p) PASSWORD="$OPTARG"
;; ;;
?) echo "Incorrect usage" ?) echo "Incorrect usage"
unlock_exit 1 $lock $locked ;;
;;
esac esac
done done
TOKEN_FILE="/tmp/passwdsrvrtoken"
[ -f $PASSWD_FILE ] || touch $PASSWD_FILE TOKEN=""
if [ -f $TOKEN_FILE ]; then
sed -i /$VM_IP=/d $PASSWD_FILE TOKEN=$(cat $TOKEN_FILE)
fi
ps aux | grep serve_password.sh |grep -v grep 2>&1 > /dev/null ps aux | grep passwd_server_ip.py |grep -v grep 2>&1 > /dev/null
if [ $? -eq 0 ] if [ $? -eq 0 ]
then then
echo "$VM_IP=$PASSWORD" >> $PASSWD_FILE curl --header "DomU_Request: save_password" http://127.0.0.1:8080/ -F "ip=$VM_IP" -F "password=$PASSWORD" -F "token=$TOKEN"
else
echo "$VM_IP=saved_password" >> $PASSWD_FILE
fi fi
unlock_exit $? $lock $locked

View File

@ -1,103 +0,0 @@
#!/bin/bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# set -x
source /root/func.sh
lock="passwdlock"
locked=$(getLockFile $lock)
if [ "$locked" != "1" ]
then
exit 1
fi
PASSWD_FILE=/var/cache/cloud/passwords
# $1 filename
# $2 keyname
# $3 value
replace_in_file() {
local filename=$1
local keyname=$2
local value=$3
sed -i /$keyname=/d $filename
echo "$keyname=$value" >> $filename
return $?
}
# $1 filename
# $2 keyname
get_value() {
local filename=$1
local keyname=$2
grep -i $keyname= $filename | cut -d= -f2
}
ip=$1
logger -t cloud "serve_password called to service a request for $ip."
while read input
do
if [ "$input" == "" ]
then
break
fi
request=$(echo "$input" | grep "DomU_Request:" | cut -d: -f2 | sed 's/^[ \t]*//')
if [ "$request" != "" ]
then
break
fi
done
# echo -e \"\\\"HTTP/1.0 200 OK\\\nDocumentType: text/plain\\\n\\\n\\\"\";
if [ "$request" == "send_my_password" ]
then
password=$(get_value $PASSWD_FILE $ip)
if [ "$password" == "" ]
then
logger -t cloud "serve_password sent bad_request to $ip."
# echo "bad_request"
# Return "saved_password" for non-existed entry, to make it
# work if domR was once destroyed.
echo "saved_password"
else
logger -t cloud "serve_password sent a password to $ip."
echo $password
fi
else
if [ "$request" == "saved_password" ]
then
replace_in_file $PASSWD_FILE $ip "saved_password"
logger -t cloud "serve_password sent saved_password to $ip."
echo "saved_password"
else
logger -t cloud "serve_password sent bad_request to $ip."
echo "bad_request"
fi
fi
# echo -e \"\\\"\\\n\\\"\"
unlock_exit 0 $lock $locked