senlin/senlin/profiles/os/nova/server.py

2036 lines
78 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.
import base64
import copy
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import encodeutils
from senlin.common import constraints
from senlin.common import consts
from senlin.common import context
from senlin.common import exception as exc
from senlin.common.i18n import _
from senlin.common import schema
from senlin.objects import node as node_obj
from senlin.profiles import base
LOG = logging.getLogger(__name__)
class ServerProfile(base.Profile):
"""Profile for an OpenStack Nova server."""
VERSIONS = {
'1.0': [
{'status': consts.SUPPORTED, 'since': '2016.04'}
]
}
KEYS = (
CONTEXT, ADMIN_PASS, AUTO_DISK_CONFIG, AVAILABILITY_ZONE,
BLOCK_DEVICE_MAPPING_V2,
CONFIG_DRIVE, FLAVOR, IMAGE, KEY_NAME, METADATA,
NAME, NETWORKS, PERSONALITY, SECURITY_GROUPS,
USER_DATA, SCHEDULER_HINTS,
) = (
'context', 'admin_pass', 'auto_disk_config', 'availability_zone',
'block_device_mapping_v2',
'config_drive', 'flavor', 'image', 'key_name', 'metadata',
'name', 'networks', 'personality', 'security_groups',
'user_data', 'scheduler_hints',
)
BDM2_KEYS = (
BDM2_UUID, BDM2_SOURCE_TYPE, BDM2_DESTINATION_TYPE,
BDM2_DISK_BUS, BDM2_DEVICE_NAME, BDM2_VOLUME_SIZE,
BDM2_GUEST_FORMAT, BDM2_BOOT_INDEX, BDM2_DEVICE_TYPE,
BDM2_DELETE_ON_TERMINATION,
) = (
'uuid', 'source_type', 'destination_type', 'disk_bus',
'device_name', 'volume_size', 'guest_format', 'boot_index',
'device_type', 'delete_on_termination',
)
NETWORK_KEYS = (
PORT, VNIC_TYPE, FIXED_IP, NETWORK, PORT_SECURITY_GROUPS,
FLOATING_NETWORK, FLOATING_IP,
) = (
'port', 'vnic_type', 'fixed_ip', 'network', 'security_groups',
'floating_network', 'floating_ip',
)
PERSONALITY_KEYS = (
PATH, CONTENTS,
) = (
'path', 'contents',
)
SCHEDULER_HINTS_KEYS = (
GROUP,
) = (
'group',
)
properties_schema = {
CONTEXT: schema.Map(
_('Customized security context for operating servers.'),
),
ADMIN_PASS: schema.String(
_('Password for the administrator account.'),
),
AUTO_DISK_CONFIG: schema.Boolean(
_('Whether the disk partition is done automatically.'),
default=True,
),
AVAILABILITY_ZONE: schema.String(
_('Name of availability zone for running the server.'),
),
BLOCK_DEVICE_MAPPING_V2: schema.List(
_('A list specifying the properties of block devices to be used '
'for this server.'),
schema=schema.Map(
_('A map specifying the properties of a block device to be '
'used by the server.'),
schema={
BDM2_UUID: schema.String(
_('ID of the source image, snapshot or volume'),
),
BDM2_SOURCE_TYPE: schema.String(
_("Volume source type, must be one of 'image', "
"'snapshot', 'volume' or 'blank'"),
required=True,
),
BDM2_DESTINATION_TYPE: schema.String(
_("Volume destination type, must be 'volume' or "
"'local'"),
required=True,
),
BDM2_DISK_BUS: schema.String(
_('Bus of the device.'),
),
BDM2_DEVICE_NAME: schema.String(
_('Name of the device(e.g. vda, xda, ....).'),
),
BDM2_VOLUME_SIZE: schema.Integer(
_('Size of the block device in MB(for swap) and '
'in GB(for other formats)'),
required=True,
),
BDM2_GUEST_FORMAT: schema.String(
_('Specifies the disk file system format(e.g. swap, '
'ephemeral, ...).'),
),
BDM2_BOOT_INDEX: schema.Integer(
_('Define the boot order of the device'),
),
BDM2_DEVICE_TYPE: schema.String(
_('Type of the device(e.g. disk, cdrom, ...).'),
),
BDM2_DELETE_ON_TERMINATION: schema.Boolean(
_('Whether to delete the volume when the server '
'stops.'),
),
}
),
),
CONFIG_DRIVE: schema.Boolean(
_('Whether config drive should be enabled for the server.'),
),
FLAVOR: schema.String(
_('ID of flavor used for the server.'),
required=True,
updatable=True,
),
IMAGE: schema.String(
# IMAGE is not required, because there could be BDM or BDMv2
# support and the corresponding settings effective
_('ID of image to be used for the new server.'),
updatable=True,
),
KEY_NAME: schema.String(
_('Name of Nova keypair to be injected to server.'),
),
METADATA: schema.Map(
_('A collection of key/value pairs to be associated with the '
'server created. Both key and value must be <=255 chars.'),
updatable=True,
),
NAME: schema.String(
_('Name of the server. When omitted, the node name will be used.'),
updatable=True,
),
NETWORKS: schema.List(
_('List of networks for the server.'),
schema=schema.Map(
_('A map specifying the properties of a network for uses.'),
schema={
NETWORK: schema.String(
_('Name or ID of network to create a port on.'),
),
PORT: schema.String(
_('Port ID to be used by the network.'),
),
VNIC_TYPE: schema.String(
_('Define vnic_type to be used by port'),
),
FIXED_IP: schema.String(
_('Fixed IP to be used by the network.'),
),
PORT_SECURITY_GROUPS: schema.List(
_('A list of security groups to be attached to '
'this port.'),
schema=schema.String(
_('Name of a security group'),
required=True,
),
),
FLOATING_NETWORK: schema.String(
_('The network on which to create a floating IP'),
),
FLOATING_IP: schema.String(
_('The floating IP address to be associated with '
'this port.'),
),
},
),
updatable=True,
),
PERSONALITY: schema.List(
_('List of files to be injected into the server, where each.'),
schema=schema.Map(
_('A map specifying the path & contents for an injected '
'file.'),
schema={
PATH: schema.String(
_('In-instance path for the file to be injected.'),
required=True,
),
CONTENTS: schema.String(
_('Contents of the file to be injected.'),
required=True,
),
},
),
),
SCHEDULER_HINTS: schema.Map(
_('A collection of key/value pairs to be associated with the '
'Scheduler hints. Both key and value must be <=255 chars.'),
),
SECURITY_GROUPS: schema.List(
_('List of security groups.'),
schema=schema.String(
_('Name of a security group'),
required=True,
),
),
USER_DATA: schema.String(
_('User data to be exposed by the metadata server.'),
),
}
OP_NAMES = (
OP_REBOOT, OP_REBUILD, OP_CHANGE_PASSWORD, OP_PAUSE, OP_UNPAUSE,
OP_SUSPEND, OP_RESUME, OP_LOCK, OP_UNLOCK, OP_START, OP_STOP,
OP_RESCUE, OP_UNRESCUE, OP_EVACUATE, OP_MIGRATE,
) = (
'reboot', 'rebuild', 'change_password', 'pause', 'unpause',
'suspend', 'resume', 'lock', 'unlock', 'start', 'stop',
'rescue', 'unrescue', 'evacuate', 'migrate',
)
ADMIN_PASSWORD = 'admin_pass'
RESCUE_IMAGE = 'image_ref'
EVACUATE_OPTIONS = (
EVACUATE_HOST, EVACUATE_FORCE
) = (
'host', 'force'
)
OPERATIONS = {
OP_REBOOT: schema.Operation(
_("Reboot the nova server."),
schema={
consts.REBOOT_TYPE: schema.StringParam(
_("Type of reboot which can be 'SOFT' or 'HARD'."),
default=consts.REBOOT_SOFT,
constraints=[
constraints.AllowedValues(consts.REBOOT_TYPES),
]
)
}
),
OP_REBUILD: schema.Operation(
_("Rebuild the server using current image and admin password."),
),
OP_CHANGE_PASSWORD: schema.Operation(
_("Change the administrator password."),
schema={
ADMIN_PASSWORD: schema.StringParam(
_("New password for the administrator.")
)
}
),
OP_PAUSE: schema.Operation(
_("Pause the server from running."),
),
OP_UNPAUSE: schema.Operation(
_("Unpause the server to running state."),
),
OP_SUSPEND: schema.Operation(
_("Suspend the running of the server."),
),
OP_RESUME: schema.Operation(
_("Resume the running of the server."),
),
OP_LOCK: schema.Operation(
_("Lock the server."),
),
OP_UNLOCK: schema.Operation(
_("Unlock the server."),
),
OP_START: schema.Operation(
_("Start the server."),
),
OP_STOP: schema.Operation(
_("Stop the server."),
),
OP_RESCUE: schema.Operation(
_("Rescue the server."),
schema={
RESCUE_IMAGE: schema.StringParam(
_("A string referencing the image to use."),
),
}
),
OP_UNRESCUE: schema.Operation(
_("Unrescue the server."),
),
OP_EVACUATE: schema.Operation(
_("Evacuate the server to a different host."),
schema={
EVACUATE_HOST: schema.StringParam(
_("The target host to evacuate the server."),
),
EVACUATE_FORCE: schema.StringParam(
_("Whether the evacuation should be a forced one.")
)
}
)
}
def __init__(self, type_name, name, **kwargs):
super(ServerProfile, self).__init__(type_name, name, **kwargs)
self.server_id = None
self.stop_timeout = cfg.CONF.default_nova_timeout
def _validate_az(self, obj, az_name, reason=None):
try:
res = self.compute(obj).validate_azs([az_name])
except exc.InternalError as ex:
if reason == 'create':
raise exc.EResourceCreation(type='server',
message=str(ex))
else:
raise
if not res:
msg = _("The specified %(key)s '%(value)s' could not be found"
) % {'key': self.AVAILABILITY_ZONE, 'value': az_name}
if reason == 'create':
raise exc.EResourceCreation(type='server', message=msg)
else:
raise exc.InvalidSpec(message=msg)
return az_name
def _validate_flavor(self, obj, name_or_id, reason=None):
flavor = None
msg = ''
try:
flavor = self.compute(obj).flavor_find(name_or_id, False)
except exc.InternalError as ex:
msg = str(ex)
if reason is None: # reason is 'validate'
if ex.code == 404:
msg = _("The specified %(k)s '%(v)s' could not be found."
) % {'k': self.FLAVOR, 'v': name_or_id}
raise exc.InvalidSpec(message=msg)
else:
raise
if flavor is not None:
if not flavor.is_disabled:
return flavor
msg = _("The specified %(k)s '%(v)s' is disabled"
) % {'k': self.FLAVOR, 'v': name_or_id}
if reason == 'create':
raise exc.EResourceCreation(type='server', message=msg)
elif reason == 'update':
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=msg)
else:
raise exc.InvalidSpec(message=msg)
def _validate_image(self, obj, name_or_id, reason=None):
try:
return self.glance(obj).image_find(name_or_id, False)
except exc.InternalError as ex:
if reason == 'create':
raise exc.EResourceCreation(type='server',
message=str(ex))
elif reason == 'update':
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
elif ex.code == 404:
msg = _("The specified %(k)s '%(v)s' could not be found."
) % {'k': self.IMAGE, 'v': name_or_id}
raise exc.InvalidSpec(message=msg)
else:
raise
def _validate_keypair(self, obj, name_or_id, reason=None):
try:
return self.compute(obj).keypair_find(name_or_id, False)
except exc.InternalError as ex:
if reason == 'create':
raise exc.EResourceCreation(type='server',
message=str(ex))
elif reason == 'update':
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
elif ex.code == 404:
msg = _("The specified %(k)s '%(v)s' could not be found."
) % {'k': self.KEY_NAME, 'v': name_or_id}
raise exc.InvalidSpec(message=msg)
else:
raise
def _validate_volume(self, obj, name_or_id, reason=None):
try:
volume = self.block_storage(obj).volume_get(name_or_id)
if volume.status == 'available':
return volume
msg = _("The volume %(k)s should be in 'available' status "
"but is in '%(v)s' status."
) % {'k': name_or_id, 'v': volume.status}
raise exc.InvalidSpec(message=msg)
except exc.InternalError as ex:
if reason == 'create':
raise exc.EResourceCreation(type='server',
message=str(ex))
elif ex.code == 404:
msg = _("The specified volume '%(k)s' could not be found."
) % {'k': name_or_id}
raise exc.InvalidSpec(message=msg)
else:
raise
def do_validate(self, obj):
"""Validate if the spec has provided valid info for server creation.
:param obj: The node object.
"""
# validate availability_zone
az_name = self.properties[self.AVAILABILITY_ZONE]
if az_name is not None:
self._validate_az(obj, az_name)
# validate flavor
flavor = self.properties[self.FLAVOR]
self._validate_flavor(obj, flavor)
# validate image
image = self.properties[self.IMAGE]
if image is not None:
self._validate_image(obj, image)
# validate key_name
keypair = self.properties[self.KEY_NAME]
if keypair is not None:
self._validate_keypair(obj, keypair)
# validate networks
networks = self.properties[self.NETWORKS]
for net in networks:
self._validate_network(obj, net)
return True
def _resolve_bdm(self, obj, bdm, reason=None):
for bd in bdm:
for key in self.BDM2_KEYS:
if bd[key] is None:
del bd[key]
if 'uuid' in bd and 'source_type' in bd:
if bd['source_type'] == 'image':
self._validate_image(obj, bd['uuid'], reason)
elif bd['source_type'] == 'volume':
self._validate_volume(obj, bd['uuid'], reason)
return bdm
def _check_security_groups(self, nc, net_spec, result):
"""Check security groups.
:param nc: network driver connection.
:param net_spec: the specification to check.
:param result: the result that is used as return value.
:returns: None if succeeded or an error message if things go wrong.
"""
sgs = net_spec.get(self.PORT_SECURITY_GROUPS)
if not sgs:
return
res = []
try:
for sg in sgs:
try:
# try to find sg scoped by project first
sg_obj = nc.security_group_find(
sg, project_id=self.project)
except exc.InternalError:
# if it fails to find sg, try without project scope
sg_obj = nc.security_group_find(sg)
res.append(sg_obj.id)
except exc.InternalError as ex:
return str(ex)
result[self.PORT_SECURITY_GROUPS] = res
return
def _check_network(self, nc, net, result):
"""Check the specified network.
:param nc: network driver connection.
:param net: the name or ID of network to check.
:param result: the result that is used as return value.
:returns: None if succeeded or an error message if things go wrong.
"""
if net is None:
return
try:
net_obj = nc.network_get(net)
if net_obj is None:
return _("The specified network %s could not be found.") % net
result[self.NETWORK] = net_obj.id
except exc.InternalError as ex:
return str(ex)
def _check_port(self, nc, port, result):
"""Check the specified port.
:param nc: network driver connection.
:param port: the name or ID of port to check.
:param result: the result that is used as return value.
:returns: None if succeeded or an error message if things go wrong.
"""
if port is None:
return
try:
port_obj = nc.port_find(port)
if port_obj.status != 'DOWN':
return _("The status of the port %(p)s must be DOWN"
) % {'p': port}
result[self.PORT] = port_obj.id
return
except exc.InternalError as ex:
return str(ex)
def _check_floating_ip(self, nc, net_spec, result):
"""Check floating IP and network, if specified.
:param nc: network driver connection.
:param net_spec: the specification to check.
:param result: the result that is used as return value.
:returns: None if succeeded or an error message if things go wrong.
"""
net = net_spec.get(self.FLOATING_NETWORK)
if net:
try:
net_obj = nc.network_get(net)
if net_obj is None:
return _("The floating network %s could not be found."
) % net
result[self.FLOATING_NETWORK] = net_obj.id
except exc.InternalError as ex:
return str(ex)
flt_ip = net_spec.get(self.FLOATING_IP)
if not flt_ip:
return
try:
# Find floating ip with this address
fip = nc.floatingip_find(flt_ip)
if fip:
if fip.status == 'ACTIVE':
return _('the floating IP %s has been used.') % flt_ip
result['floating_ip_id'] = fip.id
# Create a floating IP with address if floating ip unspecified
if not net:
return _('Must specify a network to create floating IP')
result[self.FLOATING_IP] = flt_ip
return
except exc.InternalError as ex:
return str(ex)
def _validate_network(self, obj, net_spec, reason=None):
def _verify(error):
if error is None:
return
if reason == 'create':
raise exc.EResourceCreation(type='server', message=error)
elif reason == 'update':
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=error)
else:
raise exc.InvalidSpec(message=error)
nc = self.network(obj)
result = {}
# check network
net = net_spec.get(self.NETWORK)
error = self._check_network(nc, net, result)
_verify(error)
# check port
port = net_spec.get(self.PORT)
error = self._check_port(nc, port, result)
_verify(error)
if port is None and net is None:
_verify(_("One of '%(p)s' and '%(n)s' must be provided"
) % {'p': self.PORT, 'n': self.NETWORK})
fixed_ip = net_spec.get(self.FIXED_IP)
if fixed_ip:
if port is not None:
_verify(_("The '%(p)s' property and the '%(fip)s' property "
"cannot be specified at the same time"
) % {'p': self.PORT, 'fip': self.FIXED_IP})
result[self.FIXED_IP] = fixed_ip
# Validate vnic_type input
vnic_type = net_spec.get(self.VNIC_TYPE, None)
if vnic_type is not None:
if vnic_type not in ['normal', 'direct', 'macvtap']:
_verify(_("vnic_type: '%(v)s' is not supported."
"(supported types are: normal, direct, macvtap)"
) % {'v': vnic_type})
result[self.VNIC_TYPE] = vnic_type
# Check security_groups
error = self._check_security_groups(nc, net_spec, result)
_verify(error)
# Check floating IP
error = self._check_floating_ip(nc, net_spec, result)
_verify(error)
return result
def _get_port(self, obj, net_spec):
"""Fetch or create a port.
:param obj: The node object.
:param net_spec: The parameters to create a port.
:returns: Created port object and error message.
"""
port_id = net_spec.get(self.PORT, None)
if port_id:
try:
port = self.network(obj).port_find(port_id)
return port, None
except exc.InternalError as ex:
return None, ex
port_attr = {
'network_id': net_spec.get(self.NETWORK),
}
fixed_ip = net_spec.get(self.FIXED_IP, None)
if fixed_ip:
port_attr['fixed_ips'] = [fixed_ip]
security_groups = net_spec.get(self.PORT_SECURITY_GROUPS, [])
if security_groups:
port_attr['security_groups'] = security_groups
vnic_type = net_spec.get(self.VNIC_TYPE, None)
if vnic_type:
port_attr['binding_vnic_type'] = vnic_type
try:
port = self.network(obj).port_create(**port_attr)
LOG.debug('Network port_attr : %s', port)
return port, None
except exc.InternalError as ex:
return None, ex
def _delete_ports(self, obj, ports):
"""Delete ports.
:param obj: The node object
:param ports: A list of internal ports.
:returns: None for succeed or error for failure.
"""
pp = copy.deepcopy(ports)
for port in pp:
# remove port created by senlin
if port.get('remove', False):
try:
# remove floating IP created by senlin
if port.get('floating', None) and port[
'floating'].get('remove', False):
self.network(obj).floatingip_delete(
port['floating']['id'])
self.network(obj).port_delete(port['id'])
except exc.InternalError as ex:
return ex
ports.remove(port)
node_data = obj.data
node_data['internal_ports'] = ports
node_obj.Node.update(self.context, obj.id, {'data': node_data})
def _get_floating_ip(self, obj, fip_spec, port_id):
"""Find or Create a floating IP.
:param obj: The node object.
:param fip_spec: The parameters to create a floating ip
:param port_id: The port ID to associate with
:returns: A floating IP object and error message.
"""
floating_ip_id = fip_spec.get('floating_ip_id', None)
if floating_ip_id:
try:
fip = self.network(obj).floatingip_find(floating_ip_id)
if fip.port_id is None:
attr = {'port_id': port_id}
fip = self.network(obj).floatingip_update(fip, **attr)
return fip, None
except exc.InternalError as ex:
return None, ex
net_id = fip_spec.get(self.FLOATING_NETWORK)
fip_addr = fip_spec.get(self.FLOATING_IP)
attr = {
'port_id': port_id,
'floating_network_id': net_id,
}
if fip_addr:
attr.update({'floating_ip_address': fip_addr})
try:
fip = self.network(obj).floatingip_create(**attr)
return fip, None
except exc.InternalError as ex:
return None, ex
def _create_ports_from_properties(self, obj, networks, action_type):
"""Create or find ports based on networks property.
:param obj: The node object.
:param networks: The networks property used for node.
:param action_type: Either 'create' or 'update'.
:returns: A list of created port's attributes.
"""
internal_ports = obj.data.get('internal_ports', [])
if not networks:
return []
for net_spec in networks:
net = self._validate_network(obj, net_spec, action_type)
# Create port
port, ex = self._get_port(obj, net)
# Delete created ports before raise error
if ex:
d_ex = self._delete_ports(obj, internal_ports)
if d_ex:
raise d_ex
else:
raise ex
port_attrs = {
'id': port.id,
'network_id': port.network_id,
'security_group_ids': port.security_group_ids,
'fixed_ips': port.fixed_ips
}
if self.PORT not in net:
port_attrs.update({'remove': True})
# Create floating ip
if 'floating_ip_id' in net or self.FLOATING_NETWORK in net:
fip, ex = self._get_floating_ip(obj, net, port_attrs['id'])
if ex:
d_ex = self._delete_ports(obj, internal_ports)
if d_ex:
raise d_ex
else:
raise ex
port_attrs['floating'] = {
'id': fip.id,
'floating_ip_address': fip.floating_ip_address,
'floating_network_id': fip.floating_network_id,
}
if self.FLOATING_NETWORK in net:
port_attrs['floating'].update({'remove': True})
internal_ports.append(port_attrs)
if internal_ports:
try:
node_data = obj.data
node_data.update(internal_ports=internal_ports)
node_obj.Node.update(self.context, obj.id, {'data': node_data})
except exc.ResourceNotFound:
self._rollback_ports(obj, internal_ports)
raise
return internal_ports
def _build_metadata(self, obj, usermeta):
"""Build custom metadata for server.
:param obj: The node object to operate on.
:return: A dictionary containing the new metadata.
"""
metadata = usermeta or {}
metadata['cluster_node_id'] = obj.id
if obj.cluster_id:
metadata['cluster_id'] = obj.cluster_id
metadata['cluster_node_index'] = str(obj.index)
return metadata
def _update_zone_info(self, obj, server):
"""Update the actual zone placement data.
:param obj: The node object associated with this server.
:param server: The server object returned from creation.
"""
if server.availability_zone:
placement = obj.data.get('placement', None)
if not placement:
obj.data['placement'] = {'zone': server.availability_zone}
else:
obj.data['placement'].setdefault('zone',
server.availability_zone)
# It is safe to use admin context here
ctx = context.get_admin_context()
node_obj.Node.update(ctx, obj.id, {'data': obj.data})
def do_create(self, obj):
"""Create a server for the node object.
:param obj: The node object for which a server will be created.
"""
kwargs = self._generate_kwargs()
admin_pass = self.properties[self.ADMIN_PASS]
if admin_pass:
kwargs.pop(self.ADMIN_PASS)
kwargs['adminPass'] = admin_pass
auto_disk_config = self.properties[self.AUTO_DISK_CONFIG]
kwargs.pop(self.AUTO_DISK_CONFIG)
kwargs['OS-DCF:diskConfig'] = 'AUTO' if auto_disk_config else 'MANUAL'
image_ident = self.properties[self.IMAGE]
if image_ident is not None:
image = self._validate_image(obj, image_ident, 'create')
kwargs.pop(self.IMAGE)
kwargs['imageRef'] = image.id
flavor_ident = self.properties[self.FLAVOR]
flavor = self._validate_flavor(obj, flavor_ident, 'create')
kwargs.pop(self.FLAVOR)
kwargs['flavorRef'] = flavor.id
keypair_name = self.properties[self.KEY_NAME]
if keypair_name:
keypair = self._validate_keypair(obj, keypair_name, 'create')
kwargs['key_name'] = keypair.name
kwargs['name'] = self.properties[self.NAME] or obj.name
metadata = self._build_metadata(obj, self.properties[self.METADATA])
kwargs['metadata'] = metadata
block_device_mapping_v2 = self.properties[self.BLOCK_DEVICE_MAPPING_V2]
if block_device_mapping_v2 is not None:
kwargs['block_device_mapping_v2'] = self._resolve_bdm(
obj, block_device_mapping_v2, 'create')
user_data = self.properties[self.USER_DATA]
if user_data is not None:
ud = encodeutils.safe_encode(user_data)
kwargs['user_data'] = encodeutils.safe_decode(base64.b64encode(ud))
networks = self.properties[self.NETWORKS]
ports = None
if networks is not None:
ports = self._create_ports_from_properties(
obj, networks, 'create')
kwargs['networks'] = [
{'port': port['id']} for port in ports]
secgroups = self.properties[self.SECURITY_GROUPS]
if secgroups:
kwargs['security_groups'] = [{'name': sg} for sg in secgroups]
if 'placement' in obj.data:
if 'zone' in obj.data['placement']:
kwargs['availability_zone'] = obj.data['placement']['zone']
if 'servergroup' in obj.data['placement']:
group_id = obj.data['placement']['servergroup']
hints = self.properties.get(self.SCHEDULER_HINTS) or {}
hints.update({'group': group_id})
kwargs['scheduler_hints'] = hints
server = None
resource_id = None
try:
server = self.compute(obj).server_create(**kwargs)
self.compute(obj).wait_for_server(
server.id, timeout=cfg.CONF.default_nova_timeout)
server = self.compute(obj).server_get(server.id)
# Update zone placement info if available
self._update_zone_info(obj, server)
return server.id
except exc.ResourceNotFound:
self._rollback_ports(obj, ports)
self._rollback_instance(obj, server)
raise
except exc.InternalError as ex:
if server and server.id:
resource_id = server.id
LOG.debug('Deleting server %s that is ERROR state after'
' create.', server.id)
try:
obj.physical_id = server.id
self.do_delete(obj, internal_ports=ports)
except Exception:
LOG.error('Failed to delete server %s', server.id)
pass
elif ports:
self._delete_ports(obj, ports)
raise exc.EResourceCreation(type='server',
message=str(ex),
resource_id=resource_id)
def _generate_kwargs(self):
"""Generate the base kwargs for a server.
:return:
"""
kwargs = {}
for key in self.KEYS:
# context is treated as connection parameters
if key == self.CONTEXT:
continue
if self.properties[key] is not None:
kwargs[key] = self.properties[key]
return kwargs
def _rollback_ports(self, obj, ports):
"""Rollback any ports created after a ResourceNotFound exception.
:param obj: The node object.
:param ports: A list of ports which attached to this server.
:return:
"""
if not ports:
return
LOG.warning(
'Rolling back ports for Node %s.',
obj.id
)
for port in ports:
if not port.get('remove', False):
continue
try:
if (port.get('floating') and
port['floating'].get('remove', False)):
self.network(obj).floatingip_delete(
port['floating']['id']
)
self.network(obj).port_delete(port['id'])
except exc.InternalError as ex:
LOG.debug(
'Failed to delete port %s during rollback for Node %s: %s',
port['id'], obj.id, ex
)
def _rollback_instance(self, obj, server):
"""Rollback an instance created after a ResourceNotFound exception.
:param obj: The node object.
:param server: A server.
:return:
"""
if not server or not server.id:
return
LOG.warning(
'Rolling back instance %s for Node %s.',
server.id, obj.id
)
try:
self.compute(obj).server_force_delete(server.id, True)
except exc.InternalError as ex:
LOG.debug(
'Failed to delete instance %s during rollback for Node %s: %s',
server.id, obj.id, ex
)
def do_delete(self, obj, **params):
"""Delete the physical resource associated with the specified node.
:param obj: The node object to operate on.
:param kwargs params: Optional keyword arguments for the delete
operation.
:returns: This operation always return True unless exception is
caught.
:raises: `EResourceDeletion` if interaction with compute service fails.
"""
server_id = obj.physical_id
ignore_missing = params.get('ignore_missing', True)
internal_ports = obj.data.get('internal_ports', [])
force = params.get('force', False)
timeout = params.get('timeout', cfg.CONF.default_nova_timeout)
try:
if server_id:
driver = self.compute(obj)
if force:
driver.server_force_delete(server_id, ignore_missing)
else:
driver.server_delete(server_id, ignore_missing)
driver.wait_for_server_delete(server_id, timeout=timeout)
except exc.InternalError as ex:
raise exc.EResourceDeletion(type='server', id=server_id,
message=str(ex))
finally:
if internal_ports:
ex = self._delete_ports(obj, internal_ports)
if ex:
raise exc.EResourceDeletion(type='server', d=server_id,
message=str(ex))
return True
def _check_server_name(self, obj, profile):
"""Check if there is a new name to be assigned to the server.
:param obj: The node object to operate on.
:param new_profile: The new profile which may contain a name for
the server instance.
:return: A tuple consisting a boolean indicating whether the name
needs change and the server name determined.
"""
old_name = self.properties[self.NAME] or obj.name
new_name = profile.properties[self.NAME] or obj.name
if old_name == new_name:
return False, new_name
return True, new_name
def _update_name(self, obj, new_name):
"""Update the name of the server.
:param obj: The node object to operate.
:param new_name: The new name for the server instance.
:return: ``None``.
:raises: ``EResourceUpdate``.
"""
try:
self.compute(obj).server_update(obj.physical_id, name=new_name)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
def _check_password(self, obj, new_profile):
"""Check if the admin password has been changed in the new profile.
:param obj: The server node to operate, not used currently.
:param new_profile: The new profile which may contain a new password
for the server instance.
:return: A tuple consisting a boolean indicating whether the password
needs a change and the password determined which could be
'' if new password is not set.
"""
old_passwd = self.properties.get(self.ADMIN_PASS) or ''
new_passwd = new_profile.properties[self.ADMIN_PASS] or ''
if old_passwd == new_passwd:
return False, new_passwd
return True, new_passwd
def _update_password(self, obj, new_password):
"""Update the admin password for the server.
:param obj: The node object to operate.
:param new_password: The new password for the server instance.
:return: ``None``.
:raises: ``EResourceUpdate``.
"""
try:
self.compute(obj).server_change_password(obj.physical_id,
new_password)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
def _update_metadata(self, obj, new_profile):
"""Update the server metadata.
:param obj: The node object to operate on.
:param new_profile: The new profile that may contain some changes to
the metadata.
:returns: ``None``
:raises: `EResourceUpdate`.
"""
old_meta = self._build_metadata(obj, self.properties[self.METADATA])
new_meta = self._build_metadata(obj,
new_profile.properties[self.METADATA])
if new_meta == old_meta:
return
try:
self.compute(obj).server_metadata_update(obj.physical_id, new_meta)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
def _update_flavor(self, obj, new_profile):
"""Update server flavor.
:param obj: The node object to operate on.
:param old_flavor: The identity of the current flavor.
:param new_flavor: The identity of the new flavor.
:returns: Returns true if the flavor was updated or false otherwise.
:raises: `EResourceUpdate` when operation was a failure.
"""
old_flavor = self.properties[self.FLAVOR]
new_flavor = new_profile.properties[self.FLAVOR]
cc = self.compute(obj)
oldflavor = self._validate_flavor(obj, old_flavor, 'update')
newflavor = self._validate_flavor(obj, new_flavor, 'update')
if oldflavor.id == newflavor.id:
return False
try:
# server has to be active or stopped in order to resize
# stop server if it is active
server = cc.server_get(obj.physical_id)
if server.status == consts.VS_ACTIVE:
cc.server_stop(obj.physical_id)
cc.wait_for_server(obj.physical_id, consts.VS_SHUTOFF,
timeout=self.stop_timeout)
elif server.status != consts.VS_SHUTOFF:
raise exc.InternalError(
message='Server needs to be ACTIVE or STOPPED in order to'
' update flavor.')
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
try:
cc.server_resize(obj.physical_id, newflavor.id)
cc.wait_for_server(obj.physical_id, 'VERIFY_RESIZE')
except exc.InternalError as ex:
msg = str(ex)
try:
server = cc.server_get(obj.physical_id)
if server.status == 'RESIZE':
cc.server_resize_revert(obj.physical_id)
cc.wait_for_server(obj.physical_id, consts.VS_SHUTOFF)
# start server back up in case of exception during resize
cc.server_start(obj.physical_id)
cc.wait_for_server(obj.physical_id, consts.VS_ACTIVE)
except exc.InternalError as ex1:
msg = str(ex1)
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=msg)
try:
cc.server_resize_confirm(obj.physical_id)
cc.wait_for_server(obj.physical_id, consts.VS_SHUTOFF)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
return True
def _update_image(self, obj, new_profile, new_name, new_password):
"""Update image used by server node.
:param obj: The node object to operate on.
:param new_profile: The profile which may contain a new image name or
ID to use.
:param new_name: The name for the server node.
:param newn_password: The new password for the administrative account
if provided.
:returns: A boolean indicating whether the image needs an update.
:raises: ``InternalError`` if operation was a failure.
"""
new_image = new_profile.properties[self.IMAGE]
if not new_image:
msg = _("Updating Nova server with image set to None is not "
"supported by Nova")
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=msg)
# check the new image first
img_new = self._validate_image(obj, new_image, reason='update')
new_image_id = img_new.id
driver = self.compute(obj)
try:
server = driver.server_get(obj.physical_id)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
old_image_id = self._get_image_id(obj, server, 'updating')
if new_image_id == old_image_id:
return False
try:
# server has to be active or stopped in order to resize
# stop server if it is active
if server.status == consts.VS_ACTIVE:
driver.server_stop(obj.physical_id)
driver.wait_for_server(obj.physical_id, consts.VS_SHUTOFF,
timeout=self.stop_timeout)
elif server.status != consts.VS_SHUTOFF:
raise exc.InternalError(
message='Server needs to be ACTIVE or STOPPED in order to'
' update image.')
driver.server_rebuild(obj.physical_id, new_image_id,
new_name, new_password)
driver.wait_for_server(obj.physical_id, consts.VS_SHUTOFF)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
return True
def _update_network_add_port(self, obj, networks):
"""Create new interfaces for the server node.
:param obj: The node object to operate.
:param networks: A list containing information about new network
interfaces to be created.
:returns: ``None``.
:raises: ``EResourceUpdate`` if interaction with drivers failed.
"""
cc = self.compute(obj)
try:
server = cc.server_get(obj.physical_id)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
ports = self._create_ports_from_properties(
obj, networks, 'update')
for port in ports:
params = {'port_id': port['id']}
try:
cc.server_interface_create(server, **params)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server',
id=obj.physical_id,
message=str(ex))
def _find_port_by_net_spec(self, obj, net_spec, ports):
"""Find existing ports match with specific network properties.
:param obj: The node object.
:param net_spec: Network property of this profile.
:param ports: A list of ports which attached to this server.
:returns: A list of candidate ports matching this network spec.
"""
# TODO(anyone): handle security_groups
net = self._validate_network(obj, net_spec, 'update')
selected_ports = []
for p in ports:
floating = p.get('floating', {})
floating_network = net.get(self.FLOATING_NETWORK, None)
if floating_network and floating.get(
'floating_network_id') != floating_network:
continue
floating_ip_address = net.get(self.FLOATING_IP, None)
if floating_ip_address and floating.get(
'floating_ip_address') != floating_ip_address:
continue
# If network properties didn't contain floating ip,
# then we should better not make a port with floating ip
# as candidate.
if (floating and not floating_network and not floating_ip_address):
continue
port_id = net.get(self.PORT, None)
if port_id and p['id'] != port_id:
continue
fixed_ip = net.get(self.FIXED_IP, None)
if fixed_ip:
fixed_ips = [ff['ip_address'] for ff in p['fixed_ips']]
if fixed_ip not in fixed_ips:
continue
network = net.get(self.NETWORK, None)
if network:
net_id = self.network(obj).network_get(network).id
if p['network_id'] != net_id:
continue
selected_ports.append(p)
return selected_ports
def _update_network_remove_port(self, obj, networks):
"""Delete existing interfaces from the node.
:param obj: The node object to operate.
:param networks: A list containing information about network
interfaces to be created.
:returns: ``None``
:raises: ``EResourceUpdate``
"""
cc = self.compute(obj)
nc = self.network(obj)
internal_ports = obj.data.get('internal_ports', [])
if networks:
try:
# stop server if it is active
server = cc.server_get(obj.physical_id)
if server.status == consts.VS_ACTIVE:
cc.server_stop(obj.physical_id)
cc.wait_for_server(obj.physical_id, consts.VS_SHUTOFF,
timeout=self.stop_timeout)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
for n in networks:
candidate_ports = self._find_port_by_net_spec(
obj, n, internal_ports)
port = candidate_ports[0]
try:
# Detach port from server
cc.server_interface_delete(port['id'], obj.physical_id)
# delete port if created by senlin
if port.get('remove', False):
nc.port_delete(port['id'], ignore_missing=True)
# delete floating IP if created by senlin
if (port.get('floating', None) and
port['floating'].get('remove', False)):
nc.floatingip_delete(port['floating']['id'],
ignore_missing=True)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
internal_ports.remove(port)
obj.data['internal_ports'] = internal_ports
node_obj.Node.update(self.context, obj.id, {'data': obj.data})
def _nw_compare(self, n1, n2, property):
return n1.get(property, None) == n2.get(property, None)
def _update_network_update_port(self, obj, networks):
"""Update existing port in network from the node.
Currently only update to security group is supported.
:param obj: The node object to operate.
:param networks: A list networks that contain updated security groups.
:returns: ``None``
:raises: ``EResourceUpdate``
"""
nc = self.network(obj)
internal_ports = obj.data.get('internal_ports', [])
# process each network to be updated
for n in networks:
# verify network properties and resolve names into ids
net = self._validate_network(obj, n, 'update')
# find existing port that matches network
candidate_ports = self._find_port_by_net_spec(
obj, net, internal_ports)
port = candidate_ports[0]
try:
# set updated security groups for port
port_attr = {
'security_groups': net.get(self.PORT_SECURITY_GROUPS, []),
}
LOG.debug("Setting security groups %s for port %s",
port_attr, port['id'])
nc.port_update(port['id'], **port_attr)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
def _update_network(self, obj, new_profile):
"""Updating server network interfaces.
:param obj: The node object to operate.
:param new_profile: The new profile which may contain new network
settings.
:return: Returns a tuple of booleans if network was created or deleted.
:raises: ``EResourceUpdate`` if there are driver failures.
"""
networks_current = self.properties[self.NETWORKS]
networks_create = new_profile.properties[self.NETWORKS]
networks_delete = copy.deepcopy(networks_current)
networks_update = []
for nw in networks_current:
if nw in networks_create:
# network already exist. no need to create or delete it.
LOG.debug("Network %s already exists, skip create/delete", nw)
networks_create.remove(nw)
networks_delete.remove(nw)
# find networks for which only security group changed
for nw in networks_current:
# networks to be created with only sg changes
sg_create_nw = [n for n in networks_create
if (self._nw_compare(n, nw, 'network') and
self._nw_compare(n, nw, 'port') and
self._nw_compare(n, nw, 'fixed_ip') and
self._nw_compare(n, nw, 'floating_network') and
self._nw_compare(n, nw, 'floating_ip'))]
for n in sg_create_nw:
# don't create networks with only security group changes
LOG.debug("Network %s only has security group changes, "
"don't create/delete it. Only update it.", n)
networks_create.remove(n)
networks_update.append(n)
if nw in networks_delete:
networks_delete.remove(nw)
# update network
if networks_update:
self._update_network_update_port(obj, networks_update)
# Detach some existing interfaces
if networks_delete:
self._update_network_remove_port(obj, networks_delete)
# Attach new interfaces
if networks_create:
self._update_network_add_port(obj, networks_create)
return networks_create, networks_delete
def do_update(self, obj, new_profile=None, **params):
"""Perform update on the server.
:param obj: the server to operate on
:param new_profile: the new profile for the server.
:param params: a dictionary of optional parameters.
:returns: True if update was successful or False otherwise.
:raises: `EResourceUpdate` if operation fails.
"""
if not obj.physical_id:
return False
if not new_profile:
return False
if not self.validate_for_update(new_profile):
return False
self.stop_timeout = params.get('cluster.stop_timeout_before_update',
cfg.CONF.default_nova_timeout)
if not isinstance(self.stop_timeout, int):
raise exc.EResourceUpdate(
type='server', id=obj.physical_id,
message='cluster.stop_timeout_before_update value must be of '
'type int.')
name_changed, new_name = self._check_server_name(obj, new_profile)
passwd_changed, new_passwd = self._check_password(obj, new_profile)
# Update server image: may have side effect of changing server name
# and/or admin password
image_changed = self._update_image(obj, new_profile, new_name,
new_passwd)
if not image_changed:
# we do this separately only when rebuild wasn't performed
if name_changed:
self._update_name(obj, new_name)
if passwd_changed:
self._update_password(obj, new_passwd)
# Update server flavor: note that flavor is a required property
flavor_changed = self._update_flavor(obj, new_profile)
network_created, network_deleted = self._update_network(
obj, new_profile)
# TODO(Yanyan Hu): Update block_device properties
# Update server metadata
self._update_metadata(obj, new_profile)
# start server if it was stopped as part of this update operation
if image_changed or flavor_changed or network_deleted:
cc = self.compute(obj)
try:
server = cc.server_get(obj.physical_id)
if server.status == consts.VS_SHUTOFF:
cc.server_start(obj.physical_id)
cc.wait_for_server(obj.physical_id, consts.VS_ACTIVE)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
return True
def do_get_details(self, obj):
known_keys = {
'OS-DCF:diskConfig',
'OS-EXT-AZ:availability_zone',
'OS-EXT-STS:power_state',
'OS-EXT-STS:vm_state',
'accessIPv4',
'accessIPv6',
'config_drive',
'created',
'hostId',
'id',
'key_name',
'locked',
'metadata',
'name',
'os-extended-volumes:volumes_attached',
'progress',
'status',
'updated'
}
if obj.physical_id is None or obj.physical_id == '':
return {}
driver = self.compute(obj)
try:
server = driver.server_get(obj.physical_id)
except exc.InternalError as ex:
return {
'Error': {
'code': ex.code,
'message': str(ex)
}
}
if server is None:
return {}
server_data = server.to_dict()
if 'id' in server_data['image']:
image_id = server_data['image']['id']
else:
image_id = server_data['image']
attached_volumes = []
if ('attached_volumes' in server_data and
len(server_data['attached_volumes']) > 0):
for volume in server_data['attached_volumes']:
attached_volumes.append(volume['id'])
details = {
'image': image_id,
'attached_volumes': attached_volumes,
'flavor': self._get_flavor_id(obj, server_data),
}
for key in known_keys:
if key in server_data:
details[key] = server_data[key]
# process special keys like 'OS-EXT-STS:task_state': these keys have
# a default value '-' when not existing
special_keys = [
'OS-EXT-STS:task_state',
'OS-SRV-USG:launched_at',
'OS-SRV-USG:terminated_at',
]
for key in special_keys:
if key in server_data:
val = server_data[key]
details[key] = val if val else '-'
# process network addresses
details['addresses'] = copy.deepcopy(server_data['addresses'])
# process security groups
sgroups = []
if 'security_groups' in server_data:
if server_data['security_groups'] is not None:
for sg in server_data['security_groups']:
sgroups.append(sg['name'])
# when we have multiple nics the info will include the
# security groups N times where N == number of nics. Be nice
# and only display it once.
sgroups = list(set(sgroups))
if len(sgroups) == 0:
details['security_groups'] = ''
elif len(sgroups) == 1:
details['security_groups'] = sgroups[0]
else:
details['security_groups'] = sgroups
return dict((k, details[k]) for k in sorted(details))
def _get_flavor_id(self, obj, server):
"""Get flavor id.
:param obj: The node object.
:param dict server: The server object.
:return: The flavor_id for the server.
"""
flavor = server['flavor']
if 'id' in flavor:
return flavor['id']
return self.compute(obj).flavor_find(flavor['original_name'], False).id
def _get_image_id(self, obj, server, op):
"""Get image id.
:param obj: The node object.
:param server: The server object.
:param op: The operate on the node.
:return: The image_id for the server.
"""
image_id = None
if server.image:
image_id = server.image['id'] or server.image
# when booting a nova server from volume, the image property
# can be ignored.
# we try to find a volume which is bootable and use its image_id
# for the server.
elif server.attached_volumes:
cinder_driver = self.block_storage(obj)
for volume_ids in server.attached_volumes:
try:
vs = cinder_driver.volume_get(volume_ids['id'])
if vs.is_bootable:
image_id = vs.volume_image_metadata['image_id']
except exc.InternalError as ex:
raise exc.EResourceOperation(op=op, type='server',
id=obj.physical_id,
message=str(ex))
else:
msg = _("server doesn't have an image and it has no "
"bootable volume")
raise exc.EResourceOperation(op=op, type="server",
id=obj.physical_id,
message=msg)
return image_id
def _handle_generic_op(self, obj, driver_func_name,
op_name, expected_server_status=None,
**kwargs):
"""Generic handler for standard server operations."""
if not obj.physical_id:
return False
server_id = obj.physical_id
nova_driver = self.compute(obj)
try:
driver_func = getattr(nova_driver, driver_func_name)
driver_func(server_id, **kwargs)
if expected_server_status:
nova_driver.wait_for_server(server_id, expected_server_status)
return True
except exc.InternalError as ex:
raise exc.EResourceOperation(op=op_name, type='server',
id=server_id,
message=str(ex))
def do_adopt(self, obj, overrides=None, snapshot=False):
"""Adopt an existing server node for management.
:param obj: A node object for this operation. It could be a puppet
node that provides only 'user', 'project' and 'physical_id'
properties when doing a preview. It can be a real Node object for
node adoption.
:param overrides: A dict containing the properties that will be
overridden when generating a profile for the server.
:param snapshot: A boolean flag indicating whether the profile should
attempt a snapshot operation before adopting the server. If set to
True, the ID of the snapshot will be used as the image ID.
:returns: A dict containing the spec created from the server object or
a dict containing error information if failure occurred.
"""
driver = self.compute(obj)
# TODO(Qiming): Add snapshot support
# snapshot = driver.snapshot_create(...)
error = {}
try:
server = driver.server_get(obj.physical_id)
except exc.InternalError as ex:
error = {'code': ex.code, 'message': str(ex)}
if error:
return {'Error': error}
spec = {}
# Context?
# TODO(Qiming): Need to fetch admin password from a different API
spec[self.AUTO_DISK_CONFIG] = server.disk_config == 'AUTO'
spec[self.AVAILABILITY_ZONE] = server.availability_zone
# TODO(Anyone): verify if this needs a format conversion
bdm = server.block_device_mapping or []
spec[self.BLOCK_DEVICE_MAPPING_V2] = bdm
spec[self.CONFIG_DRIVE] = server.has_config_drive or False
spec[self.FLAVOR] = server.flavor['id']
spec[self.IMAGE] = self._get_image_id(obj, server, 'adopting')
spec[self.KEY_NAME] = server.key_name
# metadata
metadata = server.metadata or {}
metadata.pop('cluster_id', None)
metadata.pop('cluster_node_id', None)
metadata.pop('cluster_node_index', None)
spec[self.METADATA] = metadata
# name
spec[self.NAME] = server.name
networks = server.addresses
net_list = []
for network, interfaces in networks.items():
for intf in interfaces:
ip_type = intf.get('OS-EXT-IPS:type')
net = {self.NETWORK: network}
if ip_type == 'fixed' and net not in net_list:
net_list.append({self.NETWORK: network})
spec[self.NETWORKS] = net_list
# NOTE: the personality attribute is missing for ever.
spec[self.SECURITY_GROUPS] = [
sg['name'] for sg in server.security_groups
]
# TODO(Qiming): get server user_data and parse it.
# Note: user_data is returned in 2.3 microversion API, in a different
# property name.
# spec[self.USER_DATA] = server.user_data
if overrides:
spec.update(overrides)
return spec
def do_join(self, obj, cluster_id):
if not obj.physical_id:
return False
driver = self.compute(obj)
try:
metadata = {}
metadata['cluster_id'] = cluster_id
metadata['cluster_node_index'] = str(obj.index)
driver.server_metadata_update(obj.physical_id, metadata)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
return super(ServerProfile, self).do_join(obj, cluster_id)
def do_leave(self, obj):
if not obj.physical_id:
return False
keys = ['cluster_id', 'cluster_node_index']
try:
self.compute(obj).server_metadata_delete(obj.physical_id, keys)
except exc.InternalError as ex:
raise exc.EResourceDeletion(type='server', id=obj.physical_id,
message=str(ex))
return super(ServerProfile, self).do_leave(obj)
def do_check(self, obj):
if not obj.physical_id:
return False
try:
server = self.compute(obj).server_get(obj.physical_id)
except exc.InternalError as ex:
if ex.code == 404:
raise exc.EServerNotFound(type='server',
id=obj.physical_id,
message=str(ex))
else:
raise exc.EResourceOperation(op='checking', type='server',
id=obj.physical_id,
message=str(ex))
if (server is None or server.status != consts.VS_ACTIVE):
return False
return True
def do_healthcheck(self, obj):
"""Healthcheck operation.
This method checks if a server node is healthy by getting the server
status from nova. A server is considered unhealthy if it does not
exist or its status is one of the following:
- ERROR
- SHUTOFF
- DELETED
:param obj: The node object to operate on.
:return status: True indicates node is healthy, False indicates
it is unhealthy.
"""
unhealthy_server_status = [consts.VS_ERROR, consts.VS_SHUTOFF,
consts.VS_DELETED]
if not obj.physical_id:
if obj.status == 'BUILD' or obj.status == 'CREATING':
return True
LOG.info('%s for %s: server has no physical ID.',
consts.POLL_STATUS_FAIL, obj.name)
return False
try:
server = self.compute(obj).server_get(obj.physical_id)
except Exception as ex:
if isinstance(ex, exc.InternalError) and ex.code == 404:
# treat resource not found exception as unhealthy
LOG.info('%s for %s: server was not found.',
consts.POLL_STATUS_FAIL, obj.name)
return False
else:
# treat all other exceptions as healthy
LOG.info(
'%s for %s: Exception when trying to get server info but '
'ignoring this error: %s.',
consts.POLL_STATUS_PASS, obj.name, ex.message)
return True
if server is None:
# no server information is available, treat the node as healthy
LOG.info(
'%s for %s: No server information was returned but ignoring '
'this error.',
consts.POLL_STATUS_PASS, obj.name)
return True
if server.status in unhealthy_server_status:
LOG.info('%s for %s: server status is unhealthy.',
consts.POLL_STATUS_FAIL, obj.name)
return False
LOG.info('%s for %s', consts.POLL_STATUS_PASS, obj.name)
return True
def do_recover(self, obj, **options):
"""Handler for recover operation.
:param obj: The node object.
:param dict options: A list for operations each of which has a name
and optionally a map from parameter to values.
:return id: New id of the recovered resource or None if recovery
failed.
:return status: True indicates successful recovery, False indicates
failure.
"""
# default is recreate if not specified
if 'operation' not in options or not options['operation']:
options['operation'] = consts.RECOVER_RECREATE
operation = options.get('operation')
if operation.upper() not in consts.RECOVERY_ACTIONS:
LOG.error("The operation '%s' is not supported",
operation)
return obj.physical_id, False
op_params = options.get('operation_params', {})
if operation.upper() == consts.RECOVER_REBOOT:
# default to hard reboot if operation_params was not specified
if not isinstance(op_params, dict):
op_params = {}
if consts.REBOOT_TYPE not in op_params.keys():
op_params[consts.REBOOT_TYPE] = consts.REBOOT_HARD
if operation.upper() == consts.RECOVER_RECREATE:
# recreate is implemented in base class
return super(ServerProfile, self).do_recover(obj, **options)
else:
method = getattr(self, "handle_" + operation.lower())
return method(obj, **op_params)
def handle_reboot(self, obj, **options):
"""Handler for the reboot operation."""
if not obj.physical_id:
return None, False
server_id = obj.physical_id
reboot_type = options.get(consts.REBOOT_TYPE, consts.REBOOT_SOFT)
if (not isinstance(reboot_type, str) or
reboot_type.upper() not in consts.REBOOT_TYPES):
return server_id, False
nova_driver = self.compute(obj)
try:
server = nova_driver.server_get(server_id)
if server is None:
return None, False
nova_driver.server_reboot(server_id, reboot_type)
nova_driver.wait_for_server(obj.physical_id,
consts.VS_ACTIVE)
return server_id, True
except exc.InternalError as ex:
raise exc.EResourceOperation(op='rebooting', type='server',
id=server_id,
message=str(ex))
def handle_rebuild(self, obj, **options):
"""Handler for the rebuild operation.
:param obj: The node object.
:param dict options: A list for operations each of which has a name
and optionally a map from parameter to values.
:return id: New id of the recovered resource or None if recovery
failed.
:return status: True indicates successful recovery, False indicates
failure.
"""
if not obj.physical_id:
return None, False
server_id = obj.physical_id
nova_driver = self.compute(obj)
try:
server = nova_driver.server_get(server_id)
except exc.InternalError as ex:
raise exc.EResourceOperation(op='rebuilding', type='server',
id=server_id,
message=str(ex))
if server is None:
return None, False
image_id = options.get(self.IMAGE, None)
if not image_id:
image_id = self._get_image_id(obj, server, 'rebuilding')
admin_pass = self.properties.get(self.ADMIN_PASS)
name = self.properties[self.NAME] or obj.name
try:
nova_driver.server_rebuild(server_id, image_id,
name, admin_pass)
nova_driver.wait_for_server(server_id, consts.VS_ACTIVE)
return server_id, True
except exc.InternalError as ex:
raise exc.EResourceOperation(op='rebuilding', type='server',
id=server_id,
message=str(ex))
def handle_change_password(self, obj, **options):
"""Handler for the change_password operation."""
password = options.get(self.ADMIN_PASSWORD, None)
if (password is None or not isinstance(password, str)):
return False
return self._handle_generic_op(obj, 'server_change_password',
'change_password',
new_password=password)
def handle_suspend(self, obj):
"""Handler for the suspend operation."""
return self._handle_generic_op(obj, 'server_suspend',
'suspend', consts.VS_SUSPENDED)
def handle_resume(self, obj):
"""Handler for the resume operation."""
return self._handle_generic_op(obj, 'server_resume',
'resume', consts.VS_ACTIVE)
def handle_start(self, obj):
"""Handler for the start operation."""
return self._handle_generic_op(obj, 'server_start',
'start', consts.VS_ACTIVE)
def handle_stop(self, obj):
"""Handler for the stop operation."""
return self._handle_generic_op(obj, 'server_stop',
'stop', consts.VS_SHUTOFF)
def handle_lock(self, obj):
"""Handler for the lock operation."""
return self._handle_generic_op(obj, 'server_lock',
'lock')
def handle_unlock(self, obj):
"""Handler for the unlock operation."""
return self._handle_generic_op(obj, 'server_unlock',
'unlock')
def handle_pause(self, obj):
"""Handler for the pause operation."""
return self._handle_generic_op(obj, 'server_pause',
'pause', consts.VS_PAUSED)
def handle_unpause(self, obj):
"""Handler for the unpause operation."""
return self._handle_generic_op(obj, 'server_unpause',
'unpause', consts.VS_ACTIVE)
def handle_rescue(self, obj, **options):
"""Handler for the rescue operation."""
password = options.get(self.ADMIN_PASSWORD, None)
image = options.get(self.IMAGE, None)
if not image:
return False
self._validate_image(obj, image)
return self._handle_generic_op(obj, 'server_rescue',
'rescue', consts.VS_RESCUE,
admin_pass=password,
image_ref=image)
def handle_unrescue(self, obj):
"""Handler for the unrescue operation."""
return self._handle_generic_op(obj, 'server_unrescue',
'unrescue', consts.VS_ACTIVE)
def handle_migrate(self, obj):
"""Handler for the migrate operation."""
return self._handle_generic_op(obj, 'server_migrate',
'migrate', consts.VS_ACTIVE)
def handle_snapshot(self, obj):
"""Handler for the snapshot operation."""
return self._handle_generic_op(obj, 'server_create_image',
'snapshot', consts.VS_ACTIVE,
name=obj.name)
def handle_restore(self, obj, **options):
"""Handler for the restore operation."""
image = options.get(self.IMAGE, None)
if not image:
return False
return self.handle_rebuild(obj, **options)