config/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/pv.py

896 lines
34 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 UnitedStack Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Copyright (c) 2013-2017 Wind River Systems, Inc.
#
import jsonpatch
import six
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from sysinv.api.controllers.v1 import base
from sysinv.api.controllers.v1 import collection
from sysinv.api.controllers.v1 import disk as disk_api
from sysinv.api.controllers.v1 import link
from sysinv.api.controllers.v1 import types
from sysinv.api.controllers.v1 import utils
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common.storage_backend_conf import StorageBackendConfig
from sysinv.common import utils as cutils
from sysinv import objects
from sysinv.openstack.common.gettextutils import _
from sysinv.openstack.common import log
from sysinv.openstack.common.rpc import common as rpc_common
from sysinv.openstack.common import uuidutils
LOG = log.getLogger(__name__)
class PVPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/address', '/ihost_uuid']
class PV(base.APIBase):
"""API representation of an LVM Physical Volume.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of
an pv.
"""
uuid = types.uuid
"Unique UUID for this pv"
pv_state = wtypes.text
"Represent the transition state of the ipv"
pv_type = wtypes.text
"Represent the type of pv"
disk_or_part_uuid = types.uuid
"idisk or partition UUID for this pv"
disk_or_part_device_node = wtypes.text
"idisk or partition device node name for this pv on the ihost"
disk_or_part_device_path = wtypes.text
"idisk or partition device path for this pv on the ihost"
lvm_pv_name = wtypes.text
"LVM physical volume name"
lvm_vg_name = wtypes.text
"LVM physical volume's reported volume group name"
lvm_pv_uuid = wtypes.text
"LVM physical volume's reported uuid string"
lvm_pv_size = int
"LVM physical volume's size"
lvm_pe_total = int
"LVM physical volume's PE total"
lvm_pe_alloced = int
"LVM physical volume's allocated PEs"
capabilities = {wtypes.text: utils.ValidTypes(wtypes.text,
six.integer_types)}
"This pv's meta data"
forihostid = int
"The ihostid that this ipv belongs to"
ihost_uuid = types.uuid
"The UUID of the host this pv belongs to"
forilvgid = int
"The ilvgid that this ipv belongs to"
ilvg_uuid = types.uuid
"The UUID of the lvg this pv belongs to"
links = [link.Link]
"A list containing a self link and associated pv links"
idisks = [link.Link]
"Links to the collection of idisks on this pv"
partitions = [link.Link]
"Links to the collection of partitions on this pv"
def __init__(self, **kwargs):
self.fields = objects.pv.fields.keys()
for k in self.fields:
setattr(self, k, kwargs.get(k))
if not self.uuid:
self.uuid = uuidutils.generate_uuid()
@classmethod
def convert_with_links(cls, rpc_pv, expand=True):
pv = PV(**rpc_pv.as_dict())
if not expand:
pv.unset_fields_except([
'uuid', 'pv_state', 'pv_type', 'capabilities',
'disk_or_part_uuid', 'disk_or_part_device_node',
'disk_or_part_device_path', 'lvm_pv_name', 'lvm_vg_name',
'lvm_pv_uuid', 'lvm_pv_size', 'lvm_pe_alloced', 'lvm_pe_total',
'ilvg_uuid', 'forilvgid', 'ihost_uuid', 'forihostid',
'created_at', 'updated_at'])
# never expose the ihost_id attribute, allow exposure for now
pv.forihostid = wtypes.Unset
pv.links = [link.Link.make_link('self', pecan.request.host_url,
'ipvs', pv.uuid),
link.Link.make_link('bookmark',
pecan.request.host_url,
'ipvs', pv.uuid,
bookmark=True)
]
if expand:
pv.idisks = [link.Link.make_link('self',
pecan.request.host_url,
'ipvs',
pv.uuid + "/idisks"),
link.Link.make_link(
'bookmark',
pecan.request.host_url,
'ipvs',
pv.uuid + "/idisks",
bookmark=True)
]
pv.partitions = [link.Link.make_link('self',
pecan.request.host_url,
'ipvs',
pv.uuid + "/partitions"),
link.Link.make_link(
'bookmark',
pecan.request.host_url,
'ipvs',
pv.uuid + "/partitions",
bookmark=True)
]
return pv
class PVCollection(collection.Collection):
"""API representation of a collection of pvs."""
ipvs = [PV]
"A list containing pv objects"
def __init__(self, **kwargs):
self._type = 'ipvs'
@classmethod
def convert_with_links(cls, rpc_pvs, limit, url=None,
expand=False, **kwargs):
collection = PVCollection()
collection.ipvs = [PV.convert_with_links(p, expand)
for p in rpc_pvs]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
LOCK_NAME = 'PVController'
class PVController(rest.RestController):
"""REST controller for ipvs."""
idisks = disk_api.DiskController(from_ihosts=True, from_ipv=True)
"Expose idisks as a sub-element of ipvs"
_custom_actions = {
'detail': ['GET'],
}
def __init__(self, from_ihosts=False, from_ilvg=False):
self._from_ihosts = from_ihosts
self._from_ilvg = from_ilvg
def _get_pvs_collection(self, ihost_uuid, marker, limit, sort_key,
sort_dir, expand=False, resource_url=None):
if self._from_ihosts and not ihost_uuid:
raise exception.InvalidParameterValue(_(
"Host id not specified."))
limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.pv.get_by_uuid(
pecan.request.context,
marker)
if ihost_uuid:
pvs = pecan.request.dbapi.ipv_get_by_ihost(ihost_uuid, limit,
marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
else:
pvs = pecan.request.dbapi.ipv_get_list(limit, marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
return PVCollection.convert_with_links(pvs, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(PVCollection, types.uuid, types.uuid, int,
wtypes.text, wtypes.text)
def get_all(self, ihost_uuid=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of pvs."""
return self._get_pvs_collection(ihost_uuid, marker, limit,
sort_key, sort_dir)
@wsme_pecan.wsexpose(PVCollection, types.uuid, types.uuid, int,
wtypes.text, wtypes.text)
def detail(self, ihost_uuid=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of pvs with detail."""
# NOTE(lucasagomes): /detail should only work against collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "ipvs":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['pvs', 'detail'])
return self._get_pvs_collection(ihost_uuid,
marker, limit,
sort_key, sort_dir,
expand, resource_url)
@wsme_pecan.wsexpose(PV, types.uuid)
def get_one(self, pv_uuid):
"""Retrieve information about the given pv."""
if self._from_ihosts:
raise exception.OperationNotPermitted
rpc_pv = objects.pv.get_by_uuid(
pecan.request.context, pv_uuid)
return PV.convert_with_links(rpc_pv)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(PV, body=PV)
def post(self, pv):
"""Create a new pv."""
if self._from_ihosts:
raise exception.OperationNotPermitted
try:
pv = pv.as_dict()
LOG.debug("pv post dict= %s" % pv)
new_pv = _create(pv)
except exception.SysinvException as e:
LOG.exception(e)
raise wsme.exc.ClientSideError(_("Invalid data: failed to create "
"a physical volume object"))
return PV.convert_with_links(new_pv)
@cutils.synchronized(LOCK_NAME)
@wsme.validate(types.uuid, [PVPatchType])
@wsme_pecan.wsexpose(PV, types.uuid,
body=[PVPatchType])
def patch(self, pv_uuid, patch):
"""Update an existing pv."""
if self._from_ihosts:
raise exception.OperationNotPermitted
LOG.debug("patch_data: %s" % patch)
rpc_pv = objects.pv.get_by_uuid(
pecan.request.context, pv_uuid)
# replace ihost_uuid and ipv_uuid with corresponding
patch_obj = jsonpatch.JsonPatch(patch)
for p in patch_obj:
if p['path'] == '/ihost_uuid':
p['path'] = '/forihostid'
ihost = objects.host.get_by_uuid(pecan.request.context,
p['value'])
p['value'] = ihost.id
try:
pv = PV(**jsonpatch.apply_patch(rpc_pv.as_dict(),
patch_obj))
except utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
# Semantic Checks
_check("modify", pv)
try:
# Update only the fields that have changed
for field in objects.pv.fields:
if rpc_pv[field] != getattr(pv, field):
rpc_pv[field] = getattr(pv, field)
# Save and return
rpc_pv.save()
return PV.convert_with_links(rpc_pv)
except exception.HTTPNotFound:
msg = _("PV update failed: host %s pv %s : patch %s"
% (ihost['hostname'], pv['lvm_pv_name'], patch))
raise wsme.exc.ClientSideError(msg)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
def delete(self, pv_uuid):
"""Delete a pv."""
if self._from_ihosts:
raise exception.OperationNotPermitted
delete_pv(pv_uuid)
# This method allows creating a physical volume through a non-HTTP
# request e.g. through profile.py while still passing
# through physical volume semantic checks and osd configuration
# Hence, not declared inside a class
#
# Param:
# pv - dictionary of physical volume values
# iprofile - True when created by a storage profile
def _create(pv, iprofile=None):
LOG.debug("pv._create with initial params: %s" % pv)
# Get host
ihostId = pv.get('forihostid') or pv.get('ihost_uuid')
ihost = pecan.request.dbapi.ihost_get(ihostId)
if uuidutils.is_uuid_like(ihostId):
forihostid = ihost['id']
else:
forihostid = ihostId
pv.update({'forihostid': forihostid})
pv['ihost_uuid'] = ihost['uuid']
# Set defaults - before checks to allow for optional attributes
pv = _set_defaults(pv)
# Semantic checks
pv = _check("add", pv)
LOG.debug("pv._create with validated params: %s" % pv)
# See if this volume group already exists
ipvs = pecan.request.dbapi.ipv_get_all(forihostid=forihostid)
pv_in_db = False
for ipv in ipvs:
if ipv['disk_or_part_device_path'] == pv['disk_or_part_device_path']:
pv_in_db = True
# TODO(rchurch): Refactor PV_ERR. Still needed?
# User is adding again so complain
if (ipv['pv_state'] in [constants.PV_ADD,
constants.PROVISIONED,
constants.PV_ERR]):
raise wsme.exc.ClientSideError(_("Physical Volume (%s) "
"already present" %
ipv['lvm_pv_name']))
# User changed mind and is re-adding
if ipv['pv_state'] == constants.PV_DEL:
values = {'pv_state': constants.PV_ADD}
try:
pecan.request.dbapi.ipv_update(ipv.id, values)
except exception.HTTPNotFound:
msg = _("PV update failed: host (%s) PV (%s)"
% (ihost['hostname'], ipv['lvm_pv_name']))
raise wsme.exc.ClientSideError(msg)
ret_pv = ipv
break
if not pv_in_db:
ret_pv = pecan.request.dbapi.ipv_create(forihostid, pv)
LOG.debug("pv._create final, created, pv: %s" % ret_pv.as_dict())
# Associate the pv to the disk or partition record.
values = {'foripvid': ret_pv.id}
if pv['pv_type'] == constants.PV_TYPE_DISK:
pecan.request.dbapi.idisk_update(ret_pv.disk_or_part_uuid,
values)
elif pv['pv_type'] == constants.PV_TYPE_PARTITION:
pecan.request.dbapi.partition_update(ret_pv.disk_or_part_uuid,
values)
# semantic check for root disk
if iprofile is not True and constants.WARNING_MESSAGE_INDEX in pv:
warning_message_index = pv.get(constants.WARNING_MESSAGE_INDEX)
raise wsme.exc.ClientSideError(
constants.PV_WARNINGS[warning_message_index])
# for CPE nodes we allow extending of cgts-vg to an unused partition.
# this will inform the conductor and agent to apply the lvm manifest
# without requiring a lock-unlock cycle.
# for non-cpe nodes, the rootfs disk is already partitioned to be fully
# used by the cgts-vg volume group.
if ret_pv.lvm_vg_name == constants.LVG_CGTS_VG:
pecan.request.rpcapi.update_lvm_config(pecan.request.context)
return ret_pv
def _set_defaults(pv):
defaults = {
'pv_state': constants.PV_ADD,
'pv_type': constants.PV_TYPE_DISK,
'lvm_pv_uuid': None,
'lvm_pv_size': 0,
'lvm_pe_total': 0,
'lvm_pe_alloced': 0,
}
pv_merged = pv.copy()
for key in pv_merged:
if pv_merged[key] is None and key in defaults:
pv_merged[key] = defaults[key]
return pv_merged
def _check_host(pv, ihost, op):
ilvgid = pv.get('forilvgid') or pv.get('ilvg_uuid')
ilvgid = pv.get('forilvgid') or pv.get('ilvg_uuid')
if ilvgid is None:
LOG.warn("check_host: lvg is None from pv. return.")
return
ilvg = pecan.request.dbapi.ilvg_get(ilvgid)
if (ilvg.lvm_vg_name == constants.LVG_CGTS_VG):
if (ihost['personality'] != constants.CONTROLLER and
ihost['personality'] != constants.WORKER):
raise wsme.exc.ClientSideError(
_("Physical volume operations for %s are only "
"supported on %s and %s hosts" %
(constants.LVG_CGTS_VG,
constants.WORKER,
constants.CONTROLLER)))
# semantic check: host must be locked for a nova-local change on
# a host with a worker subfunction (worker or AIO)
if (constants.WORKER in ihost['subfunctions'] and
ilvg.lvm_vg_name == constants.LVG_NOVA_LOCAL and
(ihost['administrative'] != constants.ADMIN_LOCKED or
ihost['ihost_action'] == constants.UNLOCK_ACTION)):
raise wsme.exc.ClientSideError(_("Host must be locked"))
# semantic check: host must be locked for a CGTS change on
# a worker host.
if (ihost['personality'] == constants.WORKER and
ilvg.lvm_vg_name == constants.LVG_CGTS_VG and
(ihost['administrative'] != constants.ADMIN_LOCKED or
ihost['ihost_action'] == constants.UNLOCK_ACTION)):
raise wsme.exc.ClientSideError(_("Host must be locked"))
def _get_vg_size_from_pvs(lvg, filter_pv=None):
ipvs = pecan.request.dbapi.ipv_get_by_ihost(lvg['forihostid'])
if not ipvs:
raise wsme.exc.ClientSideError(
_("Volume Group %s does not have any PVs assigned. "
"Assign PVs first." % lvg['lvm_vg_name']))
size = 0
for pv in ipvs:
# Skip the physical volume. Used to calculate potential new size of a
# physical volume is deleted
if filter_pv and pv['uuid'] == filter_pv['uuid']:
continue
# Only use physical volumes that belong to this volume group and are
# not in the removing state
if ((pv['lvm_vg_name'] == lvg['lvm_vg_name']) and
(pv['pv_state'] != constants.LVG_DEL)):
idisks = pecan.request.dbapi.idisk_get_by_ipv(pv['uuid'])
partitions = pecan.request.dbapi.partition_get_by_ipv(pv['uuid'])
if not idisks and not partitions:
raise wsme.exc.ClientSideError(
_("Internal Error: PV %s does not have an associated idisk"
" or partition" % pv.uuid))
if len(idisks) > 1:
raise wsme.exc.ClientSideError(
_("Internal Error: More than one idisk associated with PV "
"%s " % pv.uuid))
elif len(partitions) > 1:
raise wsme.exc.ClientSideError(
_("Internal Error: More than one partition associated with"
"PV %s " % pv.uuid))
elif len(idisks) + len(partitions) > 1:
raise wsme.exc.ClientSideError(
_("Internal Error: At least one disk and one partition "
"associated with PV %s " % pv.uuid))
if idisks:
size += idisks[0]['size_mib']
elif partitions:
size += partitions[0]['size_mib']
# Might have the case of a single PV being added, then removed.
# Or on the combo node we have other VGs with PVs present.
if size == 0:
raise wsme.exc.ClientSideError(
_("Volume Group %s must contain physical volumes. "
% lvg['lvm_vg_name']))
return size
def _check_lvg(op, pv):
# semantic check whether idisk is associated
ilvgid = pv.get('forilvgid') or pv.get('ilvg_uuid')
if ilvgid is None:
LOG.warn("check_lvg: lvg is None from pv. return.")
return
# Get the associated volume group record
ilvg = pecan.request.dbapi.ilvg_get(ilvgid)
# In a combo node we also have cinder and drbd physical volumes.
if ilvg.lvm_vg_name not in constants.LVG_ALLOWED_VGS:
raise wsme.exc.ClientSideError(_("This operation can not be performed"
" on Local Volume Group %s"
% ilvg.lvm_vg_name))
# Make sure that the volume group is in the adding/provisioned state
if ilvg.vg_state == constants.LVG_DEL:
raise wsme.exc.ClientSideError(
_("Local volume Group. %s set to be deleted. Add it again to allow"
" adding physical volumes. " % ilvg.lvm_vg_name))
# Semantic Checks: Based on PV operations
if op == "add":
if ilvg.lvm_vg_name == constants.LVG_CGTS_VG:
controller_fs_list = pecan.request.dbapi.controller_fs_get_list()
for controller_fs in controller_fs_list:
if controller_fs.state == constants.CONTROLLER_FS_RESIZING_IN_PROGRESS:
msg = _(
"Filesystem (%s) resize is in progress. Wait fot the resize "
"to finish before adding a physical volume to the cgts-vg "
"volume group." % controller_fs.name)
raise wsme.exc.ClientSideError(msg)
elif op == "delete":
if (ilvg.lvm_vg_name == constants.LVG_CGTS_VG):
raise wsme.exc.ClientSideError(
_("Physical volumes cannot be removed from the cgts-vg volume "
"group."))
if ilvg.lvm_vg_name == constants.LVG_CINDER_VOLUMES:
if ((pv['pv_state'] in
[constants.PROVISIONED, constants.PV_ADD]) and
StorageBackendConfig.has_backend(
pecan.request.dbapi, constants.CINDER_BACKEND_LVM)):
raise wsme.exc.ClientSideError(
_("Physical volume %s cannot be removed from cinder-volumes LVG once "
"it is provisioned and LVM backend is added." % pv['lvm_pv_name']))
elif op == "modify":
pass
else:
raise wsme.exc.ClientSideError(
_("Internal Error: Invalid Physical Volume operation: %s" % op))
# LVG check passes
pv['lvm_vg_name'] = ilvg.lvm_vg_name
return
def _check_parameters(pv):
# Disk/Partition should be provided for all cases
if 'disk_or_part_uuid' not in pv:
LOG.error(_("Missing idisk_uuid."))
raise wsme.exc.ClientSideError(_("Invalid data: Missing "
"disk_or_part_uuid. Failed to create a"
" physical volume object"))
# LVG should be provided for all cases
if 'ilvg_uuid' not in pv and 'forilvgid' not in pv:
LOG.error(_("Missing ilvg_uuid."))
raise wsme.exc.ClientSideError(_("Invalid data: Missing ilvg_uuid."
" Failed to create a physical "
"volume object"))
def _check_device(new_pv, ihost):
"""Check that the PV is not requesting a device that is already used."""
# derive the correct pv_type based on the UUID provided
try:
new_pv_device = pecan.request.dbapi.idisk_get(
new_pv['disk_or_part_uuid'])
new_pv['pv_type'] = constants.PV_TYPE_DISK
except exception.DiskNotFound:
try:
new_pv_device = pecan.request.dbapi.partition_get(
new_pv['disk_or_part_uuid'])
new_pv['pv_type'] = constants.PV_TYPE_PARTITION
except exception.DiskPartitionNotFound:
raise wsme.exc.ClientSideError(
_("Invalid data: The device %s associated with %s does not "
"exist.") % new_pv['disk_or_part_uuid'])
# Fill in the volume group info
ilvgid = new_pv.get('forilvgid') or new_pv.get('ilvg_uuid')
ilvg = pecan.request.dbapi.ilvg_get(ilvgid)
new_pv['forilvgid'] = ilvg['id']
new_pv['lvm_vg_name'] = ilvg['lvm_vg_name']
if new_pv['pv_type'] == constants.PV_TYPE_DISK:
# semantic check: Can't associate cinder-volumes to a disk
if ilvg.lvm_vg_name == constants.LVG_CINDER_VOLUMES:
raise wsme.exc.ClientSideError(
_("Invalid data: cinder-volumes PV has to be partition based."))
capabilities = new_pv_device['capabilities']
# semantic check: Can't associate the rootfs disk with a physical volume
if ('stor_function' in capabilities and
capabilities['stor_function'] == 'rootfs'):
raise wsme.exc.ClientSideError(_("Cannot assign the rootfs disk "
"to a physical volume."))
# semantic check: Can't add the disk if it's already associated
# with a physical volume
if new_pv_device.foripvid is not None:
raise wsme.exc.ClientSideError(_("Disk already assigned to a "
"physical volume."))
# semantic check: Can't add the disk if it's already associated
# with a storage volume
if new_pv_device.foristorid is not None:
raise wsme.exc.ClientSideError(_("Disk already assigned to a "
"storage volume."))
# semantic check: whether idisk_uuid belongs to another host
if new_pv_device.forihostid != new_pv['forihostid']:
raise wsme.exc.ClientSideError(_("Disk is attached to a different "
"host"))
else:
# Perform a quick validation check on this partition as it may be added
# immediately.
if (ilvg.lvm_vg_name == constants.LVG_CGTS_VG and
((ihost['invprovision'] in [constants.PROVISIONED,
constants.PROVISIONING]) and
(new_pv_device.status != constants.PARTITION_READY_STATUS)) or
((ihost['invprovision'] not in [constants.PROVISIONED,
constants.PROVISIONING]) and
(new_pv_device.status not in [
constants.PARTITION_CREATE_ON_UNLOCK_STATUS,
constants.PARTITION_READY_STATUS]))):
raise wsme.exc.ClientSideError(
_("The partition %s is not in an acceptable state to be added "
"as a physical volume: %s.") %
(new_pv_device.device_path,
constants.PARTITION_STATUS_MSG[new_pv_device.status]))
new_pv['disk_or_part_device_path'] = new_pv_device.device_path
# Since physical volumes are reported as device nodes and not device
# paths, we need to translate this, but not for local storage profiles.
if ihost['recordtype'] != 'profile':
if new_pv_device.device_node:
new_pv['disk_or_part_device_node'] = new_pv_device.device_node
new_pv['lvm_pv_name'] = new_pv['disk_or_part_device_node']
# relationship checks
# - Only one pv for cinder-volumes
# - if the PV is using a disk, make sure there is no other PV using
# a partition on that disk.
# - if the PV is using a partition, make sure there is no other PV
# using the entire disk
# perform relative PV checks
pvs = pecan.request.dbapi.ipv_get_by_ihost(ihost['uuid'])
for pv in pvs:
# semantic check: cinder_volumes supports a single physical volume
if (pv['lvm_vg_name'] ==
new_pv['lvm_vg_name'] ==
constants.LVG_CINDER_VOLUMES):
msg = _("A physical volume is already configured "
"for %s." % constants.LVG_CINDER_VOLUMES)
raise wsme.exc.ClientSideError(msg)
if (pv.disk_or_part_device_path in new_pv_device.device_path or
new_pv_device.device_path in pv.disk_or_part_device_path):
# Guard against reusing a partition PV and adding a disk PV if
# currently being used
if pv.pv_state != constants.PV_DEL:
if new_pv['pv_type'] == constants.PV_TYPE_DISK:
raise wsme.exc.ClientSideError(
_("Invalid data: This disk is in use by another "
"physical volume. Cannot use this disk: %s") %
new_pv_device.device_path)
else:
raise wsme.exc.ClientSideError(
_("Invalid data: The device requested for this Physical "
"Volume is already in use by another physical volume"
": %s") %
new_pv_device.device_path)
# Guard against a second partition on a cinder disk from being used in
# another volume group. This will potentially prevent cinder volume
# resizes. The exception is the root disk for 1-disk installs.
if new_pv['pv_type'] == constants.PV_TYPE_PARTITION:
# Get the disk associated with the new partition, if it exists.
idisk = pecan.request.dbapi.idisk_get(new_pv_device.idisk_uuid)
capabilities = idisk['capabilities']
# see if this is the root disk
if not ('stor_function' in capabilities and
capabilities['stor_function'] == 'rootfs'):
# Not a root disk so look for other cinder PVs and check for conflict
for pv in pvs:
if (pv['lvm_vg_name'] == constants.LVG_CINDER_VOLUMES and
idisk.device_path in pv.disk_or_part_device_path):
msg = (
_("Cannot use this partition. A partition (%s) on this "
"disk is already in use by %s.") % (
pv.disk_or_part_device_path,
constants.LVG_CINDER_VOLUMES))
raise wsme.exc.ClientSideError(msg)
def _check(op, pv):
# Semantic checks
LOG.debug("Semantic check for %s operation" % op)
# Check parameters
_check_parameters(pv)
# Get the host record
ihost = pecan.request.dbapi.ihost_get(pv['forihostid']).as_dict()
# Check host and host state
_check_host(pv, ihost, op)
if op == "add":
# Check that the device is available:
_check_device(pv, ihost)
elif op == "delete":
if pv['pv_state'] == constants.PV_DEL:
raise wsme.exc.ClientSideError(
_("Physical Volume (%s) "
"already marked for removal." %
pv['lvm_pv_name']))
elif op == "modify":
pass
else:
raise wsme.exc.ClientSideError(
_("Internal Error: Invalid Physical Volume operation: %s" % op))
# Add additional checks here
_check_lvg(op, pv)
return pv
def _prepare_cinder_db_for_volume_restore():
"""
Send a request to cinder to remove all volume snapshots and set all volumes
to error state in preparation for restoring all volumes.
This is needed for cinder disk replacement.
"""
try:
pecan.request.rpcapi.cinder_prepare_db_for_volume_restore(
pecan.request.context)
except rpc_common.RemoteError as e:
raise wsme.exc.ClientSideError(str(e.value))
def _update_disk_or_partition(func, pv):
ihost = pecan.request.dbapi.ihost_get(pv.get('forihostid'))
get_method = getattr(pecan.request.dbapi, func + '_get')
get_all_method = getattr(pecan.request.dbapi, func + '_get_all')
update_method = getattr(pecan.request.dbapi, func + '_update')
# Find the disk or partitions and update the foripvid field.
disks_or_partitions = get_all_method(foripvid=pv['id'])
for phys in disks_or_partitions:
if phys['uuid'] == pv['disk_or_part_uuid']:
values = {'foripvid': None}
try:
update_method(phys.id, values)
except exception.HTTPNotFound:
msg = _("%s update of foripvid failed: "
"host %s PV %s"
% (func, ihost['hostname'], pv.lvm_pv_name))
raise wsme.exc.ClientSideError(msg)
phys = None
if pv['disk_or_part_uuid']:
phys = get_method(pv['disk_or_part_uuid'])
# Mark the pv for deletion
if pv['pv_state'] == constants.PV_ADD:
err_msg = "Failed to delete pv %s on host %s"
else:
err_msg = "Marking pv %s for deletion failed on host %s"
values = {'pv_state': constants.PV_DEL}
try:
# If the PV will be created on unlock it is safe to remove the DB
# entry for this PV instead of putting it to removing(on unlock).
if pv['pv_state'] == constants.PV_ADD:
pecan.request.dbapi.ipv_destroy(pv['id'])
else:
pecan.request.dbapi.ipv_update(pv['id'], values)
except exception.HTTPNotFound:
msg = _(err_msg % (pv['lvm_pv_name'], ihost['hostname']))
raise wsme.exc.ClientSideError(msg)
# Return the disk or partition
return phys
def delete_pv(pv_uuid, force=False):
"""Delete a PV"""
pv = objects.pv.get_by_uuid(pecan.request.context, pv_uuid)
pv = pv.as_dict()
# Semantic checks
if not force:
_check("delete", pv)
# Update disk
if pv['pv_type'] == constants.PV_TYPE_DISK:
_update_disk_or_partition('idisk', pv)
elif pv['pv_type'] == constants.PV_TYPE_PARTITION:
_update_disk_or_partition('partition', pv)
# If the partition already exists, don't modify its status. Wait
# for when the PV is actually deleted to do so.
# If the host hasn't been provisioned yet, then the partition will
# be created on unlock, so it's status should remain the same.
# TODO (rchurch): Fix system host-pv-add 1 cinder-volumes <disk uuid> => no error message
# TODO (rchurch): Fix system host-pv-add -t disk 1 cinder-volumes <disk uuid> => confusing message
# TODO (rchurch): remove the -t options and use path/node/uuid to derive the type of PV