Merge "Support keystore password rotation"
This commit is contained in:
commit
4e9283a465
15 files changed
+426
-218
No files matched your search
@@ -279,6 +279,37 @@ The following sections of ``zuul.conf`` are used by the scheduler:
|
||||
|
||||
Encryption password for private data stored in Zookeeper.
|
||||
|
||||
Typically this is a single value. To rotate keys, multiple
|
||||
values may be supplied, each on its own line. The first value
|
||||
is always the desired (new) key, subsequent values are older
|
||||
keys. Once all of the components in the cluster have been
|
||||
restarted with the new values, all existing key material will be
|
||||
re-encrypted with the new key. After that point, old keys may
|
||||
be removed.
|
||||
|
||||
For example, with an original configuration of:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[keystore]
|
||||
password = password1
|
||||
|
||||
Zuul may be instructed to rotate to `password` with:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[keystore]
|
||||
password =
|
||||
password2
|
||||
password1
|
||||
|
||||
After rotation, this may be simplified to:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[keystore]
|
||||
password = password2
|
||||
|
||||
.. attr:: scheduler
|
||||
|
||||
.. attr:: command_socket
|
||||
|
||||
+47
-28
@@ -71,7 +71,7 @@ from kazoo.exceptions import NoNodeError
|
||||
|
||||
from zuul import model
|
||||
from zuul.model import (
|
||||
BuildRequest, MergeRequest, WebInfo, HoldRequest
|
||||
BuildRequest, MergeRequest, WebInfo, HoldRequest, KeystorePasswordManager
|
||||
)
|
||||
|
||||
from zuul.driver.zuul import ZuulDriver
|
||||
@@ -2617,29 +2617,9 @@ class ZuulTestCase(BaseTestCase):
|
||||
self._configureMqtt()
|
||||
self._configureElasticsearch()
|
||||
|
||||
executor_connections = TestConnectionRegistry(
|
||||
self.config, self.test_config,
|
||||
self.additional_event_queues,
|
||||
self.upstream_root, self.poller_events,
|
||||
self.git_url_with_auth, self.addCleanup)
|
||||
executor_connections.configure(self.config, sources=True)
|
||||
self.executor_api = TestingExecutorApi(self.zk_client)
|
||||
self.merger_api = TestingMergerApi(self.zk_client)
|
||||
self.executor_server = RecordingExecutorServer(
|
||||
self.config,
|
||||
executor_connections,
|
||||
jobdir_root=self.jobdir_root,
|
||||
_run_ansible=self.run_ansible,
|
||||
_test_root=self.test_root,
|
||||
keep_jobdir=KEEP_TEMPDIRS,
|
||||
log_console_port=self.log_console_port)
|
||||
for return_data in self.test_config.return_data:
|
||||
self.executor_server.returnData(
|
||||
return_data['job'],
|
||||
return_data['ref'],
|
||||
return_data['data'],
|
||||
)
|
||||
self.executor_server.start()
|
||||
self.executor_server = self.createExecutor(self.config)
|
||||
self.history = self.executor_server.build_history
|
||||
self.builds = self.executor_server.running_builds
|
||||
|
||||
@@ -2698,6 +2678,40 @@ class ZuulTestCase(BaseTestCase):
|
||||
launcher.start()
|
||||
return launcher
|
||||
|
||||
def createExecutor(self, config=None):
|
||||
if config is None:
|
||||
config = self.config
|
||||
executor_config = copyConfig(config)
|
||||
executor_connections = TestConnectionRegistry(
|
||||
self.config, self.test_config,
|
||||
self.additional_event_queues,
|
||||
self.upstream_root, self.poller_events,
|
||||
self.git_url_with_auth, self.addCleanup)
|
||||
executor_connections.configure(executor_config, sources=True)
|
||||
executor = RecordingExecutorServer(
|
||||
executor_config,
|
||||
connections=executor_connections,
|
||||
jobdir_root=self.jobdir_root,
|
||||
_run_ansible=self.run_ansible,
|
||||
_test_root=self.test_root,
|
||||
log_console_port=self.log_console_port)
|
||||
for return_data in self.test_config.return_data:
|
||||
executor.returnData(
|
||||
return_data['job'],
|
||||
return_data['ref'],
|
||||
return_data['data'],
|
||||
)
|
||||
executor.start()
|
||||
|
||||
def shutdown_executor():
|
||||
if executor._running:
|
||||
executor.hold_jobs_in_build = False
|
||||
executor.release()
|
||||
executor.stop()
|
||||
executor.join()
|
||||
self.addCleanup(shutdown_executor)
|
||||
return executor
|
||||
|
||||
def createZKContext(self, lock=None):
|
||||
if lock is None:
|
||||
# Just make sure the lock is acquired
|
||||
@@ -2920,9 +2934,11 @@ class ZuulTestCase(BaseTestCase):
|
||||
if self.create_project_keys:
|
||||
return
|
||||
|
||||
password = self.config.get("keystore", "password")
|
||||
keystore_password_manager = KeystorePasswordManager(self.config)
|
||||
keystore_password_manager.setPassword(
|
||||
keystore_password_manager.password_hashes)
|
||||
keystore = zuul.lib.keystorage.KeyStorage(
|
||||
self.zk_client, password=password, start_cache=False)
|
||||
self.zk_client, keystore_password_manager, start_cache=False)
|
||||
|
||||
# This is only needed for the keystore1 -> keystore3 migration
|
||||
COMPONENT_REGISTRY.create(self.zk_client)
|
||||
@@ -2965,9 +2981,11 @@ class ZuulTestCase(BaseTestCase):
|
||||
def setupTenantKeys(self, tenant_name):
|
||||
# Make sure we set up an RSA key for the tenant so that we
|
||||
# don't spend time generating one:
|
||||
password = self.config.get("keystore", "password")
|
||||
keystore_password_manager = KeystorePasswordManager(self.config)
|
||||
keystore_password_manager.setPassword(
|
||||
keystore_password_manager.password_hashes)
|
||||
keystore = zuul.lib.keystorage.KeyStorage(
|
||||
self.zk_client, password=password, start_cache=False)
|
||||
self.zk_client, keystore_password_manager, start_cache=False)
|
||||
import_keys = {}
|
||||
import_data = {'keys': import_keys}
|
||||
|
||||
@@ -3137,8 +3155,9 @@ class ZuulTestCase(BaseTestCase):
|
||||
self.merge_server.stop()
|
||||
self.merge_server.join()
|
||||
|
||||
self.executor_server.stop()
|
||||
self.executor_server.join()
|
||||
if self.executor_server._running:
|
||||
self.executor_server.stop()
|
||||
self.executor_server.join()
|
||||
self.launcher.stop()
|
||||
self.launcher.join()
|
||||
if self.validate_tenants is None:
|
||||
|
||||
@@ -27,7 +27,12 @@ from zuul.configloader import (
|
||||
ConfigLoader, safe_load_yaml
|
||||
)
|
||||
from zuul.exceptions import AuthZRoleNotFoundError, AuthZRuleNotFoundError
|
||||
from zuul.model import Abide, MergeRequest, SourceContext
|
||||
from zuul.model import (
|
||||
Abide,
|
||||
KeystorePasswordManager,
|
||||
MergeRequest,
|
||||
SourceContext,
|
||||
)
|
||||
from zuul.zk.components import COMPONENT_REGISTRY
|
||||
from zuul.zk.locks import tenant_read_lock
|
||||
|
||||
@@ -121,9 +126,11 @@ class TenantParserTestCase(ZuulTestCase):
|
||||
CONFIG_SET = UNTRUSTED_SET | PROVIDER_SET
|
||||
|
||||
def setupAllProjectKeys(self, config: ConfigParser):
|
||||
password = self.config.get("keystore", "password")
|
||||
kpm = KeystorePasswordManager(self.config)
|
||||
kpm.setPassword(set(kpm.password_hashes))
|
||||
|
||||
keystore = zuul.lib.keystorage.KeyStorage(
|
||||
self.zk_client, password=password, start_cache=False)
|
||||
self.zk_client, kpm, start_cache=False)
|
||||
|
||||
# This is only needed for the keystore1 -> keystore3 migration
|
||||
COMPONENT_REGISTRY.create(self.zk_client)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from configparser import ConfigParser
|
||||
import io
|
||||
import time
|
||||
|
||||
@@ -21,6 +22,7 @@ from cryptography.hazmat.primitives.asymmetric.rsa import (
|
||||
RSAPrivateKey,
|
||||
)
|
||||
|
||||
from zuul.model import KeystorePasswordManager
|
||||
from zuul.lib import encryption
|
||||
from zuul.lib import keystorage3
|
||||
from zuul.zk import ZooKeeperClient
|
||||
@@ -30,7 +32,9 @@ from zuul.zk.components import (
|
||||
)
|
||||
|
||||
from tests.base import (
|
||||
copyConfig,
|
||||
BaseTestCase,
|
||||
ZuulTestCase,
|
||||
ZOOKEEPER_SESSION_TIMEOUT,
|
||||
)
|
||||
|
||||
@@ -56,9 +60,15 @@ class TestKeyStorage3(BaseTestCase):
|
||||
COMPONENT_REGISTRY.create(self.zk_client)
|
||||
self.password = 'keystorage_password'
|
||||
|
||||
self.config = ConfigParser()
|
||||
self.config.add_section('keystore')
|
||||
self.config['keystore']['password'] = self.password
|
||||
self.kpm = KeystorePasswordManager(self.config)
|
||||
self.kpm.setPassword(set(self.kpm.password_hashes))
|
||||
|
||||
def test_project_ssh_keys(self):
|
||||
start_time = int(time.time())
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.password)
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.kpm)
|
||||
self.addCleanup(ks.stop)
|
||||
# This will generate a new ssh key
|
||||
info = ks.getProjectSSHKeyInfo('gerrit', 'org/project')
|
||||
@@ -83,7 +93,7 @@ class TestKeyStorage3(BaseTestCase):
|
||||
self.assertTrue(info is info2)
|
||||
|
||||
# Another client to read it from ZK
|
||||
ks2 = keystorage3.KeyStorage3(self.zk_client, self.password)
|
||||
ks2 = keystorage3.KeyStorage3(self.zk_client, self.kpm)
|
||||
self.addCleanup(ks2.stop)
|
||||
ks2_info = ks2.getProjectSSHKeyInfo('gerrit', 'org/project')
|
||||
|
||||
@@ -98,7 +108,7 @@ class TestKeyStorage3(BaseTestCase):
|
||||
|
||||
def test_tenant_ssh_keys(self):
|
||||
start_time = int(time.time())
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.password)
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.kpm)
|
||||
self.addCleanup(ks.stop)
|
||||
# This will generate a new ssh key
|
||||
info = ks.getTenantSSHKeyInfo('tenant1')
|
||||
@@ -123,7 +133,7 @@ class TestKeyStorage3(BaseTestCase):
|
||||
self.assertTrue(info is info2)
|
||||
|
||||
# Another client to read it from ZK
|
||||
ks2 = keystorage3.KeyStorage3(self.zk_client, self.password)
|
||||
ks2 = keystorage3.KeyStorage3(self.zk_client, self.kpm)
|
||||
self.addCleanup(ks2.stop)
|
||||
ks2_info = ks2.getTenantSSHKeyInfo('tenant1')
|
||||
|
||||
@@ -138,7 +148,7 @@ class TestKeyStorage3(BaseTestCase):
|
||||
|
||||
def test_project_secret_keys(self):
|
||||
start_time = int(time.time())
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.password)
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.kpm)
|
||||
self.addCleanup(ks.stop)
|
||||
# This will generate a new secrets key
|
||||
info = ks.getProjectSecretsKeyInfo('gerrit', 'org/project')
|
||||
@@ -163,7 +173,7 @@ class TestKeyStorage3(BaseTestCase):
|
||||
self.assertTrue(info is info2)
|
||||
|
||||
# Another client to read it from ZK
|
||||
ks2 = keystorage3.KeyStorage3(self.zk_client, self.password)
|
||||
ks2 = keystorage3.KeyStorage3(self.zk_client, self.kpm)
|
||||
self.addCleanup(ks2.stop)
|
||||
ks2_info = ks2.getProjectSecretsKeyInfo('gerrit', 'org/project')
|
||||
|
||||
@@ -179,7 +189,7 @@ class TestKeyStorage3(BaseTestCase):
|
||||
ks2_key1.private_key.private_numbers())
|
||||
|
||||
def test_rotate_project_ssh_keys(self):
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.password)
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.kpm)
|
||||
self.addCleanup(ks.stop)
|
||||
# This will generate a new secrets key
|
||||
info = ks.getProjectSSHKeyInfo('gerrit', 'org/project')
|
||||
@@ -207,7 +217,7 @@ class TestKeyStorage3(BaseTestCase):
|
||||
self.assertEqual(1, key3.version)
|
||||
|
||||
def test_rotate_tenant_ssh_keys(self):
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.password)
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.kpm)
|
||||
self.addCleanup(ks.stop)
|
||||
# This will generate a new secrets key
|
||||
info = ks.getTenantSSHKeyInfo('tenant-one')
|
||||
@@ -235,7 +245,7 @@ class TestKeyStorage3(BaseTestCase):
|
||||
self.assertEqual(1, key3.version)
|
||||
|
||||
def test_rotate_project_secret_keys(self):
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.password)
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, self.kpm)
|
||||
self.addCleanup(ks.stop)
|
||||
# This will generate a new secrets key
|
||||
info = ks.getProjectSecretsKeyInfo('gerrit', 'org/project')
|
||||
@@ -261,3 +271,61 @@ class TestKeyStorage3(BaseTestCase):
|
||||
self.assertEqual(1, len(info.keys))
|
||||
key3 = info.keys[0]
|
||||
self.assertEqual(1, key3.version)
|
||||
|
||||
|
||||
class TestKeystorePasswordRotation(ZuulTestCase):
|
||||
tenant_config_file = 'config/one-project/main.yaml'
|
||||
|
||||
def test_keystore_password_rotation(self):
|
||||
first = self.scheds.first
|
||||
ks = first.sched.keystore
|
||||
info = ks.getProjectSecretsKeyInfo('gerrit', 'org/project')
|
||||
key = info.keys[0]
|
||||
encryption.deserialize_rsa_keypair(
|
||||
key._encrypted_private_key.encode('utf8'), b'keystorepassword')
|
||||
|
||||
ssh_info = ks.getProjectSSHKeyInfo('gerrit', 'org/project')
|
||||
ssh_key = ssh_info.keys[0]
|
||||
with io.StringIO(ssh_key._encrypted_private_key) as o:
|
||||
paramiko.RSAKey.from_private_key(o, 'keystorepassword')
|
||||
|
||||
new_pw = "newpassword\nkeystorepassword\n"
|
||||
self.config['keystore']['password'] = new_pw
|
||||
|
||||
# Replace scheduler
|
||||
first.sched.stop()
|
||||
first.sched.join()
|
||||
self.createScheduler()
|
||||
self.scheds.instances.pop(0)
|
||||
first = self.scheds.first
|
||||
first.start()
|
||||
self.waitUntilSettled()
|
||||
|
||||
# Replace Executor
|
||||
self.executor_server.stop()
|
||||
self.executor_server.join()
|
||||
self.executor_server = self.createExecutor()
|
||||
|
||||
# Verify that the item has been re-keyed
|
||||
ks = first.sched.keystore
|
||||
info = ks.getProjectSecretsKeyInfo('gerrit', 'org/project')
|
||||
key = info.keys[0]
|
||||
encryption.deserialize_rsa_keypair(
|
||||
key._encrypted_private_key.encode('utf8'), b'newpassword')
|
||||
|
||||
# Also verify we wrote the update to zk
|
||||
config = copyConfig(self.config)
|
||||
config['keystore']['password'] = "newpassword"
|
||||
kpm = KeystorePasswordManager(config)
|
||||
kpm.setPassword(set(kpm.password_hashes))
|
||||
ks = keystorage3.KeyStorage3(self.zk_client, kpm)
|
||||
extra = dict(keystore_password_manager=kpm)
|
||||
ctx = ks.createZKContext()
|
||||
|
||||
data, zstat = self.zk_client.client.get(info._path)
|
||||
keystorage3.ZuulSecretKeyInfo._fromRaw(ctx, data, zstat, extra=extra)
|
||||
|
||||
data, zstat = self.zk_client.client.get(ssh_info._path)
|
||||
keystorage3.ZuulSSHKeyInfo._fromRaw(ctx, data, zstat, extra=extra)
|
||||
|
||||
self.waitUntilSettled()
|
||||
@@ -44,7 +44,6 @@ from tests.base import (
|
||||
FIXTURE_DIR,
|
||||
RecordingExecutorServer,
|
||||
SSLZuulTestCase,
|
||||
TestConnectionRegistry,
|
||||
ZuulTestCase,
|
||||
iterate_timeout,
|
||||
okay_tracebacks,
|
||||
@@ -97,26 +96,7 @@ class TestSchedulerZone(ZuulTestCase):
|
||||
config.remove_option('executor', 'zone')
|
||||
config.set('executor', 'command_socket',
|
||||
os.path.join(self.test_root, 'executor2.socket'))
|
||||
executor_connections = TestConnectionRegistry(
|
||||
self.config, self.test_config, self.additional_event_queues,
|
||||
self.upstream_root, self.poller_events,
|
||||
self.git_url_with_auth, self.addCleanup)
|
||||
executor_connections.configure(self.config, sources=True)
|
||||
self.executor_server_unzoned = RecordingExecutorServer(
|
||||
config,
|
||||
connections=executor_connections,
|
||||
jobdir_root=self.jobdir_root,
|
||||
_run_ansible=self.run_ansible,
|
||||
_test_root=self.test_root,
|
||||
log_console_port=self.log_console_port)
|
||||
self.executor_server_unzoned.start()
|
||||
self.addCleanup(self._shutdown_executor)
|
||||
|
||||
def _shutdown_executor(self):
|
||||
self.executor_server_unzoned.hold_jobs_in_build = False
|
||||
self.executor_server_unzoned.release()
|
||||
self.executor_server_unzoned.stop()
|
||||
self.executor_server_unzoned.join()
|
||||
self.executor_server_unzoned = self.createExecutor(config)
|
||||
|
||||
def setup_config(self, config_file: str):
|
||||
config = super(TestSchedulerZone, self).setup_config(config_file)
|
||||
|
||||
@@ -4389,7 +4389,7 @@ class FunctionalAnsibleMixIn(object):
|
||||
# Add a site variables file, used by check-vars
|
||||
path = os.path.join(FIXTURE_DIR, 'config', 'ansible',
|
||||
'variables.yaml')
|
||||
self.config.set('executor', 'variables', path)
|
||||
self.executor_server.config.set('executor', 'variables', path)
|
||||
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
|
||||
self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
|
||||
self.waitUntilSettled()
|
||||
|
||||
@@ -45,8 +45,9 @@ class TestUpgradeOld(LauncherBaseTestCase):
|
||||
self.merge_server.stop()
|
||||
self.merge_server.join()
|
||||
|
||||
self.executor_server.stop()
|
||||
self.executor_server.join()
|
||||
if self.executor_server._running:
|
||||
self.executor_server.stop()
|
||||
self.executor_server.join()
|
||||
self.launcher.stop()
|
||||
self.launcher.join()
|
||||
self.statsd.stop()
|
||||
|
||||
+24
-71
@@ -38,6 +38,7 @@ from zuul.model import (
|
||||
Abide,
|
||||
ImageBuildArtifact,
|
||||
ImageUpload,
|
||||
KeystorePasswordManager,
|
||||
Pipeline,
|
||||
PipelineChangeList,
|
||||
PipelineState,
|
||||
@@ -1202,16 +1203,21 @@ class Client(zuul.cmd.ZuulApp):
|
||||
finally:
|
||||
sys.exit(err_code)
|
||||
|
||||
def getKeystore(self, zk_client):
|
||||
try:
|
||||
self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
kpm = KeystorePasswordManager(self.config)
|
||||
kpm.setPassword(set(kpm.password_hashes))
|
||||
return KeyStorage(zk_client, kpm)
|
||||
|
||||
def export_keys(self):
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
export = keystore.exportKeys()
|
||||
with open(os.open(self.args.path,
|
||||
os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f:
|
||||
@@ -1223,11 +1229,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
with open(self.args.path, 'r') as f:
|
||||
import_data = json.load(f)
|
||||
keystore.importKeys(import_data, self.args.force)
|
||||
@@ -1241,11 +1243,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
keystore.copyProjectSSHKeys(args.src_connection,
|
||||
args.src_project,
|
||||
@@ -1267,12 +1265,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
zk_client.connect()
|
||||
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
keystore.deleteProjectSSHKeys(args.connection, args.project)
|
||||
keystore.deleteProjectSecretsKeys(args.connection, args.project)
|
||||
@@ -1286,11 +1279,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
keystore.addProjectSSHKey(args.connection, args.project)
|
||||
sys.exit(0)
|
||||
@@ -1300,11 +1289,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
keystore.addProjectSecretsKey(args.connection, args.project)
|
||||
sys.exit(0)
|
||||
@@ -1314,11 +1299,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
keystore.addTenantSSHKey(args.tenant)
|
||||
sys.exit(0)
|
||||
@@ -1328,11 +1309,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
info = keystore.getProjectSSHKeyInfo(args.connection, args.project,
|
||||
create=False)
|
||||
@@ -1347,11 +1324,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
info = keystore.getProjectSecretsKeyInfo(args.connection, args.project,
|
||||
create=False)
|
||||
@@ -1366,11 +1339,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
info = keystore.getTenantSSHKeyInfo(args.tenant, create=False)
|
||||
table = prettytable.PrettyTable(field_names=['Version', 'Created'])
|
||||
@@ -1384,11 +1353,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
keystore.deleteProjectSSHKey(
|
||||
args.connection, args.project, args.version)
|
||||
@@ -1399,11 +1364,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
keystore.deleteProjectSecretsKey(
|
||||
args.connection, args.project, args.version)
|
||||
@@ -1414,11 +1375,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
COMPONENT_REGISTRY.create(zk_client)
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
args = self.args
|
||||
keystore.deleteTenantSSHKey(args.tenant, args.version)
|
||||
sys.exit(0)
|
||||
@@ -1428,11 +1385,7 @@ class Client(zuul.cmd.ZuulApp):
|
||||
|
||||
zk_client = ZooKeeperClient.fromConfig(self.config)
|
||||
zk_client.connect()
|
||||
try:
|
||||
password = self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
keystore = KeyStorage(zk_client, password=password)
|
||||
keystore = self.getKeystore(zk_client)
|
||||
algorithm = self.args.algorithm
|
||||
keystore.deleteOidcSigningKeys(algorithm)
|
||||
self.log.info("Delete OIDC signing keys for %s", algorithm)
|
||||
|
||||
+15
-5
@@ -93,6 +93,7 @@ from zuul.model import (
|
||||
BuildStatusEvent,
|
||||
FrozenJob,
|
||||
Job,
|
||||
KeystorePasswordManager,
|
||||
RepoState,
|
||||
)
|
||||
import zuul.model
|
||||
@@ -4129,19 +4130,28 @@ class ExecutorServer(BaseMergeServer):
|
||||
|
||||
self.keep_jobdir = keep_jobdir
|
||||
self.jobdir_root = jobdir_root
|
||||
self.keystore = KeyStorage(
|
||||
self.zk_client,
|
||||
password=self._get_key_store_password())
|
||||
self._running = False
|
||||
self._command_running = False
|
||||
# TODOv3(mordred): make the executor name more unique --
|
||||
# perhaps hostname+pid.
|
||||
self.hostname = get_default(self.config, 'executor', 'hostname',
|
||||
socket.getfqdn())
|
||||
|
||||
self.keystore_password_manager = KeystorePasswordManager(self.config)
|
||||
self.component_info = ExecutorComponent(
|
||||
self.zk_client, self.hostname, version=get_version_string())
|
||||
self.zk_client, self.hostname, version=get_version_string(),
|
||||
keystore_passwords=self.keystore_password_manager.password_hashes,
|
||||
)
|
||||
self.component_info.register()
|
||||
COMPONENT_REGISTRY.create(self.zk_client)
|
||||
component_registry = COMPONENT_REGISTRY.create(self.zk_client)
|
||||
# This is mostly for unit tests, where we can have an empty,
|
||||
# initialized registry object.
|
||||
component_registry.waitForComponent(self.component_info)
|
||||
self.keystore_password_manager.connectToRegistry(component_registry)
|
||||
self.keystore = KeyStorage(
|
||||
self.zk_client,
|
||||
self.keystore_password_manager)
|
||||
|
||||
self.secrets_cache = cachetools.LFUCache(maxsize=128)
|
||||
self.zk_context = ZKContext(self.zk_client, None, None, self.log)
|
||||
self.monitoring_server = MonitoringServer(self.config, 'executor',
|
||||
|
||||
+25
-9
@@ -31,17 +31,27 @@ RSA_KEY_SIZE = 2048
|
||||
class KeyStorage:
|
||||
log = logging.getLogger("zuul.KeyStorage")
|
||||
|
||||
def __init__(self, zookeeper_client, password, start_cache=True):
|
||||
self.password = password
|
||||
self.password_bytes = password.encode('utf8')
|
||||
def __init__(self, zookeeper_client, keystore_password_manager,
|
||||
start_cache=True):
|
||||
self.kp_manager = keystore_password_manager
|
||||
self.all_passwords = self.kp_manager.passwords
|
||||
self.all_passwords_bytes = self.kp_manager.passwords_bytes
|
||||
self.client = zookeeper_client
|
||||
self.keystorage1 = KeyStorage1(zookeeper_client, password)
|
||||
self.keystorage3 = KeyStorage3(zookeeper_client, password)
|
||||
self.keystorage1 = KeyStorage1(zookeeper_client, self.password)
|
||||
self.keystorage3 = KeyStorage3(zookeeper_client, self.kp_manager)
|
||||
if start_cache:
|
||||
self.oidc_signing_keys_cache = OIDCSigningKeysCache(self.client)
|
||||
else:
|
||||
self.oidc_signing_keys_cache = None
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return self.kp_manager.password
|
||||
|
||||
@property
|
||||
def password_bytes(self):
|
||||
return self.kp_manager.password_bytes
|
||||
|
||||
@property
|
||||
def _keystorage(self):
|
||||
# TODO: When we remove this backwards compat code, we
|
||||
@@ -265,10 +275,16 @@ class KeyStorage:
|
||||
pem_private_key = latest_key["private_key"].encode("utf-8")
|
||||
version = latest_key["version"]
|
||||
|
||||
private_key, public_key = encryption.deserialize_rsa_keypair(
|
||||
pem_private_key, self.password_bytes)
|
||||
|
||||
return private_key, public_key, version
|
||||
# TODO use old keys:
|
||||
for password_bytes in self.all_passwords_bytes:
|
||||
try:
|
||||
private_key, public_key =\
|
||||
encryption.deserialize_rsa_keypair(
|
||||
pem_private_key, password_bytes)
|
||||
return private_key, public_key, version
|
||||
except ValueError:
|
||||
pass
|
||||
raise ValueError("Decryption failed")
|
||||
|
||||
def deleteOidcSigningKeys(self, algorithm):
|
||||
"""Delete the complete internal data structure"""
|
||||
|
||||
+88
-51
@@ -38,6 +38,11 @@ class ZuulSSHKey:
|
||||
self.created = None
|
||||
self.version = None
|
||||
|
||||
def _rekey(self, password):
|
||||
with io.StringIO() as o:
|
||||
self._key_object.write_private_key(o, password)
|
||||
self._encrypted_private_key = o.getvalue()
|
||||
|
||||
@classmethod
|
||||
def new(cls, password, version):
|
||||
self = cls()
|
||||
@@ -85,6 +90,10 @@ class ZuulSecretKey:
|
||||
self.created = None
|
||||
self.version = None
|
||||
|
||||
def _rekey(self, password):
|
||||
self._encrypted_private_key = encryption.serialize_rsa_private_key(
|
||||
self.private_key, password).decode("utf-8")
|
||||
|
||||
@classmethod
|
||||
def new(cls, password, version):
|
||||
self = cls()
|
||||
@@ -142,75 +151,94 @@ class ZuulKeyInfo(ZKObject):
|
||||
def private_keys(self):
|
||||
return [k.private_key for k in self.keys]
|
||||
|
||||
|
||||
class ZuulSSHKeyInfo(ZuulKeyInfo):
|
||||
def deserialize(self, data, context, extra=None):
|
||||
data = super().deserialize(data, context, extra)
|
||||
data['keys'] = [ZuulSSHKey.fromDict(k, extra['password'])
|
||||
for k in data['keys']]
|
||||
self._handleKeys(data, extra)
|
||||
return data
|
||||
|
||||
def updateFromDict(self, context, extra=None, **data):
|
||||
if 'keys' in data:
|
||||
data['keys'] = [ZuulSSHKey.fromDict(k, extra['password'])
|
||||
for k in data['keys']]
|
||||
self._handleKeys(data, extra)
|
||||
self.updateAttributes(context, **data)
|
||||
|
||||
@classmethod
|
||||
def newFromDict(klass, context, extra=None, **data):
|
||||
if 'keys' in data:
|
||||
data['keys'] = [ZuulSSHKey.fromDict(k, extra['password'])
|
||||
for k in data['keys']]
|
||||
def newFromDict(cls, context, extra=None, **data):
|
||||
cls._handleKeys(data, extra)
|
||||
return super().new(context, **data)
|
||||
|
||||
|
||||
class ZuulSSHKeyInfo(ZuulKeyInfo):
|
||||
@classmethod
|
||||
def _handleKeys(cls, data, extra):
|
||||
kp_manager = extra['keystore_password_manager']
|
||||
keys = []
|
||||
for keydata in data.get('keys', []):
|
||||
key = None
|
||||
error = None
|
||||
for password in kp_manager.passwords:
|
||||
try:
|
||||
key = ZuulSSHKey.fromDict(keydata, password)
|
||||
break
|
||||
except paramiko.ssh_exception.SSHException as e:
|
||||
error = e
|
||||
if key is None:
|
||||
raise error
|
||||
if password != kp_manager.password:
|
||||
data['_rekey'] = True
|
||||
key._rekey(kp_manager.password)
|
||||
keys.append(key)
|
||||
data['keys'] = keys
|
||||
|
||||
|
||||
class ZuulSecretKeyInfo(ZuulKeyInfo):
|
||||
def deserialize(self, data, context, extra=None):
|
||||
data = super().deserialize(data, context, extra)
|
||||
data['keys'] = [ZuulSecretKey.fromDict(k, extra['password_bytes'])
|
||||
for k in data['keys']]
|
||||
return data
|
||||
|
||||
def updateFromDict(self, context, extra=None, **data):
|
||||
if 'keys' in data:
|
||||
data['keys'] = [ZuulSecretKey.fromDict(k, extra['password_bytes'])
|
||||
for k in data['keys']]
|
||||
self.updateAttributes(context, **data)
|
||||
|
||||
@classmethod
|
||||
def newFromDict(klass, context, extra=None, **data):
|
||||
if 'keys' in data:
|
||||
data['keys'] = [ZuulSecretKey.fromDict(k, extra['password_bytes'])
|
||||
for k in data['keys']]
|
||||
return super().new(context, **data)
|
||||
def _handleKeys(cls, data, extra):
|
||||
kp_manager = extra['keystore_password_manager']
|
||||
keys = []
|
||||
for keydata in data.get('keys', []):
|
||||
key = None
|
||||
error = None
|
||||
for password in kp_manager.passwords_bytes:
|
||||
try:
|
||||
key = ZuulSecretKey.fromDict(keydata, password)
|
||||
break
|
||||
except ValueError as e:
|
||||
error = e
|
||||
if key is None:
|
||||
raise error
|
||||
if password != kp_manager.password_bytes:
|
||||
data['_rekey'] = True
|
||||
key._rekey(kp_manager.password_bytes)
|
||||
keys.append(key)
|
||||
data['keys'] = keys
|
||||
|
||||
|
||||
class KeystorageCache(ZuulTreeCache):
|
||||
def __init__(self, *args, **kw):
|
||||
self.password = kw.pop('password')
|
||||
self.password_bytes = self.password.encode('utf8')
|
||||
self.extra = kw.pop('extra')
|
||||
super().__init__(*args, **kw, auto_reinit=False)
|
||||
|
||||
def objectFromRaw(self, key, data, zstat):
|
||||
extra = dict(password=self.password,
|
||||
password_bytes=self.password_bytes)
|
||||
if key.endswith('/ssh'):
|
||||
info = ZuulSSHKeyInfo._fromRaw(
|
||||
self._zk_context, data, zstat, extra)
|
||||
self._zk_context, data, zstat, self.extra)
|
||||
info._set(_path=key)
|
||||
if getattr(info, '_rekey', False):
|
||||
self.log.info("Re-encrypting %s with new password", key)
|
||||
info.updateAttributes(self._zk_context)
|
||||
return info
|
||||
elif key.endswith('/secrets'):
|
||||
info = ZuulSecretKeyInfo._fromRaw(
|
||||
self._zk_context, data, zstat, extra)
|
||||
self._zk_context, data, zstat, self.extra)
|
||||
info._set(_path=key)
|
||||
if getattr(info, '_rekey', False):
|
||||
self.log.info("Re-encrypting %s with new password", key)
|
||||
info.updateAttributes(self._zk_context)
|
||||
return info
|
||||
else:
|
||||
raise Exception("Unknown key class")
|
||||
|
||||
def updateFromRaw(self, obj, key, data, zstat):
|
||||
extra = dict(password=self.password,
|
||||
password_bytes=self.password_bytes)
|
||||
obj._updateFromRaw(self._zk_context, data, zstat, extra)
|
||||
obj._updateFromRaw(self._zk_context, data, zstat, self.extra)
|
||||
|
||||
def parsePath(self, path):
|
||||
key = None
|
||||
@@ -299,13 +327,19 @@ class KeyStorage3(ZooKeeperBase):
|
||||
log = logging.getLogger("zuul.KeyStorage")
|
||||
ROOT_PATH = "/keystorage3"
|
||||
|
||||
def __init__(self, zookeeper_client, password, start_cache=True):
|
||||
def __init__(self, zookeeper_client, keystore_password_manager,
|
||||
start_cache=True):
|
||||
super().__init__(zookeeper_client)
|
||||
self.password = password
|
||||
self.password_bytes = password.encode("utf-8")
|
||||
self.kp_manager = keystore_password_manager
|
||||
self.all_passwords = self.kp_manager.passwords
|
||||
self.all_passwords_bytes = self.kp_manager.passwords_bytes
|
||||
self.extra = dict(
|
||||
keystore_password_manager=keystore_password_manager,
|
||||
)
|
||||
|
||||
if start_cache:
|
||||
self.keystorage_cache = KeystorageCache(
|
||||
self.client, self.ROOT_PATH, password=self.password)
|
||||
self.client, self.ROOT_PATH, extra=self.extra)
|
||||
else:
|
||||
self.keystorage_cache = None
|
||||
|
||||
@@ -326,8 +360,6 @@ class KeyStorage3(ZooKeeperBase):
|
||||
|
||||
def importKeys(self, import_data, overwrite):
|
||||
self.keystorage_cache.ensureReady()
|
||||
extra = dict(password=self.password,
|
||||
password_bytes=self.password_bytes)
|
||||
with self.createZKContext() as context:
|
||||
for path, data in import_data['keys'].items():
|
||||
if (path.startswith('/keystorage/') or
|
||||
@@ -346,14 +378,15 @@ class KeyStorage3(ZooKeeperBase):
|
||||
if existing := self.keystorage_cache._cached_objects.get(path):
|
||||
if overwrite:
|
||||
existing.updateFromDict(
|
||||
context, extra=extra, **data)
|
||||
context, extra=self.extra, **data)
|
||||
self.log.info("Overwrote key at %s", path)
|
||||
else:
|
||||
self.log.warning(
|
||||
"Not overwriting existing key at %s", path)
|
||||
continue
|
||||
else:
|
||||
cls.newFromDict(context, extra=extra, _path=path, **data)
|
||||
cls.newFromDict(context, extra=self.extra,
|
||||
_path=path, **data)
|
||||
self.log.info("Imported key at %s", path)
|
||||
|
||||
def getProjectSSHKeysPath(self, connection_name, project_name):
|
||||
@@ -371,7 +404,7 @@ class KeyStorage3(ZooKeeperBase):
|
||||
return None
|
||||
self.log.info("Generating a new SSH key for %s/%s",
|
||||
connection_name, project_name)
|
||||
zuul_key = ZuulSSHKey.new(self.password, version=0)
|
||||
zuul_key = ZuulSSHKey.new(self.kp_manager.password, version=0)
|
||||
keys = [zuul_key]
|
||||
try:
|
||||
with self.createZKContext() as context:
|
||||
@@ -395,7 +428,8 @@ class KeyStorage3(ZooKeeperBase):
|
||||
connection_name, project_name)
|
||||
keys = info.keys.copy()
|
||||
version = len(keys)
|
||||
zuul_key = ZuulSSHKey.new(self.password_bytes, version=version)
|
||||
zuul_key = ZuulSSHKey.new(
|
||||
self.kp_manager.password_bytes, version=version)
|
||||
keys.append(zuul_key)
|
||||
with self.createZKContext() as context:
|
||||
info.updateAttributes(context, keys=keys)
|
||||
@@ -456,7 +490,7 @@ class KeyStorage3(ZooKeeperBase):
|
||||
return None
|
||||
self.log.info("Generating a new SSH key for %s",
|
||||
tenant_name)
|
||||
zuul_key = ZuulSSHKey.new(self.password, version=0)
|
||||
zuul_key = ZuulSSHKey.new(self.kp_manager.password, version=0)
|
||||
keys = [zuul_key]
|
||||
try:
|
||||
with self.createZKContext() as context:
|
||||
@@ -478,7 +512,8 @@ class KeyStorage3(ZooKeeperBase):
|
||||
self.log.info("Generating a new SSH key for %s", tenant_name)
|
||||
keys = info.keys.copy()
|
||||
version = len(keys)
|
||||
zuul_key = ZuulSSHKey.new(self.password_bytes, version=version)
|
||||
zuul_key = ZuulSSHKey.new(
|
||||
self.kp_manager.password_bytes, version=version)
|
||||
keys.append(zuul_key)
|
||||
with self.createZKContext() as context:
|
||||
info.updateAttributes(context, keys=keys)
|
||||
@@ -531,7 +566,8 @@ class KeyStorage3(ZooKeeperBase):
|
||||
return None
|
||||
self.log.info("Generating a new secrets key for %s/%s",
|
||||
connection_name, project_name)
|
||||
zuul_key = ZuulSecretKey.new(self.password_bytes, version=0)
|
||||
zuul_key = ZuulSecretKey.new(
|
||||
self.kp_manager.password_bytes, version=0)
|
||||
keys = [zuul_key]
|
||||
try:
|
||||
with self.createZKContext() as context:
|
||||
@@ -555,7 +591,8 @@ class KeyStorage3(ZooKeeperBase):
|
||||
connection_name, project_name)
|
||||
keys = info.keys.copy()
|
||||
version = len(keys)
|
||||
zuul_key = ZuulSecretKey.new(self.password_bytes, version=version)
|
||||
zuul_key = ZuulSecretKey.new(
|
||||
self.kp_manager.password_bytes, version=version)
|
||||
keys.append(zuul_key)
|
||||
with self.createZKContext() as context:
|
||||
info.updateAttributes(context, keys=keys)
|
||||
|
||||
@@ -12345,3 +12345,52 @@ class TenantEventState(zkobject.ZKObject):
|
||||
self._save(context, data, create=True)
|
||||
except NodeExistsError:
|
||||
self._save(context, data)
|
||||
|
||||
|
||||
class KeystorePasswordManager:
|
||||
def __init__(self, config):
|
||||
try:
|
||||
passwords = config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
passwords = [x.strip() for x in passwords.split('\n')]
|
||||
|
||||
self.password = None
|
||||
self.password_bytes = None
|
||||
self.passwords_by_hash = {}
|
||||
# We rely on dict order; we will use the first password that
|
||||
# all components have.
|
||||
for password in passwords:
|
||||
hasher = hashlib.sha256()
|
||||
hasher.update(password.encode('utf8'))
|
||||
pw_hash = hasher.hexdigest()
|
||||
self.passwords_by_hash[pw_hash] = password
|
||||
self.password_hashes = list(self.passwords_by_hash.keys())
|
||||
self.passwords = passwords
|
||||
self.passwords_bytes = [x.encode('utf8') for x in passwords]
|
||||
|
||||
def setPassword(self, common_hashes):
|
||||
"Set the current password to the first common one"
|
||||
for pw_hash, password in self.passwords_by_hash.items():
|
||||
if pw_hash in common_hashes:
|
||||
self.password = password
|
||||
self.password_bytes = password.encode('utf8')
|
||||
return
|
||||
|
||||
def connectToRegistry(self, component_registry):
|
||||
component_registry.registerCallback(
|
||||
lambda: self.registryCallback(component_registry))
|
||||
self.registryCallback(component_registry)
|
||||
|
||||
def registryCallback(self, registry):
|
||||
common_hashes = set()
|
||||
for ctype, components in registry.all():
|
||||
for component in components:
|
||||
# Ignore components without passwords or the empty list
|
||||
# (the component could be initializing).
|
||||
if hashes := getattr(component, 'keystore_passwords', []):
|
||||
if not common_hashes:
|
||||
common_hashes = set(hashes)
|
||||
else:
|
||||
common_hashes &= set(hashes)
|
||||
self.setPassword(common_hashes)
|
||||
+11
-8
@@ -67,6 +67,7 @@ from zuul.model import (
|
||||
HoldRequest,
|
||||
ImageBuildArtifact,
|
||||
ImageUpload,
|
||||
KeystorePasswordManager,
|
||||
MergeCompletedEvent,
|
||||
NodeRequest,
|
||||
NodesProvisionedEvent,
|
||||
@@ -289,10 +290,18 @@ class Scheduler(threading.Thread):
|
||||
|
||||
self.zuul_version = get_version_string()
|
||||
|
||||
self.keystore_password_manager = KeystorePasswordManager(self.config)
|
||||
self.component_info = SchedulerComponent(
|
||||
self.zk_client, self.hostname, version=self.zuul_version)
|
||||
self.zk_client, self.hostname, version=self.zuul_version,
|
||||
keystore_passwords=self.keystore_password_manager.password_hashes,
|
||||
)
|
||||
self.component_info.register()
|
||||
self.component_registry = COMPONENT_REGISTRY.create(self.zk_client)
|
||||
# This is mostly for unit tests, where we can have an empty,
|
||||
# initialized registry object.
|
||||
self.component_registry.waitForComponent(self.component_info)
|
||||
self.keystore_password_manager.connectToRegistry(
|
||||
self.component_registry)
|
||||
self.system_config_cache = SystemConfigCache(self.zk_client,
|
||||
self.wake_event.set)
|
||||
self.unparsed_config_cache = UnparsedConfigCache(self.zk_client)
|
||||
@@ -337,7 +346,7 @@ class Scheduler(threading.Thread):
|
||||
|
||||
self.keystore = KeyStorage(
|
||||
self.zk_client,
|
||||
password=self._get_key_store_password())
|
||||
self.keystore_password_manager)
|
||||
|
||||
self.abide = Abide()
|
||||
self.unparsed_abide = UnparsedAbideConfig()
|
||||
@@ -1327,12 +1336,6 @@ class Scheduler(threading.Thread):
|
||||
result.wait()
|
||||
log.debug("Enqueue complete")
|
||||
|
||||
def _get_key_store_password(self):
|
||||
try:
|
||||
return self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
|
||||
def runTenantLayoutUpdates(self):
|
||||
log = logging.getLogger("zuul.Scheduler.LayoutUpdate")
|
||||
# Only run this after config priming is complete
|
||||
|
||||
+13
-8
@@ -62,6 +62,7 @@ from zuul.model import (
|
||||
DequeueEvent,
|
||||
EnqueueEvent,
|
||||
HoldRequest,
|
||||
KeystorePasswordManager,
|
||||
NodesetRequest,
|
||||
PromoteEvent,
|
||||
ProviderNode,
|
||||
@@ -3947,8 +3948,12 @@ class ZuulWeb(object):
|
||||
|
||||
self.executor_api = ExecutorApi(self.zk_client, use_cache=False)
|
||||
|
||||
self.keystore_password_manager = KeystorePasswordManager(self.config)
|
||||
|
||||
self.component_info = WebComponent(
|
||||
self.zk_client, self.hostname, version=get_version_string())
|
||||
self.zk_client, self.hostname, version=get_version_string(),
|
||||
keystore_passwords=self.keystore_password_manager.password_hashes,
|
||||
)
|
||||
self.component_info.register()
|
||||
|
||||
self.event_watcher = EventWatcher(self.zk_client, None)
|
||||
@@ -3958,6 +3963,11 @@ class ZuulWeb(object):
|
||||
self.monitoring_server.start()
|
||||
|
||||
self.component_registry = COMPONENT_REGISTRY.create(self.zk_client)
|
||||
# This is mostly for unit tests, where we can have an empty,
|
||||
# initialized registry object.
|
||||
self.component_registry.waitForComponent(self.component_info)
|
||||
self.keystore_password_manager.connectToRegistry(
|
||||
self.component_registry)
|
||||
self.system = ZuulSystem(self.zk_client)
|
||||
|
||||
self.system_config_thread = None
|
||||
@@ -3968,7 +3978,8 @@ class ZuulWeb(object):
|
||||
|
||||
self.unparsed_config_cache = UnparsedConfigCache(self.zk_client)
|
||||
self.keystore = KeyStorage(
|
||||
self.zk_client, password=self._get_key_store_password())
|
||||
self.zk_client,
|
||||
self.keystore_password_manager)
|
||||
self.globals = SystemAttributes.fromConfig(self.config)
|
||||
self.ansible_manager = AnsibleManager(
|
||||
default_version=self.globals.default_ansible_version)
|
||||
@@ -4211,12 +4222,6 @@ class ZuulWeb(object):
|
||||
self.repl.stop()
|
||||
self.repl = None
|
||||
|
||||
def _get_key_store_password(self):
|
||||
try:
|
||||
return self.config["keystore"]["password"]
|
||||
except KeyError:
|
||||
raise RuntimeError("No key store password configured!")
|
||||
|
||||
def updateConfig(self):
|
||||
while self._system_config_running:
|
||||
try:
|
||||
|
||||
+31
-2
@@ -1,4 +1,5 @@
|
||||
# Copyright 2020 BMW Group
|
||||
# Copyright 2021-2026 Acme Gating, LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
@@ -151,17 +152,27 @@ class BaseComponent(ZooKeeperBase):
|
||||
class SchedulerComponent(BaseComponent):
|
||||
kind = "scheduler"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
keystore_passwords = kwargs.pop('keystore_passwords', [])
|
||||
super().__init__(*args, **kwargs)
|
||||
self.initial_state = {
|
||||
"keystore_passwords": keystore_passwords
|
||||
}
|
||||
self.content.update(self.initial_state)
|
||||
|
||||
|
||||
class ExecutorComponent(BaseComponent):
|
||||
kind = "executor"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
keystore_passwords = kwargs.pop('keystore_passwords', [])
|
||||
super().__init__(*args, **kwargs)
|
||||
self.initial_state = {
|
||||
"accepting_work": False,
|
||||
"zone": None,
|
||||
"allow_unzoned": False,
|
||||
"process_merge_jobs": False,
|
||||
"keystore_passwords": keystore_passwords,
|
||||
}
|
||||
self.content.update(self.initial_state)
|
||||
|
||||
@@ -186,6 +197,14 @@ class FingerGatewayComponent(BaseComponent):
|
||||
class WebComponent(BaseComponent):
|
||||
kind = "web"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
keystore_passwords = kwargs.pop('keystore_passwords', [])
|
||||
super().__init__(*args, **kwargs)
|
||||
self.initial_state = {
|
||||
"keystore_passwords": keystore_passwords
|
||||
}
|
||||
self.content.update(self.initial_state)
|
||||
|
||||
|
||||
class LauncherComponent(BaseComponent):
|
||||
kind = "launcher"
|
||||
@@ -245,6 +264,15 @@ class ComponentRegistry(ZooKeeperBase):
|
||||
if self.client.connected:
|
||||
self._onConnect()
|
||||
|
||||
def waitForComponent(self, wait_component):
|
||||
while True:
|
||||
for kind, components in self.all():
|
||||
if wait_component.kind != kind:
|
||||
continue
|
||||
for component in components:
|
||||
if component.hostname == wait_component.hostname:
|
||||
return
|
||||
|
||||
def _getComponentRoot(self, kind):
|
||||
return '/'.join([COMPONENTS_ROOT, kind])
|
||||
|
||||
@@ -344,7 +372,8 @@ class ComponentRegistry(ZooKeeperBase):
|
||||
|
||||
if kind is None:
|
||||
return [(kind, list(components.values()))
|
||||
for (kind, components) in self._cached_components.items()]
|
||||
for (kind, components) in
|
||||
list(self._cached_components.items())]
|
||||
|
||||
# Filter the cached components for the given kind
|
||||
return self._cached_components.get(kind, {}).values()
|
||||
@@ -400,7 +429,7 @@ class ComponentRegistry(ZooKeeperBase):
|
||||
self.model_api = version
|
||||
|
||||
def _handleCallbacks(self):
|
||||
for cb in self.callbacks:
|
||||
for cb in list(self.callbacks):
|
||||
try:
|
||||
cb()
|
||||
except Exception:
|
||||
|
||||
Reference in new issue
Block a user