Skip to content

Commit

Permalink
Merge pull request kubevirt#1733 from vladikr/migrate_with_shared_pvc
Browse files Browse the repository at this point in the history
Migrate with shared PVC
  • Loading branch information
Artyom Lukianov authored Dec 17, 2018
2 parents 91094a3 + 4d7a968 commit cd09f01
Show file tree
Hide file tree
Showing 24 changed files with 905 additions and 26 deletions.
8 changes: 8 additions & 0 deletions api/openapi-spec/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -4556,6 +4556,10 @@
"description": "The path to HostDisk image located on the cluster",
"type": "string"
},
"shared": {
"description": "Shared indicate whether the path is shared between nodes",
"type": "boolean"
},
"type": {
"description": "Contains information if disk.img exists or should be created\nallowed options are 'Disk' and 'DiskOrCreate'",
"type": "string"
Expand Down Expand Up @@ -5969,6 +5973,10 @@
"$ref": "#/definitions/v1.VirtualMachineInstanceNetworkInterface"
}
},
"migrationMethod": {
"description": "Represents the method using which the vmi can be migrated: live migration or block migration",
"type": "string"
},
"migrationState": {
"description": "Represents the status of a live migration",
"$ref": "#/definitions/v1.VirtualMachineInstanceMigrationState"
Expand Down
2 changes: 1 addition & 1 deletion cmd/container-disk-v1alpha/entry-point.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ if [ $? -ne 0 ]; then
echo "Failed to convert image $IMAGE_PATH to .raw file"
exit 1
fi
else
else
cp $IMAGE_PATH ${COPY_PATH}.${IMAGE_EXTENSION}
if [ $? -ne 0 ]; then
echo "Failed to copy $IMAGE_PATH to $COPY_PATH.${IMAGE_EXTENSION}"
Expand Down
7 changes: 6 additions & 1 deletion images/cdi-http-import-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ FROM fedora:28
LABEL maintainer="The KubeVirt Project <[email protected]>"
ENV container docker

RUN dnf install -y nginx qemu-guest-agent \
RUN dnf install -y nginx qemu-guest-agent qemu-img scsi-target-utils \
&& dnf -y clean all

RUN mkdir -p /usr/share/nginx/html/images \
Expand All @@ -33,3 +33,8 @@ RUN cp /usr/bin/qemu-ga /usr/share/nginx/html/
ADD nginx.conf /etc/nginx/

EXPOSE 80

ADD entry-point.sh /
ADD expose-as-iscsi.sh /

CMD ["/entry-point.sh"]
37 changes: 37 additions & 0 deletions images/cdi-http-import-server/entry-point.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash
#
# This file is part of the KubeVirt project
#
# 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 2017 Red Hat, Inc.
#

# https://fedoraproject.org/wiki/Scsi-target-utils_Quickstart_Guide

trap 'echo "Graceful exit"; exit 0' SIGINT SIGQUIT SIGTERM

ALPINE_IMAGE_PATH=/usr/share/nginx/html/images/alpine.iso
IMAGE_PATH=/images

if [ -n "$AS_ISCSI" ]; then
mkdir -p $IMAGE_PATH
/usr/bin/qemu-img convert $ALPINE_IMAGE_PATH $IMAGE_PATH/alpine.raw
if [ $? -ne 0 ]; then
echo "Failed to convert image $ALPINE_IMAGE_PATH to .raw file"
exit 1
fi

touch /tmp/healthy
bash expose-as-iscsi.sh "${IMAGE_PATH}/alpine.raw"
fi
45 changes: 45 additions & 0 deletions images/cdi-http-import-server/expose-as-iscsi.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/bash

IMAGE_PATH="$1"

if [ ! -f "$IMAGE_PATH" ]; then
echo "vm image '$IMAGE_PATH' not found"
exit 1
fi

# USING 'set -e' error detection for everything below this point.
set -e

PORT=${PORT:-3260}
WWN=${WWN:-iqn.2018-01.io.kubevirt:wrapper}
LUNID=1

echo "Starting tgtd at port $PORT"
tgtd -f --iscsi portal="0.0.0.0:${PORT}" &
sleep 5

echo "Adding target and exposing it"
tgtadm --lld iscsi --mode target --op new --tid=1 --targetname $WWN
tgtadm --lld iscsi --mode target --op bind --tid=1 -I ALL

if [ -n "$PASSWORD" ]; then
echo "Adding authentication for user $USERNAME"
tgtadm --lld iscsi --op new --mode account --user $USERNAME --password $PASSWORD
tgtadm --lld iscsi --op bind --mode account --tid=1 --user $USERNAME
fi

echo "Adding volume file as LUN"
tgtadm --lld iscsi --mode logicalunit --op new --tid=1 --lun=$LUNID -b $IMAGE_PATH
tgtadm --lld iscsi --mode logicalunit --op update --tid=1 --lun=$LUNID --params thin_provisioning=1

echo "Start monitoring"
touch previous_state
while true; do
tgtadm --lld iscsi --mode target --op show >current_state
diff -q previous_state current_state || (
date
cat current_state
)
mv -f current_state previous_state
sleep 5
done
9 changes: 9 additions & 0 deletions pkg/api/v1/deepcopy_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions pkg/api/v1/openapi_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/api/v1/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type HostDisk struct {
// Capacity of the sparse disk
// +optional
Capacity resource.Quantity `json:"capacity,omitempty"`
// Shared indicate whether the path is shared between nodes
Shared *bool `json:"shared,omitempty"`
}

// ConfigMapVolumeSource adapts a ConfigMap into a volume.
Expand Down
1 change: 1 addition & 0 deletions pkg/api/v1/schema_swagger_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions pkg/api/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ type VirtualMachineInstanceStatus struct {
Interfaces []VirtualMachineInstanceNetworkInterface `json:"interfaces,omitempty"`
// Represents the status of a live migration
MigrationState *VirtualMachineInstanceMigrationState `json:"migrationState,omitempty"`
// Represents the method using which the vmi can be migrated: live migration or block migration
MigrationMethod VirtualMachineInstanceMigrationMethod `json:"migrationMethod,omitempty"`
}

// Required to satisfy Object interface
Expand Down Expand Up @@ -269,6 +271,11 @@ const (

// Reflects whether the QEMU guest agent is connected through the channel
VirtualMachineInstanceAgentConnected VirtualMachineInstanceConditionType = "AgentConnected"

// Indicates whether the VMI is live migratable
VirtualMachineInstanceIsMigratable VirtualMachineInstanceConditionType = "LiveMigratable"
// Reason means that VMI is not live migratioable because of it's disks collection
VirtualMachineInstanceReasonDisksNotMigratable = "DisksNotLiveMigratable"
)

// ---
Expand Down Expand Up @@ -334,6 +341,17 @@ type VirtualMachineInstanceMigrationState struct {
MigrationUID types.UID `json:"migrationUid,omitempty"`
}

// ---
// +k8s:openapi-gen=true
type VirtualMachineInstanceMigrationMethod string

const (
// BlockMigration means that all VirtualMachineInstance disks should be copied over to the destination host
BlockMigration VirtualMachineInstanceMigrationMethod = "BlockMigration"
// LiveMigration means that VirtualMachineInstance disks will not be copied over to the destination host
LiveMigration VirtualMachineInstanceMigrationMethod = "LiveMigration"
)

// VirtualMachineInstancePhase is a label for the condition of a VirtualMachineInstance at the current time.
// ---
// +k8s:openapi-gen=true
Expand Down
15 changes: 8 additions & 7 deletions pkg/api/v1/types_swagger_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/host-disk/host-disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ func ReplacePVCByHostDisk(vmi *v1.VirtualMachineInstance, clientset kubecli.Kube
} else if isBlockVolumePVC {
continue
}
isSharedPvc := types.IsPVCShared(pvc)

volumeSource.HostDisk = &v1.HostDisk{
Path: getPVCDiskImgPath(vmi.Spec.Volumes[i].Name),
Type: v1.HostDiskExistsOrCreate,
Capacity: pvc.Status.Capacity[k8sv1.ResourceStorage],
Shared: &isSharedPvc,
}
// PersistenVolumeClaim is replaced by HostDisk
volumeSource.PersistentVolumeClaim = nil
Expand Down
20 changes: 20 additions & 0 deletions pkg/util/types/pvc.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,23 @@ func isPVCBlock(pvc *k8sv1.PersistentVolumeClaim) bool {
// VolumeMode is Block, that unambiguously answers the question
return pvc.Spec.VolumeMode != nil && *pvc.Spec.VolumeMode == k8sv1.PersistentVolumeBlock
}

func IsPVCShared(pvc *k8sv1.PersistentVolumeClaim) (isShared bool) {
for _, accessMode := range pvc.Spec.AccessModes {
if accessMode == k8sv1.ReadWriteMany {
isShared = true
break
}
}
return
}

func IsSharedPVCFromClient(client kubecli.KubevirtClient, namespace string, claimName string) (pvc *k8sv1.PersistentVolumeClaim, isShared bool, err error) {
pvc, err = client.CoreV1().PersistentVolumeClaims(namespace).Get(claimName, v1.GetOptions{})
if err != nil {
return nil, false, err
}

isShared = IsPVCShared(pvc)
return pvc, isShared, nil
}
10 changes: 9 additions & 1 deletion pkg/util/types/pvc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ var _ = Describe("PVC utils test", func() {
TypeMeta: metav1.TypeMeta{Kind: "PersistentVolumeClaim", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: blockName},
Spec: kubev1.PersistentVolumeClaimSpec{
VolumeMode: &modeBlock,
VolumeMode: &modeBlock,
AccessModes: []kubev1.PersistentVolumeAccessMode{kubev1.ReadWriteMany},
},
}

Expand Down Expand Up @@ -144,6 +145,13 @@ var _ = Describe("PVC utils test", func() {
Expect(exists).To(BeTrue(), "PVC was found")
Expect(isBlock).To(Equal(true), "Is blockdevice PVC")
})
It("should detect shared block device for block VolumeMode", func() {
pvc, isShared, err := IsSharedPVCFromClient(virtClient, namespace, blockName)
Expect(err).ToNot(HaveOccurred(), "no error occured")
Expect(pvc).ToNot(BeNil(), "PVC isn't nil")
Expect(pvc.Name).To(Equal(blockName), "correct PVC was found")
Expect(isShared).To(Equal(true), "Is PVC Shared")
})
})

})
17 changes: 17 additions & 0 deletions pkg/virt-api/webhooks/validating-webhook/validating-webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,14 @@ func admitMigrationCreate(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionRespons
return webhooks.ToAdmissionResponseError(fmt.Errorf("Cannot migrated VMI in finalized state."))
}

// Reject migration jobs for non-migratable VMIs
cond := getVMIMigrationCondition(vmi)
if cond != nil && cond.Status == k8sv1.ConditionFalse {
errMsg := fmt.Errorf("Cannot migrate VMI, Reason: %s, Message: %s",
cond.Reason, cond.Message)
return webhooks.ToAdmissionResponseError(errMsg)
}

// Don't allow new migration jobs to be introduced when previous migration jobs
// are already in flight.
if vmi.Status.MigrationState != nil &&
Expand All @@ -1560,6 +1568,15 @@ func admitMigrationCreate(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionRespons
return &reviewResponse
}

func getVMIMigrationCondition(vmi *v1.VirtualMachineInstance) (cond *v1.VirtualMachineInstanceCondition) {
for _, c := range vmi.Status.Conditions {
if c.Type == v1.VirtualMachineInstanceIsMigratable {
cond = &c
}
}
return cond
}

func ServeMigrationCreate(resp http.ResponseWriter, req *http.Request) {
serve(resp, req, admitMigrationCreate)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,46 @@ var _ = Describe("Validating Webhook", func() {
Expect(resp.Allowed).To(Equal(false))
})

It("should reject Migration spec for non-migratable VMIs", func() {
vmi := v1.NewMinimalVMI("testmigratevmi3")
vmi.Status.Phase = v1.Succeeded
vmi.Status.Conditions = []v1.VirtualMachineInstanceCondition{
{
Type: v1.VirtualMachineInstanceIsMigratable,
Status: k8sv1.ConditionFalse,
Reason: v1.VirtualMachineInstanceReasonDisksNotMigratable,
Message: "cannot migrate VMI with mixes shared and non-shared volumes",
},
}

informers := webhooks.GetInformers()
informers.VMIInformer.GetIndexer().Add(vmi)

migration := v1.VirtualMachineInstanceMigration{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
},
Spec: v1.VirtualMachineInstanceMigrationSpec{
VMIName: "testmigratevmi3",
},
}
migrationBytes, _ := json.Marshal(&migration)

os.Setenv("FEATURE_GATES", "LiveMigration")

ar := &v1beta1.AdmissionReview{
Request: &v1beta1.AdmissionRequest{
Resource: webhooks.MigrationGroupVersionResource,
Object: runtime.RawExtension{
Raw: migrationBytes,
},
},
}

resp := admitMigrationCreate(ar)
Expect(resp.Allowed).To(Equal(false))
})

It("should reject Migration on update if spec changes", func() {
vmi := v1.NewMinimalVMI("testmigratevmiupdate")

Expand Down
Loading

0 comments on commit cd09f01

Please sign in to comment.