886007b36e
New option --extract-hyperkube to declare the way how hyperkube will be delivered. By default this option is disabled which means hyperkube should be extracted before running promenade container for the first time. When it's enabled the appropriate env vars should be set for promenade container to be able to extract hyperkube binary from image. Change-Id: I2c45100e1e953d859d768ec80f268bd490ce3a81
254 lines
8.9 KiB
Python
254 lines
8.9 KiB
Python
from . import encryption_method, logging, renderer
|
|
from beaker.cache import CacheManager
|
|
from beaker.util import parse_cache_config_options
|
|
|
|
import io
|
|
import itertools
|
|
import os
|
|
import requests
|
|
import stat
|
|
import tarfile
|
|
import time
|
|
|
|
__all__ = ['Builder']
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
# Ignore bandit false positive:
|
|
# B108:hardcoded_tmp_directory
|
|
# This cache needs to be shared by all forks within the same container, and so
|
|
# must be at a well-known location.
|
|
TMP_CACHE = '/tmp/cache' # nosec
|
|
CACHE_OPTS = {
|
|
'cache.type': 'file',
|
|
'cache.data_dir': TMP_CACHE + '/data', # nosec
|
|
'cache.lock_dir': TMP_CACHE + '/lock', # nosec
|
|
}
|
|
|
|
CACHE = CacheManager(**parse_cache_config_options(CACHE_OPTS))
|
|
|
|
|
|
class Builder:
|
|
def __init__(self, config, *, validators=False):
|
|
self.config = config
|
|
self.validators = validators
|
|
self._file_cache = None
|
|
|
|
@property
|
|
def file_cache(self):
|
|
if not self._file_cache:
|
|
self._build_file_cache()
|
|
return self._file_cache
|
|
|
|
def _build_file_cache(self):
|
|
self._file_cache = {}
|
|
for file_spec in self._file_specs:
|
|
path = file_spec['path']
|
|
islink = False
|
|
if 'content' in file_spec:
|
|
data = file_spec['content']
|
|
elif 'docker_image' in file_spec:
|
|
data = _fetch_image_content(self.config,
|
|
file_spec['docker_image'],
|
|
file_spec['file_path'])
|
|
elif 'symlink' in file_spec:
|
|
data = file_spec['symlink']
|
|
islink = True
|
|
elif 'tar_url' in file_spec:
|
|
data = _fetch_tar_content(file_spec['tar_url'],
|
|
file_spec['tar_path'])
|
|
self._file_cache[path] = {
|
|
'path': path,
|
|
'data': data,
|
|
'mode': file_spec['mode'],
|
|
'islink': islink,
|
|
}
|
|
|
|
@property
|
|
def _file_specs(self):
|
|
return itertools.chain(
|
|
self.config.get_path('HostSystem:files', []),
|
|
self.config.get_path('Genesis:files', []))
|
|
|
|
def build_all(self, *, output_dir):
|
|
self.build_genesis(output_dir=output_dir)
|
|
for node_document in self.config.iterate(
|
|
schema='promenade/KubernetesNode/v1'):
|
|
self.build_node(node_document, output_dir=output_dir)
|
|
|
|
if self.validators:
|
|
validate_script = renderer.render_template(
|
|
self.config, template='scripts/validate-cluster.sh')
|
|
_write_script(output_dir, 'validate-cluster.sh', validate_script)
|
|
|
|
def build_genesis(self, *, output_dir):
|
|
script = self.build_genesis_script()
|
|
_write_script(output_dir, 'genesis.sh', script)
|
|
|
|
if self.validators:
|
|
validate_script = self._build_genesis_validate_script()
|
|
_write_script(output_dir, 'validate-genesis.sh', validate_script)
|
|
|
|
def build_genesis_script(self):
|
|
LOG.info('Building genesis script')
|
|
genesis_roles = ['common', 'genesis']
|
|
sub_config = self.config.extract_genesis_config()
|
|
tarball = renderer.build_tarball_from_roles(
|
|
config=sub_config,
|
|
roles=genesis_roles,
|
|
file_specs=self.file_cache.values())
|
|
|
|
(encrypted_tarball, decrypt_setup_command, decrypt_command,
|
|
decrypt_teardown_command) = _encrypt_genesis(sub_config, tarball)
|
|
|
|
return renderer.render_template(
|
|
sub_config,
|
|
template='scripts/genesis.sh',
|
|
context={
|
|
'decrypt_command': decrypt_command,
|
|
'decrypt_setup_command': decrypt_setup_command,
|
|
'decrypt_teardown_command': decrypt_teardown_command,
|
|
'encrypted_tarball': encrypted_tarball,
|
|
},
|
|
roles=genesis_roles)
|
|
|
|
def _build_genesis_validate_script(self):
|
|
sub_config = self.config.extract_genesis_config()
|
|
return renderer.render_template(
|
|
sub_config, template='scripts/validate-genesis.sh')
|
|
|
|
def build_node(self, node_document, *, output_dir):
|
|
node_name = node_document['metadata']['name']
|
|
LOG.info('Building script for node %s', node_name)
|
|
script = self.build_node_script(node_name)
|
|
|
|
_write_script(output_dir, _join_name(node_name), script)
|
|
|
|
if self.validators:
|
|
validate_script = self._build_node_validate_script(node_name)
|
|
_write_script(output_dir, 'validate-%s.sh' % node_name,
|
|
validate_script)
|
|
|
|
def build_node_script(self, node_name):
|
|
build_roles = ['common', 'join']
|
|
sub_config = self.config.extract_node_config(node_name)
|
|
file_spec_paths = [
|
|
f['path'] for f in self.config.get_path('HostSystem:files', [])
|
|
]
|
|
file_specs = [self.file_cache[p] for p in file_spec_paths]
|
|
tarball = renderer.build_tarball_from_roles(
|
|
config=sub_config, roles=build_roles, file_specs=file_specs)
|
|
|
|
(encrypted_tarball, decrypt_setup_command, decrypt_command,
|
|
decrypt_teardown_command) = _encrypt_node(sub_config, tarball)
|
|
|
|
return renderer.render_template(
|
|
sub_config,
|
|
template='scripts/join.sh',
|
|
context={
|
|
'decrypt_command': decrypt_command,
|
|
'decrypt_setup_command': decrypt_setup_command,
|
|
'decrypt_teardown_command': decrypt_teardown_command,
|
|
'encrypted_tarball': encrypted_tarball,
|
|
},
|
|
roles=build_roles)
|
|
|
|
def _build_node_validate_script(self, node_name):
|
|
sub_config = self.config.extract_node_config(node_name)
|
|
return renderer.render_template(
|
|
sub_config, template='scripts/validate-join.sh')
|
|
|
|
|
|
def _encrypt_genesis(config, data):
|
|
return _encrypt(config.get_path('EncryptionPolicy:scripts.genesis'), data)
|
|
|
|
|
|
def _encrypt_node(config, data):
|
|
return _encrypt(config.get_path('EncryptionPolicy:scripts.join'), data)
|
|
|
|
|
|
def _encrypt(cfg_dict, data):
|
|
method = encryption_method.EncryptionMethod.from_config(cfg_dict)
|
|
encrypted_data = method.encrypt(data)
|
|
decrypt_setup_command = method.get_decrypt_setup_command()
|
|
decrypt_command = method.get_decrypt_command()
|
|
decrypt_teardown_command = method.get_decrypt_teardown_command()
|
|
return (encrypted_data, decrypt_setup_command, decrypt_command,
|
|
decrypt_teardown_command)
|
|
|
|
|
|
# The following environment variables should be used
|
|
# to extract hyperkube from image:
|
|
# export DOCKER_HOST="unix://var/run/docker.sock"
|
|
# export PROMENADE_TMP="tmp_dir_on_host"
|
|
# export PROMENADE_TMP_LOCAL="tmp_dir_inside_container"
|
|
# PROMENADE_TMP is the full path to temp dir from host
|
|
# inside promenade container it should be bind to PROMENADE_TMP_LOCAL
|
|
@CACHE.cache('fetch_image', expire=72 * 3600)
|
|
def _fetch_image_content(config, image_url, file_path):
|
|
file_name = os.path.basename(file_path)
|
|
if config.extract_hyperkube:
|
|
container_info = config.get_container_info()
|
|
result_path = os.path.join(container_info['dir_local'], file_name)
|
|
client = container_info['client']
|
|
vol = {
|
|
container_info['dir']: {
|
|
'bind': container_info['dir_local'],
|
|
'mode': 'rw'
|
|
}
|
|
}
|
|
cmd = 'cp -v {} {}'.format(file_path, container_info['dir_local'])
|
|
image = client.images.pull(image_url)
|
|
output = client.containers.run(
|
|
image, command=cmd, auto_remove=True, volumes=vol)
|
|
LOG.debug(output)
|
|
else:
|
|
result_path = os.path.join(TMP_CACHE, file_name)
|
|
if not os.path.isfile(result_path):
|
|
raise Exception('ERROR: there is no hyperkube in cache')
|
|
f = open(result_path, 'rb')
|
|
return f.read()
|
|
|
|
|
|
@CACHE.cache('fetch_tarball_content', expire=72 * 3600)
|
|
def _fetch_tar_content(url, path):
|
|
content = _fetch_tar_url(url)
|
|
f = io.BytesIO(content)
|
|
tf = tarfile.open(fileobj=f, mode='r')
|
|
buf_reader = tf.extractfile(path)
|
|
return buf_reader.read()
|
|
|
|
|
|
@CACHE.cache('fetch_tarball_url', expire=72 * 3600)
|
|
def _fetch_tar_url(url):
|
|
LOG.debug('Fetching url=%s', url)
|
|
# NOTE(mark-burnett): Retry with linear backoff until we are killed, e.g.
|
|
# by a timeout.
|
|
for attempt in itertools.count():
|
|
try:
|
|
response = requests.get(url)
|
|
response.raise_for_status()
|
|
break
|
|
except requests.exceptions.RequestException:
|
|
backoff = 5 * attempt
|
|
LOG.exception('Failed to fetch %s, retrying in %d seconds', url,
|
|
backoff)
|
|
time.sleep(backoff)
|
|
|
|
LOG.debug('Finished downloading url=%s', url)
|
|
return response.content
|
|
|
|
|
|
def _join_name(node_name):
|
|
return 'join-%s.sh' % node_name
|
|
|
|
|
|
def _write_script(output_dir, name, script):
|
|
path = os.path.join(output_dir, name)
|
|
with open(path, 'w') as f:
|
|
f.write(script)
|
|
|
|
os.chmod(
|
|
path,
|
|
os.stat(path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|