117 lines
4.6 KiB
Python
Executable File
117 lines
4.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import sys
|
|
import urllib3
|
|
import json
|
|
|
|
from cluster import shell
|
|
|
|
CLUSTER_SERVICE_PORT = 10002
|
|
|
|
|
|
class UnauthorizedRequestError(Exception):
|
|
pass
|
|
|
|
|
|
def join():
|
|
"""Join an existing cluster as a compute node."""
|
|
|
|
cluster_config = shell.config_get('config.cluster')
|
|
control_hostname = cluster_config['hostname']
|
|
fingerprint = cluster_config['fingerprint']
|
|
credential_id = cluster_config['credential-id']
|
|
credential_secret = cluster_config['credential-secret']
|
|
|
|
request_body = json.dumps({
|
|
'credential-id': credential_id,
|
|
'credential-secret': credential_secret
|
|
})
|
|
|
|
# Create a connection pool and override the TLS certificate
|
|
# verification method to use the certificate fingerprint instead
|
|
# of hostname validation + validation via CA cert and expiration time.
|
|
# This avoids relying on any kind of PKI and DNS assumptions in the
|
|
# installation environment.
|
|
# If the fingerprint does not match, MaxRetryError will be raised
|
|
# with SSLError as a cause even with the rest of the checks disabled.
|
|
conn_pool = urllib3.HTTPSConnectionPool(
|
|
control_hostname, CLUSTER_SERVICE_PORT,
|
|
assert_fingerprint=fingerprint, assert_hostname=False,
|
|
cert_reqs='CERT_NONE',
|
|
)
|
|
|
|
try:
|
|
resp = conn_pool.urlopen(
|
|
'POST', '/join', retries=0, preload_content=True,
|
|
headers={
|
|
'API-VERSION': '1.0.0',
|
|
'Content-Type': 'application/json',
|
|
}, body=request_body)
|
|
except urllib3.exceptions.MaxRetryError as e:
|
|
if isinstance(e.reason, urllib3.exceptions.SSLError):
|
|
raise Exception(
|
|
'The actual clustering service certificate fingerprint'
|
|
' did not match the expected one, please make sure that: '
|
|
'(1) that a correct token was specified during initialization;'
|
|
' (2) a MITM attacks are not performed against HTTPS requests'
|
|
' (including transparent proxies).'
|
|
) from e.reason
|
|
raise Exception('Could not retrieve a response from the clustering'
|
|
' service.') from e
|
|
|
|
if resp.status == 401:
|
|
response_data = resp.data.decode('utf-8')
|
|
# TODO: this should be more bulletproof in case a proxy server
|
|
# returns this response - it will not have the expected format.
|
|
print('An authorization failure has occurred while joining the'
|
|
' the cluster: please make sure the connection string'
|
|
' was entered as returned by the "add-compute" command'
|
|
' and that it was used before its expiration time.',
|
|
file=sys.stderr)
|
|
if response_data:
|
|
message = json.loads(response_data)['message']
|
|
raise UnauthorizedRequestError(message)
|
|
raise UnauthorizedRequestError()
|
|
if resp.status != 200:
|
|
raise Exception('Unexpected response status received from the'
|
|
f' clustering service: {resp.status}')
|
|
|
|
try:
|
|
response_data = resp.data.decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
raise Exception('The response from the clustering service contains'
|
|
' bytes invalid for UTF-8')
|
|
if not response_data:
|
|
raise Exception('The response from the clustering service is empty'
|
|
' which is unexpected: please check its status'
|
|
' and file an issue if the problem persists')
|
|
|
|
# Load the response assuming it has the correct format. API versioning
|
|
# should rule out inconsistencies, otherwise we will get an error here.
|
|
response_dict = json.loads(response_data)
|
|
credentials = response_dict['config']['credentials']
|
|
control_creds = {f'config.credentials.{k}': v
|
|
for k, v in credentials.items()}
|
|
shell.config_set(**control_creds)
|
|
# TODO: use the hostname from the connection string instead to
|
|
# resolve an IP address (requires a valid DNS setup).
|
|
control_ip = response_dict['config']['network']['control-ip']
|
|
shell.config_set(**{'config.network.control-ip': control_ip})
|
|
|
|
# Write controller's TLS certificate data to compute node
|
|
tls_path_map = {
|
|
'cacert-path': 'tls_cacert',
|
|
'cert-path': 'tls_cert',
|
|
'key-path': 'tls_key',
|
|
}
|
|
for tls_config, tls_file in tls_path_map.items():
|
|
tls_path = response_dict['config']['tls'][tls_config]
|
|
shell.config_set(**{'config.tls.{}'.format(tls_config): tls_path})
|
|
with open(tls_path, "w") as f:
|
|
f.write(response_dict[tls_file])
|
|
shell.config_set(**{'config.tls.generate-self-signed': False})
|
|
|
|
|
|
if __name__ == '__main__':
|
|
join()
|