138 lines
4.7 KiB
Python
138 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import sys
|
|
import uuid
|
|
import secrets
|
|
import argparse
|
|
|
|
from datetime import datetime
|
|
from datetime import timezone
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from oslo_serialization import (
|
|
base64,
|
|
msgpackutils
|
|
)
|
|
|
|
from cluster.shell import config_get
|
|
|
|
from keystoneauth1.identity import v3
|
|
from keystoneauth1 import session
|
|
from keystoneclient.v3 import client
|
|
|
|
|
|
VALIDITY_PERIOD = relativedelta(minutes=20)
|
|
|
|
|
|
def _create_credential():
|
|
project_name = 'service'
|
|
domain_name = 'default'
|
|
# TODO: add support for TLS-terminated Keystone once this is supported.
|
|
auth = v3.password.Password(
|
|
auth_url="https://localhost:5000/v3",
|
|
username='nova',
|
|
password=config_get('config.credentials.nova-password'),
|
|
user_domain_name=domain_name,
|
|
project_domain_name=domain_name,
|
|
project_name=project_name
|
|
)
|
|
if config_get('config.tls.generate-self-signed'):
|
|
sess = session.Session(
|
|
auth=auth,
|
|
verify=False,
|
|
)
|
|
else:
|
|
sess = session.Session(
|
|
auth=auth,
|
|
verify=config_get('config.tls.cacert-path'),
|
|
)
|
|
keystone_client = client.Client(session=sess)
|
|
|
|
# Only allow this credential to list the Keystone catalog. After it
|
|
# expires, Keystone will return Unauthorized for requests made with tokens
|
|
# issued from that credential.
|
|
access_rules = [{
|
|
'method': 'GET',
|
|
'path': '/v3/auth/catalog',
|
|
'service': 'identity'
|
|
}]
|
|
# TODO: make the expiration time customizable since this may be used by
|
|
# automation or during live demonstrations where the lag between issuance
|
|
# and usage may be more than the expiration time.
|
|
# NOTE(wolsen): LP#1903208 expiration stamps passed to keystone without
|
|
# timezone information are assumed to be UTC. Explicitly use UTC to get
|
|
# an expiration at the right time.
|
|
expires_at = datetime.now(tz=timezone.utc) + VALIDITY_PERIOD
|
|
|
|
# Role objects themselves are not tied to a specific domain by default
|
|
# - this does not affect role assignments themselves which are scoped.
|
|
reader_role = keystone_client.roles.find(name='reader', domain_id=None)
|
|
|
|
return keystone_client.application_credentials.create(
|
|
name=f'cluster-join-{uuid.uuid4().hex}',
|
|
expires_at=expires_at,
|
|
access_rules=access_rules,
|
|
# Do not allow this app credential to create new app credentials.
|
|
unrestricted=False,
|
|
roles=[reader_role.id],
|
|
# Make the secret shorter than the default but secure enough.
|
|
secret=secrets.token_urlsafe(32)[:32]
|
|
)
|
|
|
|
|
|
def add_compute():
|
|
"""Generates connection string for adding a compute node to the cluster.
|
|
|
|
Steps:
|
|
* Make sure we are running in the clustered mode and this is a control
|
|
node which is an initial node in the cluster;
|
|
* Generate an application credential via Keystone scoped to the service
|
|
project with restricted capabilities (reader role and only able to list
|
|
the service catalog) and a short expiration time enough for a user to
|
|
copy the connection string to the compute node;
|
|
* Get an FQDN that will be used by the client to establish a connection to
|
|
the clustering service;
|
|
* Serialize the above data into a base64-encoded string.
|
|
"""
|
|
|
|
role = config_get('config.cluster.role')
|
|
if role != 'control':
|
|
raise Exception('Running add-compute is only supported on a'
|
|
' control node.')
|
|
app_cred = _create_credential()
|
|
data = {
|
|
# TODO: we do not use hostname verification, however, using
|
|
# an FQDN might be useful here since the host may be behind NAT
|
|
# with a split-horizon DNS implemented where a hostname would point
|
|
# us to a different IP.
|
|
'hostname': config_get('config.network.control-ip'),
|
|
# Store bytes since the representation will be shorter than with hex.
|
|
'fingerprint': bytes.fromhex(config_get('config.cluster.fingerprint')),
|
|
'id': app_cred.id,
|
|
'secret': app_cred.secret,
|
|
}
|
|
connection_string = base64.encode_as_text(msgpackutils.dumps(data))
|
|
|
|
# Print the connection string and an expiration notice to the user.
|
|
print('Use the following connection string to add a new compute node'
|
|
f' to the cluster (valid for {VALIDITY_PERIOD.minutes} minutes from'
|
|
f' this moment):', file=sys.stderr)
|
|
print(connection_string)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='add-compute',
|
|
usage='''add-compute
|
|
|
|
This command does not have subcommands - just run it to get a connection string
|
|
to be used when joining a node to the cluster.
|
|
''')
|
|
parser.parse_args()
|
|
|
|
add_compute()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|