mirror of
				https://github.com/apache/cloudstack.git
				synced 2025-10-26 08:42:29 +01:00 
			
		
		
		
	* Add sshkey and password manager script that works with userdata server and configdrive * fix duplicated sshkey insertion * Add Alpine support * Force curl to fail on server errors * Keep user defined ssh-keys only
		
			
				
	
	
		
			509 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
			
		
		
	
	
			509 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Bash
		
	
	
	
	
	
| #!/usr/bin/env 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.
 | |
| 
 | |
| username=root
 | |
| userDataServerPort=8080
 | |
| configDriveLabel=config-2
 | |
| 
 | |
| function findPrimaryNetwork(){
 | |
|     outputLog "Detecting primary network"
 | |
|     if command -v ip &> /dev/null
 | |
|     then
 | |
|         primaryNet=$(ip -o -4 route show to default | awk '{print $5}')
 | |
|     elif command -v netstat &> /dev/null
 | |
|     then
 | |
|         primaryNet=$(netstat -r4 | grep default | awk '{print $(NF)}')
 | |
|     elif command -v route &> /dev/null
 | |
|     then
 | |
|         primaryNet=$(route -4 2> /dev/null | grep default | awk '{print $(NF)}')
 | |
|         if [ -z "$primaryNet" ]
 | |
|         then
 | |
|             primaryNet=$(route get default 2> /dev/null | grep interface | tr -d ' ' | awk '{split($0,a,":"); print a[2]}')
 | |
|         fi
 | |
|     fi
 | |
|     if [ -z "$primaryNet" ]
 | |
|     then
 | |
|         outputLog "Could not find primary network"
 | |
|         return 1
 | |
|     fi
 | |
|     echo "$primaryNet"
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| function findUserDataServer(){
 | |
|     primaryNet=$1
 | |
|     outputLog "Trying to find userdata server"
 | |
|     if [ -z "$primaryNet" ]
 | |
|     then
 | |
|         outputLog "Unable to determine the userdata server, falling back to data-server"
 | |
|         echo "data-server"
 | |
|         return 0
 | |
|     fi
 | |
| 
 | |
|     if command -v netplan &> /dev/null
 | |
|     then
 | |
|         outputLog "Operating System is using netplan"
 | |
| 
 | |
|         userDataServer=$(netplan ip leases "$primaryNet" | grep SERVER_ADDRESS | awk '{split($0,a,"="); print a[2]}')
 | |
| 
 | |
|         if [ -n "$userDataServer" ]
 | |
|         then
 | |
|             outputLog "Found userdata server IP $userDataServer in netplan config"
 | |
|             echo "$userDataServer"
 | |
|             return 0
 | |
|         fi
 | |
|     fi
 | |
| 
 | |
|     if command -v nmcli &> /dev/null
 | |
|     then
 | |
|         outputLog "Operating System is using NetworkManager"
 | |
| 
 | |
|         userDataServer=$(nmcli -t connection show "$(nmcli -t -f UUID,DEVICE connection | grep "$primaryNet" | awk '{split($0,a,":"); print a[1]}')" | grep next_server | tr -d ' ' |awk '{split($0,a,"="); print a[2]}')
 | |
| 
 | |
|         if [ -n "$userDataServer" ]
 | |
|         then
 | |
|             outputLog "Found userdata server IP $userDataServer in NetworkManager config"
 | |
|             echo "$userDataServer"
 | |
|             return 0
 | |
|         fi
 | |
|     fi
 | |
| 
 | |
|     if command -v wicked &> /dev/null
 | |
|     then
 | |
|         outputLog "Operating System is using wicked"
 | |
| 
 | |
|         userDataServer=$(grep SERVERID /run/wicked/leaseinfo."$primaryNet"* | tr -d "'" | awk '{split($0,a,"="); print a[2]}')
 | |
| 
 | |
|         if [ -n "$userDataServer" ]
 | |
|         then
 | |
|             outputLog "Found userdata server IP $userDataServer in wicked config"
 | |
|             echo "$userDataServer"
 | |
|             return 0
 | |
|         fi
 | |
|     fi
 | |
| 
 | |
|     if command -v udhcpc &> /dev/null
 | |
|     then
 | |
|         outputLog "Operating System is using udhcpc"
 | |
| 
 | |
|         userDataServer=$(< /run/dhcp-server-ip."$primaryNet")
 | |
| 
 | |
|         if [ -n "$userDataServer" ]
 | |
|         then
 | |
|             outputLog "Found userdata server IP $userDataServer in udhcpc"
 | |
|             echo "$userDataServer"
 | |
|             return 0
 | |
|         fi
 | |
|     fi
 | |
| 
 | |
|     outputLog "Searching for DHCP server in lease files"
 | |
| 
 | |
|     primaryLease=$(
 | |
|         dhcpFolders="/var/lib/dhclient/* /var/lib/dhcp3/* /var/lib/dhcp/* /var/lib/NetworkManager/* /var/db/dhclient*"
 | |
|         for files in $dhcpFolders
 | |
|         do
 | |
|             if [ -e "$files" ]
 | |
|             then
 | |
|                 < "$files" tr -d '\n' | sed 's/  //g ; s/lease {//g ; s/}/\n/g' | grep 'option routers'
 | |
|             fi
 | |
|         done
 | |
|     )
 | |
| 
 | |
|     serverList=$(
 | |
|         IFS=$'\n'
 | |
|         for line in $(echo -e "$primaryLease")
 | |
|         do
 | |
|             splitLine=$(echo "$line" | sed -e 's/;/\n/g')
 | |
|             if date -j &> /dev/null
 | |
|             then
 | |
|                 timestamp=$(date -j -f "%Y/%m/%d %H:%M:%S" "$(echo "$splitLine" | grep 'expire' | sed -r 's/.*expire [0-9]+ (.*)/\1/')" +"%s")
 | |
|             else
 | |
|                 timestamp=$(date -d "$(echo "$splitLine" | grep 'expire' | sed -e 's/.*expire [0-9]\+ \(.*\)/\1/')" +"%s")
 | |
|             fi
 | |
|             interface=$(echo "$splitLine" | grep 'interface' | sed -e 's/.*interface "\(.*\)"/\1/')
 | |
|             server=$(echo "$splitLine" | grep 'dhcp-server-identifier' | sed -e 's/.*dhcp-server-identifier \(.*\)/\1/')
 | |
|             echo "$timestamp","$interface","$server"
 | |
|         done
 | |
|     )
 | |
| 
 | |
|     userDataServer=$(echo "$serverList" | grep "$primaryNet" | sort -n | tail -1 | awk '{split($0,a,","); print a[3]}')
 | |
| 
 | |
|     if [ -n "$userDataServer" ]
 | |
|     then
 | |
|         outputLog "Userdata server found: $userDataServer"
 | |
|         echo "$userDataServer"
 | |
|         return 0
 | |
|     fi
 | |
| 
 | |
|     outputLog "Unable to determine the userdata server, falling back to data-server"
 | |
|     echo "data-server"
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| function getPasswordFromUserDataServer(){
 | |
|     userDataServer=$1
 | |
|     userDataServerPort=$2
 | |
|     outputLog "Sending request to userdata server at $userDataServer to get the password"
 | |
|     if ! response=$(curl --fail --silent --connect-timeout 20 --retry 3 --header "DomU_Request: send_my_password" http://"$userDataServer":"$userDataServerPort")
 | |
|     then
 | |
|         outputLog "Failed to send request to userdata server at $userDataServer"
 | |
|         return 4
 | |
|     fi
 | |
|     outputLog "Got response from userdata server at $userDataServer"
 | |
|     response=$(echo "$response" | tr -d '\r')
 | |
|     case $response in
 | |
|         "")
 | |
|             outputLog "Userdata server at $userDataServer did not have any password for the VM"
 | |
|             return 2
 | |
|         ;;
 | |
|         "bad_request")
 | |
|             outputLog "VM sent an invalid request to userdata server at $userDataServer"
 | |
|             return 3
 | |
|         ;;
 | |
|         "saved_password")
 | |
|             outputLog "VM has already saved a password from the userdata server at $userDataServer"
 | |
|             return 1
 | |
|         ;;
 | |
|         *)
 | |
|             outputLog "VM got a valid password from server at $userDataServer"
 | |
|             echo "$response"
 | |
|             return 0
 | |
|     esac
 | |
| }
 | |
| 
 | |
| function findHomeDirectory(){
 | |
|     username=$1
 | |
|     getent passwd "$username"|awk -F ":" '{print $6}'
 | |
| }
 | |
| 
 | |
| function setPassword(){
 | |
|     username=$1
 | |
|     homeDir=$2
 | |
|     password=$3
 | |
|     if command -v md5sum &> /dev/null
 | |
|     then
 | |
|         newMd5=$(echo "$password" | md5sum | awk '{print $1}')
 | |
|     elif command -v md5 &> /dev/null
 | |
|     then
 | |
|         newMd5=$(echo "$password" | md5)
 | |
|     else
 | |
|         newMd5='N/A'
 | |
|     fi
 | |
|     if [ $newMd5 != 'N/A' ]
 | |
|     then
 | |
|         if [ -f "$homeDir"/.password.md5 ]
 | |
|         then
 | |
|             oldMd5=$(cat "$homeDir"/.password.md5)
 | |
|         fi
 | |
|         if [ "$newMd5" ==  "$oldMd5" ]
 | |
|         then
 | |
|             outputLog  "There is no update of VM password"
 | |
|             return 0
 | |
|         fi
 | |
|     else
 | |
|         outputLog "Cannot determine change of password"
 | |
|     fi
 | |
|     outputLog "Changing password for user $username"
 | |
|     if command -v chpasswd &> /dev/null
 | |
|     then
 | |
|         echo "$username":"$password" | chpasswd
 | |
|     elif command -v usermod &> /dev/null && command -v mkpasswd &> /dev/null
 | |
|     then
 | |
|         usermod -p "$(mkpasswd -m SHA-512 "$password")" "$username"
 | |
|     elif command -v pw &> /dev/null
 | |
|     then
 | |
|         echo "$password" | pw mod user "$username" -h 0
 | |
|     else
 | |
|         outputLog "Failed to change password for user $username"
 | |
|         return 1
 | |
|     fi
 | |
|     outputLog "Successfully changed password for user $username"
 | |
|     if [ $newMd5 != 'N/A' ]
 | |
|     then
 | |
|         echo "$newMd5" > "$homeDir"/.password.md5
 | |
|         chmod 600 "$homeDir"/.password.md5
 | |
|         chown "$username": "$homeDir"/.password.md5
 | |
|     fi
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| function sendAckToUserDataServer(){
 | |
|     userDataServer=$1
 | |
|     userDataServerPort=$2
 | |
|     outputLog "Sending acknowledgment to userdata server at $userDataServer"
 | |
|     if ! curl --fail --silent --connect-timeout 20 --retry 3 --header "DomU_Request: saved_password" "$userDataServer":"$userDataServerPort" &> /dev/null
 | |
|     then
 | |
|         outputLog "Failed to sent acknowledgment to userdata server at $userDataServer"
 | |
|         return 1
 | |
|     fi
 | |
|     outputLog "Successfully sent acknowledgment to userdata server at $userDataServer"
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| function getPublicKeyFromUserDataServer(){
 | |
|     userDataServer=$1
 | |
|     outputLog "Sending request to userdata server at $userDataServer to get public key"
 | |
|     if ! reponse=$(curl --fail --silent --connect-timeout 20 --retry 3 http://"$userDataServer"/latest/public-keys)
 | |
|     then
 | |
|         outputLog "Failed to get public key from userdata server"
 | |
|         return 2
 | |
|     fi
 | |
|     outputLog "Got response from userdata server at $userDataServer"
 | |
|     if [ -z "$reponse" ]
 | |
|     then
 | |
|         outputLog "Did not receive any public keys from userdata server"
 | |
|         return 1
 | |
|     fi
 | |
|     outputLog "Successfully get public key from userdata server"
 | |
|     echo "$reponse"
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| function setPublicKey(){
 | |
|     username=$1
 | |
|     homeDir=$2
 | |
|     publicKey=$3
 | |
|     outputLog "Applying public key for $username"
 | |
|     sshDir=$homeDir/.ssh
 | |
|     authorizedKeysFile=$sshDir/authorized_keys
 | |
| 
 | |
|     if [ ! -d "$sshDir" ]
 | |
|     then
 | |
|         outputLog ".ssh directory for $username not found, creating .ssh directory"
 | |
|         mkdir "$sshDir"
 | |
|     fi
 | |
| 
 | |
|     if [ ! -f "$authorizedKeysFile" ]
 | |
|     then
 | |
|         outputLog "authorized_keys file for $username not found, creating authorized_keys file"
 | |
|         touch "$authorizedKeysFile"
 | |
|     fi
 | |
|     if grep "$(echo "$publicKey" | awk '{print $2}')" "$authorizedKeysFile" > /dev/null
 | |
|     then
 | |
|         outputLog "No need to update authorized_keys file"
 | |
|         return 0
 | |
|     fi
 | |
|     outputLog "Writing public key in authorized_keys file"
 | |
|     sed -i "/ cloudstack@apache.org$/d" "$authorizedKeysFile"
 | |
|     echo "$publicKey cloudstack@apache.org" >> "$authorizedKeysFile"
 | |
|     chmod 600 "$authorizedKeysFile"
 | |
|     chmod 700 "$sshDir"
 | |
|     chown -R "$username": "$sshDir"
 | |
|     which restorecon &> /dev/null && restorecon -R -v "$sshDir"
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| function findConfigDrive(){
 | |
|     configDriveLabel=$1
 | |
|     outputLog "Searching for ConfigDrive"
 | |
| 
 | |
|     if [ -e /dev/disk/by-label/"$configDriveLabel" ]
 | |
|     then
 | |
|         outputLog "ConfigDrive found at /dev/disk/by-label/$configDriveLabel"
 | |
|         echo "/dev/disk/by-label/$configDriveLabel"
 | |
|         return 0
 | |
|     fi
 | |
| 
 | |
|     if [ -e /dev/iso9660/"$configDriveLabel" ]
 | |
|     then
 | |
|         outputLog "ConfigDrive found at /dev/iso9660/$configDriveLabel"
 | |
|         echo "/dev/iso9660/$configDriveLabel"
 | |
|         return 0
 | |
|     fi
 | |
| 
 | |
|     blockDevice=$(blkid -t LABEL="$configDriveLabel" /dev/hd? /dev/sd? /dev/xvd? /dev/vd? /dev/sr? -o device 2> /dev/null)
 | |
|     if [ -n "$blockDevice" ]
 | |
|     then
 | |
|         outputLog "ConfigDrive found at $blockDevice"
 | |
|         echo "$blockDevice"
 | |
|         return 0
 | |
|     fi
 | |
|     outputLog "ConfigDrive not found"
 | |
|     return 1
 | |
| }
 | |
| 
 | |
| function mountConfigDrive(){
 | |
|     disk=$1
 | |
|     outputLog "Mounting ConfigDrive"
 | |
|     mountDir=$(mktemp -d)
 | |
|     if [ ! -e "$mountDir" ]
 | |
|     then
 | |
|         mkdir "$mountDir"
 | |
|         chmod 700 "$mountDir"
 | |
|     fi
 | |
| 
 | |
|     mounted=0
 | |
|     if [ $mounted == 0 ] && mount -r "$disk" "$mountDir" &> /dev/null
 | |
|     then
 | |
|         mounted=1
 | |
|     fi
 | |
|     if [ $mounted == 0 ] && mount -r -t cd9660 "$disk" "$mountDir" &> /dev/null
 | |
|     then
 | |
|         mounted=1
 | |
|     fi
 | |
|     if [ $mounted == 0 ] && mount -r -t iso9660 "$disk" "$mountDir" &> /dev/null
 | |
|     then
 | |
|         mounted=1
 | |
|     fi
 | |
| 
 | |
|     if [ $mounted == 1 ]
 | |
|     then
 | |
|         outputLog "$disk successfully mounted on $mountDir"
 | |
|         echo "$mountDir"
 | |
|         return 0
 | |
|     fi
 | |
| 
 | |
|     outputLog "Failed mounting $disk on $mountDir"
 | |
|     rm -rf "$mountDir"
 | |
|     return 1
 | |
| }
 | |
| 
 | |
| function unmountConfigDrive(){
 | |
|     mountDir=$1
 | |
|     outputLog "Unmounting ConfigDrive"
 | |
|     if ! umount "$mountDir"
 | |
|     then
 | |
|         outputLog "Failed unmounting $mountDir"
 | |
|         return 1
 | |
|     fi
 | |
|     rm -rf "$mountDir"
 | |
|     outputLog "Successfully unmount $mountDir"
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| function getPasswordFromConfigDrive(){
 | |
|     mountDir=$1
 | |
|     passwordFile=$mountDir/cloudstack/password/vm_password.txt
 | |
|     if [ ! -f "$passwordFile" ]
 | |
|     then
 | |
|         outputLog "Password file not found in ConfigDrivee"
 | |
|         return 3
 | |
|     fi
 | |
|     outputLog "Password file found in ConfigDrive"
 | |
|     content=$(< "$passwordFile" tr -d '\r')
 | |
| 
 | |
|     case $content in
 | |
| 
 | |
|         "")
 | |
|             outputLog "ConfigDrive did not have any password for the VM"
 | |
|             return 2
 | |
|         ;;
 | |
| 
 | |
|         "saved_password")
 | |
|             outputLog "VM has already saved a password"
 | |
|             return 1
 | |
|         ;;
 | |
| 
 | |
|         *)
 | |
|             outputLog "VM got a valid password"
 | |
|             echo "$content"
 | |
|             return 0
 | |
|     esac
 | |
| }
 | |
| 
 | |
| function getPublicKeyFromConfigDrive() {
 | |
|     mountDir=$1
 | |
|     publicKeyFile=$mountDir/cloudstack/metadata/public-keys.txt
 | |
| 
 | |
|     if [ ! -f "$publicKeyFile" ]
 | |
|     then
 | |
|         outputLog "Public key file not found in ConfigDrive"
 | |
|         return 2
 | |
|     fi
 | |
|     content=$(< "$publicKeyFile" tr -d '\r')
 | |
| 
 | |
|     if [ -z "$content" ]
 | |
|     then
 | |
|         outputLog "Did not receive any public keys"
 | |
|         return 1
 | |
|     fi
 | |
|     echo "$content"
 | |
|     outputLog "Public key successfully received."
 | |
|     return 0
 | |
| }
 | |
| 
 | |
| function outputLog() {
 | |
|     stderr=1
 | |
|     logger=1
 | |
|     message=$1
 | |
|     if [ $stderr == 1 ]
 | |
|     then
 | |
|         echo "Cloud Password Manager: $message" 1>&2
 | |
|     fi
 | |
|     if [ $logger == 1 ]
 | |
|     then
 | |
|         logger -t "Cloud Password Manager" "$message"
 | |
|     fi
 | |
| }
 | |
| 
 | |
| publicKeyReceived=0
 | |
| passwordReceived=0
 | |
| dataSource=''
 | |
| 
 | |
| if disk=$(findConfigDrive "$configDriveLabel")
 | |
| then
 | |
|     if mountDir=$(mountConfigDrive "$disk")
 | |
|     then
 | |
|         dataSource='ConfigDrive'
 | |
|         if publicKey=$(getPublicKeyFromConfigDrive "$mountDir")
 | |
|         then
 | |
|             publicKeyReceived=1
 | |
|         fi
 | |
|         if password=$(getPasswordFromConfigDrive "$mountDir")
 | |
|         then
 | |
|             passwordReceived=1
 | |
|         fi
 | |
|         unmountConfigDrive "$mountDir"
 | |
|     fi
 | |
| fi
 | |
| if [ $publicKeyReceived == 0 ] || [ $passwordReceived == 0 ]
 | |
| then
 | |
|     primaryNet=$(findPrimaryNetwork)
 | |
|     userDataServer=$(findUserDataServer "$primaryNet")
 | |
|     if [ $publicKeyReceived == 0 ]
 | |
|     then
 | |
|         if publicKey=$(getPublicKeyFromUserDataServer "$userDataServer")
 | |
|         then
 | |
|             dataSource='UserDataServer'
 | |
|             publicKeyReceived=1
 | |
|         fi
 | |
|     fi
 | |
|     if [ $passwordReceived == 0 ]
 | |
|     then
 | |
|         if password=$(getPasswordFromUserDataServer "$userDataServer" "$userDataServerPort")
 | |
|         then
 | |
|             dataSource='UserDataServer'
 | |
|             passwordReceived=1
 | |
|         fi
 | |
|     fi
 | |
| fi
 | |
| homeDir=$(findHomeDirectory "$username")
 | |
| if [ $passwordReceived == 1 ]
 | |
| then
 | |
|     setPassword "$username" "$homeDir" "$password"
 | |
|     if [ $dataSource == 'UserDataServer' ]
 | |
|     then
 | |
|         sendAckToUserDataServer "$userDataServer" "$userDataServerPort"
 | |
|     fi
 | |
| fi
 | |
| if [ $publicKeyReceived == 1 ]
 | |
| then
 | |
|     setPublicKey "$username" "$homeDir" "$publicKey"
 | |
| fi
 |