* Added a ping check before trying to SSH * Using servers config to determine timeouts * (patch) Statically set ssh connect timeout to 20s. This should come from the config Change-Id: Id8c7314079dd0af4b196111de0668ab230c28fde
377 lines
15 KiB
Python
377 lines
15 KiB
Python
"""
|
|
Copyright 2013 Rackspace
|
|
|
|
Licensed 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.
|
|
"""
|
|
|
|
import time
|
|
import re
|
|
|
|
from cafe.engine.clients.ssh import SSHBaseClient
|
|
from cafe.common.reporting import cclogging
|
|
from cafe.engine.clients.ping import PingClient
|
|
from cloudcafe.compute.common.models.file_details import FileDetails
|
|
from cloudcafe.compute.common.models.partition import Partition, DiskSize
|
|
from cafe.engine.clients.remote_instance.linux.base_client import BasePersistentLinuxClient
|
|
from cloudcafe.compute.common.exceptions import FileNotFoundException, ServerUnreachable, SshConnectionException
|
|
|
|
|
|
class LinuxClient(BasePersistentLinuxClient):
|
|
|
|
def __init__(self, ip_address=None, server_id=None, username=None,
|
|
password=None, config=None, os_distro=None):
|
|
self.client_log = cclogging.getLogger \
|
|
(cclogging.get_object_namespace(self.__class__))
|
|
ssh_timeout = config.connection_timeout
|
|
if ip_address is None:
|
|
raise ServerUnreachable("None")
|
|
self.ip_address = ip_address
|
|
self.username = username
|
|
if self.username is None:
|
|
self.username = 'root'
|
|
self.password = password
|
|
self.server_id = server_id
|
|
|
|
start = int(time.time())
|
|
reachable = False
|
|
while not reachable:
|
|
reachable = PingClient.ping(ip_address,
|
|
config.ip_address_version_for_ssh)
|
|
time.sleep(config.connection_retry_interval)
|
|
if int(time.time()) - start >= config.connection_timeout:
|
|
raise ServerUnreachable(ip_address)
|
|
|
|
self.ssh_client = SSHBaseClient(self.ip_address,
|
|
self.username,
|
|
self.password,
|
|
timeout=ssh_timeout)
|
|
if not self.ssh_client.test_connection_auth():
|
|
self.client_log.error("Ssh connection failed for: IP:{0} \
|
|
Username:{1} Password: {2}".format(self.ip_address,
|
|
self.username, self.password))
|
|
raise SshConnectionException("ssh connection failed")
|
|
|
|
def can_connect_to_public_ip(self):
|
|
"""
|
|
@summary: Checks if you can connect to server using public ip
|
|
@return: True if you can connect, False otherwise
|
|
@rtype: bool
|
|
"""
|
|
# This returns true since the connection has already been tested in the
|
|
# init method
|
|
|
|
return self.ssh_client is not None
|
|
|
|
def can_ping_public_ip(self, public_addresses, ip_address_version_for_ssh):
|
|
"""
|
|
@summary: Checks if you can ping a public ip
|
|
@param addresses: List of public addresses
|
|
@type addresses: Address List
|
|
@return: True if you can ping, False otherwise
|
|
@rtype: bool
|
|
"""
|
|
for public_address in public_addresses:
|
|
if public_address.version == 4 and not PingClient.ping(public_address.addr, ip_address_version_for_ssh):
|
|
return False
|
|
return True
|
|
|
|
def can_authenticate(self):
|
|
"""
|
|
@summary: Checks if you can authenticate to the server
|
|
@return: True if you can connect, False otherwise
|
|
@rtype: bool
|
|
"""
|
|
return self.ssh_client.test_connection_auth()
|
|
|
|
def reboot(self, timeout=100):
|
|
'''
|
|
@timeout: max timeout for the machine to reboot
|
|
'''
|
|
ssh_connector = SSHConnector(self.ip_address, self.username,
|
|
self.password)
|
|
response, prompt = ssh_connector.exec_shell_command("sudo reboot")
|
|
response, prompt = ssh_connector.exec_shell_command(self.password)
|
|
self.client_log.info("Reboot response for %s: %s" % (self.ip_address,
|
|
response))
|
|
max_time = time.time() + timeout
|
|
while time.time() < max_time:
|
|
time.sleep(5)
|
|
if self.ssh_client.test_connection_auth():
|
|
self.client_log.info("Reboot successful for %s"
|
|
% (self.ip_address))
|
|
return True
|
|
|
|
def get_hostname(self):
|
|
"""
|
|
@summary: Gets the host name of the server
|
|
@return: The host name of the server
|
|
@rtype: string
|
|
"""
|
|
return self.ssh_client.exec_command("hostname").rstrip()
|
|
|
|
def can_remote_ping_private_ip(self, private_addresses):
|
|
"""
|
|
@summary: Checks if you can ping a private ip from this server.
|
|
@param private_addresses: List of private addresses
|
|
@type private_addresses: Address List
|
|
@return: True if you can ping, False otherwise
|
|
@rtype: bool
|
|
"""
|
|
for private_address in private_addresses:
|
|
if private_address.version == 4 and not PingClient.ping_using_remote_machine(self.ssh_client, private_address.addr):
|
|
return False
|
|
return True
|
|
|
|
def get_files(self, path):
|
|
"""
|
|
@summary: Gets the list of filenames from the path
|
|
@param path: Path from where to get the filenames
|
|
@type path: string
|
|
@return: List of filenames
|
|
@rtype: List of strings
|
|
"""
|
|
command = "ls -m " + path
|
|
return self.ssh_client.exec_command(command).rstrip('\n').split(', ')
|
|
|
|
def get_ram_size_in_mb(self):
|
|
"""
|
|
@summary: Returns the RAM size in MB
|
|
@return: The RAM size in MB
|
|
@rtype: string
|
|
"""
|
|
output = self.ssh_client.exec_command('free -m | grep Mem')
|
|
# TODO (dwalleck): We should handle the failure case here
|
|
if output:
|
|
return output.split()[1]
|
|
|
|
def get_swap_size_in_mb(self):
|
|
"""
|
|
@summary: Returns the Swap size in MB
|
|
@return: The Swap size in MB
|
|
@rtype: int
|
|
"""
|
|
output = self.ssh_client.exec_command(
|
|
'fdisk -l /dev/xvdc1 2>/dev/null | grep "Disk.*bytes"').rstrip('\n')
|
|
if output:
|
|
return int(output.split()[2])
|
|
|
|
def get_disk_size_in_gb(self, disk_path):
|
|
"""
|
|
@summary: Returns the disk size in GB
|
|
@return: The disk size in GB
|
|
@rtype: int
|
|
"""
|
|
command = "df -h | grep '{0}'".format(disk_path)
|
|
output = self.ssh_client.exec_command(command)
|
|
size = output.split()[1]
|
|
|
|
def is_decimal(char):
|
|
return str.isdigit(char) or char == "."
|
|
size = filter(is_decimal, size)
|
|
return float(size)
|
|
|
|
def get_number_of_vcpus(self):
|
|
"""
|
|
@summary: Get the number of vcpus assigned to the server
|
|
@return: The number of vcpus assigned to the server
|
|
@rtype: int
|
|
"""
|
|
command = 'cat /proc/cpuinfo | grep processor | wc -l'
|
|
output = self.ssh_client.exec_command(command)
|
|
return int(output)
|
|
|
|
def get_partitions(self):
|
|
"""
|
|
@summary: Returns the contents of /proc/partitions
|
|
@return: The partitions attached to the instance
|
|
@rtype: string
|
|
"""
|
|
command = 'cat /proc/partitions'
|
|
output = self.ssh_client.exec_command(command)
|
|
return output
|
|
|
|
def get_uptime(self):
|
|
"""
|
|
@summary: Get the uptime time of the server
|
|
@return: The uptime of the server
|
|
"""
|
|
result = self.ssh_client.exec_command('cat /proc/uptime')
|
|
uptime = float(result.split(' ')[0])
|
|
return uptime
|
|
|
|
def create_file(self, file_name, file_content, file_path=None):
|
|
'''
|
|
@summary: Create a new file
|
|
@param file_name: File Name
|
|
@type file_name: String
|
|
@param file_content: File Content
|
|
@type file_content: String
|
|
@return filedetails: File details such as content, name and path
|
|
@rtype filedetails; FileDetails
|
|
'''
|
|
if file_path is None:
|
|
file_path = "/root/" + file_name
|
|
self.ssh_client.exec_command(
|
|
'echo -n ' + file_content + '>>' + file_path)
|
|
return FileDetails("644", file_content, file_path)
|
|
|
|
def get_file_details(self, filepath):
|
|
"""
|
|
@summary: Get the file details
|
|
@param filepath: Path to the file
|
|
@type filepath: string
|
|
@return: File details including permissions and content
|
|
@rtype: FileDetails
|
|
"""
|
|
output = self.ssh_client.exec_command(
|
|
'[ -f ' + filepath + ' ] && echo "File exists" || echo "File does not exist"')
|
|
if not output.rstrip('\n') == 'File exists':
|
|
raise FileNotFoundException(
|
|
"File:" + filepath + " not found on instance.")
|
|
|
|
file_permissions = self.ssh_client.exec_command(
|
|
'stat -c %a ' + filepath).rstrip("\n")
|
|
file_contents = self.ssh_client.exec_command('cat ' + filepath)
|
|
return FileDetails(file_permissions, file_contents, filepath)
|
|
|
|
def is_file_present(self, filepath):
|
|
"""
|
|
@summary: Check if the given file is present
|
|
@param filepath: Path to the file
|
|
@type filepath: string
|
|
@return: True if File exists, False otherwise
|
|
"""
|
|
output = self.ssh_client.exec_command(
|
|
'[ -f ' + filepath + ' ] && echo "File exists" || echo "File does not exist"')
|
|
return output.rstrip('\n') == 'File exists'
|
|
|
|
def get_partition_types(self):
|
|
"""
|
|
@summary: Return the partition types for all partitions
|
|
@return: The partition types for all partitions
|
|
@rtype: Dictionary
|
|
"""
|
|
partitions_list = self.ssh_client.exec_command(
|
|
'blkid').rstrip('\n').split('\n')
|
|
partition_types = {}
|
|
for row in partitions_list:
|
|
partition_name = row.split()[0].rstrip(':')
|
|
partition_types[partition_name] = re.findall(
|
|
r'TYPE="([^"]+)"', row)[0]
|
|
return partition_types
|
|
|
|
def get_partition_details(self):
|
|
"""
|
|
@summary: Return the partition details
|
|
@return: The partition details
|
|
@rtype: Partition List
|
|
"""
|
|
# Return a list of partition objects that each contains the name and
|
|
# size of the partition in bytes and the type of the partition
|
|
partition_types = self.get_partition_types()
|
|
partition_names = ' '.join(partition_types.keys())
|
|
|
|
partition_size_output = self.ssh_client.exec_command(
|
|
'fdisk -l %s 2>/dev/null | grep "Disk.*bytes"' % (partition_names)).rstrip('\n').split('\n')
|
|
partitions = []
|
|
for row in partition_size_output:
|
|
row_details = row.split()
|
|
partition_name = row_details[1].rstrip(':')
|
|
partition_type = partition_types[partition_name]
|
|
if partition_type == 'swap':
|
|
partition_size = DiskSize(
|
|
float(row_details[2]), row_details[3].rstrip(','))
|
|
else:
|
|
partition_size = DiskSize(
|
|
int(row_details[4]) / 1073741824, 'GB')
|
|
partitions.append(
|
|
Partition(partition_name, partition_size, partition_type))
|
|
return partitions
|
|
|
|
def verify_partitions(self, expected_disk_size, expected_swap_size, server_status, actual_partitions):
|
|
"""
|
|
@summary: Verify the partition details of the server
|
|
@param expected_disk_size: The expected value of the Disk size in GB
|
|
@type expected_disk_size: string
|
|
@param expected_swap_size: The expected value of the Swap size in GB
|
|
@type expected_swap_size: string
|
|
@param server_status: The status of the server
|
|
@type server_status: string
|
|
@param actual_partitions: The actual partition details of the server
|
|
@type actual_partitions: Partition List
|
|
@return: The result of verification and the message to be displayed
|
|
@rtype: Tuple (bool,string)
|
|
"""
|
|
expected_partitions = self._get_expected_partitions(
|
|
expected_disk_size, expected_swap_size, server_status)
|
|
if actual_partitions is None:
|
|
actual_partitions = self.get_partition_details()
|
|
|
|
for partition in expected_partitions:
|
|
if partition not in actual_partitions:
|
|
return False, self._construct_partition_mismatch_message(expected_partitions, actual_partitions)
|
|
return True, "Partitions Matched"
|
|
|
|
def _get_expected_partitions(self, expected_disk_size, expected_swap_size, server_status):
|
|
"""
|
|
@summary: Returns the expected partitions for a server based on server status
|
|
@param expected_disk_size: The Expected disk size of the server in GB
|
|
@type expected_disk_size: string
|
|
@param expected_swap_size: The Expected swap size of the server in MB
|
|
@type expected_swap_size: string
|
|
@param server_status: Status of the server (ACTIVE or RESCUE)
|
|
@type server_status: string
|
|
@return: The expected partitions
|
|
@rtype: Partition List
|
|
"""
|
|
# ignoring swap untill the rescue functionality is clarified
|
|
|
|
expected_partitions = [Partition(
|
|
'/dev/xvda1', DiskSize(expected_disk_size, 'GB'), 'ext3'),
|
|
Partition('/dev/xvdc1', DiskSize(expected_swap_size, 'MB'), 'swap')]
|
|
if str.upper(server_status) == 'RESCUE':
|
|
expected_partitions = [Partition(
|
|
'/dev/xvdb1', DiskSize(expected_disk_size, 'GB'), 'ext3')]
|
|
# expected_partitions.append(Partition('/dev/xvdd1',
|
|
# DiskSize(expected_swap_size, 'MB'), 'swap'))
|
|
return expected_partitions
|
|
|
|
def _construct_partition_mismatch_message(self, expected_partitions, actual_partitions):
|
|
"""
|
|
@summary: Constructs the partition mismatch message based on expected_partitions and actual_partitions
|
|
@param expected_partitions: Expected partitions of the server
|
|
@type expected_partitions: Partition List
|
|
@param actual_partitions: Actual Partitions of the server
|
|
@type actual_partitions: Partition List
|
|
@return: The partition mismatch message
|
|
@rtype: string
|
|
"""
|
|
message = 'Partitions Mismatch \n Expected Partitions:\n'
|
|
for partition in expected_partitions:
|
|
message += str(partition) + '\n'
|
|
message += ' Actual Partitions:\n'
|
|
for partition in actual_partitions:
|
|
message += str(partition) + '\n'
|
|
return message
|
|
|
|
def mount_file_to_destination_directory(self, source_path, destination_path):
|
|
'''
|
|
@summary: Mounts the file to destination directory
|
|
@param source_path: Path to file source
|
|
@type source_path: String
|
|
@param destination_path: Path to mount destination
|
|
@type destination_path: String
|
|
'''
|
|
self.ssh_client.exec_command(
|
|
'mount ' + source_path + ' ' + destination_path)
|