def verify_certs(self, root_crt_path, node_crt_path, ssh_options, verify_hostname=False): remote_shell = RemoteShell(ssh_options) try: remote_shell.run_command('which openssl') except YBOpsRuntimeError: logging.debug( "Openssl not found, skipping certificate verification.") return # Verify if the node cert is not expired validity_verify = remote_shell.run_command_raw( "openssl x509 -noout -checkend 86400 -in {}".format(node_crt_path)) if validity_verify.exited == 1: raise YBOpsRuntimeError( "Node cert: {} is expired or will expire in one day.".format( node_crt_path)) # Verify if node cert has valid signature signature_verify = remote_shell.run_command_raw( "openssl verify -CAfile {} {} | egrep error".format( root_crt_path, node_crt_path)) if signature_verify.exited != 1: raise YBOpsRuntimeError( "Node cert: {} is not signed by the provided root cert: {}.". format(node_crt_path, root_crt_path)) if verify_hostname: self.__verify_certs_hostname(node_crt_path, ssh_options)
def create_encryption_at_rest_file(self, extra_vars, ssh_options): node_ip = ssh_options["ssh_host"] encryption_key_path = extra_vars["encryption_key_file"] # Source file path key_node_dir = extra_vars["encryption_key_dir"] # Target file path with open(encryption_key_path, "r") as f: encryption_key = f.read() key_file = os.path.basename(encryption_key_path) common_path = os.path.join(self.CERTS_TEMP_DIR, node_ip) try: os.makedirs(common_path) except OSError as exc: # Guard against race condition if exc.errno != errno.EEXIST: raise YBOpsRuntimeError(common_path + " could not be be created") # Write encryption-at-rest key to file with open(os.path.join(common_path, key_file), 'wb') as key_out: key_out.write(encryption_key) # Copy files over to node remote_shell = RemoteShell(ssh_options) remote_shell.run_command('mkdir -p ' + key_node_dir) remote_shell.put_file(os.path.join(common_path, key_file), os.path.join(key_node_dir, key_file)) try: shutil.rmtree(common_path) except OSError as e: raise YBOpsRuntimeError("Error: %s - %s." % (e.filename, e.strerror))
def configure_secondary_interface(self, args, extra_vars, subnet_cidr): logging.info("[app] Configuring second NIC") self.wait_for_ssh_port(extra_vars["ssh_host"], args.search_pattern, extra_vars["ssh_port"]) subnet_network, subnet_netmask = subnet_cidr.split('/') # Copy and run script to configure routes scp_to_tmp(get_datafile_path('configure_nic.sh'), extra_vars["ssh_host"], extra_vars["ssh_user"], extra_vars["ssh_port"], args.private_key_file) cmd = ("sudo /tmp/configure_nic.sh " "--subnet_network {} --subnet_netmask {} --cloud {}").format( subnet_network, subnet_netmask, self.name) rc, stdout, stderr = remote_exec_command(extra_vars["ssh_host"], extra_vars["ssh_port"], extra_vars["ssh_user"], args.private_key_file, cmd) if rc: raise YBOpsRuntimeError( "Could not configure second nic {} {}".format(stdout, stderr)) # Since this is on start, wait for ssh on default port # Reboot instance remote_exec_command(extra_vars["ssh_host"], extra_vars["ssh_port"], extra_vars["ssh_user"], args.private_key_file, 'sudo reboot') self.wait_for_ssh_port(extra_vars["ssh_host"], args.search_pattern, extra_vars["ssh_port"]) # Verify that the command ran successfully: rc, stdout, stderr = remote_exec_command(extra_vars["ssh_host"], extra_vars["ssh_port"], extra_vars["ssh_user"], args.private_key_file, 'ls /tmp/dhclient-script-*') if rc: raise YBOpsRuntimeError("Second nic not configured at start up")
def verify_certs(self, root_crt_path, node_crt_path, ssh_options, verify_hostname=False): remote_shell = RemoteShell(ssh_options) # Verify that both cert are present in FS and have read rights root_file_verify = remote_shell.run_command_raw( "test -r {}".format(root_crt_path)) if root_file_verify.exited == 1: raise YBOpsRuntimeError( "Root cert: {} is absent or is not readable.".format( root_crt_path)) node_file_verify = remote_shell.run_command_raw( "test -r {}".format(node_crt_path)) if node_file_verify.exited == 1: raise YBOpsRuntimeError( "Node cert: {} is absent or is not readable.".format( node_crt_path)) try: remote_shell.run_command('which openssl') except YBOpsRuntimeError: logging.debug( "Openssl not found, skipping certificate verification.") return # Verify if root and node certs are valid cert_verify = remote_shell.run_command_raw( ("openssl crl2pkcs7 -nocrl -certfile {} -certfile {} " "| openssl pkcs7 -print_certs -text -noout").format( root_crt_path, node_crt_path)) if cert_verify.exited == 1: raise YBOpsRuntimeError( "Provided certs ({}, {}) are not valid.".format( root_crt_path, node_crt_path)) # Verify if the node cert is not expired validity_verify = remote_shell.run_command_raw( "openssl x509 -noout -checkend 86400 -in {}".format(node_crt_path)) if validity_verify.exited == 1: raise YBOpsRuntimeError( "Node cert: {} is expired or will expire in one day.".format( node_crt_path)) # Verify if node cert has valid signature signature_verify = remote_shell.run_command_raw( "openssl verify -CAfile {} {} | egrep error".format( root_crt_path, node_crt_path)) if signature_verify.exited != 1: raise YBOpsRuntimeError( "Node cert: {} is not signed by the provided root cert: {}.". format(node_crt_path, root_crt_path)) if verify_hostname: self.__verify_certs_hostname(node_crt_path, ssh_options)
def verify_startup_script(self, args, host_info): cmd = "cat /etc/yb-boot-script-complete" rc, stdout, stderr = remote_exec_command(host_info['ssh_host'], host_info['ssh_port'], host_info['ssh_user'], args.private_key_file, cmd) if rc != 0: raise YBOpsRuntimeError( 'Failed to read /etc/yb-boot-script-complete {}\nSTDOUT: {}\nSTDERR: {}\n' .format(args.search_pattern, stdout, stderr)) if len(stdout) > 0: if stdout[0].rstrip(os.linesep) != args.boot_script_token: raise YBOpsRuntimeError( '/etc/yb-boot-script-complete on {} has incorrect token {}' .format(args.search_pattern, stdout))
def compare_root_certs(self, extra_vars, ssh_options): has_openssl = True root_cert_path = extra_vars["root_cert_path"] certs_node_dir = extra_vars["certs_node_dir"] yb_root_cert_path = os.path.join(certs_node_dir, self.ROOT_CERT_NAME) remote_shell = RemoteShell(ssh_options) try: remote_shell.run_command('which openssl') except YBOpsRuntimeError as e: # No openssl, just compare files. has_openssl = False # Check if files exist. If not, let it error out (since the root files should be there.) remote_shell.run_command('test -f {}'.format(root_cert_path)) remote_shell.run_command('test -f {}'.format(yb_root_cert_path)) # Compare the openssl hash if openssl is present. if has_openssl: md5_cmd = "openssl x509 -noout -modulus -in '{}' | openssl md5" curr_root = remote_shell.run_command(md5_cmd.format(yb_root_cert_path)) new_root = remote_shell.run_command(md5_cmd.format(root_cert_path)) if curr_root.stdout != new_root.stdout: raise YBOpsRuntimeError("Root certs are different.") # Openssl not present, just compare the files after removing whitespace. else: # If there is an error code, that means the files are different. Should fail. remote_shell.run_command('diff -Z {} {}'.format(root_cert_path, yb_root_cert_path))
def wait_for_ssh_ports(self, private_ip, instance_name, ssh_ports): sock = None retry_count = 0 while retry_count < self.SSH_RETRY_COUNT: logging.info("[app] Waiting for ssh: {}:{}".format( private_ip, str(ssh_ports))) time.sleep(self.SSH_WAIT_SECONDS) # Try connecting with the given ssh ports in succession. for ssh_port in ssh_ports: ssh_port = int(ssh_port) logging.info("[app] Attempting ssh: {}:{}".format( private_ip, str(ssh_port))) try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(self.SSH_TIMEOUT_SECONDS) result = sock.connect_ex((private_ip, ssh_port)) if result == 0: logging.info("[app] Connected to {}:{}".format( private_ip, str(ssh_port))) return ssh_port finally: if sock: sock.close() # Increment retry only after attempts on all ports fail. retry_count += 1 else: logging.error( "[app] Start instance {} exceeded maxRetries!".format( instance_name)) raise YBOpsRuntimeError( "Cannot reach the instance {} after its start at ports {}". format(instance_name, str(ssh_ports)))
def wait_for_ssh_port(self, private_ip, instance_name, ssh_port): try: sock = None retry_count = 0 ssh_port = int(ssh_port) while retry_count < self.SSH_RETRY_COUNT: logging.info("[app] Waiting for ssh: {}:{}".format( private_ip, str(ssh_port))) time.sleep(self.SSH_WAIT_SECONDS) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((private_ip, ssh_port)) if result == 0: break retry_count += 1 else: logging.error( "[app] Start instance {} exceeded maxRetries!".format( instance_name)) raise YBOpsRuntimeError( "Cannot reach the instance {} after its start at port {}". format(instance_name, ssh_port)) finally: if sock: sock.close()
def check_rm_result(rm_result): if rm_result.exited and rm_result.stderr.find( "No such file or directory") == -1: raise YBOpsRuntimeError( "Remote shell command 'rm' failed with " "return code '{}' and error '{}'".format( rm_result.stderr.encode('utf-8'), rm_result.exited))
def __verify_certs_hostname(self, node_crt_path, ssh_options): host = ssh_options["ssh_host"] remote_shell = RemoteShell(ssh_options) logging.info("Verifying Subject for certs {}".format(node_crt_path)) # Get readable text version of cert cert_text = remote_shell.run_command( "openssl x509 -noout -text -in {}".format(node_crt_path)) if "Certificate:" not in cert_text.stdout: raise YBOpsRuntimeError( "Unable to decode the node cert: {}.".format(node_crt_path)) # Extract commonName and subjectAltName from the cert text output regex_out = re.findall( " Subject:.*CN=([\\S]*)$| (DNS|IP Address):([\\S]*?)(,|$)", cert_text.stdout, re.M) # Hostname will be present in group 0 for CN and in group 1 and 2 for SAN cn_entry = [x[0] for x in regex_out if x[0] != ''] san_entry = {(x[1], x[2]) for x in regex_out if x[0] == ''} # Create cert object following the below dictionary format # https://docs.python.org/3/library/ssl.html#ssl.SSLSocket.getpeercert cert_cn = { 'subject': ((('commonName', cn_entry[0] if len(cn_entry) > 0 else ''), ), ) } cert_san = {'subjectAltName': tuple(san_entry)} # Check if the provided hostname matches with either CN or SAN cn_matched = False san_matched = False try: ssl.match_hostname(cert_cn, host) cn_matched = True except ssl.CertificateError: pass try: ssl.match_hostname(cert_san, host) san_matched = True except ssl.CertificateError: pass if not cn_matched and not san_matched: raise YBOpsRuntimeError( "'{}' does not match with any entry in CN or SAN of the node cert: {}, " "cert_cn: {}, cert_san: {}".format(host, node_crt_path, cert_cn, cert_san))
def run_control_script(self, process, command, args, extra_vars, host_info): updated_vars = { "process": process, "command": command } updated_vars.update(extra_vars) updated_vars.update(get_ssh_host_port(host_info, args.custom_ssh_port)) remote_shell = RemoteShell(updated_vars) if args.num_volumes: volume_cnt = remote_shell.run_command( "df | awk '{{print $6}}' | egrep '^{}[0-9]+' | wc -l".format( AbstractCloud.MOUNT_PATH_PREFIX ) ) if int(volume_cnt.stdout) < int(args.num_volumes): raise YBOpsRuntimeError( "Not all data volumes attached: needed {} found {}".format( args.num_volumes, volume_cnt.stdout ) ) if process == "thirdparty" or process == "platform-services": self.setup_ansible(args).run("yb-server-ctl.yml", updated_vars, host_info) return if args.systemd_services: if command == "start": remote_shell.run_command( "sudo systemctl enable yb-{}".format(process) ) remote_shell.run_command( "sudo systemctl {} yb-{}".format(command, process) ) if command == "stop": remote_shell.run_command( "sudo systemctl disable yb-{}".format(process) ) return if os.environ.get("YB_USE_FABRIC", False): file_path = os.path.join(YB_HOME_DIR, "bin/yb-server-ctl.sh") remote_shell.run_command( "{} {} {}".format(file_path, process, command) ) else: self.setup_ansible(args).run("yb-server-ctl.yml", updated_vars, host_info)
def execute_boot_script(self, args, extra_vars): dest_path = os.path.join("/tmp", os.path.basename(args.boot_script)) # Make it executable, in case it isn't one. st = os.stat(args.boot_script) os.chmod(args.boot_script, st.st_mode | stat.S_IEXEC) scp_to_tmp( args.boot_script, extra_vars["ssh_host"], extra_vars["ssh_user"], extra_vars["ssh_port"], args.private_key_file) cmd = "sudo {}".format(dest_path) rc, stdout, stderr = remote_exec_command( extra_vars["ssh_host"], extra_vars["ssh_port"], extra_vars["ssh_user"], args.private_key_file, cmd) if rc: raise YBOpsRuntimeError( "[app] Could not run bootscript {} {}".format(stdout, stderr))
def validate_credentials(self): potential_env_vars = self.metadata.get('credential_vars') missing_var = None if potential_env_vars: for var in potential_env_vars: if var not in os.environ: missing_var = var break # If we found cloud credentials, then we're good to go and will explicitly use those! if missing_var is None: logging.info("Found {} cloud credentials in env.".format(self.name)) return # If no cloud credentials, see if we have credentials on the machine itself. if self.has_machine_credentials(): logging.info("Found {} cloud credentials in machine metadata.".format(self.name)) return raise YBOpsRuntimeError( "Cloud {} missing {} and has no machine credentials to default to.".format( self.name, missing_var))
def _wait_for_ssh_port(self, private_ip, instance_name, ssh_port): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) retry_count = 0 while retry_count < self.SSH_RETRY_COUNT: time.sleep(self.SSH_WAIT_SECONDS) retry_count = retry_count + 1 result = sock.connect_ex((private_ip, ssh_port)) if result == 0: break else: logging.error("Start instance {} exceeded maxRetries!".format( instance_name)) raise YBOpsRuntimeError( "Cannot reach the instance {} after its start at port {}". format(instance_name, ssh_port)) finally: sock.close()
def generate_client_cert(self, extra_vars, ssh_options): node_ip = ssh_options["ssh_host"] root_cert_path = extra_vars["rootCA_cert"] root_key_path = extra_vars["rootCA_key"] certs_node_dir = extra_vars["certs_node_dir"] with open(root_cert_path, 'r') as cert_in: certlines = cert_in.read() root_cert = x509.load_pem_x509_certificate(certlines, default_backend()) with open(root_key_path, 'r') as key_in: keylines = key_in.read() root_key = load_pem_private_key(keylines, None, default_backend()) private_key = rsa.generate_private_key( public_exponent=self.PUBLIC_EXPONENT, key_size=self.KEY_SIZE, backend=default_backend() ) public_key = private_key.public_key() builder = x509.CertificateBuilder() builder = builder.subject_name(x509.Name([ x509.NameAttribute(NameOID.COMMON_NAME, six.text_type(node_ip)), x509.NameAttribute(NameOID.ORGANIZATION_NAME, six.text_type(extra_vars["org_name"])) ])) builder = builder.issuer_name(root_cert.subject) builder = builder.not_valid_before(datetime.datetime.utcnow()) builder = builder.not_valid_after(datetime.datetime.utcnow() + datetime.timedelta( extra_vars["cert_valid_duration"])) builder = builder.serial_number(x509.random_serial_number()) builder = builder.public_key(public_key) builder = builder.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True) certificate = builder.sign(private_key=root_key, algorithm=hashes.SHA256(), backend=default_backend()) # Write private key to file pem = private_key.private_bytes( encoding=Encoding.PEM, format=PrivateFormat.TraditionalOpenSSL, encryption_algorithm=NoEncryption() ) key_file = 'node.{}.key'.format(node_ip) cert_file = 'node.{}.crt'.format(node_ip) common_path = '{}/{}'.format(self.CERTS_TEMP_DIR, node_ip) try: os.makedirs(common_path) except OSError as exc: # Guard against race condition if exc.errno != errno.EEXIST: raise YBOpsRuntimeError(common_path + " could not be be created") with open(os.path.join(common_path, key_file), 'wb') as pem_out: pem_out.write(pem) # Write certificate to file pem = certificate.public_bytes(encoding=Encoding.PEM) with open(os.path.join(common_path, cert_file), 'wb') as pem_out: pem_out.write(pem) # Copy files over to node remote_shell = RemoteShell(ssh_options) remote_shell.run_command('mkdir -p ' + certs_node_dir) # Give write permission in case file exists. If the command fails, ignore. remote_shell.run_command('chmod -f 666 {}/* || true'.format(certs_node_dir)) remote_shell.put_file(os.path.join(common_path, key_file), os.path.join(certs_node_dir, key_file)) remote_shell.put_file(os.path.join(common_path, cert_file), os.path.join(certs_node_dir, cert_file)) remote_shell.put_file(root_cert_path, os.path.join(certs_node_dir, self.ROOT_CERT_NAME)) remote_shell.run_command('chmod 400 {}/*'.format(certs_node_dir)) if "client_cert" in extra_vars: client_cert_path = extra_vars["client_cert"] client_key_path = extra_vars["client_key"] remote_shell.run_command('mkdir -p ' + self.YSQLSH_CERT_DIR) # Give write permission in case file exists. If the command fails, ignore. remote_shell.run_command('chmod -f 666 {}/* || true'.format(self.YSQLSH_CERT_DIR)) remote_shell.put_file(root_cert_path, os.path.join(self.YSQLSH_CERT_DIR, self.CLIENT_ROOT_NAME)) remote_shell.put_file(client_cert_path, os.path.join(self.YSQLSH_CERT_DIR, self.CLIENT_CERT_NAME)) remote_shell.put_file(client_key_path, os.path.join(self.YSQLSH_CERT_DIR, self.CLIENT_KEY_NAME)) remote_shell.run_command('chmod 400 {}/*'.format(self.YSQLSH_CERT_DIR)) try: shutil.rmtree(common_path) except OSError as e: raise YBOpsRuntimeError("Error: %s - %s." % (e.filename, e.strerror))
def copy_server_certs(self, ssh_options, root_cert_path, server_cert_path, server_key_path, certs_location, certs_dir, rotate_certs, skip_cert_validation): remote_shell = RemoteShell(ssh_options) node_ip = ssh_options["ssh_host"] cert_file = 'node.{}.crt'.format(node_ip) key_file = 'node.{}.key'.format(node_ip) yb_root_cert_path = os.path.join(certs_dir, self.ROOT_CERT_NAME) yb_server_cert_path = os.path.join(certs_dir, cert_file) yb_server_key_path = os.path.join(certs_dir, key_file) copy_root = True if rotate_certs: root_cert_command = remote_shell.run_command_raw( "cat '{}'".format(yb_root_cert_path)) # In case of tls toggle root cert might not be present if not root_cert_command.exited: root_cert = root_cert_command.stdout root_cert_new = None if certs_location == self.CERT_LOCATION_NODE: root_cert_new = remote_shell.run_command( "cat '{}'".format(root_cert_path)).stdout if certs_location == self.CERT_LOCATION_PLATFORM: with open(root_cert_path) as file: root_cert_new = file.read() if root_cert is not None and root_cert_new is not None: compare_result = self.compare_certs( root_cert_new, root_cert) if compare_result == 0 or compare_result == 1: # Don't copy root certs if the new root cert is # same or subset of the existing root cert copy_root = False else: raise YBOpsRuntimeError( "Unable to fetch the certificate {}".format( root_cert_path)) logging.info("Moving server certs located at {}, {}, {}.".format( root_cert_path, server_cert_path, server_key_path)) remote_shell.run_command('mkdir -p ' + certs_dir) # Give write permissions. If the command fails, ignore. remote_shell.run_command('chmod -f 666 {}/* || true'.format(certs_dir)) if certs_location == self.CERT_LOCATION_NODE: if skip_cert_validation == 'ALL': logging.info( "Skipping all validations for certs for node {}".format( node_ip)) else: verify_hostname = True if skip_cert_validation == 'HOSTNAME': logging.info( "Skipping host name validation for certs for node {}". format(node_ip)) verify_hostname = False self.verify_certs(root_cert_path, server_cert_path, ssh_options, verify_hostname) if copy_root: remote_shell.run_command("cp '{}' '{}'".format( root_cert_path, yb_root_cert_path)) remote_shell.run_command("cp '{}' '{}'".format( server_cert_path, yb_server_cert_path)) remote_shell.run_command("cp '{}' '{}'".format( server_key_path, yb_server_key_path)) if certs_location == self.CERT_LOCATION_PLATFORM: if copy_root: remote_shell.put_file(root_cert_path, yb_root_cert_path) remote_shell.put_file(server_cert_path, yb_server_cert_path) remote_shell.put_file(server_key_path, yb_server_key_path) # Reset the write permission as a sanity check. remote_shell.run_command('chmod 400 {}/*'.format(certs_dir))