freezer/freezer/storage/ssh.py

172 lines
6.0 KiB
Python

# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P.
#
# 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 errno
import os
import stat
import paramiko
from freezer.storage import fslike
from freezer.utils import utils
CHUNK_SIZE = 32768
class SshStorage(fslike.FsLikeStorage):
"""
:type ftp: paramiko.SFTPClient
"""
_type = 'ssh'
def __init__(self, storage_path, remote_username, remote_ip, port,
max_segment_size, ssh_key_path=None, remote_pwd=None):
"""
:param storage_path: directory of storage
:type storage_path: str
:return:
"""
self.ssh_key_path = ssh_key_path
self.remote_pwd = remote_pwd
self.remote_username = remote_username
self.remote_ip = remote_ip
self.port = port
self.ssh = None
self.ftp = None
self._validate()
self.init()
super(SshStorage, self).__init__(
storage_path=storage_path,
max_segment_size=max_segment_size)
def _validate(self):
"""
Validates if all parameters required to ssh are available.
:return: True or raises ValueError
"""
if not self.remote_ip:
raise ValueError('Please provide --ssh-host value.')
elif not self.remote_username:
raise ValueError('Please provide --ssh-username value.')
elif self.remote_pwd:
return True
elif not self.ssh_key_path:
raise ValueError('Please provide path to ssh key using '
'--ssh-key argument')
elif not os.path.exists(self.ssh_key_path):
raise ValueError('1.The {0} is required, please ensure '
'it exists and ensure backup node can '
'login to {1} as user {2} '
'without password.\n'
'2. --ssh-password argument is required and '
'ensure backup node can login to {1} '
'as user {2} with password.'
.format(self.ssh_key_path, self.remote_ip,
self.remote_username))
return True
def init(self):
ssh = paramiko.SSHClient()
ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
if self.remote_pwd:
ssh.connect(self.remote_ip, username=self.remote_username,
password=self.remote_pwd, port=self.port)
else:
ssh.connect(self.remote_ip, username=self.remote_username,
key_filename=self.ssh_key_path, port=self.port)
# we should keep link to ssh to prevent garbage collection
self.ssh = ssh
self.ftp = self.ssh.open_sftp()
def _is_dir(self, check_dir):
return stat.S_IFMT(self.ftp.stat(check_dir).st_mode) == stat.S_IFDIR
def rmtree(self, path):
files = self.ftp.listdir(path=path)
for f in files:
filepath = utils.path_join(path, f)
if self._is_dir(filepath):
self.rmtree(filepath)
else:
self.ftp.remove(filepath)
self.ftp.rmdir(path)
def create_dirs(self, path):
"""Change to this directory, recursively making new folders if needed.
Returns True if any folders were created."""
if path == '/':
# absolute path so change directory to root
self.ftp.chdir('/')
return
if path == '':
# top-level relative directory must exist
return
try:
self.ftp.chdir(path) # sub-directory exists
except IOError:
dirname, basename = os.path.split(path.rstrip('/'))
self.create_dirs(dirname) # make parent directories
self.ftp.mkdir(basename) # sub-directory missing, so created it
self.ftp.chdir(basename)
return True
def get_file(self, from_path, to_path):
if not self.ssh.get_transport().is_alive():
self.init()
self.ftp.get(from_path, to_path)
def put_file(self, from_path, to_path):
self.ftp.put(from_path, to_path)
def listdir(self, directory):
try:
# paramiko SFTPClient.listdir_attr returns
# directories in arbitarary order, so we should
# sort results of this command
res = self.ftp.listdir(directory)
return sorted(res)
except IOError as e:
if e.errno == errno.ENOENT:
return list()
else:
raise
def open(self, filename, mode):
return self.ftp.open(filename, mode=mode,
bufsize=self.max_segment_size)
def read_metadata_file(self, path):
file_stats = self.ftp.stat(path)
file_size = file_stats.st_size
data = ""
received_size = 0
with self.open(path, 'r') as reader:
reader.prefetch(file_size)
chunk = reader.read(CHUNK_SIZE)
while chunk:
received_size += len(chunk)
data += chunk
chunk = reader.read(CHUNK_SIZE)
if file_size != received_size:
raise IOError('Size mismatch: expected {} received {}'.format(
file_size, received_size))
return data
def backup_blocks(self, backup):
self.init() # should recreate ssh for new process
return super(SshStorage, self).backup_blocks(backup)