def _remote_init_items(self, comm_meth): """Return list of items to install based on communication method. Return (list): Each item is (source_path, dest_path) where: - source_path is the path to the source file to install. - dest_path is relative path under suite run directory at target remote. """ items = [] if comm_meth in ['ssh', 'zmq']: # Contact file items.append((get_contact_file(self.suite), os.path.join(SuiteFiles.Service.DIRNAME, SuiteFiles.Service.CONTACT))) if comm_meth in ['zmq']: suite_srv_dir = get_suite_srv_dir(self.suite) server_pub_keyinfo = KeyInfo(KeyType.PUBLIC, KeyOwner.SERVER, suite_srv_dir=suite_srv_dir) client_pri_keyinfo = KeyInfo(KeyType.PRIVATE, KeyOwner.CLIENT, suite_srv_dir=suite_srv_dir) dest_path_srvr_public_key = os.path.join( SuiteFiles.Service.DIRNAME, server_pub_keyinfo.file_name) items.append( (server_pub_keyinfo.full_key_path, dest_path_srvr_public_key)) dest_path_cli_pri_key = os.path.join(SuiteFiles.Service.DIRNAME, client_pri_keyinfo.file_name) items.append( (client_pri_keyinfo.full_key_path, dest_path_cli_pri_key)) return items
def _socket_bind(self, min_port, max_port, srv_prv_key_loc=None): """Bind socket. Will use a port range provided to select random ports. """ if srv_prv_key_loc is None: # Create new KeyInfo object for the server private key suite_srv_dir = get_suite_srv_dir(self.suite) srv_prv_key_info = KeyInfo(KeyType.PRIVATE, KeyOwner.SERVER, suite_srv_dir=suite_srv_dir) else: srv_prv_key_info = KeyInfo(KeyType.PRIVATE, KeyOwner.SERVER, full_key_path=srv_prv_key_loc) # create socket self.socket = self.context.socket(self.pattern) self._socket_options() try: server_public_key, server_private_key = zmq.auth.load_certificate( srv_prv_key_info.full_key_path) except (ValueError): raise SuiteServiceFileError(f"Failed to find server's public " f"key in " f"{srv_prv_key_info.full_key_path}.") except (OSError): raise SuiteServiceFileError(f"IO error opening server's private " f"key from " f"{srv_prv_key_info.full_key_path}.") if server_private_key is None: # this can't be caught by exception raise SuiteServiceFileError(f"Failed to find server's private " f"key in " f"{srv_prv_key_info.full_key_path}.") self.socket.curve_publickey = server_public_key self.socket.curve_secretkey = server_private_key self.socket.curve_server = True try: if min_port == max_port: self.port = min_port self.socket.bind(f'tcp://*:{min_port}') else: self.port = self.socket.bind_to_random_port( 'tcp://*', min_port, max_port) except (zmq.error.ZMQError, zmq.error.ZMQBindError) as exc: raise CylcError(f'could not start Cylc ZMQ server: {exc}') if self.barrier is not None: self.barrier.wait()
def _socket_connect(self, host, port, srv_public_key_loc=None): """Connect socket to stub.""" suite_srv_dir = get_suite_srv_dir(self.suite) if srv_public_key_loc is None: # Create new KeyInfo object for the server public key srv_pub_key_info = KeyInfo(KeyType.PUBLIC, KeyOwner.SERVER, suite_srv_dir=suite_srv_dir) else: srv_pub_key_info = KeyInfo(KeyType.PUBLIC, KeyOwner.SERVER, full_key_path=srv_public_key_loc) self.host = host self.port = port self.socket = self.context.socket(self.pattern) self._socket_options() client_priv_key_info = KeyInfo(KeyType.PRIVATE, KeyOwner.CLIENT, suite_srv_dir=suite_srv_dir) error_msg = "Failed to find user's private key, so cannot connect." try: client_public_key, client_priv_key = zmq.auth.load_certificate( client_priv_key_info.full_key_path) except (OSError, ValueError): raise ClientError(error_msg) if client_priv_key is None: # this can't be caught by exception raise ClientError(error_msg) self.socket.curve_publickey = client_public_key self.socket.curve_secretkey = client_priv_key # A client can only connect to the server if it knows its public key, # so we grab this from the location it was created on the filesystem: try: # 'load_certificate' will try to load both public & private keys # from a provided file but will return None, not throw an error, # for the latter item if not there (as for all public key files) # so it is OK to use; there is no method to load only the # public key. server_public_key = zmq.auth.load_certificate( srv_pub_key_info.full_key_path)[0] self.socket.curve_serverkey = server_public_key except (OSError, ValueError): # ValueError raised w/ no public key raise ClientError( "Failed to load the suite's public key, so cannot connect.") self.socket.connect(f'tcp://{host}:{port}')
def remove_keys_on_client(srvd, install_target, full_clean=False): """Removes client authentication keys""" keys = { "client_private_key": KeyInfo( KeyType.PRIVATE, KeyOwner.CLIENT, suite_srv_dir=srvd), "client_public_key": KeyInfo( KeyType.PUBLIC, KeyOwner.CLIENT, suite_srv_dir=srvd, install_target=install_target, server_held=False), } # WARNING, DESTRUCTIVE. Removes old keys if they already exist. if full_clean: keys.update({"server_public_key": KeyInfo( KeyType.PUBLIC, KeyOwner.SERVER, suite_srv_dir=srvd)}) for k in keys.values(): if os.path.exists(k.full_key_path): os.remove(k.full_key_path)
def key_housekeeping(reg, platform=None, create=True): """Clean any existing authentication keys and create new ones. If create is set to false, keys will only be cleaned from server.""" suite_srv_dir = get_suite_srv_dir(reg) keys = { "client_public_key": KeyInfo(KeyType.PUBLIC, KeyOwner.CLIENT, suite_srv_dir=suite_srv_dir, install_target=platform), "client_private_key": KeyInfo(KeyType.PRIVATE, KeyOwner.CLIENT, suite_srv_dir=suite_srv_dir), "server_public_key": KeyInfo(KeyType.PUBLIC, KeyOwner.SERVER, suite_srv_dir=suite_srv_dir), "server_private_key": KeyInfo(KeyType.PRIVATE, KeyOwner.SERVER, suite_srv_dir=suite_srv_dir) } remove_keys_on_server(keys) if create: create_server_keys(keys, suite_srv_dir)
def remote_init(install_target, rund, indirect_comm=None): """cylc remote-init Arguments: install_target (str): target to be initialised rund (str): suite run directory *indirect_comm (str): use indirect communication via e.g. 'ssh' """ rund = os.path.expandvars(rund) srvd = os.path.join(rund, SuiteFiles.Service.DIRNAME) os.makedirs(srvd, exist_ok=True) client_pub_keyinfo = KeyInfo( KeyType.PUBLIC, KeyOwner.CLIENT, suite_srv_dir=srvd, install_target=install_target, server_held=False) pattern = re.compile(r"^client_\S*key$") for filepath in os.listdir(srvd): if pattern.match(filepath) and f"{install_target}" not in filepath: # client key for a different install target exists print(REMOTE_INIT_FAILED) try: remove_keys_on_client(srvd, install_target) create_client_keys(srvd, install_target) except Exception: # Catching all exceptions as need to fail remote init if any problems # with key generation. print(REMOTE_INIT_FAILED) return oldcwd = os.getcwd() os.chdir(rund) # Extract job.sh from library, for use in job scripts. extract_resources(SuiteFiles.Service.DIRNAME, ['etc/job.sh']) try: tarhandle = tarfile.open(fileobj=sys.stdin.buffer, mode='r|') tarhandle.extractall() tarhandle.close() finally: os.chdir(oldcwd) if indirect_comm: fname = os.path.join(srvd, SuiteFiles.Service.CONTACT) with open(fname, 'a') as handle: handle.write('%s=%s\n' % ( ContactFileFields.COMMS_PROTOCOL_2, indirect_comm)) print("KEYSTART", end='') with open(client_pub_keyinfo.full_key_path) as keyfile: print(keyfile.read(), end='KEYEND') print(REMOTE_INIT_DONE) return
def setup_keys(suite_name): suite_srv_dir = get_suite_srv_dir(suite_name) server_keys = { "client_public_key": KeyInfo( KeyType.PUBLIC, KeyOwner.CLIENT, suite_srv_dir=suite_srv_dir), "client_private_key": KeyInfo( KeyType.PRIVATE, KeyOwner.CLIENT, suite_srv_dir=suite_srv_dir), "server_public_key": KeyInfo( KeyType.PUBLIC, KeyOwner.SERVER, suite_srv_dir=suite_srv_dir), "server_private_key": KeyInfo( KeyType.PRIVATE, KeyOwner.SERVER, suite_srv_dir=suite_srv_dir) } remove_keys_on_server(server_keys) remove_keys_on_client(suite_srv_dir, None, full_clean=True) create_server_keys(server_keys, suite_srv_dir) create_client_keys(suite_srv_dir, None)
def _remote_init_callback(self, proc_ctx, platform, tmphandle, curve_auth, client_pub_key_dir): """Callback when "cylc remote-init" exits. Write public key for install target into client public key directory. Set remote_init__map status to REMOTE_INIT_DONE on success which in turn will trigger file installation to start. Set remote_init_map status to REMOTE_INIT_FAILED on error. """ try: tmphandle.close() except OSError: # E.g. ignore bad unlink, etc pass install_target = platform['install target'] if proc_ctx.ret_code == 0: if "KEYSTART" in proc_ctx.out: regex_result = re.search('KEYSTART((.|\n|\r)*)KEYEND', proc_ctx.out) key = regex_result.group(1) suite_srv_dir = get_suite_srv_dir(self.suite) public_key = KeyInfo(KeyType.PUBLIC, KeyOwner.CLIENT, suite_srv_dir=suite_srv_dir, install_target=install_target) old_umask = os.umask(0o177) with open(public_key.full_key_path, 'w', encoding='utf8') as text_file: text_file.write(key) os.umask(old_umask) # configure_curve must be called every time certificates are # added or removed, in order to update the Authenticator's # state. curve_auth.configure_curve(domain='*', location=(client_pub_key_dir)) self.remote_init_map[install_target] = REMOTE_INIT_DONE self.ready = True return # Bad status LOG.error( TaskRemoteMgmtError(TaskRemoteMgmtError.MSG_INIT, install_target, ' '.join(quote(item) for item in proc_ctx.cmd), proc_ctx.ret_code, proc_ctx.out, proc_ctx.err)) self.remote_init_map[platform['install target']] = REMOTE_INIT_FAILED self.ready = True
def create_client_keys(srvd, install_target): """Create or renew authentication keys for suite 'reg' in the .service directory. Generate a pair of ZMQ authentication keys""" cli_pub_key = KeyInfo(KeyType.PUBLIC, KeyOwner.CLIENT, suite_srv_dir=srvd, install_target=install_target, server_held=False) # ZMQ keys generated in .service directory. # ZMQ keys need to be created with stricter file permissions, changing # umask default denials. old_umask = os.umask(0o177) # u=rw only set as default for file creation client_public_full_key_path, _client_private_full_key_path = ( zmq.auth.create_certificates(srvd, KeyOwner.CLIENT.value)) os.rename(client_public_full_key_path, cli_pub_key.full_key_path) # Return file permissions to default settings. os.umask(old_umask)
def test_client_requires_valid_server_public_key_in_private_key_file(): """Client should not be able to connect to host/port without server public key.""" suite_name = f"test_suite-{time()}" port = random.choice(PORT_RANGE) client = ZMQSocketBase(zmq.REP, suite=suite_name) test_suite_srv_dir = get_suite_srv_dir(reg=suite_name) key_info = KeyInfo( KeyType.PRIVATE, KeyOwner.CLIENT, suite_srv_dir=test_suite_srv_dir) directory = os.path.expanduser("~/cylc-run") tmpdir = os.path.join(directory, suite_name) os.makedirs(key_info.key_path, exist_ok=True) _pub, _priv = zmq.auth.create_certificates(key_info.key_path, "client") with pytest.raises(ClientError, match=r"Failed to load the suite's public " r"key, so cannot connect."): client.start(HOST, port, srv_public_key_loc="fake_location") client.stop() rmtree(tmpdir, ignore_errors=True)
def _remote_init_callback( self, proc_ctx, platform, tmphandle, curve_auth, client_pub_key_dir): """Callback when "cylc remote-init" exits""" self.ready = True try: tmphandle.close() except OSError: # E.g. ignore bad unlink, etc pass self.install_target = platform['install target'] if proc_ctx.ret_code == 0: if REMOTE_INIT_DONE in proc_ctx.out: src_path = get_suite_run_dir(self.suite) dst_path = get_remote_suite_run_dir(platform, self.suite) try: process = procopen(construct_rsync_over_ssh_cmd( src_path, dst_path, platform, self.rsync_includes), stdoutpipe=True, stderrpipe=True, universal_newlines=True) out, err = process.communicate(timeout=600) install_target = platform['install target'] if out: RSYNC_LOG.info( 'File installation information for ' f'{install_target}:\n {out}') if err: LOG.error( 'File installation error on ' f'{install_target}:\n {err}') except Exception as ex: LOG.error(f"Problem during rsync: {ex}") self.remote_init_map[self.install_target] = ( REMOTE_INIT_FAILED) return if "KEYSTART" in proc_ctx.out: regex_result = re.search( 'KEYSTART((.|\n|\r)*)KEYEND', proc_ctx.out) key = regex_result.group(1) suite_srv_dir = get_suite_srv_dir(self.suite) public_key = KeyInfo( KeyType.PUBLIC, KeyOwner.CLIENT, suite_srv_dir=suite_srv_dir, install_target=self.install_target ) old_umask = os.umask(0o177) with open( public_key.full_key_path, 'w', encoding='utf8') as text_file: text_file.write(key) os.umask(old_umask) # configure_curve must be called every time certificates are # added or removed, in order to update the Authenticator's # state. curve_auth.configure_curve( domain='*', location=(client_pub_key_dir)) for status in (REMOTE_INIT_DONE, REMOTE_INIT_NOT_REQUIRED): if status in proc_ctx.out: # Good status LOG.debug(proc_ctx) self.remote_init_map[self.install_target] = status return # Bad status LOG.error(TaskRemoteMgmtError( TaskRemoteMgmtError.MSG_INIT, platform['install target'], ' '.join( quote(item) for item in proc_ctx.cmd), proc_ctx.ret_code, proc_ctx.out, proc_ctx.err)) LOG.error(proc_ctx) self.remote_init_map[platform['install target']] = REMOTE_INIT_FAILED
def remote_init(install_target, rund, *dirs_to_symlink): """cylc remote-init Arguments: install_target (str): target to be initialised rund (str): suite run directory dirs_to_symlink (list): directories to be symlinked in form [directory=symlink_location, ...] """ rund = os.path.expandvars(rund) for item in dirs_to_symlink: key, val = item.split("=", 1) if key == 'run': dst = rund else: dst = os.path.join(rund, key) src = os.path.expandvars(val) if '$' in src: print(REMOTE_INIT_FAILED) print(f'Error occurred when symlinking.' f' {src} contains an invalid environment variable.') return make_symlink(src, dst) srvd = os.path.join(rund, SuiteFiles.Service.DIRNAME) os.makedirs(srvd, exist_ok=True) client_pub_keyinfo = KeyInfo(KeyType.PUBLIC, KeyOwner.CLIENT, suite_srv_dir=srvd, install_target=install_target, server_held=False) # Check for existence of client key dir (should only exist on server) # Fail if one exists - this may occur on mis-configuration of install # target in global.cylc client_key_dir = os.path.join( srvd, f"{KeyOwner.CLIENT.value}_{KeyType.PUBLIC.value}_keys") if os.path.exists(client_key_dir): print(REMOTE_INIT_FAILED) print(f"Unexpected key directory exists: {client_key_dir}" " Check global.cylc install target is configured correctly " "for this platform.") return pattern = re.compile(r"^client_\S*key$") for filepath in os.listdir(srvd): if pattern.match(filepath) and f"{install_target}" not in filepath: # client key for a different install target exists print(REMOTE_INIT_FAILED) print(f"Unexpected authentication key \"{filepath}\" exists. " "Check global.cylc install target is configured correctly " "for this platform.") return try: remove_keys_on_client(srvd, install_target) create_client_keys(srvd, install_target) except Exception: # Catching all exceptions as need to fail remote init if any problems # with key generation. print(REMOTE_INIT_FAILED) return oldcwd = os.getcwd() os.chdir(rund) # Extract job.sh from library, for use in job scripts. extract_resources(SuiteFiles.Service.DIRNAME, ['etc/job.sh']) try: tarhandle = tarfile.open(fileobj=sys.stdin.buffer, mode='r|') tarhandle.extractall() tarhandle.close() finally: os.chdir(oldcwd) print("KEYSTART", end='') with open(client_pub_keyinfo.full_key_path) as keyfile: print(keyfile.read(), end='KEYEND') print(REMOTE_INIT_DONE) return