174 lines
5.5 KiB
Python
174 lines
5.5 KiB
Python
|
# 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.
|
||
|
|
||
|
"""Helpers for building configdrive compatible with the Bare Metal service."""
|
||
|
|
||
|
import base64
|
||
|
import contextlib
|
||
|
import gzip
|
||
|
import json
|
||
|
import os
|
||
|
import shutil
|
||
|
import subprocess
|
||
|
import tempfile
|
||
|
|
||
|
|
||
|
@contextlib.contextmanager
|
||
|
def populate_directory(
|
||
|
metadata,
|
||
|
user_data=None,
|
||
|
versions=None,
|
||
|
network_data=None,
|
||
|
vendor_data=None,
|
||
|
):
|
||
|
"""Populate a directory with configdrive files.
|
||
|
|
||
|
:param dict metadata: Metadata.
|
||
|
:param bytes user_data: Vendor-specific user data.
|
||
|
:param versions: List of metadata versions to support.
|
||
|
:param dict network_data: Networking configuration.
|
||
|
:param dict vendor_data: Extra supplied vendor data.
|
||
|
:return: a context manager yielding a directory with files
|
||
|
"""
|
||
|
d = tempfile.mkdtemp()
|
||
|
versions = versions or ('2012-08-10', 'latest')
|
||
|
try:
|
||
|
for version in versions:
|
||
|
subdir = os.path.join(d, 'openstack', version)
|
||
|
if not os.path.exists(subdir):
|
||
|
os.makedirs(subdir)
|
||
|
|
||
|
with open(os.path.join(subdir, 'meta_data.json'), 'w') as fp:
|
||
|
json.dump(metadata, fp)
|
||
|
|
||
|
if network_data:
|
||
|
with open(
|
||
|
os.path.join(subdir, 'network_data.json'), 'w'
|
||
|
) as fp:
|
||
|
json.dump(network_data, fp)
|
||
|
|
||
|
if vendor_data:
|
||
|
with open(
|
||
|
os.path.join(subdir, 'vendor_data2.json'), 'w'
|
||
|
) as fp:
|
||
|
json.dump(vendor_data, fp)
|
||
|
|
||
|
if user_data:
|
||
|
# Strictly speaking, user data is binary, but in many cases
|
||
|
# it's actually a text (cloud-init, ignition, etc).
|
||
|
flag = 't' if isinstance(user_data, str) else 'b'
|
||
|
with open(
|
||
|
os.path.join(subdir, 'user_data'), 'w%s' % flag
|
||
|
) as fp:
|
||
|
fp.write(user_data)
|
||
|
|
||
|
yield d
|
||
|
finally:
|
||
|
shutil.rmtree(d)
|
||
|
|
||
|
|
||
|
def build(
|
||
|
metadata,
|
||
|
user_data=None,
|
||
|
versions=None,
|
||
|
network_data=None,
|
||
|
vendor_data=None,
|
||
|
):
|
||
|
"""Make a configdrive compatible with the Bare Metal service.
|
||
|
|
||
|
Requires the genisoimage utility to be available.
|
||
|
|
||
|
:param dict metadata: Metadata.
|
||
|
:param user_data: Vendor-specific user data.
|
||
|
:param versions: List of metadata versions to support.
|
||
|
:param dict network_data: Networking configuration.
|
||
|
:param dict vendor_data: Extra supplied vendor data.
|
||
|
:return: configdrive contents as a base64-encoded string.
|
||
|
"""
|
||
|
with populate_directory(
|
||
|
metadata, user_data, versions, network_data, vendor_data
|
||
|
) as path:
|
||
|
return pack(path)
|
||
|
|
||
|
|
||
|
def pack(path):
|
||
|
"""Pack a directory with files into a Bare Metal service configdrive.
|
||
|
|
||
|
Creates an ISO image with the files and label "config-2".
|
||
|
|
||
|
:param str path: Path to directory with files
|
||
|
:return: configdrive contents as a base64-encoded string.
|
||
|
"""
|
||
|
with tempfile.NamedTemporaryFile() as tmpfile:
|
||
|
# NOTE(toabctl): Luckily, genisoimage, mkisofs and xorrisofs understand
|
||
|
# the same parameters which are currently used.
|
||
|
cmds = ['genisoimage', 'mkisofs', 'xorrisofs']
|
||
|
for c in cmds:
|
||
|
try:
|
||
|
p = subprocess.Popen(
|
||
|
[
|
||
|
c,
|
||
|
'-o',
|
||
|
tmpfile.name,
|
||
|
'-ldots',
|
||
|
'-allow-lowercase',
|
||
|
'-allow-multidot',
|
||
|
'-l',
|
||
|
'-publisher',
|
||
|
'metalsmith',
|
||
|
'-quiet',
|
||
|
'-J',
|
||
|
'-r',
|
||
|
'-V',
|
||
|
'config-2',
|
||
|
path,
|
||
|
],
|
||
|
stdout=subprocess.PIPE,
|
||
|
stderr=subprocess.PIPE,
|
||
|
)
|
||
|
except OSError as e:
|
||
|
error = e
|
||
|
else:
|
||
|
error = None
|
||
|
break
|
||
|
|
||
|
if error:
|
||
|
raise RuntimeError(
|
||
|
'Error generating the configdrive. Make sure the '
|
||
|
'"genisoimage", "mkisofs" or "xorrisofs" tool is installed. '
|
||
|
'Error: %s' % error
|
||
|
)
|
||
|
|
||
|
stdout, stderr = p.communicate()
|
||
|
if p.returncode != 0:
|
||
|
raise RuntimeError(
|
||
|
'Error generating the configdrive.'
|
||
|
'Stdout: "%(stdout)s". Stderr: "%(stderr)s"'
|
||
|
% {'stdout': stdout, 'stderr': stderr}
|
||
|
)
|
||
|
|
||
|
tmpfile.seek(0)
|
||
|
|
||
|
with tempfile.NamedTemporaryFile() as tmpzipfile:
|
||
|
with gzip.GzipFile(fileobj=tmpzipfile, mode='wb') as gz_file:
|
||
|
shutil.copyfileobj(tmpfile, gz_file)
|
||
|
|
||
|
tmpzipfile.seek(0)
|
||
|
cd = base64.b64encode(tmpzipfile.read())
|
||
|
|
||
|
# NOTE(dtantsur): Ironic expects configdrive to be a string, but base64
|
||
|
# returns bytes on Python 3.
|
||
|
if not isinstance(cd, str):
|
||
|
cd = cd.decode('utf-8')
|
||
|
|
||
|
return cd
|