| // 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 <mutex> |
| #include <string> |
| #include <vector> |
| |
| #include <mesos/module.hpp> |
| |
| #include <mesos/module/anonymous.hpp> |
| #include <mesos/module/module.hpp> |
| |
| #include <stout/json.hpp> |
| #include <stout/numify.hpp> |
| #include <stout/os.hpp> |
| #include <stout/strings.hpp> |
| #include <stout/stringify.hpp> |
| #include <stout/version.hpp> |
| |
| #include "common/parse.hpp" |
| #include "messages/messages.hpp" |
| |
| #include "module/manager.hpp" |
| |
| using std::list; |
| using std::string; |
| using std::vector; |
| using process::Owned; |
| |
| using namespace mesos; |
| using namespace mesos::internal; |
| using namespace mesos::modules; |
| |
| std::mutex ModuleManager::mutex; |
| |
| // TODO(karya): MESOS-4917: Replace the following non-pod static variables with |
| // pod equivalents. Cleanup further by introducing additional data structures to |
| // avoid keeping multiple mappings from module names. |
| hashmap<string, string> ModuleManager::kindToVersion; |
| hashmap<string, ModuleBase*> ModuleManager::moduleBases; |
| hashmap<string, Parameters> ModuleManager::moduleParameters; |
| hashmap<string, string> ModuleManager::moduleLibraries; |
| hashmap<string, DynamicLibrary*> ModuleManager::dynamicLibraries; |
| |
| |
| void ModuleManager::initialize() |
| { |
| // ATTENTION: Every time a Mesos developer breaks compatibility with |
| // a module kind type, this table needs to be updated. |
| // Specifically, the version value in the entry corresponding to the |
| // kind needs to be set to the Mesos version that affects the |
| // current change. Typically that should be the version currently |
| // under development. |
| |
| kindToVersion["Allocator"] = MESOS_VERSION; |
| kindToVersion["Anonymous"] = MESOS_VERSION; |
| kindToVersion["Authenticatee"] = MESOS_VERSION; |
| kindToVersion["Authenticator"] = MESOS_VERSION; |
| kindToVersion["Authorizer"] = MESOS_VERSION; |
| kindToVersion["ContainerLogger"] = MESOS_VERSION; |
| kindToVersion["Hook"] = MESOS_VERSION; |
| kindToVersion["HttpAuthenticatee"] = MESOS_VERSION; |
| kindToVersion["HttpAuthenticator"] = MESOS_VERSION; |
| kindToVersion["Isolator"] = MESOS_VERSION; |
| kindToVersion["MasterContender"] = MESOS_VERSION; |
| kindToVersion["MasterDetector"] = MESOS_VERSION; |
| kindToVersion["QoSController"] = MESOS_VERSION; |
| kindToVersion["ResourceEstimator"] = MESOS_VERSION; |
| kindToVersion["SecretResolver"] = MESOS_VERSION; |
| kindToVersion["TestModule"] = MESOS_VERSION; |
| kindToVersion["DiskProfileAdaptor"] = MESOS_VERSION; |
| |
| // What happens then when Mesos is built with a certain version, |
| // 'kindToVersion' states a certain other minimum version, and a |
| // module library is built against "module.hpp" belonging to yet |
| // another Mesos version? |
| // |
| // Mesos can admit modules built against earlier versions of itself |
| // by stating so explicitly in 'kindToVersion'. If a module is |
| // built with a Mesos version greater than or equal to the one |
| // stated in 'kindToVersion', it passes this verification step. |
| // Otherwise it is rejected when attempting to load it. |
| // |
| // Here are some examples: |
| // |
| // Mesos kindToVersion library modules loadable? |
| // 0.18.0 0.18.0 0.18.0 YES |
| // 1.0.0 0.18.0 0.18.0 YES |
| // 1.0.0 0.18.0 0.21.0 YES |
| // 0.18.0 0.18.0 1.0.0 NO |
| // 1.0.0 0.21.0 0.18.0 NO |
| // 1.0.0 1.0.0 0.18.0 NO |
| |
| // ATTENTION: This mechanism only protects the interfaces of |
| // modules, not how they maintain functional compatibility with |
| // Mesos and among each other. This is covered by their own |
| // "isCompatible" call. |
| } |
| |
| |
| // For testing only. Unload a given module and remove it from the list |
| // of ModuleBases. |
| Try<Nothing> ModuleManager::unload(const string& moduleName) |
| { |
| synchronized (mutex) { |
| if (!moduleBases.contains(moduleName)) { |
| return Error( |
| "Error unloading module '" + moduleName + "': module not loaded"); |
| } |
| |
| // Do not remove the dynamiclibrary as it could result in |
| // unloading the library from the process memory. |
| moduleBases.erase(moduleName); |
| } |
| return Nothing(); |
| } |
| |
| |
| // TODO(karya): Show library author info for failed library/module. |
| Try<Nothing> ModuleManager::verifyModule( |
| const string& moduleName, |
| const ModuleBase* moduleBase) |
| { |
| CHECK_NOTNULL(moduleBase); |
| if (moduleBase->mesosVersion == nullptr || |
| moduleBase->moduleApiVersion == nullptr || |
| moduleBase->authorName == nullptr || |
| moduleBase->authorEmail == nullptr || |
| moduleBase->description == nullptr || |
| moduleBase->kind == nullptr) { |
| return Error("Error loading module '" + moduleName + "'; missing fields"); |
| } |
| |
| // Verify module API version. |
| if (stringify(moduleBase->moduleApiVersion) != MESOS_MODULE_API_VERSION) { |
| return Error( |
| "Module API version mismatch. Mesos has: " MESOS_MODULE_API_VERSION ", " |
| "library requires: " + stringify(moduleBase->moduleApiVersion)); |
| } |
| |
| if (!kindToVersion.contains(moduleBase->kind)) { |
| return Error("Unknown module kind: " + stringify(moduleBase->kind)); |
| } |
| |
| Try<Version> mesosVersion = Version::parse(MESOS_VERSION); |
| CHECK_SOME(mesosVersion); |
| |
| Try<Version> minimumVersion = Version::parse(kindToVersion[moduleBase->kind]); |
| CHECK_SOME(minimumVersion); |
| |
| Try<Version> moduleMesosVersion = Version::parse(moduleBase->mesosVersion); |
| if (moduleMesosVersion.isError()) { |
| return Error(moduleMesosVersion.error()); |
| } |
| |
| if (moduleMesosVersion.get() < minimumVersion.get()) { |
| return Error( |
| "Minimum supported mesos version for '" + stringify(moduleBase->kind) + |
| "' is " + stringify(minimumVersion.get()) + ", but module is compiled " |
| "with version " + stringify(moduleMesosVersion.get())); |
| } |
| |
| if (moduleBase->compatible == nullptr) { |
| if (moduleMesosVersion.get() != mesosVersion.get()) { |
| return Error( |
| "Mesos has version " + stringify(mesosVersion.get()) + |
| ", but module is compiled with version " + |
| stringify(moduleMesosVersion.get())); |
| } |
| return Nothing(); |
| } |
| |
| if (moduleMesosVersion.get() > mesosVersion.get()) { |
| return Error( |
| "Mesos has version " + stringify(mesosVersion.get()) + |
| ", but module is compiled with version " + |
| stringify(moduleMesosVersion.get())); |
| } |
| |
| bool result = moduleBase->compatible(); |
| if (!result) { |
| return Error("Module " + moduleName + "has determined to be incompatible"); |
| } |
| |
| return Nothing(); |
| } |
| |
| |
| // This check is to ensure that the two modules, with the same module name, are |
| // indeed identical. We verify that they belong to the same module library and |
| // have identical attributes such as parameters, kind, version, etc. |
| // |
| // Notice that this check doesn't prevent one from having multiple instances of |
| // the same module. In the current implementation, it is up to the module |
| // developer to return an error if the module in question doesn't support |
| // multiple instances. |
| // |
| // TODO(karya): MESOS-4960: Enhance the module API to allow module developers to |
| // express whether the modules are multi-instantiable and thread-safe. |
| Try<Nothing> ModuleManager::verifyIdenticalModule( |
| const string& libraryName, |
| const Modules::Library::Module& module, |
| const ModuleBase* base) |
| { |
| const string& moduleName = module.name(); |
| |
| // Verify that the two modules come from the same module library. |
| CHECK(moduleLibraries.contains(moduleName)); |
| if (libraryName != moduleLibraries[moduleName]) { |
| return Error( |
| "The same module appears in two different module libraries - " |
| "'" + libraryName + "' and '" + moduleLibraries[moduleName] + "'"); |
| } |
| |
| // Verify that the two modules contain the same set of parameters that appear |
| // in the same order. |
| CHECK(moduleParameters.contains(moduleName)); |
| const Parameters& parameters = moduleParameters[moduleName]; |
| bool parameterError = |
| module.parameters().size() != parameters.parameter().size(); |
| |
| for (int i = 0; i < module.parameters().size() && !parameterError; i++) { |
| const Parameter& lhs = parameters.parameter().Get(i); |
| const Parameter& rhs = module.parameters().Get(i); |
| if (lhs.key() != rhs.key() || lhs.value() != rhs.value()) { |
| parameterError = true; |
| } |
| } |
| |
| if (parameterError) { |
| return Error( |
| "A module with same name but different parameters already exists"); |
| } |
| |
| // Verify that the two `ModuleBase` definitions match. |
| CHECK_NOTNULL(base); |
| CHECK(moduleBases.contains(moduleName)); |
| ModuleBase* duplicateBase = moduleBases[moduleName]; |
| |
| // TODO(karya): MESOS-4918: Cache module manifests to avoid potential |
| // overwrite of `ModuleBase` fields by the module itself. |
| if (strcmp(base->moduleApiVersion, duplicateBase->moduleApiVersion) != 0 || |
| strcmp(base->mesosVersion, duplicateBase->mesosVersion) != 0 || |
| strcmp(base->kind, duplicateBase->kind) != 0 || |
| strcmp(base->authorName, duplicateBase->authorName) != 0 || |
| strcmp(base->authorEmail, duplicateBase->authorEmail) != 0 || |
| strcmp(base->description, duplicateBase->description) != 0 || |
| base->compatible != duplicateBase->compatible) { |
| return Error( |
| "A module with same name but different module manifest already exists"); |
| } |
| |
| return Nothing(); |
| } |
| |
| |
| Try<Nothing> ModuleManager::loadManifest(const Modules& modules) |
| { |
| synchronized (mutex) { |
| initialize(); |
| |
| foreach (const Modules::Library& library, modules.libraries()) { |
| string libraryName; |
| if (library.has_file()) { |
| libraryName = library.file(); |
| } else if (library.has_name()) { |
| libraryName = os::libraries::expandName(library.name()); |
| } else { |
| return Error("Library name or path not provided"); |
| } |
| |
| if (!dynamicLibraries.contains(libraryName)) { |
| Owned<DynamicLibrary> dynamicLibrary(new DynamicLibrary()); |
| Try<Nothing> result = dynamicLibrary->open(libraryName); |
| if (!result.isSome()) { |
| return Error( |
| "Error opening library: '" + libraryName + |
| "': " + result.error()); |
| } |
| |
| dynamicLibraries[libraryName] = dynamicLibrary.release(); |
| } |
| |
| // Load module manifests. |
| foreach (const Modules::Library::Module& module, library.modules()) { |
| if (!module.has_name()) { |
| return Error( |
| "Error: module name not provided with library '" + libraryName + |
| "'"); |
| } |
| |
| const string& moduleName = module.name(); |
| |
| // Load ModuleBase. |
| Try<void*> symbol = |
| dynamicLibraries[libraryName]->loadSymbol(moduleName); |
| if (symbol.isError()) { |
| return Error( |
| "Error loading module '" + moduleName + "': " + symbol.error()); |
| } |
| |
| ModuleBase* moduleBase = (ModuleBase*) symbol.get(); |
| |
| // Verify module compatibility including version, etc. |
| Try<Nothing> result = verifyModule(moduleName, moduleBase); |
| if (result.isError()) { |
| return Error( |
| "Error verifying module '" + moduleName + "': " + result.error()); |
| } |
| |
| // We verify module compatibilty before checking for identical modules |
| // to ensure that all the fields in the module manifest are valid before |
| // we start comparing them with an already loaded manifest with the same |
| // module name. |
| if (moduleBases.contains(moduleName)) { |
| Try<Nothing> result = |
| verifyIdenticalModule(libraryName, module, moduleBase); |
| |
| if (result.isError()) { |
| return Error( |
| "Error loading module '" + moduleName + "'; this is" |
| " potentially due to duplicate module names; " + |
| result.error()); |
| } |
| |
| continue; |
| } |
| |
| moduleBases[moduleName] = moduleBase; |
| moduleLibraries[moduleName] = libraryName; |
| |
| // Now copy the supplied module-specific parameters. |
| moduleParameters[moduleName].mutable_parameter()->CopyFrom( |
| module.parameters()); |
| } |
| } |
| } |
| |
| return Nothing(); |
| } |
| |
| |
| // We load the module manifests sequentially in an alphabetical order. If an |
| // error is encountered while processing a particular manifest, we do not load |
| // the remaining manifests and exit with the appropriate error message. |
| Try<Nothing> ModuleManager::load(const string& modulesDir) |
| { |
| Try<list<string>> moduleManifests = os::ls(modulesDir); |
| if (moduleManifests.isError()) { |
| return Error( |
| "Error loading module manifests from '" + modulesDir + "' directory: " + |
| moduleManifests.error()); |
| } |
| |
| moduleManifests->sort(); |
| foreach (const string& filename, moduleManifests.get()) { |
| const string filepath = path::join(modulesDir, filename); |
| VLOG(1) << "Processing module manifest from '" << filepath << "'"; |
| |
| Try<string> read = os::read(filepath); |
| if (read.isError()) { |
| return Error( |
| "Error reading module manifest file '" + filepath + "': " + |
| read.error()); |
| } |
| |
| Try<Modules> modules = flags::parse<Modules>(read.get()); |
| if (modules.isError()) { |
| return Error( |
| "Error parsing module manifest file '" + filepath + "': " + |
| modules.error()); |
| } |
| |
| Try<Nothing> result = loadManifest(modules.get()); |
| if (result.isError()) { |
| return Error( |
| "Error loading modules from '" + filepath + "': " + result.error()); |
| } |
| } |
| |
| return Nothing(); |
| } |