a1336e2527
Move the resources which inherit from 'Resource' to engine/resources, such as stack_resource, stack_user and signal_responder, and move non-resources to engine/, such as iso_8601.py module. Change-Id: Ic26c34cc8d34ca03ce674dc8c6ac32c10885b75f Implements: blueprint reorganize-resources-code-structure
397 lines
14 KiB
Python
397 lines
14 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.
|
|
"""Utilities for Resources that use the OpenStack Nova API."""
|
|
|
|
import email
|
|
from email.mime import multipart
|
|
from email.mime import text
|
|
import os
|
|
import pkgutil
|
|
import string
|
|
import warnings
|
|
|
|
from novaclient import exceptions as nova_exceptions
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_serialization import jsonutils
|
|
import six
|
|
from six.moves.urllib import parse as urlparse
|
|
|
|
from heat.common import exception
|
|
from heat.common.i18n import _
|
|
from heat.common.i18n import _LW
|
|
from heat.engine import scheduler
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
deferred_server_statuses = ['BUILD',
|
|
'HARD_REBOOT',
|
|
'PASSWORD',
|
|
'REBOOT',
|
|
'RESCUE',
|
|
'RESIZE',
|
|
'REVERT_RESIZE',
|
|
'SHUTOFF',
|
|
'SUSPENDED',
|
|
'VERIFY_RESIZE']
|
|
|
|
|
|
def refresh_server(server):
|
|
'''
|
|
Refresh server's attributes and log warnings for non-critical API errors.
|
|
'''
|
|
warnings.warn('nova_utils.refresh_server is deprecated. '
|
|
'Use self.client_plugin("nova").refresh_server')
|
|
try:
|
|
server.get()
|
|
except nova_exceptions.OverLimit as exc:
|
|
LOG.warn(_LW("Server %(name)s (%(id)s) received an OverLimit "
|
|
"response during server.get(): %(exception)s"),
|
|
{'name': server.name,
|
|
'id': server.id,
|
|
'exception': exc})
|
|
except nova_exceptions.ClientException as exc:
|
|
http_status = (getattr(exc, 'http_status', None) or
|
|
getattr(exc, 'code', None))
|
|
if http_status in (500, 503):
|
|
LOG.warn(_LW('Server "%(name)s" (%(id)s) received the following '
|
|
'exception during server.get(): %(exception)s'),
|
|
{'name': server.name,
|
|
'id': server.id,
|
|
'exception': exc})
|
|
else:
|
|
raise
|
|
|
|
|
|
def get_ip(server, net_type, ip_version):
|
|
"""Return the server's IP of the given type and version."""
|
|
warnings.warn('nova_utils.get_ip is deprecated. '
|
|
'Use self.client_plugin("nova").get_ip')
|
|
if net_type in server.addresses:
|
|
for ip in server.addresses[net_type]:
|
|
if ip['version'] == ip_version:
|
|
return ip['addr']
|
|
|
|
|
|
def get_flavor_id(nova_client, flavor):
|
|
warnings.warn('nova_utils.get_flavor_id is deprecated. '
|
|
'Use self.client_plugin("nova").get_flavor_id')
|
|
'''
|
|
Get the id for the specified flavor name.
|
|
If the specified value is flavor id, just return it.
|
|
|
|
:param nova_client: the nova client to use
|
|
:param flavor: the name of the flavor to find
|
|
:returns: the id of :flavor:
|
|
:raises: exception.FlavorMissing
|
|
'''
|
|
flavor_id = None
|
|
flavor_list = nova_client.flavors.list()
|
|
for o in flavor_list:
|
|
if o.name == flavor:
|
|
flavor_id = o.id
|
|
break
|
|
if o.id == flavor:
|
|
flavor_id = o.id
|
|
break
|
|
if flavor_id is None:
|
|
raise exception.FlavorMissing(flavor_id=flavor)
|
|
return flavor_id
|
|
|
|
|
|
def get_keypair(nova_client, key_name):
|
|
warnings.warn('nova_utils.get_keypair is deprecated. '
|
|
'Use self.client_plugin("nova").get_keypair')
|
|
'''
|
|
Get the public key specified by :key_name:
|
|
|
|
:param nova_client: the nova client to use
|
|
:param key_name: the name of the key to look for
|
|
:returns: the keypair (name, public_key) for :key_name:
|
|
:raises: exception.UserKeyPairMissing
|
|
'''
|
|
try:
|
|
return nova_client.keypairs.get(key_name)
|
|
except nova_exceptions.NotFound:
|
|
raise exception.UserKeyPairMissing(key_name=key_name)
|
|
|
|
|
|
def build_userdata(resource, userdata=None, instance_user=None,
|
|
user_data_format='HEAT_CFNTOOLS'):
|
|
warnings.warn('nova_utils.build_userdata is deprecated. '
|
|
'Use self.client_plugin("nova").build_userdata')
|
|
'''
|
|
Build multipart data blob for CloudInit which includes user-supplied
|
|
Metadata, user data, and the required Heat in-instance configuration.
|
|
|
|
:param resource: the resource implementation
|
|
:type resource: heat.engine.Resource
|
|
:param userdata: user data string
|
|
:type userdata: str or None
|
|
:param instance_user: the user to create on the server
|
|
:type instance_user: string
|
|
:param user_data_format: Format of user data to return
|
|
:type user_data_format: string
|
|
:returns: multipart mime as a string
|
|
'''
|
|
|
|
if user_data_format == 'RAW':
|
|
return userdata
|
|
|
|
is_cfntools = user_data_format == 'HEAT_CFNTOOLS'
|
|
is_software_config = user_data_format == 'SOFTWARE_CONFIG'
|
|
|
|
def make_subpart(content, filename, subtype=None):
|
|
if subtype is None:
|
|
subtype = os.path.splitext(filename)[0]
|
|
msg = text.MIMEText(content, _subtype=subtype)
|
|
msg.add_header('Content-Disposition', 'attachment',
|
|
filename=filename)
|
|
return msg
|
|
|
|
def read_cloudinit_file(fn):
|
|
return pkgutil.get_data('heat', 'cloudinit/%s' % fn)
|
|
|
|
if instance_user:
|
|
config_custom_user = 'user: %s' % instance_user
|
|
# FIXME(shadower): compatibility workaround for cloud-init 0.6.3. We
|
|
# can drop this once we stop supporting 0.6.3 (which ships with Ubuntu
|
|
# 12.04 LTS).
|
|
#
|
|
# See bug https://bugs.launchpad.net/heat/+bug/1257410
|
|
boothook_custom_user = r"""useradd -m %s
|
|
echo -e '%s\tALL=(ALL)\tNOPASSWD: ALL' >> /etc/sudoers
|
|
""" % (instance_user, instance_user)
|
|
else:
|
|
config_custom_user = ''
|
|
boothook_custom_user = ''
|
|
|
|
cloudinit_config = string.Template(
|
|
read_cloudinit_file('config')).safe_substitute(
|
|
add_custom_user=config_custom_user)
|
|
cloudinit_boothook = string.Template(
|
|
read_cloudinit_file('boothook.sh')).safe_substitute(
|
|
add_custom_user=boothook_custom_user)
|
|
|
|
attachments = [(cloudinit_config, 'cloud-config'),
|
|
(cloudinit_boothook, 'boothook.sh', 'cloud-boothook'),
|
|
(read_cloudinit_file('part_handler.py'),
|
|
'part-handler.py')]
|
|
|
|
if is_cfntools:
|
|
attachments.append((userdata, 'cfn-userdata', 'x-cfninitdata'))
|
|
elif is_software_config:
|
|
# attempt to parse userdata as a multipart message, and if it
|
|
# is, add each part as an attachment
|
|
userdata_parts = None
|
|
try:
|
|
userdata_parts = email.message_from_string(userdata)
|
|
except Exception:
|
|
pass
|
|
if userdata_parts and userdata_parts.is_multipart():
|
|
for part in userdata_parts.get_payload():
|
|
attachments.append((part.get_payload(),
|
|
part.get_filename(),
|
|
part.get_content_subtype()))
|
|
else:
|
|
attachments.append((userdata, 'userdata', 'x-shellscript'))
|
|
|
|
if is_cfntools:
|
|
attachments.append((read_cloudinit_file('loguserdata.py'),
|
|
'loguserdata.py', 'x-shellscript'))
|
|
|
|
metadata = resource.metadata_get()
|
|
if metadata:
|
|
attachments.append((jsonutils.dumps(metadata),
|
|
'cfn-init-data', 'x-cfninitdata'))
|
|
|
|
attachments.append((cfg.CONF.heat_watch_server_url,
|
|
'cfn-watch-server', 'x-cfninitdata'))
|
|
|
|
if is_cfntools:
|
|
attachments.append((cfg.CONF.heat_metadata_server_url,
|
|
'cfn-metadata-server', 'x-cfninitdata'))
|
|
|
|
# Create a boto config which the cfntools on the host use to know
|
|
# where the cfn and cw API's are to be accessed
|
|
cfn_url = urlparse.urlparse(cfg.CONF.heat_metadata_server_url)
|
|
cw_url = urlparse.urlparse(cfg.CONF.heat_watch_server_url)
|
|
is_secure = cfg.CONF.instance_connection_is_secure
|
|
vcerts = cfg.CONF.instance_connection_https_validate_certificates
|
|
boto_cfg = "\n".join(["[Boto]",
|
|
"debug = 0",
|
|
"is_secure = %s" % is_secure,
|
|
"https_validate_certificates = %s" % vcerts,
|
|
"cfn_region_name = heat",
|
|
"cfn_region_endpoint = %s" %
|
|
cfn_url.hostname,
|
|
"cloudwatch_region_name = heat",
|
|
"cloudwatch_region_endpoint = %s" %
|
|
cw_url.hostname])
|
|
attachments.append((boto_cfg,
|
|
'cfn-boto-cfg', 'x-cfninitdata'))
|
|
|
|
subparts = [make_subpart(*args) for args in attachments]
|
|
mime_blob = multipart.MIMEMultipart(_subparts=subparts)
|
|
|
|
return mime_blob.as_string()
|
|
|
|
|
|
def delete_server(server):
|
|
'''
|
|
A co-routine that deletes the server and waits for it to
|
|
disappear from Nova.
|
|
'''
|
|
warnings.warn('nova_utils.delete_server is deprecated. '
|
|
'Use self.client_plugin("nova").delete_server')
|
|
if not server:
|
|
return
|
|
try:
|
|
server.delete()
|
|
except nova_exceptions.NotFound:
|
|
return
|
|
|
|
while True:
|
|
yield
|
|
|
|
try:
|
|
refresh_server(server)
|
|
except nova_exceptions.NotFound:
|
|
break
|
|
else:
|
|
# Some clouds append extra (STATUS) strings to the status
|
|
short_server_status = server.status.split('(')[0]
|
|
if short_server_status == "DELETED":
|
|
break
|
|
if short_server_status == "ERROR":
|
|
fault = getattr(server, 'fault', {})
|
|
message = fault.get('message', 'Unknown')
|
|
code = fault.get('code')
|
|
errmsg = (_("Server %(name)s delete failed: (%(code)s) "
|
|
"%(message)s"))
|
|
raise exception.Error(errmsg % {"name": server.name,
|
|
"code": code,
|
|
"message": message})
|
|
|
|
|
|
@scheduler.wrappertask
|
|
def resize(server, flavor, flavor_id):
|
|
"""Resize the server and then call check_resize task to verify."""
|
|
warnings.warn('nova_utils.resize is deprecated. '
|
|
'Use self.client_plugin("nova").resize')
|
|
server.resize(flavor_id)
|
|
yield check_resize(server, flavor, flavor_id)
|
|
|
|
|
|
def rename(server, name):
|
|
"""Update the name for a server."""
|
|
warnings.warn('nova_utils.rename is deprecated. '
|
|
'Use self.client_plugin("nova").rename')
|
|
server.update(name)
|
|
|
|
|
|
def check_resize(server, flavor, flavor_id):
|
|
"""
|
|
Verify that a resizing server is properly resized.
|
|
If that's the case, confirm the resize, if not raise an error.
|
|
"""
|
|
warnings.warn('nova_utils.check_resize is deprecated. '
|
|
'Use self.client_plugin("nova").check_resize')
|
|
refresh_server(server)
|
|
while server.status == 'RESIZE':
|
|
yield
|
|
refresh_server(server)
|
|
if server.status == 'VERIFY_RESIZE':
|
|
server.confirm_resize()
|
|
else:
|
|
raise exception.Error(
|
|
_("Resizing to '%(flavor)s' failed, status '%(status)s'") %
|
|
dict(flavor=flavor, status=server.status))
|
|
|
|
|
|
@scheduler.wrappertask
|
|
def rebuild(server, image_id, preserve_ephemeral=False):
|
|
"""Rebuild the server and call check_rebuild to verify."""
|
|
warnings.warn('nova_utils.rebuild is deprecated. '
|
|
'Use self.client_plugin("nova").rebuild')
|
|
server.rebuild(image_id, preserve_ephemeral=preserve_ephemeral)
|
|
yield check_rebuild(server, image_id)
|
|
|
|
|
|
def check_rebuild(server, image_id):
|
|
"""
|
|
Verify that a rebuilding server is rebuilt.
|
|
Raise error if it ends up in an ERROR state.
|
|
"""
|
|
warnings.warn('nova_utils.check_rebuild is deprecated. '
|
|
'Use self.client_plugin("nova").check_rebuild')
|
|
refresh_server(server)
|
|
while server.status == 'REBUILD':
|
|
yield
|
|
refresh_server(server)
|
|
if server.status == 'ERROR':
|
|
raise exception.Error(
|
|
_("Rebuilding server failed, status '%s'") % server.status)
|
|
|
|
|
|
def meta_serialize(metadata):
|
|
"""
|
|
Serialize non-string metadata values before sending them to
|
|
Nova.
|
|
"""
|
|
warnings.warn('nova_utils.meta_serialize is deprecated. '
|
|
'Use self.client_plugin("nova").meta_serialize')
|
|
return dict((key, (value if isinstance(value,
|
|
six.string_types)
|
|
else jsonutils.dumps(value))
|
|
) for (key, value) in metadata.items())
|
|
|
|
|
|
def meta_update(client, server, metadata):
|
|
"""Delete/Add the metadata in nova as needed."""
|
|
warnings.warn('nova_utils.meta_update is deprecated. '
|
|
'Use self.client_plugin("nova").meta_update')
|
|
metadata = meta_serialize(metadata)
|
|
current_md = server.metadata
|
|
to_del = [key for key in current_md.keys() if key not in metadata]
|
|
if len(to_del) > 0:
|
|
client.servers.delete_meta(server, to_del)
|
|
|
|
client.servers.set_meta(server, metadata)
|
|
|
|
|
|
def server_to_ipaddress(client, server):
|
|
'''
|
|
Return the server's IP address, fetching it from Nova.
|
|
'''
|
|
warnings.warn('nova_utils.server_to_ipaddress is deprecated. '
|
|
'Use self.client_plugin("nova").server_to_ipaddress')
|
|
try:
|
|
server = client.servers.get(server)
|
|
except nova_exceptions.NotFound as ex:
|
|
LOG.warn(_LW('Instance (%(server)s) not found: %(ex)s'),
|
|
{'server': server, 'ex': ex})
|
|
else:
|
|
for n in server.networks:
|
|
if len(server.networks[n]) > 0:
|
|
return server.networks[n][0]
|
|
|
|
|
|
def absolute_limits(nova_client):
|
|
"""Return the absolute limits as a dictionary."""
|
|
warnings.warn('nova_utils.absolute_limits is deprecated. '
|
|
'Use self.client_plugin("nova").absolute_limits')
|
|
limits = nova_client.limits.get()
|
|
return dict([(limit.name, limit.value) for limit in list(limits.absolute)])
|