|
|
@@ -709,133 +709,48 @@ class HTTPBackupWriterImpl(BaseBackupWriterImpl):
|
|
|
self._compressor_evt = None
|
|
|
|
|
|
|
|
|
-class HTTPBackupWriter(BaseBackupWriter):
|
|
|
+class HTTPBackupWriterBoostrapper(object):
|
|
|
|
|
|
- def __init__(self, ip, port, volumes_info, ssh=None,
|
|
|
- compressor_count=3, certificates=None):
|
|
|
- self._ip = ip
|
|
|
- self._port = port
|
|
|
- self._volumes_info = volumes_info
|
|
|
- self._writer_port = port
|
|
|
+ def __init__(self, ssh_conn_info, writer_port):
|
|
|
self._lock = threading.Lock()
|
|
|
- self._id = str(uuid.uuid4())
|
|
|
- self._compressor_count = compressor_count
|
|
|
self._writer_cmd = os.path.join(
|
|
|
"/usr/bin", _CORIOLIS_HTTP_WRITER_CMD)
|
|
|
- self._ssh = ssh
|
|
|
- self._certificates = certificates
|
|
|
- self._crt_dir = tempfile.mkdtemp()
|
|
|
- if not self._ssh and not self._certificates:
|
|
|
+ self._writer_port = writer_port
|
|
|
+ self._ip = ssh_conn_info.get("ip")
|
|
|
+ self._port = ssh_conn_info.get("port", 22)
|
|
|
+ self._username = ssh_conn_info.get("username")
|
|
|
+ self._password = ssh_conn_info.get("password")
|
|
|
+ self._pkey = ssh_conn_info.get("pkey")
|
|
|
+ if not all([self._ip, self._port, self._username]):
|
|
|
raise exception.CoriolisException(
|
|
|
- "Either ssh or writer_creds must be specified")
|
|
|
- if self._ssh and self._certificates:
|
|
|
+ "Invalid SSH connection info. IP, port and"
|
|
|
+ " username are mandatory")
|
|
|
+ if self._password is None and self._pkey is None:
|
|
|
raise exception.CoriolisException(
|
|
|
- "ssh and writer_creds are mutually exclusive")
|
|
|
- self._cert_paths = None
|
|
|
-
|
|
|
- @classmethod
|
|
|
- def from_connection_info(cls, conn_info, volumes_info):
|
|
|
- """Instantiate a HTTP backup writer from connection info.
|
|
|
+ "Either password or pkey are required")
|
|
|
+ if self._pkey:
|
|
|
+ self._pkey = utils.deserialize_key(
|
|
|
+ self._pkey, CONF.serialization.temp_keypair_password)
|
|
|
+ self._ssh = self._connect_ssh()
|
|
|
|
|
|
- Connection info has the following schema:
|
|
|
-
|
|
|
- {
|
|
|
- # IP address or hostname where we can reach the backup writer
|
|
|
- "ip": "192.168.0.1",
|
|
|
- # Backup writer port
|
|
|
- "port": 4433,
|
|
|
-
|
|
|
- # ssh_conn_info is only needed in environments where the HTTP
|
|
|
- # backup writer cannot be set up via userdata. This can be
|
|
|
- # used to copy the coriolis-writer binary, create certificates
|
|
|
- # and initialize everything. This option and the "certificates"
|
|
|
- # option are mutually exclusive. Set to None if the writer has
|
|
|
- # already been set up via userdata, or other means.
|
|
|
- "ssh_conn_info": {
|
|
|
- # The IP here is usually the same as the IP/hostname of the
|
|
|
- # backup writer, shown above.
|
|
|
- "ip": "192.168.0.1",
|
|
|
- # SSH port. Defaults to 22.
|
|
|
- "port": 22,
|
|
|
- "username": "example",
|
|
|
- # Password is only needed if no pkey is specified
|
|
|
- "password": "super secret password",
|
|
|
- # pkey can be omited if password is used. Pkey must be a
|
|
|
- # PEM formatted OpenSSH private key, paramiko.RSA private key
|
|
|
- # or None
|
|
|
- "pkey": "RSA_PRIVATE_KEY"
|
|
|
- },
|
|
|
-
|
|
|
- # The "certificates" field and the "ssh_conn_info" are mutually
|
|
|
- # exclusive. Either the writer is set up via userdata, or we set
|
|
|
- # it up ourselves using a SSH connection. But whether or not it's
|
|
|
- # set up must be unambiguous. Setting this option means we already
|
|
|
- # have the certificates, and the coriolis-writer binary is already
|
|
|
- # set up and started
|
|
|
- "certificates": {
|
|
|
- # PEM encoded client certificate
|
|
|
- "client_crt": "",
|
|
|
- # PEM encoded client private key
|
|
|
- "client_key": "",
|
|
|
- # PEM encoded CA certificate we use to validate the server
|
|
|
- "ca_crt": ""
|
|
|
- }
|
|
|
- }
|
|
|
- """
|
|
|
- ip = conn_info.get("ip")
|
|
|
- port = conn_info.get("port")
|
|
|
-
|
|
|
- required = ["ip", "port"]
|
|
|
- if not all([ip, port]):
|
|
|
- raise exception.CoriolisException(
|
|
|
- "Missing required connection info: %s" % ", ".join(required))
|
|
|
-
|
|
|
- ssh_cli = None
|
|
|
- ssh = conn_info.get("ssh_conn_info")
|
|
|
- certs = conn_info.get("certificates")
|
|
|
- if ssh:
|
|
|
- if type(ssh) is not dict:
|
|
|
- raise exception.CoriolisException(
|
|
|
- "ssh connection info is invalid")
|
|
|
- ssh_ip = ssh.get("ip")
|
|
|
- ssh_port = ssh.get("port", 22)
|
|
|
- ssh_username = ssh.get("username")
|
|
|
- if not all([ssh_ip, ssh_port, ssh_username]):
|
|
|
- raise exception.CoriolisException(
|
|
|
- "Missing required fields in SSH conn info. "
|
|
|
- "Required fields are: %s" % ", ".join(
|
|
|
- ["ip", "port", "username"]))
|
|
|
- password = ssh.get("password")
|
|
|
- pkey = ssh.get("pkey")
|
|
|
- if pkey is not None:
|
|
|
- if type(pkey) is not str:
|
|
|
- raise exception.CoriolisException(
|
|
|
- "pkey must be a PEM encoded RSA private key")
|
|
|
- pkey = utils.deserialize_key(
|
|
|
- pkey, CONF.serialization.temp_keypair_password)
|
|
|
- ssh_cli = paramiko.SSHClient()
|
|
|
- ssh_cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
|
- utils.retry_on_error(sleep_seconds=10)(ssh_cli.connect)(
|
|
|
- hostname=ssh_ip,
|
|
|
- port=ssh_port,
|
|
|
- username=ssh_username,
|
|
|
- pkey=pkey,
|
|
|
- password=password)
|
|
|
- return cls(ip, port, volumes_info, ssh=ssh_cli, certificates=certs)
|
|
|
-
|
|
|
- def __del__(self):
|
|
|
- if self._crt_dir and os.path.isdir(self._crt_dir):
|
|
|
- try:
|
|
|
- shutil.rmtree(self._crt_dir)
|
|
|
- except BaseException:
|
|
|
- pass
|
|
|
-
|
|
|
- def _wait_for_conn(self):
|
|
|
- LOG.debug(
|
|
|
- "waiting for coriolis-writer connectivity %s:%s" % (
|
|
|
- self._ip, self._writer_port))
|
|
|
- utils.wait_for_port_connectivity(
|
|
|
- self._ip, self._writer_port)
|
|
|
+ @utils.retry_on_error(sleep_seconds=30)
|
|
|
+ def _connect_ssh(self):
|
|
|
+ LOG.info("Connecting to SSH host: %(ip)s:%(port)s" %
|
|
|
+ {"ip": self._ip, "port": self._port})
|
|
|
+ ssh = paramiko.SSHClient()
|
|
|
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
|
+ try:
|
|
|
+ ssh.connect(
|
|
|
+ hostname=self._ip,
|
|
|
+ port=self._port,
|
|
|
+ username=self._username,
|
|
|
+ pkey=self._pkey,
|
|
|
+ password=self._password)
|
|
|
+ except (Exception, KeyboardInterrupt):
|
|
|
+ # No need to log the error as we just raise
|
|
|
+ ssh.close()
|
|
|
+ raise
|
|
|
+ return ssh
|
|
|
|
|
|
def _inject_iptables_allow(self, ssh):
|
|
|
utils.exec_ssh_cmd(
|
|
|
@@ -843,53 +758,6 @@ class HTTPBackupWriter(BaseBackupWriter):
|
|
|
"sudo /sbin/iptables -I INPUT -p tcp --dport %s "
|
|
|
"-j ACCEPT" % self._writer_port, get_pty=True)
|
|
|
|
|
|
- def _write_cert_files(self):
|
|
|
- if not self._certificates:
|
|
|
- raise exception.CoriolisException(
|
|
|
- "certificates not set")
|
|
|
- if self._cert_paths:
|
|
|
- return self._cert_paths
|
|
|
-
|
|
|
- crt_file = tempfile.mkstemp(dir=self._crt_dir)[1]
|
|
|
- key_file = tempfile.mkstemp(dir=self._crt_dir)[1]
|
|
|
- ca_crt_file = tempfile.mkstemp(dir=self._crt_dir)[1]
|
|
|
- with open(crt_file, "w") as fd:
|
|
|
- fd.write(self._certificates["client_crt"])
|
|
|
- with open(key_file, "w") as fd:
|
|
|
- fd.write(self._certificates["client_key"])
|
|
|
- with open(ca_crt_file, "w") as fd:
|
|
|
- fd.write(self._certificates["ca_crt"])
|
|
|
- self._cert_paths = {
|
|
|
- "client_crt": crt_file,
|
|
|
- "client_key": key_file,
|
|
|
- "ca_crt": ca_crt_file,
|
|
|
- }
|
|
|
- return self._cert_paths
|
|
|
-
|
|
|
- def _get_impl(self, path, disk_id):
|
|
|
- if self._ssh:
|
|
|
- _disable_lvm2_lvmetad(self._ssh)
|
|
|
- cert_paths = self._setup_writer(self._ssh)["local"]
|
|
|
- else:
|
|
|
- cert_paths = self._write_cert_files()
|
|
|
- self._wait_for_conn()
|
|
|
-
|
|
|
- path = [v for v in self._volumes_info
|
|
|
- if v["disk_id"] == disk_id][0]["volume_dev"]
|
|
|
- impl = HTTPBackupWriterImpl(
|
|
|
- path, disk_id,
|
|
|
- compressor_count=self._compressor_count,
|
|
|
- compress_transfer=CONF.compress_transfers)
|
|
|
- impl._set_info({
|
|
|
- "ip": self._ip,
|
|
|
- "port": self._writer_port,
|
|
|
- "client_crt": cert_paths["client_crt"],
|
|
|
- "client_key": cert_paths["client_key"],
|
|
|
- "ca_crt": cert_paths["ca_crt"],
|
|
|
- "id": self._id,
|
|
|
- })
|
|
|
- return impl
|
|
|
-
|
|
|
@utils.retry_on_error()
|
|
|
def _copy_writer(self, ssh):
|
|
|
local_path = os.path.join(
|
|
|
@@ -943,16 +811,11 @@ class HTTPBackupWriter(BaseBackupWriter):
|
|
|
remote_srv_crt = os.path.join(remote_base_dir, srv_crt_name)
|
|
|
remote_srv_key = os.path.join(remote_base_dir, srv_key_name)
|
|
|
|
|
|
- ca_crt = os.path.join(self._crt_dir, ca_crt_name)
|
|
|
- client_crt = os.path.join(self._crt_dir, client_crt_name)
|
|
|
- client_key = os.path.join(self._crt_dir, client_key_name)
|
|
|
-
|
|
|
exist = []
|
|
|
for i in (remote_ca_crt, remote_client_crt, remote_client_key,
|
|
|
remote_srv_crt, remote_srv_key):
|
|
|
exist.append(utils.test_ssh_path(ssh, i))
|
|
|
|
|
|
- force_fetch = False
|
|
|
if not all(exist):
|
|
|
utils.exec_ssh_cmd(
|
|
|
ssh, "sudo mkdir -p %s" % remote_base_dir, get_pty=True)
|
|
|
@@ -965,32 +828,20 @@ class HTTPBackupWriter(BaseBackupWriter):
|
|
|
"extra_hosts": self._ip,
|
|
|
},
|
|
|
get_pty=True)
|
|
|
- force_fetch = True
|
|
|
-
|
|
|
- exists = []
|
|
|
- for i in (ca_crt, client_crt, client_key):
|
|
|
- exists.append(os.path.isfile(i))
|
|
|
-
|
|
|
- if not all(exists) or force_fetch:
|
|
|
- # certificates either are missing, or have been regenerated
|
|
|
- # on the writer worker. We need to fetch them.
|
|
|
- self._fetch_remote_file(ssh, remote_ca_crt, ca_crt)
|
|
|
- self._fetch_remote_file(ssh, remote_client_crt, client_crt)
|
|
|
- self._fetch_remote_file(ssh, remote_client_key, client_key)
|
|
|
|
|
|
return {
|
|
|
- "local": {
|
|
|
- "client_crt": client_crt,
|
|
|
- "client_key": client_key,
|
|
|
- "ca_crt": ca_crt,
|
|
|
- },
|
|
|
- "remote": {
|
|
|
- "srv_crt": remote_srv_crt,
|
|
|
- "srv_key": remote_srv_key,
|
|
|
- "ca_crt": remote_ca_crt,
|
|
|
- },
|
|
|
+ "srv_crt": remote_srv_crt,
|
|
|
+ "srv_key": remote_srv_key,
|
|
|
+ "ca_crt": remote_ca_crt,
|
|
|
+ "client_crt": remote_client_crt,
|
|
|
+ "client_key": remote_client_key
|
|
|
}
|
|
|
|
|
|
+ def _read_remote_file_sudo(self, remote_path):
|
|
|
+ contents = utils.exec_ssh_cmd(
|
|
|
+ self._ssh, "sudo cat %s" % remote_path, get_pty=True)
|
|
|
+ return contents.decode()
|
|
|
+
|
|
|
def _init_writer(self, ssh, cert_paths):
|
|
|
cmdline = ("%(cmd)s run -ca-cert %(ca_cert)s -key "
|
|
|
"%(srv_key)s -cert %(srv_cert)s -listen-port "
|
|
|
@@ -1005,10 +856,126 @@ class HTTPBackupWriter(BaseBackupWriter):
|
|
|
ssh, cmdline, _CORIOLIS_HTTP_WRITER_CMD, start=True)
|
|
|
self._inject_iptables_allow(ssh)
|
|
|
|
|
|
- def _setup_writer(self, ssh):
|
|
|
- self._copy_writer(ssh)
|
|
|
+ def setup_writer(self):
|
|
|
+ _disable_lvm2_lvmetad(self._ssh)
|
|
|
+ self._copy_writer(self._ssh)
|
|
|
paths = utils.retry_on_error()(
|
|
|
- self._setup_certificates)(ssh)
|
|
|
+ self._setup_certificates)(self._ssh)
|
|
|
utils.retry_on_error()(
|
|
|
- self._init_writer)(ssh, paths["remote"])
|
|
|
- return paths
|
|
|
+ self._init_writer)(self._ssh, paths)
|
|
|
+ return {
|
|
|
+ "ip": self._ip,
|
|
|
+ "port": self._writer_port,
|
|
|
+ "certificates": {
|
|
|
+ "client_crt": self._read_remote_file_sudo(paths["client_crt"]),
|
|
|
+ "client_key": self._read_remote_file_sudo(paths["client_key"]),
|
|
|
+ "ca_crt": self._read_remote_file_sudo(paths["ca_crt"])
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+class HTTPBackupWriter(BaseBackupWriter):
|
|
|
+
|
|
|
+ def __init__(self, ip, port, volumes_info, certificates,
|
|
|
+ compressor_count=3):
|
|
|
+ self._ip = ip
|
|
|
+ self._port = port
|
|
|
+ self._volumes_info = volumes_info
|
|
|
+ self._writer_port = port
|
|
|
+ self._id = str(uuid.uuid4())
|
|
|
+ self._compressor_count = compressor_count
|
|
|
+
|
|
|
+ self._certificates = certificates
|
|
|
+ self._crt_dir = tempfile.mkdtemp()
|
|
|
+ if not self._certificates:
|
|
|
+ raise exception.CoriolisException(
|
|
|
+ "certificates is mandatory")
|
|
|
+ self._cert_paths = None
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def from_connection_info(cls, conn_info, volumes_info):
|
|
|
+ """Instantiate a HTTP backup writer from connection info.
|
|
|
+
|
|
|
+ Connection info has the following schema:
|
|
|
+
|
|
|
+ {
|
|
|
+ # IP address or hostname where we can reach the backup writer
|
|
|
+ "ip": "192.168.0.1",
|
|
|
+ # Backup writer port
|
|
|
+ "port": 4433,
|
|
|
+ "certificates": {
|
|
|
+ # PEM encoded client certificate
|
|
|
+ "client_crt": "",
|
|
|
+ # PEM encoded client private key
|
|
|
+ "client_key": "",
|
|
|
+ # PEM encoded CA certificate we use to validate the server
|
|
|
+ "ca_crt": ""
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ ip = conn_info.get("ip")
|
|
|
+ port = conn_info.get("port")
|
|
|
+ certs = conn_info.get("certificates")
|
|
|
+
|
|
|
+ required = ["ip", "port", "certificates"]
|
|
|
+ if not all([ip, port, certs]):
|
|
|
+ raise exception.CoriolisException(
|
|
|
+ "Missing required connection info: %s" % ", ".join(required))
|
|
|
+ return cls(ip, port, volumes_info, certs)
|
|
|
+
|
|
|
+ def __del__(self):
|
|
|
+ if self._crt_dir and os.path.isdir(self._crt_dir):
|
|
|
+ try:
|
|
|
+ shutil.rmtree(self._crt_dir)
|
|
|
+ except BaseException:
|
|
|
+ pass
|
|
|
+
|
|
|
+ def _wait_for_conn(self):
|
|
|
+ LOG.debug(
|
|
|
+ "waiting for coriolis-writer connectivity %s:%s" % (
|
|
|
+ self._ip, self._writer_port))
|
|
|
+ utils.wait_for_port_connectivity(
|
|
|
+ self._ip, self._writer_port)
|
|
|
+
|
|
|
+ def _write_cert_files(self):
|
|
|
+ if not self._certificates:
|
|
|
+ raise exception.CoriolisException(
|
|
|
+ "certificates not set")
|
|
|
+ if self._cert_paths:
|
|
|
+ return self._cert_paths
|
|
|
+
|
|
|
+ crt_file = tempfile.mkstemp(dir=self._crt_dir)[1]
|
|
|
+ key_file = tempfile.mkstemp(dir=self._crt_dir)[1]
|
|
|
+ ca_crt_file = tempfile.mkstemp(dir=self._crt_dir)[1]
|
|
|
+ with open(crt_file, "w") as fd:
|
|
|
+ fd.write(self._certificates["client_crt"])
|
|
|
+ with open(key_file, "w") as fd:
|
|
|
+ fd.write(self._certificates["client_key"])
|
|
|
+ with open(ca_crt_file, "w") as fd:
|
|
|
+ fd.write(self._certificates["ca_crt"])
|
|
|
+ self._cert_paths = {
|
|
|
+ "client_crt": crt_file,
|
|
|
+ "client_key": key_file,
|
|
|
+ "ca_crt": ca_crt_file,
|
|
|
+ }
|
|
|
+ return self._cert_paths
|
|
|
+
|
|
|
+ def _get_impl(self, path, disk_id):
|
|
|
+ cert_paths = self._write_cert_files()
|
|
|
+ self._wait_for_conn()
|
|
|
+
|
|
|
+ path = [v for v in self._volumes_info
|
|
|
+ if v["disk_id"] == disk_id][0]["volume_dev"]
|
|
|
+ impl = HTTPBackupWriterImpl(
|
|
|
+ path, disk_id,
|
|
|
+ compressor_count=self._compressor_count,
|
|
|
+ compress_transfer=CONF.compress_transfers)
|
|
|
+ impl._set_info({
|
|
|
+ "ip": self._ip,
|
|
|
+ "port": self._writer_port,
|
|
|
+ "client_crt": cert_paths["client_crt"],
|
|
|
+ "client_key": cert_paths["client_key"],
|
|
|
+ "ca_crt": cert_paths["ca_crt"],
|
|
|
+ "id": self._id,
|
|
|
+ })
|
|
|
+ return impl
|