2020 lines
77 KiB
Python
2020 lines
77 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, FIXED_IP, NETWORK, PORT_SECURITY_GROUPS,
|
|
FLOATING_NETWORK, FLOATING_IP,
|
|
) = (
|
|
'port', '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.'),
|
|
),
|
|
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
|
|
|
|
# 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
|
|
try:
|
|
port = self.network(obj).port_create(**port_attr)
|
|
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)
|