This commit includes documentation too.
Signed-off-by: Michael Friedrich <michael.friedrich@icinga.com>
/usr/lib\*/icinga2 | Libraries and the Icinga 2 binary (use `find /usr -type f -name icinga2` to locate the binary path).
/usr/share/doc/icinga2 | Documentation files that come with Icinga 2.
/usr/share/icinga2/include | The Icinga Template Library and plugin command configuration.
+ /var/lib/icinga2 | Icinga 2 state file, cluster log, master CA, node certificates and configuration files (cluster, api).
/var/run/icinga2 | PID file.
/var/run/icinga2/cmd | Command pipe and Livestatus socket.
/var/cache/icinga2 | status.dat/objects.cache, icinga2.debug files
/var/spool/icinga2 | Used for performance data spool files.
- /var/lib/icinga2 | Icinga 2 state file, cluster log, local CA and configuration files (cluster, api).
/var/log/icinga2 | Log file location and compat/ directory for the CompatLogger feature.
FreeBSD uses slightly different paths:
/usr/local/lib/icinga2 | Libraries and the Icinga 2 binary.
/usr/local/share/doc/icinga2 | Documentation files that come with Icinga 2.
/usr/local/share/icinga2/include | The Icinga Template Library and plugin command configuration.
+ /var/lib/icinga2 | Icinga 2 state file, cluster log, master CA, node certificates and configuration files (cluster, api).
/var/run/icinga2 | PID file.
/var/run/icinga2/cmd | Command pipe and Livestatus socket.
/var/cache/icinga2 | status.dat/objects.cache, icinga2.debug files
/var/spool/icinga2 | Used for performance data spool files.
- /var/lib/icinga2 | Icinga 2 state file, cluster log, local CA and configuration files (cluster, api).
/var/log/icinga2 | Log file location and compat/ directory for the CompatLogger feature.
## Setting up Check Plugins <a id="setting-up-check-plugins"></a>
Ensure to include the following in your backups:
* Configuration files in `/etc/icinga2`
-* Runtime files in `/var/lib/icinga2` (the master's CA is stored here as well)
+* Certificate files in `/var/lib/icinga2/ca` (Master CA key pair) and `/var/lib/icinga2/certs` (node certificates)
+* Runtime files in `/var/lib/icinga2`
* Optional: IDO database backup
You can verify that the certificate files are stored in the `/var/lib/icinga2/certs` directory.
+> **Note**
+>
+> The certificate location changed in v2.8 to `/var/lib/icinga2/certs`. Please read the [upgrading chapter](16-upgrading-icinga-2.md#upgrading-to-2-8-certificate-paths)
+> for more details.
+
> **Note**
>
> If the client is not directly connected to the certificate signing master,
[root@icinga2-master1.localdomain /root]# icinga2 pki sign-csr --csr icinga2-master1.localdomain.csr --cert icinga2-master1.localdomain
+> **Note**
+>
+> The certificate location changed in v2.8 to `/var/lib/icinga2/certs`. Please read the [upgrading chapter](16-upgrading-icinga-2.md#upgrading-to-2-8-certificate-paths)
+> for more details.
+
Copy the host's certificate files and the public CA certificate to `/var/lib/icinga2/certs`:
[root@icinga2-master1.localdomain /root]# mkdir -p /var/lib/icinga2/certs
#### Node Setup with Satellites/Clients <a id="distributed-monitoring-automation-cli-node-setup-satellite-client"></a>
-Make sure that the `/var/lib/icinga2/certs` exists and is owned by the `icinga`
+> **Note**
+>
+> The certificate location changed in v2.8 to `/var/lib/icinga2/certs`. Please read the [upgrading chapter](16-upgrading-icinga-2.md#upgrading-to-2-8-certificate-paths)
+> for more details.
+
+Make sure that the `/var/lib/icinga2/certs` directory exists and is owned by the `icinga`
user (or the user Icinga 2 is running as).
[root@icinga2-client1.localdomain /]# mkdir -p /var/lib/icinga2/certs
Accept config | **Optional.** Whether this node accepts configuration sync from the master node (required for [config sync mode](06-distributed-monitoring.md#distributed-monitoring-top-down-config-sync)).
Accept commands | **Optional.** Whether this node accepts command execution messages from the master node (required for [command endpoint mode](06-distributed-monitoring.md#distributed-monitoring-top-down-command-endpoint)).
-Example:
+Example for Icinga 2 v2.8:
[root@icinga2-client1.localdomain /]# icinga2 node setup --ticket ead2d570e18c78abf285d6b85524970a0f69c22d \
--cn icinga2-client1.localdomain \
```
object ApiListener "api" {
- cert_path = LocalStateDir + "/lib/icinga2/certs/" + NodeName + ".crt"
- key_path = LocalStateDir + "/lib/icinga2/certs/" + NodeName + ".key"
- ca_path = LocalStateDir + "/lib/icinga2/certs/ca.crt"
+ accept_commands = true
+ accept_config = true
ticket_salt = TicketSalt
}
Name | Type | Description
--------------------------------------|-----------------------|----------------------------------
- cert\_path | String | **Required.** Path to the public key.
- key\_path | String | **Required.** Path to the private key.
- ca\_path | String | **Required.** Path to the CA certificate file.
+ cert\_path | String | **Deprecated.** Path to the public key.
+ key\_path | String | **Deprecated.** Path to the private key.
+ ca\_path | String | **Deprecated.** Path to the CA certificate file.
ticket\_salt | String | **Optional.** Private key for [CSR auto-signing](06-distributed-monitoring.md#distributed-monitoring-setup-csr-auto-signing). **Required** for a signing master instance.
crl\_path | String | **Optional.** Path to the CRL file.
bind\_host | String | **Optional.** The IP address the api listener should be bound to. Defaults to `0.0.0.0`.
access\_control\_allow\_headers | String | **Optional.** Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request. Defaults to `Authorization`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Headers)
access\_control\_allow\_methods | String | **Optional.** Used in response to a preflight request to indicate which HTTP methods can be used when making the actual request. Defaults to `GET, POST, PUT, DELETE`. [(MDN docs)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Access-Control-Allow-Methods)
+The ApiListener type expects its certificate files to be in the following locations:
+
+ Type | Location
+ ---------------------|-------------------------------------
+ Private key | `LocalStateDir + "/lib/icinga2/certs/" + NodeName + ".key"`
+ Certificate file | `LocalStateDir + "/lib/icinga2/certs/" + NodeName + ".crt"`
+ CA certificate file | `LocalStateDir + "/lib/icinga2/certs/ca.crt"`
+
+If the deprecated attributes `cert_path`, `key_path` and/or `ca_path` are specified Icinga 2
+copies those files to the new location in `LocalStateDir + "/lib/icinga2/certs"` unless the
+file(s) there are newer.
+
+Please check the [upgrading chapter](16-upgrading-icinga-2.md#upgrading-to-2-8-certificate-paths) for more details.
+
## ApiUser <a id="objecttype-apiuser"></a>
ApiUser objects are used for authentication against the [Icinga 2 API](12-icinga2-api.md#icinga2-api-authentication).
The default certificate path was changed from `/etc/icinga2/pki` to
`/var/lib/icinga2/certs`.
+ Old Path | New Path
+ ---------------------------------------------------|---------------------------------------------------
+ `/etc/icinga2/pki/icinga2-client1.localdomain.crt` | `/var/lib/icinga2/certs/icinga2-client1.localdomain.crt`
+ `/etc/icinga2/pki/icinga2-client1.localdomain.key` | `/var/lib/icinga2/certs/icinga2-client1.localdomain.key`
+ `/etc/icinga2/pki/ca.crt` | `/var/lib/icinga2/certs/ca.crt`
+
This applies to Windows clients in the same way: `%ProgramData%\etc\icinga2\pki`
-was moved to `%ProgramData%`\var\lib\icinga2\certs`.
+was moved to `%ProgramData%\var\lib\icinga2\certs`.
+
+ Old Path | New Path
+ ----------------------------------------------------------------|----------------------------------------------------------------
+ `%ProgramData%\etc\icinga2\pki\icinga2-client1.localdomain.crt` | `%ProgramData%\var\lib\icinga2\certs\icinga2-client1.localdomain.crt`
+ `%ProgramData%\etc\icinga2\pki\icinga2-client1.localdomain.key` | `%ProgramData%\var\lib\icinga2\certs\icinga2-client1.localdomain.key`
+ `%ProgramData%\etc\icinga2\pki\ca.crt` | `%ProgramData%\var\lib\icinga2\certs\ca.crt`
+
+
+> **Note**
+>
+> The default expected path for client certificates is `/var/lib/icinga2/certs/ + NodeName + {.crt,.key}`.
+> The `NodeName` constant is usually the FQDN and certificate common name (CN). Check the [conventions](06-distributed-monitoring.md#distributed-monitoring-conventions)
+> section inside the Distributed Monitoring chapter.
The [setup CLI commands](06-distributed-monitoring.md#distributed-monitoring-setup-master) and the
default [ApiListener configuration](06-distributed-monitoring.md#distributed-monitoring-apilistener)
have been adjusted to these paths too.
+The [ApiListener](09-object-types.md#objecttype-apilistener) object attributes `cert_path`, `key_path`
+and `ca_path` have been deprecated and removed from the example configuration.
+
+#### Migration Path <a id="upgrading-to-2-8-certificate-paths-migration-path"></a>
+
+> **Note**
+>
+> Icinga 2 automatically migrates the certificates to the new default location if they
+> are configured and detected in `/etc/icinga2/pki`.
+
+During startup, the migration kicks in and ensures to copy the certificates to the new
+location. This will also happen if someone updates the certificate files in `/etc/icinga2/pki`
+to ensure that the new certificate location always has the latest files.
+
+This has been implemented in the Icinga 2 binary to ensure it works on both Linux/Unix
+and the Windows platform.
+
+If you are not using the built-in CLI commands and setup wizards to deploy the client certificates,
+please ensure to update your deployment tools/scripts. This mainly affects
+
+* Puppet modules
+* Ansible playbooks
+* Chef cookbooks
+* Salt recipes
+* Custom scripts, e.g. Windows Powershell or self-made implementations
+
+In order to support a smooth migration between versions older than 2.8 and future releases,
+the built-in certificate migration path is planned to exist as long as the deprecated
+`ApiListener` object attributes exist.
+
+You are safe to use the existing configuration paths inside the `api` feature. If you plan your migration,
+look at the following example taken from the Director Linux deployment script for clients.
+
+* Ensure that the default certificate path is changed from `/etc/icinga2/pki` to `/var/lib/icinga2/certs`.
+
+```
+-ICINGA2_SSL_DIR="${ICINGA2_CONF_DIR}/pki"
++ICINGA2_SSL_DIR="${ICINGA2_STATE_DIR}/lib/icinga2/certs"
+```
+
+* Remove the ApiListener configuration attributes.
+
+```
+object ApiListener "api" {
+- cert_path = SysconfDir + "/icinga2/pki/${ICINGA2_NODENAME}.crt"
+- key_path = SysconfDir + "/icinga2/pki/${ICINGA2_NODENAME}.key"
+- ca_path = SysconfDir + "/icinga2/pki/ca.crt"
+ accept_commands = true
+ accept_config = true
+}
+```
+
+Test the script with a fresh client installation before putting it into production.
+
+> **Tip**
+>
+> Please support module and script developers in their migration. If you find
+> any project which would require these changes, create an issue or a patchset in a PR
+> and help them out. Thanks in advance!
+
+
### Removed Bottom Up Client Mode <a id="upgrading-to-2-8-removed-bottom-up-client-mode"></a>
This client mode was deprecated in 2.6 and was removed in 2.8.
The node CLI command does not provide `list` or `update-config` anymore.
+> **Note**
+>
+> The old migration guide can be found on [GitHub](https://github.com/Icinga/icinga2/blob/v2.7.0/doc/06-distributed-monitoring.md#bottom-up-migration-to-top-down-).
+
The clients don't need to have a local `conf.d` directory included.
The setup wizards for Linux and Windows attempt to disable this by default.
any existing configuration to the "top down" mode with the help of the
Icinga Director or config management tools such as Puppet, Ansible, etc.
+
### Removed Classic UI Config Package <a id="upgrading-to-2-8-removed-classicui-config-package"></a>
The config meta package `classicui-config` and the configuration files
*/
object ApiListener "api" {
- cert_path = LocalStateDir + "/lib/icinga2/certs/" + NodeName + ".crt"
- key_path = LocalStateDir + "/lib/icinga2/certs/" + NodeName + ".key"
- ca_path = LocalStateDir + "/lib/icinga2/certs/ca.crt"
+ //accept_config = false
+ //accept_commands = false
ticket_salt = TicketSalt
}
{
[config] String "name";
[config] bool side_effect_free;
- [config] bool deprecated;
+ [config] bool "deprecated";
[config] Array::Ptr arguments;
};
FARequired = 256,
FANavigation = 512,
FANoUserModify = 1024,
- FANoUserView = 2048
+ FANoUserView = 2048,
+ FADeprecated = 4096,
};
class Type;
paths.push_back(path);
}
+/*
+ * Copies a source file to a target location.
+ * Caller must ensure that the target's base directory exists and is writable.
+ */
void Utility::CopyFile(const String& source, const String& target)
{
std::ifstream ifs(source.CStr(), std::ios::binary);
fp << "/**\n"
<< " * The API listener is used for distributed monitoring setups.\n"
<< " */\n"
- << "object ApiListener \"api\" {\n"
- << " cert_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".crt\"\n"
- << " key_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".key\"\n"
- << " ca_path = LocalStateDir + \"/lib/icinga2/certs/ca.crt\"\n";
+ << "object ApiListener \"api\" {\n";
if (vm.count("listen")) {
std::vector<String> tokens;
fp << "/**\n"
<< " * The API listener is used for distributed monitoring setups.\n"
<< " */\n"
- << "object ApiListener \"api\" {\n"
- << " cert_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".crt\"\n"
- << " key_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".key\"\n"
- << " ca_path = LocalStateDir + \"/lib/icinga2/certs/ca.crt\"\n";
+ << "object ApiListener \"api\" {\n";
if (vm.count("listen")) {
std::vector<String> tokens;
<< " * The API listener is used for distributed monitoring setups.\n"
<< " */\n"
<< "object ApiListener \"api\" {\n"
- << " cert_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".crt\"\n"
- << " key_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".key\"\n"
- << " ca_path = LocalStateDir + \"/lib/icinga2/certs/ca.crt\"\n"
- << "\n"
<< " accept_config = " << acceptConfig << "\n"
<< " accept_commands = " << acceptCommands << "\n";
fp << "/**\n"
<< " * The API listener is used for distributed monitoring setups.\n"
<< " */\n"
- << "object ApiListener \"api\" {\n"
- << " cert_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".crt\"\n"
- << " key_path = LocalStateDir + \"/lib/icinga2/certs/\" + NodeName + \".key\"\n"
- << " ca_path = LocalStateDir + \"/lib/icinga2/certs/ca.crt\"\n";
+ << "object ApiListener \"api\" {\n";
if (!bindHost.IsEmpty())
fp << " bind_host = \"" << bindHost << "\"\n";
return Application::GetLocalStateDir() + "/lib/icinga2/certificate-requests/";
}
+String ApiListener::GetDefaultCertPath(void)
+{
+ return GetCertsDir() + "/" + ScriptGlobal::Get("NodeName") + ".crt";
+}
+
+String ApiListener::GetDefaultKeyPath(void)
+{
+ return GetCertsDir() + "/" + ScriptGlobal::Get("NodeName") + ".key";
+}
+
+String ApiListener::GetDefaultCaPath(void)
+{
+ return GetCertsDir() + "/ca.crt";
+}
+
+void ApiListener::CopyCertificateFile(const String& oldCertPath, const String& newCertPath)
+{
+ struct stat st1, st2;
+
+ if (!oldCertPath.IsEmpty() && stat(oldCertPath.CStr(), &st1) >= 0 && (stat(newCertPath.CStr(), &st2) < 0 || st1.st_mtime > st2.st_mtime)) {
+ Log(LogWarning, "ApiListener")
+ << "Copying '" << oldCertPath << "' certificate file to '" << newCertPath << "'";
+
+ Utility::MkDirP(Utility::DirName(newCertPath), 0700);
+ Utility::CopyFile(oldCertPath, newCertPath);
+ }
+}
+
void ApiListener::OnConfigLoaded(void)
{
if (m_Instance)
m_Instance = this;
+ String defaultCertPath = GetDefaultCertPath();
+ String defaultKeyPath = GetDefaultKeyPath();
+ String defaultCaPath = GetDefaultCaPath();
+
+ /* Migrate certificate location < 2.8 to the new default path. */
+ String oldCertPath = GetCertPath();
+ String oldKeyPath = GetKeyPath();
+ String oldCaPath = GetCaPath();
+
+ CopyCertificateFile(oldCertPath, defaultCertPath);
+ CopyCertificateFile(oldKeyPath, defaultKeyPath);
+ CopyCertificateFile(oldCaPath, defaultCaPath);
+
+ if (!oldCertPath.IsEmpty() && !oldKeyPath.IsEmpty() && !oldCaPath.IsEmpty()) {
+ Log(LogWarning, "ApiListener", "Please read the upgrading documentation for v2.8: https://www.icinga.com/docs/icinga2/latest/doc/16-upgrading-icinga-2/");
+ }
+
/* set up SSL context */
boost::shared_ptr<X509> cert;
try {
- cert = GetX509Certificate(GetCertPath());
+ cert = GetX509Certificate(defaultCertPath);
} catch (const std::exception&) {
BOOST_THROW_EXCEPTION(ScriptError("Cannot get certificate from cert path: '"
- + GetCertPath() + "'.", GetDebugInfo()));
+ + defaultCertPath + "'.", GetDebugInfo()));
}
try {
SetIdentity(GetCertificateCN(cert));
} catch (const std::exception&) {
BOOST_THROW_EXCEPTION(ScriptError("Cannot get certificate common name from cert path: '"
- + GetCertPath() + "'.", GetDebugInfo()));
+ + defaultCertPath + "'.", GetDebugInfo()));
}
Log(LogInformation, "ApiListener")
boost::shared_ptr<SSL_CTX> context;
try {
- context = MakeSSLContext(GetCertPath(), GetKeyPath(), GetCaPath());
+ context = MakeSSLContext(GetDefaultCertPath(), GetDefaultKeyPath(), GetDefaultCaPath());
} catch (const std::exception&) {
BOOST_THROW_EXCEPTION(ScriptError("Cannot make SSL context for cert path: '"
- + GetCertPath() + "' key path: '" + GetKeyPath() + "' ca path: '" + GetCaPath() + "'.", GetDebugInfo()));
+ + GetDefaultCertPath() + "' key path: '" + GetDefaultKeyPath() + "' ca path: '" + GetDefaultCaPath() + "'.", GetDebugInfo()));
}
if (!GetCrlPath().IsEmpty()) {
identity = GetCertificateCN(cert);
} catch (const std::exception&) {
Log(LogCritical, "ApiListener")
- << "Cannot get certificate common name from cert path: '" << GetCertPath() << "'.";
+ << "Cannot get certificate common name from cert path: '" << GetDefaultCertPath() << "'.";
return;
}
static bool IsHACluster(void);
static String GetFromZoneName(const Zone::Ptr& fromZone);
+ static String GetDefaultCertPath(void);
+ static String GetDefaultKeyPath(void);
+ static String GetDefaultCaPath(void);
+
protected:
virtual void OnConfigLoaded(void) override;
virtual void OnAllConfigLoaded(void) override;
static void LogGlobHandler(std::vector<int>& files, const String& file);
void ReplayLog(const JsonRpcConnection::Ptr& client);
+ static void CopyCertificateFile(const String& oldCertPath, const String& newCertPath);
+
/* filesync */
static ConfigDirInformation LoadConfigDir(const String& dir);
static Dictionary::Ptr MergeConfigUpdate(const ConfigDirInformation& config);
class ApiListener : ConfigObject
{
- [config, required] String cert_path;
- [config, required] String key_path;
- [config, required] String ca_path;
+ [config, deprecated] String cert_path;
+ [config, deprecated] String key_path;
+ [config, deprecated] String ca_path;
[config] String crl_path;
[config] String cipher_list {
default {{{ return "ALL:!LOW:!WEAK:!MEDIUM:!EXP:!NULL"; }}}
cert = StringToCertificate(certText);
ApiListener::Ptr listener = ApiListener::GetInstance();
- boost::shared_ptr<X509> cacert = GetX509Certificate(listener->GetCaPath());
+ boost::shared_ptr<X509> cacert = GetX509Certificate(listener->GetDefaultCaPath());
String cn = GetCertificateCN(cert);
* a certificate it wouldn't be able to use to connect to us anyway) */
if (!VerifyCertificate(cacert, newcert)) {
Log(LogWarning, "JsonRpcConnection")
- << "The CA in '" << listener->GetCaPath() << "' does not match the CA which Icinga uses "
+ << "The CA in '" << listener->GetDefaultCaPath() << "' does not match the CA which Icinga uses "
<< "for its own cluster connections. This is most likely a configuration problem.";
goto delayed_request;
}
if (!listener)
return Empty;
- boost::shared_ptr<X509> oldCert = GetX509Certificate(listener->GetCertPath());
+ boost::shared_ptr<X509> oldCert = GetX509Certificate(listener->GetDefaultCertPath());
boost::shared_ptr<X509> newCert = StringToCertificate(cert);
String cn = GetCertificateCN(newCert);
}
/* Update CA certificate. */
- String caPath = listener->GetCaPath();
+ String caPath = listener->GetDefaultCaPath();
Log(LogInformation, "JsonRpcConnection")
<< "Updating CA certificate in '" << caPath << "'.";
}
/* Update signed certificate. */
- String certPath = listener->GetCertPath();
+ String certPath = listener->GetDefaultCertPath();
Log(LogInformation, "JsonRpcConnection")
<< "Updating client certificate for CN '" << cn << "' in '" << certPath << "'.";
attributeInfo->Set("navigation", static_cast<bool>(field.Attributes & FANavigation));
attributeInfo->Set("no_user_modify", static_cast<bool>(field.Attributes & FANoUserModify));
attributeInfo->Set("no_user_view", static_cast<bool>(field.Attributes & FANoUserView));
+ attributeInfo->Set("deprecated", static_cast<bool>(field.Attributes & FADeprecated));
}
}
no_storage { yylval->num = FANoStorage; return T_FIELD_ATTRIBUTE; }
no_user_modify { yylval->num = FANoUserModify; return T_FIELD_ATTRIBUTE; }
no_user_view { yylval->num = FANoUserView; return T_FIELD_ATTRIBUTE; }
+deprecated { yylval->num = FADeprecated; return T_FIELD_ATTRIBUTE; }
navigation { return T_NAVIGATION; }
validator { return T_VALIDATOR; }
required { return T_REQUIRED; }
m_Impl << "\t\t" << "BOOST_THROW_EXCEPTION(ValidationError(dynamic_cast<ConfigObject *>(this), boost::assign::list_of(\"" << field.Name << "\"), \"Attribute must not be empty.\"));" << std::endl << std::endl;
}
+ if (field.Attributes & FADeprecated) {
+ if (field.Type.GetRealType().find("::Ptr") != std::string::npos)
+ m_Impl << "\t" << "if (" << argName << ")" << std::endl;
+ else
+ m_Impl << "\t" << "if (!" << argName << ".IsEmpty())" << std::endl;
+
+ m_Impl << "\t\t" << "Log(LogWarning, \"" << klass.Name << "\") << \"Attribute '" << field.Name << "' for object '\" << dynamic_cast<ConfigObject *>(this)->GetName() << \"' of type '\" << dynamic_cast<ConfigObject *>(this)->GetReflectionType()->GetName() << \"' is deprecated and should not be used.\";" << std::endl;
+ }
+
if (field.Type.ArrayRank > 0) {
m_Impl << "\t" << "if (avalue) {" << std::endl
<< "\t\t" << "ObjectLock olock(avalue);" << std::endl
FARequired = 256,
FANavigation = 512,
FANoUserModify = 1024,
- FANoUserView = 2048
+ FANoUserView = 2048,
+ FADeprecated = 4096
};
struct FieldType