zuul/zuul/lib/keystorage.py

179 lines
6.1 KiB
Python

# Copyright 2018 Red Hat, Inc.
#
# 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
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import tempfile
import logging
import os
import paramiko
RSA_KEY_SIZE = 2048
class Migration(object):
log = logging.getLogger("zuul.KeyStorage")
version = 0
parent = None
def verify(self, root):
fn = os.path.join(root, '.version')
if not os.path.exists(fn):
return False
with open(fn) as f:
data = int(f.read().strip())
if data == self.version:
return True
return False
def writeVersion(self, root):
fn = os.path.join(root, '.version')
with open(fn, 'w') as f:
f.write(str(self.version))
def upgrade(self, root):
pass
def verifyAndUpgrade(self, root):
if self.verify(root):
return
if self.parent:
self.parent.verifyAndUpgrade(root)
self.log.info("Upgrading key storage to version %s" % self.version)
self.upgrade(root)
self.writeVersion(root)
self.log.info("Finished upgrading key storage to version %s" %
self.version)
if not self.verify(root):
raise Exception("Inconsistent result after migration")
class MigrationV1(Migration):
version = 1
parent = None
"""Upgrade from the unversioned schema to version 1.
The original schema had secret keys in key_dir/connection/project.pem
This updates us to:
key_dir/
secrets/
project/
<connection>/
<project>/
<keyid>.pem
ssh/
project/
<connection>/
<project>/
<keyid>.pem
tenant/
<tenant>/
<keyid>.pem
Where keyids are integers to support future key rollover. In this
case, they will all be 0.
"""
def upgrade(self, root):
tmpdir = tempfile.mkdtemp(dir=root)
tmpdirname = os.path.basename(tmpdir)
connection_names = []
for connection_name in os.listdir(root):
if connection_name == tmpdirname:
continue
# Move existing connections out of the way (in case one of
# them was called 'secrets' or 'ssh'.
os.rename(os.path.join(root, connection_name),
os.path.join(tmpdir, connection_name))
connection_names.append(connection_name)
os.makedirs(os.path.join(root, 'secrets', 'project'), 0o700)
os.makedirs(os.path.join(root, 'ssh', 'project'), 0o700)
os.makedirs(os.path.join(root, 'ssh', 'tenant'), 0o700)
for connection_name in connection_names:
connection_root = os.path.join(tmpdir, connection_name)
for (dirpath, dirnames, filenames) in os.walk(connection_root):
subdir = os.path.relpath(dirpath, connection_root)
for fn in filenames:
key_name = os.path.join(subdir, fn)
project_name = key_name[:-len('.pem')]
key_dir = os.path.join(root, 'secrets', 'project',
connection_name, project_name)
os.makedirs(key_dir, 0o700)
old = os.path.join(tmpdir, connection_name, key_name)
new = os.path.join(key_dir, '0.pem')
self.log.debug("Moving key from %s to %s", old, new)
os.rename(old, new)
for (dirpath, dirnames, filenames) in os.walk(
connection_root, topdown=False):
os.rmdir(dirpath)
os.rmdir(tmpdir)
class KeyStorage(object):
log = logging.getLogger("zuul.KeyStorage")
current_version = MigrationV1
def __init__(self, root):
self.root = root
migration = self.current_version()
migration.verifyAndUpgrade(root)
def getProjectSecretsKeyFile(self, connection, project, version=None):
"""Return the path to the private key used for the project's secrets"""
# We don't actually support multiple versions yet
if version is None:
version = '0'
return os.path.join(self.root, 'secrets', 'project',
connection, project, version + '.pem')
def getProjectSSHKeyFile(self, connection, project, version=None):
"""Return the path to the private ssh key for the project"""
# We don't actually support multiple versions yet
if version is None:
version = '0'
return os.path.join(self.root, 'ssh', 'project',
connection, project, version + '.pem')
def getProjectSSHKeys(self, connection, project):
"""Return the private and public SSH keys for the project
A new key will be created if necessary.
:returns: A tuple containing the PEM encoded private key and
base64 encoded public key.
"""
private_key_file = self.getProjectSSHKeyFile(connection, project)
if not os.path.exists(private_key_file):
self.log.info(
"Generating SSH public key for project %s", project
)
self._createSSHKey(private_key_file)
key = paramiko.RSAKey.from_private_key_file(private_key_file)
with open(private_key_file, 'r') as f:
private_key = f.read()
public_key = key.get_base64()
return (private_key, 'ssh-rsa ' + public_key)
def _createSSHKey(self, fn):
key_dir = os.path.dirname(fn)
if not os.path.isdir(key_dir):
os.makedirs(key_dir, 0o700)
pk = paramiko.RSAKey.generate(bits=RSA_KEY_SIZE)
pk.write_private_key_file(fn)