All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vmware.photon.controller.model.resources.IPAddressService Maven / Gradle / Ivy

/*
 * Copyright (c) 2017 VMware, 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.
 */

package com.vmware.photon.controller.model.resources;

import static com.vmware.photon.controller.model.resources.IPAddressService.IPAddressState.DEFAULT_IP_VERSION;

import java.util.UUID;

import io.netty.util.internal.StringUtil;

import com.vmware.photon.controller.model.ServiceUtils;
import com.vmware.photon.controller.model.UriPaths;
import com.vmware.photon.controller.model.resources.IPAddressService.IPAddressState.IPAddressStatus;
import com.vmware.photon.controller.model.support.IPVersion;
import com.vmware.photon.controller.model.util.AssertUtil;
import com.vmware.photon.controller.model.util.SubnetValidator;
import com.vmware.xenon.common.LocalizableValidationException;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.ServiceDocument;
import com.vmware.xenon.common.ServiceDocumentDescription;
import com.vmware.xenon.common.StatefulService;
import com.vmware.xenon.common.Utils;

/**
 * Represents a statically assigned ip address from a pre-defined subnet range.
 *
 * @see SubnetRangeService.SubnetRangeState
 */
public class IPAddressService extends StatefulService {
    public static final String FACTORY_LINK = UriPaths.RESOURCES + "/ip-addresses";

    /**
     * Represents the state of an ip address.
     */
    public static class IPAddressState extends ResourceState {

        public static final String FIELD_NAME_SUBNET_RANGE_LINK = "subnetRangeLink";
        public static final String FIELD_NAME_IP_ADDRESS_STATUS = "ipAddressStatus";
        public static final String FIELD_NAME_CONNECTED_RESOURCE_LINK = "connectedResourceLink";

        // Default values for non-required fields
        public static final IPVersion DEFAULT_IP_VERSION = IPVersion.IPv4;

        public enum IPAddressStatus {
            ALLOCATED, // IP is allocated
            RELEASED,  // IP is no longer allocated, but still not available to be re-allocated
            AVAILABLE;  // IP is available for allocation, this is an intermediate state before the IPAddressState is being deleted

            /**
             * Allocated IPs should be in 'released' state before becoming 'available' again for allocation.
             * This method validates the status transitions.
             *
             * @param currentStatus current IPAddressStatus
             * @param newStatus     IPAddressStatus to transition to
             * @return true if the transition is valid
             */
            static boolean isValidTransition(IPAddressStatus currentStatus,
                    IPAddressStatus newStatus) {
                return (currentStatus != null && currentStatus.equals(newStatus) ||
                        (AVAILABLE.equals(currentStatus) && ALLOCATED.equals(newStatus)) ||
                        (ALLOCATED.equals(currentStatus) && RELEASED.equals(newStatus)) ||
                        (RELEASED.equals(currentStatus) && AVAILABLE.equals(newStatus)));
            }
        }

        /**
         * Link to the subnet range this IP belongs to.
         */
        @Documentation(description = "Link to the parent subnet range.")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.OPTIONAL,
                ServiceDocumentDescription.PropertyUsageOption.LINK,
                ServiceDocumentDescription.PropertyUsageOption.SINGLE_ASSIGNMENT
                })
        public String subnetRangeLink;

        /**
         * Link to the resource this IP is assigned to.
         */
        @Documentation(description = "Link to the resource this IP is assigned to.")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.OPTIONAL,
                ServiceDocumentDescription.PropertyUsageOption.LINK,
                ServiceDocumentDescription.PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL
                })
        public String connectedResourceLink;

        /**
         * Ip address.
         */
        @Documentation(description = "IP address")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.REQUIRED,
                ServiceDocumentDescription.PropertyUsageOption.SINGLE_ASSIGNMENT
                })
        public String ipAddress;

        /**
         * Whether the start and end ip address is IPv4 or IPv6.
         * If not set, default to IPv4.
         */
        @Documentation(description = "IP address version: IPv4 or IPv6. Default: IPv4")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.OPTIONAL,
                ServiceDocumentDescription.PropertyUsageOption.SINGLE_ASSIGNMENT
                })
        public IPVersion ipVersion;

        /**
         * The state of the IP address.
         */
        @Documentation(description = "IP address status: ALLOCATED, RELEASED or AVAILABLE")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.REQUIRED
                })
        public IPAddressStatus ipAddressStatus;

        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("name: ").append(this.name);
            sb.append(", id: ").append(this.id);
            sb.append(", subnet range link: ").append(this.subnetRangeLink);
            sb.append(", resource link: ").append(this.connectedResourceLink);
            sb.append(", IP address: ").append(this.ipAddress);
            sb.append(", IP version: ").append(this.ipVersion);
            sb.append(", status: ").append(this.ipAddressStatus);

            return sb.toString();
        }
    }

    public IPAddressService() {
        super(IPAddressState.class);
        super.toggleOption(ServiceOption.PERSISTENCE, true);
        super.toggleOption(ServiceOption.REPLICATION, true);
        super.toggleOption(ServiceOption.OWNER_SELECTION, true);
        super.toggleOption(ServiceOption.IDEMPOTENT_POST, true);
    }

    @Override
    public void handleStart(Operation start) {
        try {
            processInput(start);
            start.complete();
        } catch (Throwable t) {
            start.fail(t);
        }
    }

    @Override
    public void handleDelete(Operation delete) {
        ResourceUtils.handleDelete(delete, this);
    }

    @Override
    public void handlePost(Operation post) {
        try {
            IPAddressState returnState = processInput(post);
            setState(post, returnState);
            post.complete();
        } catch (Throwable t) {
            post.fail(t);
        }
    }

    @Override
    public void handlePut(Operation put) {
        if (!put.hasBody()) {
            put.fail(new IllegalArgumentException("body is required"));
        }

        try {
            IPAddressState newState = put.getBody(IPAddressState.class);

            // Verify valid status changes
            IPAddressState currentState = getState(put);
            if (isNoOperation(currentState, newState)) {
                put.complete();
                return;
            }
            // Clear connected resource when releasing the ip address
            if (IPAddressStatus.RELEASED.equals(newState.ipAddressStatus)) {
                newState.connectedResourceLink = null;
            }

            validateState(newState);
            validateIPAddressStatusTransition(currentState, newState);
            setState(put, newState);
            put.complete();
        } catch (Throwable t) {
            put.fail(t);
        }
    }

    @Override
    public void handlePatch(Operation patch) {
        if (!patch.hasBody()) {
            throw (new IllegalArgumentException("body is required"));
        }

        IPAddressState currentState = getState(patch);
        IPAddressState patchState = patch.getBody(IPAddressState.class);

        if (isNoOperation(currentState, patchState)) {
            patch.complete();
            return;
        }
        if (IPAddressStatus.RELEASED.equals(patchState.ipAddressStatus)) {
            patchState.connectedResourceLink = ResourceUtils.NULL_LINK_VALUE;
        }
        ResourceUtils.handlePatch(patch, currentState, getStateDescription(),
                IPAddressState.class, op -> {
                    boolean hasChanged = false;

                    // Verify valid status changes
                    if (patchState.ipAddressStatus != null
                            && patchState.ipAddressStatus != currentState.ipAddressStatus) {
                        validateIPAddressStatusTransition(currentState, patchState);
                        currentState.ipAddressStatus = patchState.ipAddressStatus;
                        validateIPAddressStatusWithConnectedResource(currentState);
                        hasChanged = true;
                    }

                    return Boolean.valueOf(hasChanged);
                });
    }

    @Override
    public ServiceDocument getDocumentTemplate() {
        ServiceDocument td = super.getDocumentTemplate();
        ServiceUtils.setRetentionLimit(td);
        IPAddressState template = (IPAddressState) td;

        template.id = UUID.randomUUID().toString();
        template.name = "ip-address";

        return template;
    }

    /**
     * @param op operation
     * @return a valid IPAddressState
     * @throws IllegalArgumentException if input invalid
     */
    private IPAddressState processInput(Operation op) {
        if (!op.hasBody()) {
            throw (new IllegalArgumentException("body is required"));
        }
        IPAddressState state = op.getBody(IPAddressState.class);
        validateState(state);
        return state;
    }

    /**
     * Validation upon creation of an IP address object.
     * - valid IP address
     * No need to validate that the IP is within the range.
     *
     * @param state IpAddressState to validate
     * @throws IllegalArgumentException for invalid state
     */
    private void validateState(IPAddressState state) {
        // Verify values based on the document description
        Utils.validateState(getStateDescription(), state);

        if (state.ipVersion == null) {
            state.ipVersion = DEFAULT_IP_VERSION;
        }

        if (!SubnetValidator.isValidIPAddress(state.ipAddress, state.ipVersion)) {
            throw new LocalizableValidationException(String.format("Invalid IP address: %s",
                    state.ipAddress),
                    "ip.address.invalid", state.ipAddress);
        }

        validateIPAddressStatusWithConnectedResource(state);

        logFine("Completed validation of IPAddressState: " + state);
    }

    /**
     * @param currentState current IP address
     * @param desiredState requested IP address
     * @throws IllegalArgumentException if an invalid transition
     */
    private void validateIPAddressStatusTransition(IPAddressState currentState,
            IPAddressState desiredState) {
        AssertUtil.assertTrue(IPAddressStatus
                        .isValidTransition(currentState.ipAddressStatus, desiredState.ipAddressStatus),
                String.format("Invalid IP address status transition from [%s] to [%s]",
                        currentState.ipAddressStatus, desiredState.ipAddressStatus));
    }

    /**
     * Validate connectedResourceLink is set if IP address is ALLOCATED and not set otherwise
     *
     * @param ipAddressState
     */
    private void validateIPAddressStatusWithConnectedResource(IPAddressState ipAddressState) {
        AssertUtil.assertFalse(ipAddressState.ipAddressStatus == IPAddressStatus.ALLOCATED
                        && StringUtil.isNullOrEmpty(ipAddressState.connectedResourceLink),
                "ConnectedResourceLink is required if IP address status is ALLOCATED");
        AssertUtil.assertFalse((ipAddressState.ipAddressStatus == IPAddressStatus.RELEASED
                        || ipAddressState.ipAddressStatus == IPAddressStatus.AVAILABLE)
                        && linkHasValue(ipAddressState.connectedResourceLink),
                "ConnectedResourceLink must be null if IP address status is AVAILABLE or RELEASED");
    }

    /**
     * Checks if a link has value.
     *
     * @param link
     * @return true when a link field has a value
     */
    private boolean linkHasValue(String link) {
        return (!ResourceUtils.NULL_LINK_VALUE.equals(link) &&
                !StringUtil.isNullOrEmpty(link));
    }

    /**
     * Used for PUT or PATCH. Handle the case of two consecutive release calls of the same IP address document.
     * Avoid modifying currentResourceLink.
     *
     * @param currentState
     * @param newState
     * @return
     */
    private boolean isNoOperation(IPAddressState currentState, IPAddressState newState) {
        // Avoid changing status if the connected resource has changed
        // Since deallocate can be called twice (for retry) - make sure
        // it is ignored if the resource has changed
        if (linkHasValue(newState.connectedResourceLink) &&
                linkHasValue(currentState.connectedResourceLink) &&
                !newState.connectedResourceLink.equals(currentState.connectedResourceLink)) {
            logWarning("Cannot modify IP address [%s] and change the connected resource link. Operation ignored (current state: [%s] new state: [%s]).",
                    currentState.documentSelfLink, currentState, newState);
            return true;
        }

        // Ignore change from AVAILABLE to RELEASED, can happen when deallocation called twice
        if (IPAddressStatus.RELEASED.equals(newState.ipAddressStatus) &&
                IPAddressStatus.AVAILABLE.equals(currentState.ipAddressStatus)) {
            logInfo("IP address [%s] is already available, and need not be released. Operation ignored.",
                    currentState.documentSelfLink);
            return true;
        }

        return false;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy