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

com.vmware.photon.controller.model.resources.SubnetRangeService 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.SubnetRangeService.SubnetRangeState.FIELD_NAME_SUBNET_LINK;

import java.net.URI;
import java.util.EnumSet;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import com.vmware.photon.controller.model.ServiceUtils;
import com.vmware.photon.controller.model.UriPaths;
import com.vmware.photon.controller.model.query.QueryUtils;
import com.vmware.photon.controller.model.query.QueryUtils.QueryTop;
import com.vmware.photon.controller.model.resources.SubnetService.SubnetState;
import com.vmware.photon.controller.model.support.IPVersion;
import com.vmware.photon.controller.model.util.AssertUtil;
import com.vmware.photon.controller.model.util.ClusterUtil.ServiceTypeCluster;
import com.vmware.photon.controller.model.util.SubnetValidator;

import com.vmware.xenon.common.DeferredResult;
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.ServiceDocumentDescription.DocumentIndexingOption;
import com.vmware.xenon.common.StatefulService;
import com.vmware.xenon.common.UriUtils;
import com.vmware.xenon.common.Utils;
import com.vmware.xenon.services.common.QueryTask.Query;

/**
 * Represents a range of IP addresses, assigned statically or by DHCP.
 * Reserved IP addresses should not be part of the range (for example, broadcast IP)
 *
 * @see SubnetService.SubnetState
 */
public class SubnetRangeService extends StatefulService {
    public static final String FACTORY_LINK = UriPaths.RESOURCES + "/subnet-ranges";

    /**
     * Represents the state of a subnet.
     */
    public static class SubnetRangeState extends ResourceState {

        public static final String FIELD_NAME_SUBNET_LINK = "subnetLink";
        public static final String FIELD_NAME_START_IP_ADDRESS = "startIPAddress";
        public static final String FIELD_NAME_END_IP_ADDRESS = "endIPAddress";
        public static final String FIELD_NAME_IP_VERSION = "ipVersion";
        public static final String FIELD_NAME_IP_IS_DHCP = "isDHCP";
        public static final String FIELD_NAME_DNS_SERVERS = "dnsServerAddresses";
        public static final String FIELD_NAME_DOMAIN = "domain";

        /**
         * Link to the subnet this subnet range is part of.
         */
        @Documentation(description = "Link to the parent subnet.")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.SINGLE_ASSIGNMENT,
                ServiceDocumentDescription.PropertyUsageOption.LINK
                })
        public String subnetLink;

        /**
         * Start IP address.
         */
        @Documentation(description = "Start IP address of the range")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.REQUIRED,
                ServiceDocumentDescription.PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL
                })
        public String startIPAddress;

        /**
         * End IP address.
         */
        @Documentation(description = "End IP address of the range")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.REQUIRED,
                ServiceDocumentDescription.PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL
                })
        public String endIPAddress;

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

        /**
         * Whether this IP range is managed by a DHCP server or static allocation.
         * If not set, default to false.
         */
        @Documentation(description = "Indication if the range is managed by DHCP. Default: false.")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.OPTIONAL,
                ServiceDocumentDescription.PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL
                })
        public Boolean isDHCP;

        /**
         * DNS IP addresses for this subnet range.
         * May override the SubnetState values.
         */
        @Documentation(description = "DNS server addresses")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.OPTIONAL
                })
        public List dnsServerAddresses;

        /**
         * DNS domain of the subnet range.
         * May override the SubnetState values.
         */
        @PropertyOptions(usage = ServiceDocumentDescription.PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
        public String domain;

        /**
         * DNS domain search (in order)
         * May override the SubnetState values.
         */
        @Documentation(description = "DNS search domains")
        @PropertyOptions(usage = {
                ServiceDocumentDescription.PropertyUsageOption.OPTIONAL
                })
        public List dnsSearchDomains;

        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("name: ").append(this.name);
            sb.append(", id: ").append(this.id);
            sb.append(", subnet: ").append(this.subnetLink);
            sb.append(", start IP address: ").append(this.startIPAddress);
            sb.append(", end IP address: ").append(this.endIPAddress);
            sb.append(", IP version: ").append(this.ipVersion);
            sb.append(", is DHCP: ").append(this.isDHCP);
            sb.append(", domain: ").append(this.domain);
            sb.append(", dnsSearchDomains: ").append(this.dnsSearchDomains == null ? null : this.dnsSearchDomains.toString());
            sb.append(", dnsServerAddresses: ").append(this.dnsServerAddresses == null ? null : this.dnsServerAddresses.toString());

            return sb.toString();
        }
    }

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

    /**
     * Comes in here for a http post for a new document
     */
    @Override
    public void handleCreate(Operation create) {
        try {

            SubnetRangeState subnetRangeState = getOperationBody(create);
            validateAll(subnetRangeState)
                    .whenCompleteNotify(create);
        } catch (Throwable t) {
            create.fail(t);
        }
    }

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

    /**
     * Comes in here for a http put on an existing doc
     */
    @Override
    public void handlePut(Operation put) {
        try {
            SubnetRangeState subnetRangeState = getOperationBody(put);

            validateAll(subnetRangeState)
                    .thenAccept((ignored) -> setState(put, subnetRangeState))
                    .whenCompleteNotify(put);
        } catch (Throwable t) {
            put.fail(t);
        }
    }

    /**
     * Comes in here for a http patch on an existing doc
     * For patch only the values being changed are sent.
     * getState() method fills in the missing values from the existing doc.
     */
    @Override
    public void handlePatch(Operation patch) {
        checkHasBody(patch);

        try {
            SubnetRangeState currentState = getState(patch);
            SubnetRangeState patchBody = patch.getBody(SubnetRangeState.class);
            // Merge the patch values to current state
            // In order to validate the merged result
            EnumSet mergeResult =
                    Utils.mergeWithStateAdvanced(getStateDescription(), currentState,
                            SubnetRangeState.class, patch);

            boolean hasStateChanged = mergeResult.contains(Utils.MergeResult.STATE_CHANGED);

            if (patchBody.dnsSearchDomains != null) {
                // replace dnsSearchDomains
                // dnsSearchDomains are overwritten -- it's not a merge
                currentState.dnsSearchDomains = patchBody.dnsSearchDomains;
                hasStateChanged = true;
            }

            if (patchBody.dnsServerAddresses != null) {
                // replace dnsServerAddresses
                // dnsServerAddresses are overwritten -- it's not a merge
                currentState.dnsServerAddresses = patchBody.dnsServerAddresses;
                hasStateChanged = true;
            }

            if (hasStateChanged) {
                validateAll(currentState)
                        .thenAccept((ignored) -> setState(patch, currentState))
                        .whenCompleteNotify(patch);
            } else {
                patch.setStatusCode(Operation.STATUS_CODE_NOT_MODIFIED);
                patch.complete();
            }

        } catch (Exception e) {
            this.logSevere(String.format("SubnetRangeService: failed to perform patch [%s]",
                    e.getMessage()));
            patch.fail(e);
        }
    }

    @Override
    public ServiceDocument getDocumentTemplate() {
        ServiceDocument td = super.getDocumentTemplate();
        ServiceUtils.setRetentionLimit(td);
        // enable metadata indexing
        td.documentDescription.documentIndexingOptions =
                EnumSet.of(DocumentIndexingOption.INDEX_METADATA);
        SubnetRangeState template = (SubnetRangeState) td;

        template.id = UUID.randomUUID().toString();
        template.name = "subnet-range";

        return template;
    }

    /**
     * Do all validations on the ip address before the data is committed.
     * In case of any validation error an exception is raised.
     *
     * @param subnetRangeState
     * @return A deferred result with no data. The return just means no exception was raised
     * hence its okay to proceed.
     */
    private DeferredResult validateAll(SubnetRangeState subnetRangeState) {

        validateState(subnetRangeState);

        return DeferredResult
                .allOf(
                        validateIps(subnetRangeState),
                        validateNoRangeOverlap(subnetRangeState)
                );
    }

    /**
     * Returns a Deferred result if start IP address and end IP address are within the network
     * specfied  by the subnet CIDR. If not an exception is raised in the method this calls.
     *
     * @param subnetRangeState The subnet state that is being validated.
     * @return a deferred result, that has no data. This method returning just validates the data.
     */
    private DeferredResult validateIps(SubnetRangeState subnetRangeState) {
        if (subnetRangeState.subnetLink != null) {
            return getSubnetState(subnetRangeState.subnetLink)
                    .thenAccept((op) -> {
                        SubnetState subnetState = op.getBody(SubnetState.class);
                        validateIpInRange(subnetRangeState, subnetState);
                    });
        }
        return DeferredResult.completed(null);
    }

    /**
     * Returns a Deferred result if there is no overlap of the current ip range with the pre
     * existing ip ranges. Else exception is raised (in the method this calls).
     *
     * @param subnetRangeState
     * @return A deferred result which indicates there was no range overlap.
     */
    private DeferredResult validateNoRangeOverlap(SubnetRangeState subnetRangeState) {
        if (subnetRangeState.subnetLink != null) {
            return getSubnetRangesInSubnet(subnetRangeState.subnetLink)
                    .thenAccept((subnetRangeList) -> {
                        validateIpsOutsideDefinedRanges(
                                subnetRangeState.documentSelfLink,
                                subnetRangeState.startIPAddress,
                                subnetRangeState.endIPAddress,
                                subnetRangeList);
                    });
        }
        return DeferredResult.completed(null);
    }

    /**
     * Validate:
     * - valid start IP address
     * - valid end IP address
     * - valid range
     *
     * @param state SubnetRangeState to validate
     */
    private void validateState(SubnetRangeState state) {
        Utils.validateState(getStateDescription(), state);

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

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

        if (SubnetValidator
                .isStartIPGreaterThanEndIP(state.startIPAddress, state.endIPAddress,
                        state.ipVersion)) {
            throw new LocalizableValidationException(
                    "Subnet range is invalid. Start IP address must be smaller than end IP address",
                    "subnet.range.ip.start.must.be.smaller");

        }
    }

    /**
     * Validate that the start and end IP addresses are inside the network specified by the CIDR.
     * If it's outside, an exception is raised.
     *
     * @param subnetRangeState The subnetRange state. This contains the start and end ip address
     *                         that was specified in the ip address range.
     * @param subnetstate      The subnet state contains the CIDR from which the valid network address
     *                         is identified.
     */
    private void validateIpInRange(SubnetRangeState subnetRangeState, SubnetState subnetstate) {

        if (!SubnetValidator
                .isIpInValidRange(
                        subnetRangeState.startIPAddress,
                        subnetstate.subnetCIDR,
                        subnetRangeState.ipVersion)) {
            throw new LocalizableValidationException(
                    String.format("Start IP address %s is invalid. It lies outside the "
                                    + "IP range specified by the CIDR: %s",
                            subnetRangeState.startIPAddress,
                            subnetstate.subnetCIDR),
                    "subnet.range.ip.outside.range.start",
                    subnetRangeState.startIPAddress,
                    subnetstate.subnetCIDR);
        }

        if (!SubnetValidator
                .isIpInValidRange(
                        subnetRangeState.endIPAddress,
                        subnetstate.subnetCIDR,
                        subnetRangeState.ipVersion)) {
            throw new LocalizableValidationException(
                    String.format("End IP address %s is invalid. It lies outside the "
                                    + "IP range specified by the CIDR: %s",
                            subnetRangeState.endIPAddress,
                            subnetstate.subnetCIDR),
                    "subnet.range.ip.outside.range.start",
                    subnetRangeState.endIPAddress,
                    subnetstate.subnetCIDR);
        }
    }

    /**
     * We don't want ip ranges to overlap. So we check the newly created ip range,
     * with the pre existing ip ranges and make sure there is no over lap.
     * If there is an overlap, raise exception.
     *
     * @param documentSelfLink  The self link of the subnetrange document being modified
     *                          This is empty if its a create.
     * @param startIp           The start ip provided for this subnet range
     * @param endIp             The end ip for this subnet range
     * @param subnetRangeStates This is a list of all the pre-existing subnet states. We check
     *                          for overlap against these.
     */
    private void validateIpsOutsideDefinedRanges(String documentSelfLink, String startIp,
            String endIp,
            List
                    subnetRangeStates) {
        String ipUnderTest;

        for (SubnetRangeState subnetRangeState : subnetRangeStates) {
            String selfLink = subnetRangeState.documentSelfLink;

            //For create self link is empty. Check against all pre existing subnet ranges
            //For updates or patches, don't check against self
            if (selfLink == null || !selfLink.equals(documentSelfLink)) {

                String ipBegin = subnetRangeState.startIPAddress;
                String ipEnd = subnetRangeState.endIPAddress;
                IPVersion ipVersion = subnetRangeState.ipVersion;

                ipUnderTest = startIp;
                throwExceptionIfIpOverlap(ipBegin, ipEnd, ipVersion, ipUnderTest);

                ipUnderTest = endIp;
                throwExceptionIfIpOverlap(ipBegin, ipEnd, ipVersion, ipUnderTest);
            }

        }
    }

    private void throwExceptionIfIpOverlap(String ipBegin, String ipEnd, IPVersion ipVersion,
            String ipUnderTest) {
        if (SubnetValidator.isIpInBetween(ipBegin, ipEnd, ipVersion,
                ipUnderTest
        )) {
            throw new LocalizableValidationException(
                    String.format("The submitted IP address range overlaps with a "
                                    + "previously defined IP address range: %s-%s ",
                            ipBegin, ipEnd),
                    "subnet.range.ip.overlap", ipBegin, ipEnd);
        }

    }

    /**
     * Fetch subnet state by document link.
     *
     * @param link Document link for the subnet service
     * @return A deferred result of an operation, that has the subnet state
     */
    private  DeferredResult getSubnetState(String link) {
        AssertUtil.assertNotEmpty(link, "Cannot fetch subnet details with an empty subnet link");
        URI uri = UriUtils.buildUri(getHost(), link);
        return sendWithDeferredResult(Operation.createGet(uri));
    }

    /**
     * Fetch all pre existing subnet ranges
     *
     * @param subnetLink
     * @return A deferred result that contains a list of pre-existing subnet ranges
     */
    private DeferredResult> getSubnetRangesInSubnet(String subnetLink) {
        Query.Builder qBuilder = Query.Builder.create()
                .addKindFieldClause(SubnetRangeState.class)
                .addFieldClause(FIELD_NAME_SUBNET_LINK, subnetLink);

        QueryTop queryTop = new QueryUtils.QueryTop<>(
                this.getHost(),
                qBuilder.build(),
                SubnetRangeState.class,
                null
        );

        queryTop.setClusterType(ServiceTypeCluster.INVENTORY_SERVICE);

        return queryTop.collectDocuments(Collectors.toList());
    }

    private SubnetRangeState getOperationBody(Operation operation) {
        checkHasBody(operation);
        SubnetRangeState subnetRangeState = operation.getBody(SubnetRangeState.class);
        return subnetRangeState;
    }

    private void checkHasBody(Operation operation) {
        if (!operation.hasBody()) {
            operation.fail(new IllegalArgumentException("body is required"));
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy