Kaynağa Gözat

Create HTTPBackupWriterBootstrap()

Separate bootstrapping logic from the HTTPBackupWriter()
Gabriel-Adrian Samfira 6 yıl önce
ebeveyn
işleme
886545a9d6
1 değiştirilmiş dosya ile 166 ekleme ve 199 silme
  1. 166 199
      coriolis/providers/backup_writers.py

+ 166 - 199
coriolis/providers/backup_writers.py

@@ -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