Add configdrive support - metadata
This adds some simple metadata information, following up patches will add more capabilities Co-Authored-By: Shaohe Feng <shaohe.feng@intel.com> Change-Id: Idaa3ab813b5355ce44e97fa069cd7b664f8b4761
This commit is contained in:
parent
b3763ddd95
commit
8497848ac2
@ -376,4 +376,14 @@ class ConsoleTypeUnavailable(Invalid):
|
|||||||
msg_fmt = _("Unavailable console type %(console_type)s.")
|
msg_fmt = _("Unavailable console type %(console_type)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigDriveMountFailed(MoganException):
|
||||||
|
msg_fmt = _("Could not mount vfat config drive. %(operation)s failed. "
|
||||||
|
"Error: %(error)s")
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigDriveUnknownFormat(MoganException):
|
||||||
|
msg_fmt = _("Unknown config drive format %(format)s. Select one of "
|
||||||
|
"iso9660 or vfat.")
|
||||||
|
|
||||||
|
|
||||||
ObjectActionError = obj_exc.ObjectActionError
|
ObjectActionError = obj_exc.ObjectActionError
|
||||||
|
@ -15,18 +15,24 @@
|
|||||||
|
|
||||||
"""Utilities and helper functions."""
|
"""Utilities and helper functions."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from oslo_concurrency import lockutils
|
from oslo_concurrency import lockutils
|
||||||
|
from oslo_concurrency import processutils
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import encodeutils
|
from oslo_utils import encodeutils
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from mogan.common import exception
|
from mogan.common import exception
|
||||||
from mogan.common import states
|
from mogan.common import states
|
||||||
|
from mogan.conf import CONF
|
||||||
from mogan import objects
|
from mogan import objects
|
||||||
|
|
||||||
|
|
||||||
@ -209,3 +215,71 @@ def add_instance_fault_from_exc(context, instance, fault, exc_info=None,
|
|||||||
code = fault_obj.code
|
code = fault_obj.code
|
||||||
fault_obj.detail = _get_fault_detail(exc_info, code)
|
fault_obj.detail = _get_fault_detail(exc_info, code)
|
||||||
fault_obj.create()
|
fault_obj.create()
|
||||||
|
|
||||||
|
|
||||||
|
def execute(*cmd, **kwargs):
|
||||||
|
"""Convenience wrapper around oslo's execute() method.
|
||||||
|
|
||||||
|
:param cmd: Passed to processutils.execute.
|
||||||
|
:param use_standard_locale: True | False. Defaults to False. If set to
|
||||||
|
True, execute command with standard locale
|
||||||
|
added to environment variables.
|
||||||
|
:returns: (stdout, stderr) from process execution
|
||||||
|
:raises: UnknownArgumentError
|
||||||
|
:raises: ProcessExecutionError
|
||||||
|
"""
|
||||||
|
|
||||||
|
use_standard_locale = kwargs.pop('use_standard_locale', False)
|
||||||
|
if use_standard_locale:
|
||||||
|
env = kwargs.pop('env_variables', os.environ.copy())
|
||||||
|
env['LC_ALL'] = 'C'
|
||||||
|
kwargs['env_variables'] = env
|
||||||
|
result = processutils.execute(*cmd, **kwargs)
|
||||||
|
LOG.debug('Execution completed, command line is "%s"',
|
||||||
|
' '.join(map(str, cmd)))
|
||||||
|
LOG.debug('Command stdout is: "%s"', result[0])
|
||||||
|
LOG.debug('Command stderr is: "%s"', result[1])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def tempdir(**kwargs):
|
||||||
|
tempfile.tempdir = CONF.tempdir
|
||||||
|
tmpdir = tempfile.mkdtemp(**kwargs)
|
||||||
|
try:
|
||||||
|
yield tmpdir
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
shutil.rmtree(tmpdir)
|
||||||
|
except OSError as e:
|
||||||
|
LOG.error('Could not remove tmpdir: %s', e)
|
||||||
|
|
||||||
|
|
||||||
|
def mkfs(fs, path, label=None, run_as_root=False):
|
||||||
|
"""Format a file or block device
|
||||||
|
|
||||||
|
:param fs: Filesystem type (examples include 'swap', 'ext3', 'ext4'
|
||||||
|
'btrfs', etc.)
|
||||||
|
:param path: Path to file or block device to format
|
||||||
|
:param label: Volume label to use
|
||||||
|
"""
|
||||||
|
if fs == 'swap':
|
||||||
|
args = ['mkswap']
|
||||||
|
else:
|
||||||
|
args = ['mkfs', '-t', fs]
|
||||||
|
# add -F to force no interactive execute on non-block device.
|
||||||
|
if fs in ('ext3', 'ext4', 'ntfs'):
|
||||||
|
args.extend(['-F'])
|
||||||
|
if label:
|
||||||
|
if fs in ('msdos', 'vfat'):
|
||||||
|
label_opt = '-n'
|
||||||
|
else:
|
||||||
|
label_opt = '-L'
|
||||||
|
args.extend([label_opt, label])
|
||||||
|
args.append(path)
|
||||||
|
execute(*args, run_as_root=run_as_root)
|
||||||
|
|
||||||
|
|
||||||
|
def trycmd(*args, **kwargs):
|
||||||
|
"""Convenience wrapper around oslo's trycmd() method."""
|
||||||
|
return processutils.trycmd(*args, **kwargs)
|
||||||
|
@ -17,6 +17,7 @@ from oslo_config import cfg
|
|||||||
|
|
||||||
from mogan.conf import api
|
from mogan.conf import api
|
||||||
from mogan.conf import cache
|
from mogan.conf import cache
|
||||||
|
from mogan.conf import configdrive
|
||||||
from mogan.conf import database
|
from mogan.conf import database
|
||||||
from mogan.conf import default
|
from mogan.conf import default
|
||||||
from mogan.conf import engine
|
from mogan.conf import engine
|
||||||
@ -31,6 +32,7 @@ from mogan.conf import shellinabox
|
|||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
api.register_opts(CONF)
|
api.register_opts(CONF)
|
||||||
|
configdrive.register_opts(CONF)
|
||||||
database.register_opts(CONF)
|
database.register_opts(CONF)
|
||||||
default.register_opts(CONF)
|
default.register_opts(CONF)
|
||||||
engine.register_opts(CONF)
|
engine.register_opts(CONF)
|
||||||
|
34
mogan/conf/configdrive.py
Normal file
34
mogan/conf/configdrive.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Copyright 2017 Huawei Technologies Co.,LTD.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from mogan.common.i18n import _
|
||||||
|
|
||||||
|
opts = [
|
||||||
|
cfg.StrOpt('config_drive_format',
|
||||||
|
default='iso9660',
|
||||||
|
choices=('iso9660', 'vfat'),
|
||||||
|
help=_('Configuration drive format that will contain '
|
||||||
|
'metadata attached to the instance when it boots.')),
|
||||||
|
cfg.StrOpt('mkisofs_cmd',
|
||||||
|
default='genisoimage',
|
||||||
|
help=_('Name or path of the tool used for ISO image '
|
||||||
|
'creation')),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def register_opts(conf):
|
||||||
|
conf.register_opts(opts, group='configdrive')
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
@ -68,9 +69,18 @@ service_opts = [
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
utils_opts = [
|
||||||
|
cfg.StrOpt('tempdir',
|
||||||
|
default=tempfile.gettempdir(),
|
||||||
|
sample_default='/tmp',
|
||||||
|
help=_('Temporary working directory, default is Python temp '
|
||||||
|
'dir.')),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def register_opts(conf):
|
def register_opts(conf):
|
||||||
conf.register_opts(api_opts)
|
conf.register_opts(api_opts)
|
||||||
conf.register_opts(exc_log_opts)
|
conf.register_opts(exc_log_opts)
|
||||||
conf.register_opts(service_opts)
|
conf.register_opts(service_opts)
|
||||||
conf.register_opts(path_opts)
|
conf.register_opts(path_opts)
|
||||||
|
conf.register_opts(utils_opts)
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
import mogan.conf.api
|
import mogan.conf.api
|
||||||
|
import mogan.conf.configdrive
|
||||||
import mogan.conf.database
|
import mogan.conf.database
|
||||||
import mogan.conf.default
|
import mogan.conf.default
|
||||||
import mogan.conf.engine
|
import mogan.conf.engine
|
||||||
@ -28,11 +29,13 @@ _default_opt_lists = [
|
|||||||
mogan.conf.default.exc_log_opts,
|
mogan.conf.default.exc_log_opts,
|
||||||
mogan.conf.default.path_opts,
|
mogan.conf.default.path_opts,
|
||||||
mogan.conf.default.service_opts,
|
mogan.conf.default.service_opts,
|
||||||
|
mogan.conf.default.utils_opts,
|
||||||
]
|
]
|
||||||
|
|
||||||
_opts = [
|
_opts = [
|
||||||
('DEFAULT', itertools.chain(*_default_opt_lists)),
|
('DEFAULT', itertools.chain(*_default_opt_lists)),
|
||||||
('api', mogan.conf.api.opts),
|
('api', mogan.conf.api.opts),
|
||||||
|
('configdrive', mogan.conf.configdrive.opts),
|
||||||
('database', mogan.conf.database.opts),
|
('database', mogan.conf.database.opts),
|
||||||
('engine', mogan.conf.engine.opts),
|
('engine', mogan.conf.engine.opts),
|
||||||
('glance', mogan.conf.glance.opts),
|
('glance', mogan.conf.glance.opts),
|
||||||
|
152
mogan/engine/baremetal/configdrive.py
Normal file
152
mogan/engine/baremetal/configdrive.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
# Copyright 2012 Michael Still and Canonical Inc
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Config Drive v2 helper."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from oslo_utils import fileutils
|
||||||
|
from oslo_utils import units
|
||||||
|
import six
|
||||||
|
|
||||||
|
from mogan.common import exception
|
||||||
|
from mogan.common import utils
|
||||||
|
from mogan.conf import CONF
|
||||||
|
from mogan import version
|
||||||
|
|
||||||
|
|
||||||
|
# Config drives are 64mb, if we can't size to the exact size of the data
|
||||||
|
CONFIGDRIVESIZE_BYTES = 64 * units.Mi
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigDriveBuilder(object):
|
||||||
|
"""Build config drives, optionally as a context manager."""
|
||||||
|
|
||||||
|
def __init__(self, instance_md=None):
|
||||||
|
self.imagefile = None
|
||||||
|
self.mdfiles = []
|
||||||
|
|
||||||
|
if instance_md is not None:
|
||||||
|
self.add_instance_metadata(instance_md)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exctype, excval, exctb):
|
||||||
|
if exctype is not None:
|
||||||
|
# NOTE(mikal): this means we're being cleaned up because an
|
||||||
|
# exception was thrown. All bets are off now, and we should not
|
||||||
|
# swallow the exception
|
||||||
|
return False
|
||||||
|
self.cleanup()
|
||||||
|
|
||||||
|
def _add_file(self, basedir, path, data):
|
||||||
|
filepath = os.path.join(basedir, path)
|
||||||
|
dirname = os.path.dirname(filepath)
|
||||||
|
fileutils.ensure_tree(dirname)
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
# the given data can be either text or bytes. we can only write
|
||||||
|
# bytes into files.
|
||||||
|
if isinstance(data, six.text_type):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
def add_instance_metadata(self, instance_md):
|
||||||
|
for (path, data) in instance_md.metadata_for_config_drive():
|
||||||
|
self.mdfiles.append((path, data))
|
||||||
|
|
||||||
|
def _write_md_files(self, basedir):
|
||||||
|
for data in self.mdfiles:
|
||||||
|
self._add_file(basedir, data[0], data[1])
|
||||||
|
|
||||||
|
def _make_iso9660(self, path, tmpdir):
|
||||||
|
publisher = "%(product)s %(version)s" % {
|
||||||
|
'product': version.product_string(),
|
||||||
|
'version': version.version_string_with_package()}
|
||||||
|
|
||||||
|
utils.execute(CONF.configdrive.mkisofs_cmd,
|
||||||
|
'-o', path,
|
||||||
|
'-ldots',
|
||||||
|
'-allow-lowercase',
|
||||||
|
'-allow-multidot',
|
||||||
|
'-l',
|
||||||
|
'-publisher',
|
||||||
|
publisher,
|
||||||
|
'-quiet',
|
||||||
|
'-J',
|
||||||
|
'-r',
|
||||||
|
'-V', 'config-2',
|
||||||
|
tmpdir,
|
||||||
|
attempts=1,
|
||||||
|
run_as_root=False)
|
||||||
|
|
||||||
|
def _make_vfat(self, path, tmpdir):
|
||||||
|
# NOTE(mikal): This is a little horrible, but I couldn't find an
|
||||||
|
# equivalent to genisoimage for vfat filesystems.
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.truncate(CONFIGDRIVESIZE_BYTES)
|
||||||
|
|
||||||
|
utils.mkfs('vfat', path, label='config-2')
|
||||||
|
|
||||||
|
with utils.tempdir() as mountdir:
|
||||||
|
mounted = False
|
||||||
|
try:
|
||||||
|
_, err = utils.trycmd(
|
||||||
|
'mount', '-o', 'loop,uid=%d,gid=%d' % (os.getuid(),
|
||||||
|
os.getgid()),
|
||||||
|
path,
|
||||||
|
mountdir,
|
||||||
|
run_as_root=True)
|
||||||
|
if err:
|
||||||
|
raise exception.ConfigDriveMountFailed(operation='mount',
|
||||||
|
error=err)
|
||||||
|
mounted = True
|
||||||
|
|
||||||
|
# NOTE(mikal): I can't just use shutils.copytree here,
|
||||||
|
# because the destination directory already
|
||||||
|
# exists. This is annoying.
|
||||||
|
for ent in os.listdir(tmpdir):
|
||||||
|
shutil.copytree(os.path.join(tmpdir, ent),
|
||||||
|
os.path.join(mountdir, ent))
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if mounted:
|
||||||
|
utils.execute('umount', mountdir, run_as_root=True)
|
||||||
|
|
||||||
|
def make_drive(self, path):
|
||||||
|
"""Make the config drive.
|
||||||
|
|
||||||
|
:param path: the path to place the config drive image at
|
||||||
|
|
||||||
|
:raises ProcessExecuteError if a helper process has failed.
|
||||||
|
"""
|
||||||
|
with utils.tempdir() as tmpdir:
|
||||||
|
self._write_md_files(tmpdir)
|
||||||
|
|
||||||
|
if CONF.configdrive.config_drive_format == 'iso9660':
|
||||||
|
self._make_iso9660(path, tmpdir)
|
||||||
|
elif CONF.configdrive.config_drive_format == 'vfat':
|
||||||
|
self._make_vfat(path, tmpdir)
|
||||||
|
else:
|
||||||
|
raise exception.ConfigDriveUnknownFormat(
|
||||||
|
format=CONF.configdrive.config_drive_format)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
if self.imagefile:
|
||||||
|
fileutils.delete_if_exists(self.imagefile)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<ConfigDriveBuilder: " + str(self.mdfiles) + ">"
|
@ -13,6 +13,11 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import gzip
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from ironicclient import exc as ironic_exc
|
from ironicclient import exc as ironic_exc
|
||||||
from ironicclient import exceptions as client_e
|
from ironicclient import exceptions as client_e
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
@ -25,8 +30,10 @@ from mogan.common.i18n import _
|
|||||||
from mogan.common import ironic
|
from mogan.common import ironic
|
||||||
from mogan.common import states
|
from mogan.common import states
|
||||||
from mogan.conf import CONF
|
from mogan.conf import CONF
|
||||||
|
from mogan.engine.baremetal import configdrive
|
||||||
from mogan.engine.baremetal import driver as base_driver
|
from mogan.engine.baremetal import driver as base_driver
|
||||||
from mogan.engine.baremetal.ironic import ironic_states
|
from mogan.engine.baremetal.ironic import ironic_states
|
||||||
|
from mogan import metadata as instance_metadata
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -283,6 +290,35 @@ class IronicDriver(base_driver.BaseEngineDriver):
|
|||||||
except client_e.BadRequest:
|
except client_e.BadRequest:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _generate_configdrive(self, context, instance, node, extra_md=None):
|
||||||
|
"""Generate a config drive.
|
||||||
|
|
||||||
|
:param instance: The instance object.
|
||||||
|
:param node: The node object.
|
||||||
|
:param extra_md: Optional, extra metadata to be added to the
|
||||||
|
configdrive.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not extra_md:
|
||||||
|
extra_md = {}
|
||||||
|
|
||||||
|
i_meta = instance_metadata.InstanceMetadata(instance,
|
||||||
|
extra_md=extra_md)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as uncompressed:
|
||||||
|
with configdrive.ConfigDriveBuilder(instance_md=i_meta) as cdb:
|
||||||
|
cdb.make_drive(uncompressed.name)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as compressed:
|
||||||
|
# compress config drive
|
||||||
|
with gzip.GzipFile(fileobj=compressed, mode='wb') as gzipped:
|
||||||
|
uncompressed.seek(0)
|
||||||
|
shutil.copyfileobj(uncompressed, gzipped)
|
||||||
|
|
||||||
|
# base64 encode config drive
|
||||||
|
compressed.seek(0)
|
||||||
|
return base64.b64encode(compressed.read())
|
||||||
|
|
||||||
def spawn(self, context, instance):
|
def spawn(self, context, instance):
|
||||||
"""Deploy an instance.
|
"""Deploy an instance.
|
||||||
|
|
||||||
@ -316,10 +352,28 @@ class IronicDriver(base_driver.BaseEngineDriver):
|
|||||||
'deploy': validate_chk.deploy,
|
'deploy': validate_chk.deploy,
|
||||||
'power': validate_chk.power})
|
'power': validate_chk.power})
|
||||||
|
|
||||||
|
# Config drive
|
||||||
|
configdrive_value = None
|
||||||
|
extra_md = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
configdrive_value = self._generate_configdrive(
|
||||||
|
context, instance, node, extra_md=extra_md)
|
||||||
|
except Exception as e:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
msg = ("Failed to build configdrive: %s" %
|
||||||
|
six.text_type(e))
|
||||||
|
LOG.error(msg, instance=instance)
|
||||||
|
|
||||||
|
LOG.info("Config drive for instance %(instance)s on "
|
||||||
|
"baremetal node %(node)s created.",
|
||||||
|
{'instance': instance['uuid'], 'node': node_uuid})
|
||||||
|
|
||||||
# trigger the node deploy
|
# trigger the node deploy
|
||||||
try:
|
try:
|
||||||
self.ironicclient.call("node.set_provision_state", node_uuid,
|
self.ironicclient.call("node.set_provision_state", node_uuid,
|
||||||
ironic_states.ACTIVE)
|
ironic_states.ACTIVE,
|
||||||
|
configdrive=configdrive_value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
msg = ("Failed to request Ironic to provision instance "
|
msg = ("Failed to request Ironic to provision instance "
|
||||||
|
166
mogan/metadata.py
Normal file
166
mogan/metadata.py
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Instance Metadata information."""
|
||||||
|
|
||||||
|
import posixpath
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
|
|
||||||
|
OCATA = '2017-02-22'
|
||||||
|
|
||||||
|
OPENSTACK_VERSIONS = [
|
||||||
|
OCATA,
|
||||||
|
]
|
||||||
|
|
||||||
|
VERSION = "version"
|
||||||
|
MD_JSON_NAME = "meta_data.json"
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMetadataVersion(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMetadataPath(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InstanceMetadata(object):
|
||||||
|
"""Instance metadata."""
|
||||||
|
|
||||||
|
def __init__(self, instance, extra_md=None):
|
||||||
|
"""Creation of this object should basically cover all time consuming
|
||||||
|
collection. Methods after that should not cause time delays due to
|
||||||
|
network operations or lengthy cpu operations.
|
||||||
|
|
||||||
|
The user should then get a single instance and make multiple method
|
||||||
|
calls on it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.instance = instance
|
||||||
|
self.extra_md = extra_md
|
||||||
|
self.availability_zone = instance.availability_zone
|
||||||
|
# TODO(zhenguo): Add hostname to instance object
|
||||||
|
self.hostname = instance.name
|
||||||
|
self.uuid = instance.uuid
|
||||||
|
self.files = []
|
||||||
|
|
||||||
|
self.route_configuration = None
|
||||||
|
|
||||||
|
def _route_configuration(self):
|
||||||
|
if self.route_configuration:
|
||||||
|
return self.route_configuration
|
||||||
|
|
||||||
|
path_handlers = {MD_JSON_NAME: self._metadata_as_json}
|
||||||
|
|
||||||
|
self.route_configuration = RouteConfiguration(path_handlers)
|
||||||
|
return self.route_configuration
|
||||||
|
|
||||||
|
def get_openstack_item(self, path_tokens):
|
||||||
|
return self._route_configuration().handle_path(path_tokens)
|
||||||
|
|
||||||
|
def _metadata_as_json(self, version, path):
|
||||||
|
metadata = {'uuid': self.uuid}
|
||||||
|
if self.extra_md:
|
||||||
|
metadata.update(self.extra_md)
|
||||||
|
|
||||||
|
metadata['hostname'] = self.hostname
|
||||||
|
metadata['name'] = self.instance.name
|
||||||
|
metadata['availability_zone'] = self.availability_zone
|
||||||
|
|
||||||
|
return jsonutils.dump_as_bytes(metadata)
|
||||||
|
|
||||||
|
def lookup(self, path):
|
||||||
|
if path == "" or path[0] != "/":
|
||||||
|
path = posixpath.normpath("/" + path)
|
||||||
|
else:
|
||||||
|
path = posixpath.normpath(path)
|
||||||
|
|
||||||
|
# fix up requests, prepending /openstack to anything that does
|
||||||
|
# not match
|
||||||
|
path_tokens = path.split('/')[1:]
|
||||||
|
if path_tokens[0] not in ("openstack"):
|
||||||
|
if path_tokens[0] == "":
|
||||||
|
# request for /
|
||||||
|
path_tokens = ["openstack"]
|
||||||
|
else:
|
||||||
|
path_tokens = ["openstack"] + path_tokens
|
||||||
|
path = "/" + "/".join(path_tokens)
|
||||||
|
|
||||||
|
# all values of 'path' input starts with '/' and have no trailing /
|
||||||
|
|
||||||
|
# specifically handle the top level request
|
||||||
|
if len(path_tokens) == 1:
|
||||||
|
if path_tokens[0] == "openstack":
|
||||||
|
# NOTE(vish): don't show versions that are in the future
|
||||||
|
today = timeutils.utcnow().strftime("%Y-%m-%d")
|
||||||
|
versions = [v for v in OPENSTACK_VERSIONS if v <= today]
|
||||||
|
if OPENSTACK_VERSIONS != versions:
|
||||||
|
LOG.debug("future versions %s hidden in version list",
|
||||||
|
[v for v in OPENSTACK_VERSIONS
|
||||||
|
if v not in versions], instance=self.instance)
|
||||||
|
versions += ["latest"]
|
||||||
|
return versions
|
||||||
|
|
||||||
|
try:
|
||||||
|
if path_tokens[0] == "openstack":
|
||||||
|
data = self.get_openstack_item(path_tokens[1:])
|
||||||
|
except (InvalidMetadataVersion, KeyError):
|
||||||
|
raise InvalidMetadataPath(path)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def metadata_for_config_drive(self):
|
||||||
|
"""Yields (path, value) tuples for metadata elements."""
|
||||||
|
ALL_OPENSTACK_VERSIONS = OPENSTACK_VERSIONS + ["latest"]
|
||||||
|
for version in ALL_OPENSTACK_VERSIONS:
|
||||||
|
path = 'openstack/%s/%s' % (version, MD_JSON_NAME)
|
||||||
|
yield (path, self.lookup(path))
|
||||||
|
|
||||||
|
|
||||||
|
class RouteConfiguration(object):
|
||||||
|
"""Routes metadata paths to request handlers."""
|
||||||
|
|
||||||
|
def __init__(self, path_handler):
|
||||||
|
self.path_handlers = path_handler
|
||||||
|
|
||||||
|
def _version(self, version):
|
||||||
|
if version == "latest":
|
||||||
|
version = OPENSTACK_VERSIONS[-1]
|
||||||
|
|
||||||
|
if version not in OPENSTACK_VERSIONS:
|
||||||
|
raise InvalidMetadataVersion(version)
|
||||||
|
|
||||||
|
return version
|
||||||
|
|
||||||
|
def handle_path(self, path_tokens):
|
||||||
|
version = self._version(path_tokens[0])
|
||||||
|
if len(path_tokens) == 1:
|
||||||
|
path = VERSION
|
||||||
|
else:
|
||||||
|
path = '/'.join(path_tokens[1:])
|
||||||
|
|
||||||
|
path_handler = self.path_handlers[path]
|
||||||
|
|
||||||
|
if path_handler is None:
|
||||||
|
raise KeyError(path)
|
||||||
|
|
||||||
|
return path_handler(version, path)
|
@ -15,4 +15,15 @@
|
|||||||
|
|
||||||
import pbr.version
|
import pbr.version
|
||||||
|
|
||||||
|
MOGAN_PRODUCT = "OpenStack Mogan"
|
||||||
|
|
||||||
version_info = pbr.version.VersionInfo('mogan')
|
version_info = pbr.version.VersionInfo('mogan')
|
||||||
|
version_string = version_info.version_string
|
||||||
|
|
||||||
|
|
||||||
|
def product_string():
|
||||||
|
return MOGAN_PRODUCT
|
||||||
|
|
||||||
|
|
||||||
|
def version_string_with_package():
|
||||||
|
return version_info.version_string()
|
||||||
|
Loading…
Reference in New Issue
Block a user