blob: f2bc1bad79e3b0812c019be3774cd65b58ea2d07 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
#include <algorithm>
#include <string>
#include <vector>
#include <glog/logging.h>
#include <mesos/type_utils.hpp>
#include <stout/foreach.hpp>
#include <stout/hashmap.hpp>
#include <stout/hashset.hpp>
#include <stout/lambda.hpp>
#include <stout/none.hpp>
#include <stout/stringify.hpp>
#include "master/master.hpp"
#include "master/validation.hpp"
using std::string;
using std::vector;
using google::protobuf::RepeatedPtrField;
namespace mesos {
namespace internal {
namespace master {
namespace validation {
// A helper function which returns true if the given character is not
// suitable for an ID.
static bool invalid(char c)
{
return iscntrl(c) || c == '/' || c == '\\';
}
namespace scheduler {
namespace call {
Option<Error> validate(const mesos::scheduler::Call& call)
{
if (!call.IsInitialized()) {
return Error("Not initialized: " + call.InitializationErrorString());
}
if (call.type() == mesos::scheduler::Call::SUBSCRIBE) {
if (!call.has_subscribe()) {
return Error("Expecting 'subscribe' to be present");
}
if (!(call.subscribe().framework_info().id() == call.framework_id())) {
return Error("'framework_id' differs from 'subscribe.framework_info.id'");
}
return None();
}
// All calls except SUBSCRIBE should have framework id set.
if (!call.has_framework_id()) {
return Error("Expecting 'framework_id' to be present");
}
switch (call.type()) {
case mesos::scheduler::Call::TEARDOWN:
return None();
case mesos::scheduler::Call::ACCEPT:
if (!call.has_accept()) {
return Error("Expecting 'accept' to be present");
}
return None();
case mesos::scheduler::Call::DECLINE:
if (!call.has_decline()) {
return Error("Expecting 'decline' to be present");
}
return None();
case mesos::scheduler::Call::REVIVE:
return None();
case mesos::scheduler::Call::SUPPRESS:
return None();
case mesos::scheduler::Call::KILL:
if (!call.has_kill()) {
return Error("Expecting 'kill' to be present");
}
return None();
case mesos::scheduler::Call::SHUTDOWN:
if (!call.has_shutdown()) {
return Error("Expecting 'shutdown' to be present");
}
return None();
case mesos::scheduler::Call::ACKNOWLEDGE:
if (!call.has_acknowledge()) {
return Error("Expecting 'acknowledge' to be present");
}
return None();
case mesos::scheduler::Call::RECONCILE:
if (!call.has_reconcile()) {
return Error("Expecting 'reconcile' to be present");
}
return None();
case mesos::scheduler::Call::MESSAGE:
if (!call.has_message()) {
return Error("Expecting 'message' to be present");
}
return None();
case mesos::scheduler::Call::REQUEST:
if (!call.has_request()) {
return Error("Expecting 'request' to be present");
}
return None();
default:
return Error("Unknown call type");
}
}
} // namespace call {
} // namespace scheduler {
namespace resource {
// Validates the ReservationInfos specified in the given resources (if
// exist). Returns error if any ReservationInfo is found invalid or
// unsupported.
Option<Error> validateDynamicReservationInfo(
const RepeatedPtrField<Resource>& resources)
{
foreach (const Resource& resource, resources) {
if (!Resources::isDynamicallyReserved(resource)) {
continue;
}
if (Resources::isRevocable(resource)) {
return Error(
"Dynamically reserved resource " + stringify(resource) +
" cannot be created from revocable resources");
}
}
return None();
}
// Validates the DiskInfos specified in the given resources (if
// exist). Returns error if any DiskInfo is found invalid or
// unsupported.
Option<Error> validateDiskInfo(const RepeatedPtrField<Resource>& resources)
{
foreach (const Resource& resource, resources) {
if (!resource.has_disk()) {
continue;
}
if (resource.disk().has_persistence()) {
if (Resources::isRevocable(resource)) {
return Error(
"Persistent volumes cannot be created from revocable resources");
}
if (Resources::isUnreserved(resource)) {
return Error(
"Persistent volumes cannot be created from unreserved resources");
}
if (!resource.disk().has_volume()) {
return Error("Expecting 'volume' to be set for persistent volume");
}
if (resource.disk().volume().mode() == Volume::RO) {
return Error("Read-only persistent volume not supported");
}
if (resource.disk().volume().has_host_path()) {
return Error("Expecting 'host_path' to be unset for persistent volume");
}
// Ensure persistence ID does not have invalid characters.
string id = resource.disk().persistence().id();
if (std::count_if(id.begin(), id.end(), invalid) > 0) {
return Error("Persistence ID '" + id + "' contains invalid characters");
}
} else if (resource.disk().has_volume()) {
return Error("Non-persistent volume not supported");
} else if (!resource.disk().has_source()) {
return Error("DiskInfo is set but empty");
}
}
return None();
}
// Validates the uniqueness of the persistence IDs used in the given
// resources. They need to be unique per role on each slave.
Option<Error> validateUniquePersistenceID(
const RepeatedPtrField<Resource>& resources)
{
hashmap<string, hashset<string>> persistenceIds;
// Check duplicated persistence ID within the given resources.
Resources volumes = Resources(resources).persistentVolumes();
foreach (const Resource& volume, volumes) {
const string& role = volume.role();
const string& id = volume.disk().persistence().id();
if (persistenceIds.contains(role) &&
persistenceIds[role].contains(id)) {
return Error("Persistence ID '" + id + "' is not unique");
}
persistenceIds[role].insert(id);
}
return None();
}
// Validates that all the given resources are persistent volumes.
Option<Error> validatePersistentVolume(
const RepeatedPtrField<Resource>& volumes)
{
foreach (const Resource& volume, volumes) {
if (!volume.has_disk()) {
return Error("Resource " + stringify(volume) + " does not have DiskInfo");
} else if (!volume.disk().has_persistence()) {
return Error("'persistence' is not set in DiskInfo");
}
}
return None();
}
Option<Error> validate(const RepeatedPtrField<Resource>& resources)
{
Option<Error> error = Resources::validate(resources);
if (error.isSome()) {
return Error("Invalid resources: " + error.get().message);
}
error = validateDiskInfo(resources);
if (error.isSome()) {
return Error("Invalid DiskInfo: " + error.get().message);
}
error = validateDynamicReservationInfo(resources);
if (error.isSome()) {
return Error("Invalid ReservationInfo: " + error.get().message);
}
return None();
}
} // namespace resource {
namespace task {
namespace internal {
// Validates that a task id is valid, i.e., contains only valid
// characters.
Option<Error> validateTaskID(const TaskInfo& task)
{
const string& id = task.task_id().value();
if (std::count_if(id.begin(), id.end(), invalid) > 0) {
return Error("TaskID '" + id + "' contains invalid characters");
}
return None();
}
// Validates that the TaskID does not collide with any existing tasks
// for the framework.
Option<Error> validateUniqueTaskID(const TaskInfo& task, Framework*
framework)
{
const TaskID& taskId = task.task_id();
if (framework->tasks.contains(taskId)) {
return Error("Task has duplicate ID: " + taskId.value());
}
return None();
}
// Validates that the slave ID used by a task is correct.
Option<Error> validateSlaveID(const TaskInfo& task, Slave* slave)
{
if (task.slave_id() != slave->id) {
return Error(
"Task uses invalid slave " + task.slave_id().value() +
" while slave " + slave->id.value() + " is expected");
}
return None();
}
// Validates that tasks that use the "same" executor (i.e., same
// ExecutorID) have an identical ExecutorInfo.
Option<Error> validateExecutorInfo(
const TaskInfo& task, Framework* framework, Slave* slave)
{
if (task.has_executor() == task.has_command()) {
return Error(
"Task should have at least one (but not both) of CommandInfo or "
"ExecutorInfo present");
}
if (task.has_executor()) {
// The master currently expects ExecutorInfo.framework_id to be
// set even though it is an optional field. Currently, the
// scheduler driver ensures that the field is set. For schedulers
// not using the driver, we need to do the validation here.
CHECK(task.executor().has_framework_id());
if (task.executor().framework_id() != framework->id()) {
return Error(
"ExecutorInfo has an invalid FrameworkID"
" (Actual: " + stringify(task.executor().framework_id()) +
" vs Expected: " + stringify(framework->id()) + ")");
}
const ExecutorID& executorId = task.executor().executor_id();
Option<ExecutorInfo> executorInfo = None();
if (slave->hasExecutor(framework->id(), executorId)) {
executorInfo =
slave->executors.get(framework->id()).get().get(executorId);
}
if (executorInfo.isSome() && !(task.executor() == executorInfo.get())) {
return Error(
"Task has invalid ExecutorInfo (existing ExecutorInfo"
" with same ExecutorID is not compatible).\n"
"------------------------------------------------------------\n"
"Existing ExecutorInfo:\n" +
stringify(executorInfo.get()) + "\n"
"------------------------------------------------------------\n"
"Task's ExecutorInfo:\n" +
stringify(task.executor()) + "\n"
"------------------------------------------------------------\n");
}
}
return None();
}
// Validates that the task and the executor are using proper amount of
// resources. For instance, the used resources by a task on a slave
// should not exceed the total resources offered on that slave.
Option<Error> validateResourceUsage(
const TaskInfo& task,
Framework* framework,
Slave* slave,
const Resources& offered)
{
Resources taskResources = task.resources();
if (taskResources.empty()) {
return Error("Task uses no resources");
}
Resources executorResources;
if (task.has_executor()) {
executorResources = task.executor().resources();
}
// Validate minimal cpus and memory resources of executor and log
// warnings if not set.
if (task.has_executor()) {
// TODO(martin): MESOS-1807. Return Error instead of logging a
// warning in 0.22.0.
Option<double> cpus = executorResources.cpus();
if (cpus.isNone() || cpus.get() < MIN_CPUS) {
LOG(WARNING)
<< "Executor " << stringify(task.executor().executor_id())
<< " for task " << stringify(task.task_id())
<< " uses less CPUs ("
<< (cpus.isSome() ? stringify(cpus.get()) : "None")
<< ") than the minimum required (" << MIN_CPUS
<< "). Please update your executor, as this will be mandatory "
<< "in future releases.";
}
Option<Bytes> mem = executorResources.mem();
if (mem.isNone() || mem.get() < MIN_MEM) {
LOG(WARNING)
<< "Executor " << stringify(task.executor().executor_id())
<< " for task " << stringify(task.task_id())
<< " uses less memory ("
<< (mem.isSome() ? stringify(mem.get().megabytes()) : "None")
<< ") than the minimum required (" << MIN_MEM
<< "). Please update your executor, as this will be mandatory "
<< "in future releases.";
}
}
// Validate if resources needed by the task (and its executor in
// case the executor is new) are available.
Resources total = taskResources;
if (!slave->hasExecutor(framework->id(), task.executor().executor_id())) {
total += executorResources;
}
if (!offered.contains(total)) {
return Error(
"Task uses more resources " + stringify(total) +
" than available " + stringify(offered));
}
return None();
}
// Validates that the resources specified by the task are valid.
Option<Error> validateResources(const TaskInfo& task)
{
Option<Error> error = resource::validate(task.resources());
if (error.isSome()) {
return Error("Task uses invalid resources: " + error.get().message);
}
Resources total = task.resources();
if (task.has_executor()) {
error = resource::validate(task.executor().resources());
if (error.isSome()) {
return Error("Executor uses invalid resources: " + error.get().message);
}
total += task.executor().resources();
}
// A task and its executor can either use non-revocable resources
// or revocable resources of a given name but not both.
foreach (const string& name, total.names()) {
Resources resources = total.get(name);
if (!resources.revocable().empty() &&
resources != resources.revocable()) {
return Error("Task (and its executor, if exists) uses both revocable and"
" non-revocable " + name);
}
}
error = resource::validateUniquePersistenceID(total);
if (error.isSome()) {
return error;
}
return None();
}
} // namespace internal {
Option<Error> validate(
const TaskInfo& task,
Framework* framework,
Slave* slave,
const Resources& offered)
{
CHECK_NOTNULL(framework);
CHECK_NOTNULL(slave);
// NOTE: The order in which the following validate functions are
// executed does matter! For example, 'validateResourceUsage'
// assumes that ExecutorInfo is valid which is verified by
// 'validateExecutorInfo'.
vector<lambda::function<Option<Error>()>> validators = {
lambda::bind(internal::validateTaskID, task),
lambda::bind(internal::validateUniqueTaskID, task, framework),
lambda::bind(internal::validateSlaveID, task, slave),
lambda::bind(internal::validateExecutorInfo, task, framework, slave),
lambda::bind(internal::validateResources, task),
lambda::bind(
internal::validateResourceUsage, task, framework, slave, offered)
};
// TODO(benh): Add a validateHealthCheck function.
// TODO(jieyu): Add a validateCommandInfo function.
foreach (const lambda::function<Option<Error>()>& validator, validators) {
Option<Error> error = validator();
if (error.isSome()) {
return error;
}
}
return None();
}
} // namespace task {
namespace offer {
Offer* getOffer(Master* master, const OfferID& offerId)
{
CHECK_NOTNULL(master);
return master->getOffer(offerId);
}
Slave* getSlave(Master* master, const SlaveID& slaveId)
{
CHECK_NOTNULL(master);
return master->slaves.registered.get(slaveId);
}
// Validates that an offer only appears once in offer list.
Option<Error> validateUniqueOfferID(const RepeatedPtrField<OfferID>& offerIds)
{
hashset<OfferID> offers;
foreach (const OfferID& offerId, offerIds) {
if (offers.contains(offerId)) {
return Error("Duplicate offer " + stringify(offerId) + " in offer list");
}
offers.insert(offerId);
}
return None();
}
// Validates that all offers belongs to the expected framework.
Option<Error> validateFramework(
const RepeatedPtrField<OfferID>& offerIds,
Master* master,
Framework* framework)
{
foreach (const OfferID& offerId, offerIds) {
Offer* offer = getOffer(master, offerId);
if (offer == NULL) {
return Error("Offer " + stringify(offerId) + " is no longer valid");
}
if (framework->id() != offer->framework_id()) {
return Error(
"Offer " + stringify(offer->id()) +
" has invalid framework " + stringify(offer->framework_id()) +
" while framework " + stringify(framework->id()) + " is expected");
}
}
return None();
}
// Validates that all offers belong to the same valid slave.
Option<Error> validateSlave(
const RepeatedPtrField<OfferID>& offerIds, Master* master)
{
Option<SlaveID> slaveId;
foreach (const OfferID& offerId, offerIds) {
Offer* offer = getOffer(master, offerId);
if (offer == NULL) {
return Error("Offer " + stringify(offerId) + " is no longer valid");
}
Slave* slave = getSlave(master, offer->slave_id());
// This is not possible because the offer should've been removed.
CHECK(slave != NULL)
<< "Offer " << offerId
<< " outlived slave " << offer->slave_id();
// This is not possible because the offer should've been removed.
CHECK(slave->connected)
<< "Offer " << offerId
<< " outlived disconnected slave " << *slave;
if (slaveId.isNone()) {
// Set slave id and use as base case for validation.
slaveId = slave->id;
}
if (slave->id != slaveId.get()) {
return Error(
"Aggregated offers must belong to one single slave. Offer " +
stringify(offerId) + " uses slave " +
stringify(slave->id) + " and slave " +
stringify(slaveId.get()));
}
}
return None();
}
Option<Error> validate(
const RepeatedPtrField<OfferID>& offerIds,
Master* master,
Framework* framework)
{
CHECK_NOTNULL(master);
CHECK_NOTNULL(framework);
vector<lambda::function<Option<Error>()>> validators = {
lambda::bind(validateUniqueOfferID, offerIds),
lambda::bind(validateFramework, offerIds, master, framework),
lambda::bind(validateSlave, offerIds, master)
};
foreach (const lambda::function<Option<Error>()>& validator, validators) {
Option<Error> error = validator();
if (error.isSome()) {
return error;
}
}
return None();
}
} // namespace offer {
namespace operation {
Option<Error> validate(
const Offer::Operation::Reserve& reserve,
const Option<string>& role,
const Option<string>& principal)
{
Option<Error> error = resource::validate(reserve.resources());
if (error.isSome()) {
return Error("Invalid resources: " + error.get().message);
}
// TODO(greggomann): Remove this check once dynamic reservation is
// allowed without a principal in 0.28.
if (principal.isNone()) {
return Error(
"Currently must have a principal associated with the request in order "
"to reserve resources. This will change in a future version. Note "
"that this is distinct from the principal contained in the resources");
}
foreach (const Resource& resource, reserve.resources()) {
if (!Resources::isDynamicallyReserved(resource)) {
return Error(
"Resource " + stringify(resource) + " is not dynamically reserved");
}
// TODO(greggomann): Remove this check once dynamic reservation is
// allowed without a principal in 0.28.
if (!resource.reservation().has_principal()) {
return Error(
"Reserved resources currently must contain a principal. "
"This will change in a future version");
}
if (role.isSome() && resource.role() != role.get()) {
return Error(
"The reserved resource's role '" + resource.role() +
"' does not match the framework's role '" + role.get() + "'");
}
if (resource.reservation().principal() != principal.get()) {
return Error(
"The reserved resource's principal '" +
resource.reservation().principal() +
"' does not match the principal '" +
principal.get() + "'");
}
// NOTE: This check would be covered by 'contains' since there
// shouldn't be any unreserved resources with 'disk' set.
// However, we keep this check since it will be a more useful
// error message than what contains would produce.
if (Resources::isPersistentVolume(resource)) {
return Error("A persistent volume " + stringify(resource) +
" must already be reserved");
}
}
return None();
}
Option<Error> validate(
const Offer::Operation::Unreserve& unreserve,
bool hasPrincipal)
{
Option<Error> error = resource::validate(unreserve.resources());
if (error.isSome()) {
return Error("Invalid resources: " + error.get().message);
}
if (!hasPrincipal) {
return Error(
"Currently cannot unreserve resources without a principal. "
"This will change in a future version");
}
// NOTE: We don't check that 'FrameworkInfo.principal' matches
// 'Resource.ReservationInfo.principal' here because the authorization
// depends on the "unreserve" ACL which specifies which 'principal' can
// unreserve which 'principal's resources. In the absense of an ACL, we allow
// any 'principal' to unreserve any other 'principal's resources.
foreach (const Resource& resource, unreserve.resources()) {
if (!Resources::isDynamicallyReserved(resource)) {
return Error(
"Resource " + stringify(resource) + " is not dynamically reserved");
}
if (Resources::isPersistentVolume(resource)) {
return Error(
"A dynamically reserved persistent volume " +
stringify(resource) +
" cannot be unreserved directly. Please destroy the persistent"
" volume first then unreserve the resource");
}
}
return None();
}
Option<Error> validate(
const Offer::Operation::Create& create,
const Resources& checkpointedResources)
{
Option<Error> error = resource::validate(create.volumes());
if (error.isSome()) {
return Error("Invalid resources: " + error.get().message);
}
error = resource::validatePersistentVolume(create.volumes());
if (error.isSome()) {
return Error("Not a persistent volume: " + error.get().message);
}
error = resource::validateUniquePersistenceID(
checkpointedResources + create.volumes());
if (error.isSome()) {
return error;
}
return None();
}
Option<Error> validate(
const Offer::Operation::Destroy& destroy,
const Resources& checkpointedResources)
{
Option<Error> error = resource::validate(destroy.volumes());
if (error.isSome()) {
return Error("Invalid resources: " + error.get().message);
}
error = resource::validatePersistentVolume(destroy.volumes());
if (error.isSome()) {
return Error("Not a persistent volume: " + error.get().message);
}
if (!checkpointedResources.contains(destroy.volumes())) {
return Error("Persistent volumes not found");
}
return None();
}
} // namespace operation {
} // namespace validation {
} // namespace master {
} // namespace internal {
} // namespace mesos {