diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-11-25 14:45:37 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-11-25 14:48:03 +0000 |
commit | e55403ed71282d7bfd8b56df219de3c28a8af064 (patch) | |
tree | 524889e5becb81643bf8741e3082955dca076f09 /src/claim | |
parent | Releasing debian version 1.47.5-1. (diff) | |
download | netdata-e55403ed71282d7bfd8b56df219de3c28a8af064.tar.xz netdata-e55403ed71282d7bfd8b56df219de3c28a8af064.zip |
Merging upstream version 2.0.3+dfsg:
- does not include dygraphs anymore (Closes: #923993)
- does not include pako anymore (Closes: #1042533)
- does not include dashboard binaries anymore (Closes: #1045145)
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/claim')
-rw-r--r-- | src/claim/README.md | 250 | ||||
-rw-r--r-- | src/claim/claim-with-api.c | 486 | ||||
-rw-r--r-- | src/claim/claim.c | 555 | ||||
-rw-r--r-- | src/claim/claim.h | 45 | ||||
-rw-r--r-- | src/claim/claim_id.c | 123 | ||||
-rw-r--r-- | src/claim/claim_id.h | 28 | ||||
-rw-r--r-- | src/claim/cloud-conf.c | 117 | ||||
-rw-r--r-- | src/claim/cloud-status.c | 134 | ||||
-rw-r--r-- | src/claim/cloud-status.h | 26 | ||||
-rw-r--r-- | src/claim/main.c | 305 | ||||
-rw-r--r-- | src/claim/main.h | 19 | ||||
-rwxr-xr-x | src/claim/netdata-claim.sh.in | 514 | ||||
-rw-r--r-- | src/claim/ui.c | 110 | ||||
-rw-r--r-- | src/claim/ui.h | 12 |
14 files changed, 1687 insertions, 1037 deletions
diff --git a/src/claim/README.md b/src/claim/README.md index 51e2a9ebe..a0af190b9 100644 --- a/src/claim/README.md +++ b/src/claim/README.md @@ -11,8 +11,7 @@ features like centralized monitoring and easier collaboration. There are two places in the UI where you can add/connect your Node: - **Space/Room settings**: Click the cogwheel (the bottom-left corner or next to the Room name at the top) and - select "Nodes." Click the "+" button to add - a new node. + select "Nodes." Click the "+" button to add a new node. - [**Nodes tab**](/docs/dashboards-and-charts/nodes-tab.md): Click on the "Add nodes" button. Netdata Cloud will generate a command that you can execute on your Node to install and claim the Agent. The command is @@ -28,12 +27,13 @@ Once you've chosen your installation method, follow the provided instructions to ### Connect an Existing Agent -There are two methods to connect an already installed Netdata Agent to your Netdata Cloud Space: +There are three methods to connect an already installed Netdata Agent to your Netdata Cloud Space: -- using the Netdata Cloud user interface (UI). -- using the claiming script. +- Manually, via the UI +- Automatically, via a provisioning system (or the command line) +- Automatically, via environment variables (e.g. kubernetes, docker, etc) -#### Using the UI (recommended) +#### Manually, via the UI The UI method is the easiest and recommended way to connect your Agent. Here's how: @@ -42,36 +42,52 @@ The UI method is the easiest and recommended way to connect your Agent. Here's h 3. Click the "Connect" button. 4. Follow the on-screen instructions to connect your Agent. -#### Using claiming script +#### Automatically, via a provisioning system or the command line -You can connect an Agent by running -the [netdata-claim.sh](https://github.com/netdata/netdata/blob/master/src/claim/netdata-claim.sh.in) script directly. -You can either run it with root privileges using `sudo` or as the user running the Agent (typically `netdata`). - -The claiming script accepts options that control the connection process. You can specify these options using the -following format: +Netdata Agents can be connected to Netdata Cloud by creating the file `/etc/netdata/claim.conf` +(or `/opt/netdata/etc/netdata/claim.conf` depending on your installation), like this: ```bash -netdata-claim.sh -OPTION=VALUE ... +[global] + url = The Netdata Cloud base URL (optional, defaults to `https://app.netdata.cloud`) + token = The claiming token for your Netdata Cloud Space (required) + rooms = A comma-separated list of Rooms to add the Agent to (optional) + proxy = The URL of a proxy server to use for the connection, or none, or env (optional, defaults to env) + insecure = Either yes or no (optional) ``` -Claiming script options: +- `proxy` can get anything libcurl accepts as a proxy, or the `none` and `env` keywords. `none` (or just an empty value) disables proxy configuration, while `env` tells libcurl to use the environment to determine the proxy configuration (usually the `https_proxy` environment variable). +- `insecure` is a boolean (either `yes`, or `no`) and when set to `yes` it instructs libcurl to disable host verification. -| Option | Description | Required | Default value | -|--------|--------------------------------------------------------------------|:--------:|:------------------------------------------------------| -| token | The claiming token for your Netdata Cloud Space. | yes | | -| rooms | A comma-separated list of Rooms to add the Agent to. | no | The Agent will be added to the "All nodes" Room only. | -| id | The unique identifier of the Agent. | no | The Agent's MACHINE_GUID. | -| proxy | The URL of a proxy server to use for the connection, if necessary. | no | | - -Example: +example: ```bash -netdata-claim.sh -token=MYTOKEN1234567 -rooms=room1,room2 +[global] + url = https://app.netdata.cloud + token = NETDATA_CLOUD_SPACE_TOKEN + rooms = ROOM_KEY1,ROOM_KEY2,ROOM_KEY3 + proxy = http://username:password@myproxy:8080 + insecure = no ``` -This command connects the Agent and adds it to the "room1" and "room2" Rooms using your claiming token -MYTOKEN1234567. +If the agent is already running, you can either run `netdatacli reload-claiming-state` or restart the agent. +Otherwise, the agent will be claimed when it starts. + +If the claiming process fails, the reason will be logged in daemon.log (search for "CLAIM") and the `cloud` section of `http://ip:19999/api/v2/info`. + +#### Automatically, via environment variables + +Netdata will use the following environment variables: + +- `NETDATA_CLAIM_URL`: The Netdata Cloud base URL (optional, defaults to `https://app.netdata.cloud`) +- `NETDATA_CLAIM_TOKEN`: The claiming token for your Netdata Cloud Space (required) +- `NETDATA_CLAIM_ROOMS`: A comma-separated list of Rooms to add the Agent to (optional) +- `NETDATA_CLAIM_PROXY`: The URL of a proxy server to use for the connection (optional) +- `NETDATA_EXTRA_CLAIM_OPTS`, may contain a space separated list of options. The option `-insecure` is the only currently used. + +The `NETDATA_CLAIM_TOKEN` alone is enough for triggering the claiming process. + +If the claiming process fails, the reason will be logged in daemon.log (search for "CLAIM") and the `cloud` section of `http://ip:19999/api/v2/info`. ## Reconnect @@ -84,22 +100,16 @@ cd /var/lib/netdata # Replace with your Netdata library directory, if not /var sudo rm -rf cloud.d/ ``` +> **IMPORTANT** +> +> Keep in mind that the Agent will be **re-claimed automatically** if the environment variables or `claim.conf` exist when the agent is restarted. + This node no longer has access to the credentials it was used when connecting to Netdata Cloud via the ACLK. You will still be able to see this node in your Rooms in an **unreachable** state. -If you want to reconnect this node, you need to: - -1. Ensure that the `/var/lib/netdata/cloud.d` directory doesn't exist. In some installations, the path - is `/opt/netdata/var/lib/netdata/cloud.d` -2. Stop the Agent -3. Ensure that the `uuidgen-runtime` package is installed. Run ```echo "$(uuidgen)"``` and validate you get back a UUID -4. Copy the kickstart.sh command to add a node from your space and add to the end of it `--claim-id "$(uuidgen)"`. Run - the command and look for the message `Node was successfully claimed.` -5. Start the Agent - ### Docker based installations -To remove a node from you Space in Netdata Cloud, and connect it to another Space, follow these steps: +To remove a node from your Space in Netdata Cloud and connect it to another Space, follow these steps: 1. Enter the running container you wish to remove from your Space @@ -113,7 +123,6 @@ To remove a node from you Space in Netdata Cloud, and connect it to another Spac ```bash rm -rf /var/lib/netdata/cloud.d/ - rm /var/lib/netdata/registry/netdata.public.unique.id ``` @@ -123,7 +132,6 @@ To remove a node from you Space in Netdata Cloud, and connect it to another Spac ```bash docker stop CONTAINER_NAME - docker rm CONTAINER_NAME ``` @@ -144,35 +152,31 @@ To remove a node from you Space in Netdata Cloud, and connect it to another Spac ``` 4. Finally, go to your new Space, copy the installation command with the new claim token and run it. - If you are using a `docker-compose.yml` file, you will have to overwrite it with the new claiming token. + If you’re using a `docker-compose.yml` file, you will have to overwrite it with the new claiming token. The node should now appear online in that Space. ## Regenerate Claiming Token -If in case of some security reason, or other, you need to revoke your previous claiming token and generate a new one you -can achieve that from the Netdata Cloud UI. +There may be situations where you need to revoke your previous Netdata Cloud claiming token and generate a new one for security reasons. Here's how to do it: + +**Requirements**: + +- Only administrators of Space in Netdata Cloud can regenerate tokens. -On any screen where you see the connect the node to Netdata Cloud command you'll see above it, next to -the [updates channel](/docs/netdata-agent/versions-and-platforms.md), a -button to **Regenerate token**. This action will invalidate your previous token and generate a fresh new one. +**Steps**: -Only the administrators of a Space in Netdata Cloud can trigger this action. +1. Navigate to any screen within the Netdata Cloud UI where you see the "Connect the node to Netdata Cloud" command. +2. Look above this command, near the [Updates channel](/docs/netdata-agent/versions-and-platforms.md). You should see a button that says "Regenerate token." +3. Click the "Regenerate token" button. This action will invalidate your previous token and generate a new one. ## Troubleshoot If you're having trouble connecting a node, this may be because the [ACLK](/src/aclk/README.md) cannot connect to Cloud. -With the Netdata Agent running, visit `http://NODE:19999/api/v1/info` in your browser, replacing `NODE` with the IP -address or hostname of your Agent. The returned JSON contains four keys that will be helpful to diagnose any issues you -might be having with the ACLK or connection process. - -``` -"cloud-enabled" -"cloud-available" -"agent-claimed" -"aclk-available" -``` +With the Netdata Agent running, visit `http://NODE:19999/api/v2/info` in your browser, replacing `NODE` with the IP +address or hostname of your Agent. The returned JSON contains a section called `cloud` with helpful information to +diagnose any issues you might be having with the ACLK or connection process. > **Note** > @@ -199,149 +203,33 @@ installed Netdata using an unsupported package. > **Note** > -> If you are using an unsupported package, such as a third-party `.deb`/`.rpm` package provided by your distribution, +> If you’re using an unsupported package, such as a third-party `.deb`/`.rpm` package provided by your distribution, > please remove that package and reinstall using > our [recommended kickstart script](/packaging/installer/methods/kickstart.md). ### kickstart: Failed to write new machine GUID -If you run the kickstart script but don't have privileges required for the actions done on the connecting to Netdata -Cloud process you will get the following error: +You might encounter this error if you run the Netdata kickstart script without sufficient permissions: ```bash Failed to write new machine GUID. Please make sure you have rights to write to /var/lib/netdata/registry/netdata.public.unique.id. ``` -For a successful execution you will need to run the script with root privileges or run it with the user that is running -the Agent. - -### bash: netdata-claim.sh: command not found +To resolve this issue, you have two options: -If you run the claiming script and see a `command not found` error, you either installed Netdata in a non-standard -location or are using an unsupported package. If you installed Netdata in a non-standard path using -the `--install-prefix` option, you need to update your `$PATH` or run `netdata-claim.sh` using the full path. +1. Run the script with root privileges. +2. Run the script with the user that runs the Netdata Agent. -For example, if you installed Netdata to `/opt/netdata`, use `/opt/netdata/bin/netdata-claim.sh` to run the claiming -script. - -> **Note** -> -> If you are using an unsupported package, such as a third-party `.deb`/`.rpm` package provided by your distribution, -> please remove that package and reinstall using -> -our [recommended kickstart script](/packaging/installer/methods/kickstart.md). - -### Connecting on older distributions (Ubuntu 14.04, Debian 8, CentOS 6) +### Connecting to Cloud on older distributions (Ubuntu 14.04, Debian 8, CentOS 6) If you're running an older Linux distribution or one that has reached EOL, such as Ubuntu 14.04 LTS, Debian 8, or CentOS 6, your Agent may not be able to securely connect to Netdata Cloud due to an outdated version of OpenSSL. These old -versions of OpenSSL cannot perform [hostname validation](https://wiki.openssl.org/index.php/Hostname_validation), which -helps securely encrypt SSL connections. +versions of OpenSSL cannot perform [hostname validation](https://wiki.openssl.org/index.php/Hostname_validation), +which helps securely encrypt SSL connections. -We recommend you reinstall Netdata with -a [static build](/packaging/installer/methods/kickstart.md#static-builds), -which uses an up-to-date version of OpenSSL with hostname validation enabled. +We recommend you reinstall Netdata with a [static build](/packaging/installer/methods/kickstart.md#install-type), which uses an up-to-date version of OpenSSL with hostname validation enabled. If you choose to continue using the outdated version of OpenSSL, your node will still connect to Netdata Cloud, albeit with hostname verification disabled. Without verification, your Netdata Cloud connection could be vulnerable to man-in-the-middle attacks. - -### cloud-enabled is false - -If `cloud-enabled` is `false`, you probably ran the installer with `--disable-cloud` option. - -Additionally, check that the `enabled` setting in `var/lib/netdata/cloud.d/cloud.conf` is set to `true`: - -```conf -[global] - enabled = true -``` - -To fix this issue, reinstall Netdata using -your [preferred method](/packaging/installer/README.md) and do not add -the `--disable-cloud` option. - -### cloud-available is false / ACLK Available: No - -If `cloud-available` is `false` after you verified Cloud is enabled in the previous step, the most likely issue is that -Cloud features failed to build during installation. - -If Cloud features fail to build, the installer continues and finishes the process without Cloud functionality as opposed -to failing the installation altogether. - -We do this to ensure the Agent will always finish installing. - -If you can't see an explicit error in the installer's output, you can run the installer with the `--require-cloud` -option. This option causes the installation to fail if Cloud functionality can't be built and enabled, and the -installer's output should give you more error details. - -You may see one of the following error messages during installation: - -- `Failed to build libmosquitto. The install process will continue, but you will not be able to connect this node to Netdata Cloud.` -- `Unable to fetch sources for libmosquitto. The install process will continue, but you will not be able to connect this node to Netdata Cloud.` -- `Failed to build libwebsockets. The install process will continue, but you may not be able to connect this node to Netdata Cloud.` -- `Unable to fetch sources for libwebsockets. The install process will continue, but you may not be able to connect this node to Netdata Cloud.` -- `Could not find cmake, which is required to build libwebsockets. The install process will continue, but you may not be able to connect this node to Netdata Cloud.` -- `Could not find cmake, which is required to build JSON-C. The install process will continue, but Netdata Cloud support will be disabled.` -- `Failed to build JSON-C. Netdata Cloud support will be disabled.` -- `Unable to fetch sources for JSON-C. Netdata Cloud support will be disabled.` - -One common cause of the installer failing to build Cloud features is not having one of the following dependencies on -your system: `cmake`, `json-c` and `OpenSSL`, including corresponding `devel` packages. - -You can also look for error messages in `/var/log/netdata/error.log`. Try one of the following two commands to search -for ACLK-related errors. - -```bash -less /var/log/netdata/error.log -grep -i ACLK /var/log/netdata/error.log -``` - -If the installer's output does not help you enable Cloud features, contact us -by [creating an issue on GitHub](https://github.com/netdata/netdata/issues/new?assignees=&labels=bug%2Cneeds+triage&template=BUG_REPORT.yml&title=The+installer+failed+to+prepare+the+required+dependencies+for+Netdata+Cloud+functionality) -with details about your system and relevant output from `error.log`. - -### agent-claimed is false / Claimed: No - -You must [connect your node](#connect). - -### aclk-available is false / Online: No - -If `aclk-available` is `false` and all other keys are `true`, your Agent is having trouble connecting to the Cloud -through the ACLK. Please check your system's firewall. - -If your Agent needs to use a proxy to access the internet, you must set up a proxy for connecting. - -If you are certain firewall and proxy settings are not the issue, you should consult the Agent's `error.log` -at `/var/log/netdata/error.log` and contact us -by [creating an issue on GitHub](https://github.com/netdata/netdata/issues/new?assignees=&labels=bug%2Cneeds+triage&template=BUG_REPORT.yml&title=ACLK-available-is-false) -with details about your system and relevant output from `error.log`. - -## Connecting reference - -In the sections below, you can find reference material for the kickstart script, claiming script, connecting via the -Agent's command line tool, and details about the files found in `cloud.d`. - -### The `cloud.conf` file - -This section defines how and whether your Agent connects to Netdata Cloud using -the [Agent-Cloud link](/src/aclk/README.md)(ACLK). - -| setting | default | info | -|:---------------|:----------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------| -| enabled | yes | Controls whether the ACLK is active. Set to no to prevent the Agent from connecting to Netdata Cloud. | -| cloud base url | <https://app.netdata.cloud> | The URL for the Netdata Cloud web application. Typically, this should not be changed. | -| proxy | env | Specifies the proxy setting for the ACLK. Options: none (no proxy), env (use environment's proxy), or a URL (e.g., `http://proxy.example.com:1080`). | - -### Connection directory - -Netdata stores the Agent's connection-related state in the Netdata library directory under `cloud.d`. For a default -installation, this directory exists at `/var/lib/netdata/cloud.d`. The directory and its files should be owned by the -user that runs the Agent, which is typically the `netdata` user. - -The `cloud.d/token` file should contain the claiming-token and the `cloud.d/rooms` file should contain the list of War -Rooms you added that node to. - -The user can also put the Cloud endpoint's full certificate chain in `cloud.d/cloud_fullchain.pem` so that the Agent -can trust the endpoint if necessary. diff --git a/src/claim/claim-with-api.c b/src/claim/claim-with-api.c new file mode 100644 index 000000000..534d4511a --- /dev/null +++ b/src/claim/claim-with-api.c @@ -0,0 +1,486 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "claim.h" + +#include "registry/registry.h" + +#include <curl/curl.h> +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/err.h> + +static bool check_and_generate_certificates() { + FILE *fp; + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *pctx = NULL; + + CLEAN_CHAR_P *private_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "private.pem"); + CLEAN_CHAR_P *public_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "public.pem"); + + // Check if private key exists + fp = fopen(public_key_file, "r"); + if (fp) { + fclose(fp); + return true; + } + + // Generate the RSA key + pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (!pctx) { + claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_CTX_new_id() failed"); + return false; + } + + if (EVP_PKEY_keygen_init(pctx) <= 0) { + claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_keygen_init() failed"); + EVP_PKEY_CTX_free(pctx); + return false; + } + + if (EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, 2048) <= 0) { + claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_CTX_set_rsa_keygen_bits() failed"); + EVP_PKEY_CTX_free(pctx); + return false; + } + + if (EVP_PKEY_keygen(pctx, &pkey) <= 0) { + claim_agent_failure_reason_set("Cannot generate RSA key, EVP_PKEY_keygen() failed"); + EVP_PKEY_CTX_free(pctx); + return false; + } + + EVP_PKEY_CTX_free(pctx); + + // Save private key + fp = fopen(private_key_file, "wb"); + if (!fp || !PEM_write_PrivateKey(fp, pkey, NULL, NULL, 0, NULL, NULL)) { + claim_agent_failure_reason_set("Cannot write private key file: %s", private_key_file); + if (fp) fclose(fp); + EVP_PKEY_free(pkey); + return false; + } + fclose(fp); + + // Save public key + fp = fopen(public_key_file, "wb"); + if (!fp || !PEM_write_PUBKEY(fp, pkey)) { + claim_agent_failure_reason_set("Cannot write public key file: %s", public_key_file); + if (fp) fclose(fp); + EVP_PKEY_free(pkey); + return false; + } + fclose(fp); + + EVP_PKEY_free(pkey); + return true; +} + +static size_t response_write_callback(void *ptr, size_t size, size_t nmemb, void *stream) { + BUFFER *wb = stream; + size_t real_size = size * nmemb; + + buffer_memcat(wb, ptr, real_size); + + return real_size; +} + +static const char *curl_add_json_room(BUFFER *wb, const char *start, const char *end) { + size_t len = end - start; + + // copy the item to an new buffer and terminate it + char buf[len + 1]; + memcpy(buf, start, len); + buf[len] = '\0'; + + // add it to the json array + const char *trimmed = trim(buf); // remove leading and trailing spaces + if(trimmed) + buffer_json_add_array_item_string(wb, trimmed); + + // prepare for the next item + start = end + 1; + + // skip multiple separators or spaces + while(*start == ',' || *start == ' ') start++; + + return start; +} + +void curl_add_rooms_json_array(BUFFER *wb, const char *rooms) { + buffer_json_member_add_array(wb, "rooms"); + if(rooms && *rooms) { + const char *start = rooms, *end = NULL; + + // Skip initial separators or spaces + while (*start == ',' || *start == ' ') + start++; + + // Process each item in the comma-separated list + while ((end = strchr(start, ',')) != NULL) + start = curl_add_json_room(wb, start, end); + + // Process the last item if any + if (*start) + curl_add_json_room(wb, start, &start[strlen(start)]); + } + buffer_json_array_close(wb); +} + +static int debug_callback(CURL *handle, curl_infotype type, char *data, size_t size, void *userptr) { + (void)handle; // Unused + (void)userptr; // Unused + + if (type == CURLINFO_TEXT) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Info: %s", data); + else if (type == CURLINFO_HEADER_OUT) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Send header: %.*s", (int)size, data); + else if (type == CURLINFO_DATA_OUT) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Send data: %.*s", (int)size, data); + else if (type == CURLINFO_SSL_DATA_OUT) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Send SSL data: %.*s", (int)size, data); + else if (type == CURLINFO_HEADER_IN) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Receive header: %.*s", (int)size, data); + else if (type == CURLINFO_DATA_IN) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Receive data: %.*s", (int)size, data); + else if (type == CURLINFO_SSL_DATA_IN) + nd_log(NDLS_DAEMON, NDLP_INFO, "CLAIM: Receive SSL data: %.*s", (int)size, data); + + return 0; +} + +static bool send_curl_request(const char *machine_guid, const char *hostname, const char *token, const char *rooms, const char *url, const char *proxy, int insecure, bool *can_retry) { + CURL *curl; + CURLcode res; + char target_url[2048]; + char public_key[2048] = ""; + FILE *fp; + struct curl_slist *headers = NULL; + + // create a new random claim id + nd_uuid_t claimed_id; + uuid_generate_random(claimed_id); + char claimed_id_str[UUID_STR_LEN]; + uuid_unparse_lower(claimed_id, claimed_id_str); + + // generate the URL to post + snprintf(target_url, sizeof(target_url), "%s%sapi/v1/spaces/nodes/%s", + url, strendswith(url, "/") ? "" : "/", claimed_id_str); + + // Read the public key + CLEAN_CHAR_P *public_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "public.pem"); + fp = fopen(public_key_file, "r"); + if (!fp || fread(public_key, 1, sizeof(public_key), fp) == 0) { + claim_agent_failure_reason_set("cannot read public key file '%s'", public_key_file); + if (fp) fclose(fp); + *can_retry = false; + return false; + } + fclose(fp); + + // check if we have trusted.pem + // or cloud_fullchain.pem, for backwards compatibility + CLEAN_CHAR_P *trusted_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "trusted.pem"); + fp = fopen(trusted_key_file, "r"); + if(fp) + fclose(fp); + else { + freez(trusted_key_file); + trusted_key_file = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "cloud_fullchain.pem"); + fp = fopen(trusted_key_file, "r"); + if(fp) + fclose(fp); + else { + freez(trusted_key_file); + trusted_key_file = NULL; + } + } + + // generate the JSON request message + CLEAN_BUFFER *wb = buffer_create(0, NULL); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY); + + buffer_json_member_add_object(wb, "node"); + { + buffer_json_member_add_string(wb, "id", claimed_id_str); + buffer_json_member_add_string(wb, "hostname", hostname); + } + buffer_json_object_close(wb); // node + + buffer_json_member_add_string(wb, "token", token); + curl_add_rooms_json_array(wb, rooms); + buffer_json_member_add_string(wb, "publicKey", public_key); + buffer_json_member_add_string(wb, "mGUID", machine_guid); + buffer_json_finalize(wb); + + // initialize libcurl + curl = curl_easy_init(); + if(!curl) { + claim_agent_failure_reason_set("Cannot initialize request (curl_easy_init() failed)"); + *can_retry = true; + return false; + } + + // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_callback); + + // we will receive the response in this + CLEAN_BUFFER *response = buffer_create(0, NULL); + + // configure the request + headers = curl_slist_append(headers, "Content-Type: application/json"); + curl_easy_setopt(curl, CURLOPT_URL, target_url); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, buffer_tostring(wb)); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, response_write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); + + if(trusted_key_file) + curl_easy_setopt(curl, CURLOPT_CAINFO, trusted_key_file); + + // Proxy configuration + if (proxy) { + if (!*proxy || strcmp(proxy, "none") == 0) + // disable proxy configuration in libcurl + curl_easy_setopt(curl, CURLOPT_PROXY, ""); + + else if (strcmp(proxy, "env") != 0) + // set the custom proxy for libcurl + curl_easy_setopt(curl, CURLOPT_PROXY, proxy); + + // otherwise, libcurl will use its own proxy environment variables + } + + // Insecure option + if (insecure) { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + } + + // Set timeout options + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5); + + // execute the request + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + claim_agent_failure_reason_set("Request failed with error: %s", curl_easy_strerror(res)); + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + *can_retry = true; + return false; + } + + // Get HTTP response code + long http_status_code; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status_code); + + bool ret = false; + if(http_status_code == 204) { + if(!cloud_conf_regenerate(claimed_id_str, machine_guid, hostname, token, rooms, url, proxy, insecure)) { + claim_agent_failure_reason_set("Failed to save claiming info to disk"); + } + else { + claim_agent_failure_reason_set(NULL); + ret = true; + } + + *can_retry = false; + } + else if (http_status_code == 422) { + if(buffer_strlen(response)) { + struct json_object *parsed_json; + struct json_object *error_key_obj; + const char *error_key = NULL; + + parsed_json = json_tokener_parse(buffer_tostring(response)); + if(parsed_json) { + if (json_object_object_get_ex(parsed_json, "errorMsgKey", &error_key_obj)) + error_key = json_object_get_string(error_key_obj); + + if (strcmp(error_key, "ErrInvalidNodeID") == 0) + claim_agent_failure_reason_set("Failed: the node id is invalid"); + else if (strcmp(error_key, "ErrInvalidNodeName") == 0) + claim_agent_failure_reason_set("Failed: the node name is invalid"); + else if (strcmp(error_key, "ErrInvalidRoomID") == 0) + claim_agent_failure_reason_set("Failed: one or more room ids are invalid"); + else if (strcmp(error_key, "ErrInvalidPublicKey") == 0) + claim_agent_failure_reason_set("Failed: the public key is invalid"); + else + claim_agent_failure_reason_set("Failed with description '%s'", error_key); + + json_object_put(parsed_json); + } + else + claim_agent_failure_reason_set("Failed with a response code %ld", http_status_code); + } + else + claim_agent_failure_reason_set("Failed with an empty response, code %ld", http_status_code); + + *can_retry = false; + } + else if(http_status_code == 102) { + claim_agent_failure_reason_set("Claiming is in progress"); + *can_retry = false; + } + else if(http_status_code == 403) { + claim_agent_failure_reason_set("Failed: token is expired, not found, or invalid"); + *can_retry = false; + } + else if(http_status_code == 409) { + claim_agent_failure_reason_set("Failed: agent is already claimed"); + *can_retry = false; + } + else if(http_status_code == 500) { + claim_agent_failure_reason_set("Failed: received Internal Server Error"); + *can_retry = true; + } + else if(http_status_code == 503) { + claim_agent_failure_reason_set("Failed: Netdata Cloud is unavailable"); + *can_retry = true; + } + else if(http_status_code == 504) { + claim_agent_failure_reason_set("Failed: Gateway Timeout"); + *can_retry = true; + } + else { + claim_agent_failure_reason_set("Failed with response code %ld", http_status_code); + *can_retry = true; + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + return ret; +} + +bool claim_agent(const char *url, const char *token, const char *rooms, const char *proxy, bool insecure) { + static SPINLOCK spinlock = NETDATA_SPINLOCK_INITIALIZER; + spinlock_lock(&spinlock); + + if (!check_and_generate_certificates()) { + spinlock_unlock(&spinlock); + return false; + } + + bool done = false, can_retry = true; + size_t retries = 0; + do { + done = send_curl_request(registry_get_this_machine_guid(), registry_get_this_machine_hostname(), token, rooms, url, proxy, insecure, &can_retry); + if (done) break; + sleep_usec(300 * USEC_PER_MS + 100 * retries * USEC_PER_MS); + retries++; + } while(can_retry && retries < 5); + + spinlock_unlock(&spinlock); + return done; +} + +bool claim_agent_from_environment(void) { + const char *url = getenv("NETDATA_CLAIM_URL"); + if(!url || !*url) { + url = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "url", DEFAULT_CLOUD_BASE_URL); + if(!url || !*url) return false; + } + + const char *token = getenv("NETDATA_CLAIM_TOKEN"); + if(!token || !*token) + return false; + + const char *rooms = getenv("NETDATA_CLAIM_ROOMS"); + if(!rooms) + rooms = ""; + + const char *proxy = getenv("NETDATA_CLAIM_PROXY"); + if(!proxy || !*proxy) + proxy = ""; + + bool insecure = CONFIG_BOOLEAN_NO; + const char *from_env = getenv("NETDATA_EXTRA_CLAIM_OPTS"); + if(from_env && *from_env && strstr(from_env, "-insecure") == 0) + insecure = CONFIG_BOOLEAN_YES; + + return claim_agent(url, token, rooms, proxy, insecure); +} + +bool claim_agent_from_claim_conf(void) { + static struct config claim_config = APPCONFIG_INITIALIZER; + static SPINLOCK spinlock = NETDATA_SPINLOCK_INITIALIZER; + bool ret = false; + + spinlock_lock(&spinlock); + + errno_clear(); + char *filename = filename_from_path_entry_strdupz(netdata_configured_user_config_dir, "claim.conf"); + bool loaded = appconfig_load(&claim_config, filename, 1, NULL); + freez(filename); + + if(loaded) { + const char *url = appconfig_get(&claim_config, CONFIG_SECTION_GLOBAL, "url", DEFAULT_CLOUD_BASE_URL); + const char *token = appconfig_get(&claim_config, CONFIG_SECTION_GLOBAL, "token", ""); + const char *rooms = appconfig_get(&claim_config, CONFIG_SECTION_GLOBAL, "rooms", ""); + const char *proxy = appconfig_get(&claim_config, CONFIG_SECTION_GLOBAL, "proxy", ""); + bool insecure = appconfig_get_boolean(&claim_config, CONFIG_SECTION_GLOBAL, "insecure", CONFIG_BOOLEAN_NO); + + if(token && *token && url && *url) + ret = claim_agent(url, token, rooms, proxy, insecure); + } + + spinlock_unlock(&spinlock); + + return ret; +} + +bool claim_agent_from_split_files(void) { + char filename[FILENAME_MAX + 1]; + + snprintfz(filename, sizeof(filename), "%s/token", netdata_configured_cloud_dir); + long token_len = 0; + char *token = read_by_filename(filename, &token_len); + if(!token || !*token) { + freez(token); + return false; + } + + snprintfz(filename, sizeof(filename), "%s/rooms", netdata_configured_cloud_dir); + long rooms_len = 0; + char *rooms = read_by_filename(filename, &rooms_len); + if(!rooms || !*rooms) { + freez(rooms); + rooms = NULL; + } + + bool ret = claim_agent(cloud_config_url_get(), token, rooms, cloud_config_proxy_get(), cloud_config_insecure_get()); + + if(ret) { + snprintfz(filename, sizeof(filename), "%s/token", netdata_configured_cloud_dir); + unlink(filename); + + snprintfz(filename, sizeof(filename), "%s/rooms", netdata_configured_cloud_dir); + unlink(filename); + } + + return ret; +} + +bool claim_agent_automatically(void) { + // Use /etc/netdata/claim.conf + + if(claim_agent_from_claim_conf()) + return true; + + // Users may set NETDATA_CLAIM_TOKEN and NETDATA_CLAIM_ROOMS + // A good choice for docker container users. + + if(claim_agent_from_environment()) + return true; + + // Users may store token and rooms in /var/lib/netdata/cloud.d + // This was a bad choice, since users may have to create this directory + // which may end up with the wrong permissions, preventing netdata from storing + // the required information there. + + if(claim_agent_from_split_files()) + return true; + + return false; +} diff --git a/src/claim/claim.c b/src/claim/claim.c index 5383aac37..24e4e1c3c 100644 --- a/src/claim/claim.c +++ b/src/claim/claim.c @@ -1,470 +1,209 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "claim.h" -#include "registry/registry_internals.h" -#include "aclk/aclk.h" -#include "aclk/aclk_proxy.h" - -char *claiming_pending_arguments = NULL; - -static char *claiming_errors[] = { - "Agent claimed successfully", // 0 - "Unknown argument", // 1 - "Problems with claiming working directory", // 2 - "Missing dependencies", // 3 - "Failure to connect to endpoint", // 4 - "The CLI didn't work", // 5 - "Wrong user", // 6 - "Unknown HTTP error message", // 7 - "invalid node id", // 8 - "invalid node name", // 9 - "invalid room id", // 10 - "invalid public key", // 11 - "token expired/token not found/invalid token", // 12 - "already claimed", // 13 - "processing claiming", // 14 - "Internal Server Error", // 15 - "Gateway Timeout", // 16 - "Service Unavailable", // 17 - "Agent Unique Id Not Readable" // 18 -}; - -/* Retrieve the claim id for the agent. - * Caller owns the string. -*/ -char *get_agent_claimid() -{ - char *result; - rrdhost_aclk_state_lock(localhost); - result = (localhost->aclk_state.claimed_id == NULL) ? NULL : strdupz(localhost->aclk_state.claimed_id); - rrdhost_aclk_state_unlock(localhost); - return result; -} - -#define CLAIMING_COMMAND_LENGTH 16384 -#define CLAIMING_PROXY_LENGTH (CLAIMING_COMMAND_LENGTH/4) -/* rrd_init() and post_conf_load() must have been called before this function */ -CLAIM_AGENT_RESPONSE claim_agent(const char *claiming_arguments, bool force, const char **msg __maybe_unused) -{ - if (!force || !netdata_cloud_enabled) { - netdata_log_error("Refusing to claim agent -> cloud functionality has been disabled"); - return CLAIM_AGENT_CLOUD_DISABLED; - } +// -------------------------------------------------------------------------------------------------------------------- +// keep track of the last claiming failure reason -#ifndef DISABLE_CLOUD - char command_exec_buffer[CLAIMING_COMMAND_LENGTH + 1]; - char command_line_buffer[CLAIMING_COMMAND_LENGTH + 1]; +static char cloud_claim_failure_reason[4096] = ""; - // This is guaranteed to be set early in main via post_conf_load() - char *cloud_base_url = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "cloud base url", NULL); - if (cloud_base_url == NULL) { - internal_fatal(true, "Do not move the cloud base url out of post_conf_load!!"); - return CLAIM_AGENT_NO_CLOUD_URL; +void claim_agent_failure_reason_set(const char *format, ...) { + if(!format || !*format) { + cloud_claim_failure_reason[0] = '\0'; + return; } - const char *proxy_str; - ACLK_PROXY_TYPE proxy_type; - char proxy_flag[CLAIMING_PROXY_LENGTH] = "-noproxy"; - - proxy_str = aclk_get_proxy(&proxy_type); - - if (proxy_type == PROXY_TYPE_SOCKS5 || proxy_type == PROXY_TYPE_HTTP) - snprintf(proxy_flag, CLAIMING_PROXY_LENGTH, "-proxy=\"%s\"", proxy_str); - - snprintfz(command_exec_buffer, CLAIMING_COMMAND_LENGTH, - "exec \"%s%snetdata-claim.sh\"", - netdata_exe_path[0] ? netdata_exe_path : "", - netdata_exe_path[0] ? "/" : "" - ); - - snprintfz(command_line_buffer, - CLAIMING_COMMAND_LENGTH, - "%s %s -hostname=%s -id=%s -url=%s -noreload %s", - command_exec_buffer, - proxy_flag, - netdata_configured_hostname, - localhost->machine_guid, - cloud_base_url, - claiming_arguments); - - netdata_log_info("Executing agent claiming command: %s", command_exec_buffer); - POPEN_INSTANCE *instance = spawn_popen_run(command_line_buffer); - if(!instance) { - netdata_log_error("Cannot popen(\"%s\").", command_exec_buffer); - return CLAIM_AGENT_CANNOT_EXECUTE_CLAIM_SCRIPT; - } + va_list args; + va_start(args, format); + vsnprintf(cloud_claim_failure_reason, sizeof(cloud_claim_failure_reason), format, args); + va_end(args); + + nd_log(NDLS_DAEMON, NDLP_ERR, + "CLAIM: %s", cloud_claim_failure_reason); +} - netdata_log_info("Waiting for claiming command '%s' to finish.", command_exec_buffer); - char read_buffer[100 + 1]; - while (fgets(read_buffer, 100, instance->child_stdout_fp) != NULL) ; +const char *claim_agent_failure_reason_get(void) { + if(!cloud_claim_failure_reason[0]) + return "Agent is not claimed yet"; + else + return cloud_claim_failure_reason; +} - int exit_code = spawn_popen_wait(instance); +// -------------------------------------------------------------------------------------------------------------------- +// claimed_id load/save - netdata_log_info("Agent claiming command '%s' returned with code %d", command_exec_buffer, exit_code); - if (0 == exit_code) { - load_claiming_state(); - return CLAIM_AGENT_OK; - } - if (exit_code < 0) { - netdata_log_error("Agent claiming command '%s' failed to complete its run", command_exec_buffer); - return CLAIM_AGENT_CLAIM_SCRIPT_FAILED; +bool claimed_id_save_to_file(const char *claimed_id_str) { + bool ret; + const char *filename = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "claimed_id"); + FILE *fp = fopen(filename, "w"); + if(fp) { + fprintf(fp, "%s", claimed_id_str); + fclose(fp); + ret = true; } - errno_clear(); - unsigned maximum_known_exit_code = sizeof(claiming_errors) / sizeof(claiming_errors[0]) - 1; - - if ((unsigned)exit_code > maximum_known_exit_code) { - netdata_log_error("Agent failed to be claimed with an unknown error. Cmd: '%s'", command_exec_buffer); - return CLAIM_AGENT_CLAIM_SCRIPT_RETURNED_INVALID_CODE; + else { + nd_log(NDLS_DAEMON, NDLP_ERR, + "CLAIM: cannot open file '%s' for writing.", filename); + ret = false; } - netdata_log_error("Agent failed to be claimed using the command '%s' with the following error message: %s", - command_exec_buffer, claiming_errors[exit_code]); + freez((void *)filename); + return ret; +} - if(msg) *msg = claiming_errors[exit_code]; +static ND_UUID claimed_id_parse(const char *claimed_id, const char *source) { + ND_UUID uuid; -#else - UNUSED(claiming_arguments); - UNUSED(claiming_errors); -#endif + if(uuid_parse_flexi(claimed_id, uuid.uuid) != 0) { + uuid = UUID_ZERO; + nd_log(NDLS_DAEMON, NDLP_ERR, + "CLAIM: claimed_id '%s' (loaded from '%s'), is not a valid UUID.", + claimed_id, source); + } - return CLAIM_AGENT_FAILED_WITH_MESSAGE; + return uuid; } -/* Change the claimed state of the agent. - * - * This only happens when the user has explicitly requested it: - * - via the cli tool by reloading the claiming state - * - after spawning the claim because of a command-line argument - * If this happens with the ACLK active under an old claim then we MUST KILL THE LINK - */ -void load_claiming_state(void) -{ - // -------------------------------------------------------------------- - // Check if the cloud is enabled -#if defined( DISABLE_CLOUD ) || !defined( ENABLE_ACLK ) - netdata_cloud_enabled = false; -#else - nd_uuid_t uuid; - - // Propagate into aclk and registry. Be kind of atomic... - appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "cloud base url", DEFAULT_CLOUD_BASE_URL); - - rrdhost_aclk_state_lock(localhost); - if (localhost->aclk_state.claimed_id) { - if (aclk_connected) - localhost->aclk_state.prev_claimed_id = strdupz(localhost->aclk_state.claimed_id); - freez(localhost->aclk_state.claimed_id); - localhost->aclk_state.claimed_id = NULL; - } - if (aclk_connected) - { - netdata_log_info("Agent was already connected to Cloud - forcing reconnection under new credentials"); - aclk_kill_link = 1; - } - aclk_disable_runtime = 0; - - char filename[FILENAME_MAX + 1]; - snprintfz(filename, FILENAME_MAX, "%s/cloud.d/claimed_id", netdata_configured_varlib_dir); +static ND_UUID claimed_id_load_from_file(void) { + ND_UUID uuid; long bytes_read; + const char *filename = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "claimed_id"); char *claimed_id = read_by_filename(filename, &bytes_read); - if(claimed_id && uuid_parse(claimed_id, uuid)) { - netdata_log_error("claimed_id \"%s\" doesn't look like valid UUID", claimed_id); - freez(claimed_id); - claimed_id = NULL; - } - - if(claimed_id) { - localhost->aclk_state.claimed_id = mallocz(UUID_STR_LEN); - uuid_unparse_lower(uuid, localhost->aclk_state.claimed_id); - } - rrdhost_aclk_state_unlock(localhost); - invalidate_node_instances(&localhost->host_uuid, claimed_id ? &uuid : NULL); - metaqueue_store_claim_id(&localhost->host_uuid, claimed_id ? &uuid : NULL); - - if (!claimed_id) { - netdata_log_info("Unable to load '%s', setting state to AGENT_UNCLAIMED", filename); - return; - } + if(!claimed_id) + uuid = UUID_ZERO; + else + uuid = claimed_id_parse(claimed_id, filename); freez(claimed_id); - - netdata_log_info("File '%s' was found. Setting state to AGENT_CLAIMED.", filename); - netdata_cloud_enabled = appconfig_get_boolean_ondemand(&cloud_config, CONFIG_SECTION_GLOBAL, "enabled", netdata_cloud_enabled); -#endif + freez((void *)filename); + return uuid; } -struct config cloud_config = { .first_section = NULL, - .last_section = NULL, - .mutex = NETDATA_MUTEX_INITIALIZER, - .index = { .avl_tree = { .root = NULL, .compar = appconfig_section_compare }, - .rwlock = AVL_LOCK_INITIALIZER } }; - -void load_cloud_conf(int silent) -{ - char *nd_disable_cloud = getenv("NETDATA_DISABLE_CLOUD"); - if (nd_disable_cloud && !strncmp(nd_disable_cloud, "1", 1)) - netdata_cloud_enabled = CONFIG_BOOLEAN_NO; - - char *filename; - errno_clear(); - - int ret = 0; - - filename = strdupz_path_subpath(netdata_configured_varlib_dir, "cloud.d/cloud.conf"); - - ret = appconfig_load(&cloud_config, filename, 1, NULL); - if(!ret && !silent) - netdata_log_info("CONFIG: cannot load cloud config '%s'. Running with internal defaults.", filename); - - freez(filename); - - // -------------------------------------------------------------------- - // Check if the cloud is enabled - -#if defined( DISABLE_CLOUD ) || !defined( ENABLE_ACLK ) - netdata_cloud_enabled = CONFIG_BOOLEAN_NO; -#else - netdata_cloud_enabled = appconfig_get_boolean_ondemand(&cloud_config, CONFIG_SECTION_GLOBAL, "enabled", netdata_cloud_enabled); -#endif - - // This must be set before any point in the code that accesses it. Do not move it from this function. - appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "cloud base url", DEFAULT_CLOUD_BASE_URL); -} - -static char *netdata_random_session_id_filename = NULL; -static nd_uuid_t netdata_random_session_id = { 0 }; - -bool netdata_random_session_id_generate(void) { - static char guid[UUID_STR_LEN] = ""; - - uuid_generate_random(netdata_random_session_id); - uuid_unparse_lower(netdata_random_session_id, guid); - - char filename[FILENAME_MAX + 1]; - snprintfz(filename, FILENAME_MAX, "%s/netdata_random_session_id", netdata_configured_varlib_dir); - - bool ret = true; - - (void)unlink(filename); - - // save it - int fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 640); - if(fd == -1) { - netdata_log_error("Cannot create random session id file '%s'.", filename); - ret = false; +static ND_UUID claimed_id_get_from_cloud_conf(void) { + if(appconfig_exists(&cloud_config, CONFIG_SECTION_GLOBAL, "claimed_id")) { + const char *claimed_id = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "claimed_id", ""); + if(claimed_id && *claimed_id) + return claimed_id_parse(claimed_id, "cloud.conf"); } - else { - if (write(fd, guid, UUID_STR_LEN - 1) != UUID_STR_LEN - 1) { - netdata_log_error("Cannot write the random session id file '%s'.", filename); - ret = false; - } else { - ssize_t bytes = write(fd, "\n", 1); - UNUSED(bytes); - } - close(fd); - } - - if(ret && (!netdata_random_session_id_filename || strcmp(netdata_random_session_id_filename, filename) != 0)) { - freez(netdata_random_session_id_filename); - netdata_random_session_id_filename = strdupz(filename); - } - - return ret; + return UUID_ZERO; } -const char *netdata_random_session_id_get_filename(void) { - if(!netdata_random_session_id_filename) - netdata_random_session_id_generate(); +static ND_UUID claimed_id_load(void) { + ND_UUID uuid = claimed_id_get_from_cloud_conf(); + if(UUIDiszero(uuid)) + uuid = claimed_id_load_from_file(); - return netdata_random_session_id_filename; + return uuid; } -bool netdata_random_session_id_matches(const char *guid) { - if(uuid_is_null(netdata_random_session_id)) - return false; +bool is_agent_claimed(void) { + ND_UUID uuid = claim_id_get_uuid(); + return !UUIDiszero(uuid); +} - nd_uuid_t uuid; +// -------------------------------------------------------------------------------------------------------------------- - if(uuid_parse(guid, uuid)) +bool claim_id_matches(const char *claim_id) { + ND_UUID this_one = UUID_ZERO; + if(uuid_parse_flexi(claim_id, this_one.uuid) != 0 || UUIDiszero(this_one)) return false; - if(uuid_compare(netdata_random_session_id, uuid) == 0) + ND_UUID having = claim_id_get_uuid(); + if(!UUIDiszero(having) && UUIDeq(having, this_one)) return true; return false; } -static bool check_claim_param(const char *s) { - if(!s || !*s) return true; +bool claim_id_matches_any(const char *claim_id) { + ND_UUID this_one = UUID_ZERO; + if(uuid_parse_flexi(claim_id, this_one.uuid) != 0 || UUIDiszero(this_one)) + return false; - do { - if(isalnum((uint8_t)*s) || *s == '.' || *s == ',' || *s == '-' || *s == ':' || *s == '/' || *s == '_') - ; - else - return false; + ND_UUID having = claim_id_get_uuid(); + if(!UUIDiszero(having) && UUIDeq(having, this_one)) + return true; - } while(*++s); + having = localhost->aclk.claim_id_of_parent; + if(!UUIDiszero(having) && UUIDeq(having, this_one)) + return true; - return true; -} + having = localhost->aclk.claim_id_of_origin; + if(!UUIDiszero(having) && UUIDeq(having, this_one)) + return true; -void claim_reload_all(void) { - nd_log_limits_unlimited(); - load_claiming_state(); - registry_update_cloud_base_url(); - rrdpush_send_claimed_id(localhost); - nd_log_limits_reset(); + return false; } -int api_v2_claim(struct web_client *w, char *url) { - char *key = NULL; - char *token = NULL; - char *rooms = NULL; - char *base_url = NULL; - - while (url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if (!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if (!name || !*name) continue; - if (!value || !*value) continue; - - if(!strcmp(name, "key")) - key = value; - else if(!strcmp(name, "token")) - token = value; - else if(!strcmp(name, "rooms")) - rooms = value; - else if(!strcmp(name, "url")) - base_url = value; +/* Change the claimed state of the agent. + * + * This only happens when the user has explicitly requested it: + * - via the cli tool by reloading the claiming state + * - after spawning the claim because of a command-line argument + * If this happens with the ACLK active under an old claim then we MUST KILL THE LINK + */ +bool load_claiming_state(void) { + if (aclk_online()) { + nd_log(NDLS_DAEMON, NDLP_ERR, + "CLAIM: agent was already connected to NC - forcing reconnection under new credentials"); + disconnect_req = ACLK_RELOAD_CONF; } + aclk_disable_runtime = 0; - BUFFER *wb = w->response.data; - buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); - - time_t now_s = now_realtime_sec(); - CLOUD_STATUS status = buffer_json_cloud_status(wb, now_s); - - bool can_be_claimed = false; - switch(status) { - case CLOUD_STATUS_AVAILABLE: - case CLOUD_STATUS_DISABLED: - case CLOUD_STATUS_OFFLINE: - can_be_claimed = true; - break; - - case CLOUD_STATUS_UNAVAILABLE: - case CLOUD_STATUS_BANNED: - case CLOUD_STATUS_ONLINE: - can_be_claimed = false; - break; + ND_UUID uuid = claimed_id_load(); + if(UUIDiszero(uuid)) { + // not found + if(claim_agent_automatically()) + uuid = claimed_id_load(); } - buffer_json_member_add_boolean(wb, "can_be_claimed", can_be_claimed); - - if(can_be_claimed && key) { - if(!netdata_random_session_id_matches(key)) { - buffer_reset(wb); - buffer_strcat(wb, "invalid key"); - netdata_random_session_id_generate(); // generate a new key, to avoid an attack to find it - return HTTP_RESP_FORBIDDEN; - } - - if(!token || !base_url || !check_claim_param(token) || !check_claim_param(base_url) || (rooms && !check_claim_param(rooms))) { - buffer_reset(wb); - buffer_strcat(wb, "invalid parameters"); - netdata_random_session_id_generate(); // generate a new key, to avoid an attack to find it - return HTTP_RESP_BAD_REQUEST; - } - - netdata_random_session_id_generate(); // generate a new key, to avoid an attack to find it - - netdata_cloud_enabled = CONFIG_BOOLEAN_AUTO; - appconfig_set_boolean(&cloud_config, CONFIG_SECTION_GLOBAL, "enabled", CONFIG_BOOLEAN_AUTO); - appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "cloud base url", base_url); - - nd_uuid_t claimed_id; - uuid_generate_random(claimed_id); - char claimed_id_str[UUID_STR_LEN]; - uuid_unparse_lower(claimed_id, claimed_id_str); - - BUFFER *t = buffer_create(1024, NULL); - if(rooms) - buffer_sprintf(t, "-id=%s -token=%s -rooms=%s", claimed_id_str, token, rooms); - else - buffer_sprintf(t, "-id=%s -token=%s", claimed_id_str, token); - - bool success = false; - const char *msg = NULL; - CLAIM_AGENT_RESPONSE rc = claim_agent(buffer_tostring(t), true, &msg); - switch(rc) { - case CLAIM_AGENT_OK: - msg = "ok"; - success = true; - can_be_claimed = false; - claim_reload_all(); - { - int ms = 0; - do { - status = cloud_status(); - if (status == CLOUD_STATUS_ONLINE && __atomic_load_n(&localhost->node_id, __ATOMIC_RELAXED)) - break; - - sleep_usec(50 * USEC_PER_MS); - ms += 50; - } while (ms < 10000); - } - break; + bool have_claimed_id = false; + if(!UUIDiszero(uuid)) { + // we go it somehow + claim_id_set(uuid); + have_claimed_id = true; + } - case CLAIM_AGENT_NO_CLOUD_URL: - msg = "No Netdata Cloud URL."; - break; + invalidate_node_instances(&localhost->host_id.uuid, have_claimed_id ? &uuid.uuid : NULL); + metaqueue_store_claim_id(&localhost->host_id.uuid, have_claimed_id ? &uuid.uuid : NULL); - case CLAIM_AGENT_CLAIM_SCRIPT_FAILED: - msg = "Claiming script failed."; - break; + errno_clear(); - case CLAIM_AGENT_CLOUD_DISABLED: - msg = "Netdata Cloud is disabled on this agent."; - break; + if (!have_claimed_id) + nd_log(NDLS_DAEMON, NDLP_ERR, + "CLAIM: Unable to find our claimed_id, setting state to AGENT_UNCLAIMED"); + else + nd_log(NDLS_DAEMON, NDLP_INFO, + "CLAIM: Found a valid claimed_id, setting state to AGENT_CLAIMED"); - case CLAIM_AGENT_CANNOT_EXECUTE_CLAIM_SCRIPT: - msg = "Failed to execute claiming script."; - break; + return have_claimed_id; +} - case CLAIM_AGENT_CLAIM_SCRIPT_RETURNED_INVALID_CODE: - msg = "Claiming script returned invalid code."; - break; +CLOUD_STATUS claim_reload_and_wait_online(void) { + nd_log(NDLS_DAEMON, NDLP_INFO, + "CLAIM: Reloading Agent Claiming configuration."); - default: - case CLAIM_AGENT_FAILED_WITH_MESSAGE: - if(!msg) - msg = "Unknown error"; - break; - } - - // our status may have changed - // refresh the status in our output - buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); - now_s = now_realtime_sec(); - buffer_json_cloud_status(wb, now_s); - - // and this is the status of the claiming command we run - buffer_json_member_add_boolean(wb, "success", success); - buffer_json_member_add_string(wb, "message", msg); - } + nd_log_limits_unlimited(); + cloud_conf_load(0); + bool claimed = load_claiming_state(); + registry_update_cloud_base_url(); + rrdpush_sender_send_claimed_id(localhost); + nd_log_limits_reset(); - if(can_be_claimed) - buffer_json_member_add_string(wb, "key_filename", netdata_random_session_id_get_filename()); + CLOUD_STATUS status = cloud_status(); + if(claimed) { + int ms = 0; + do { + status = cloud_status(); + if ((status == CLOUD_STATUS_ONLINE) && !UUIDiszero(localhost->node_id)) + break; - buffer_json_agents_v2(wb, NULL, now_s, false, false); - buffer_json_finalize(wb); + sleep_usec(50 * USEC_PER_MS); + ms += 50; + } while (ms < 10000); + } - return HTTP_RESP_OK; + return status; } diff --git a/src/claim/claim.h b/src/claim/claim.h index ccab8aaa1..073771d1c 100644 --- a/src/claim/claim.h +++ b/src/claim/claim.h @@ -4,29 +4,32 @@ #define NETDATA_CLAIM_H 1 #include "daemon/common.h" +#include "cloud-status.h" +#include "claim_id.h" + +const char *claim_agent_failure_reason_get(void); +void claim_agent_failure_reason_set(const char *format, ...) PRINTFLIKE(1, 2); -extern char *claiming_pending_arguments; extern struct config cloud_config; -typedef enum __attribute__((packed)) { - CLAIM_AGENT_OK, - CLAIM_AGENT_CLOUD_DISABLED, - CLAIM_AGENT_NO_CLOUD_URL, - CLAIM_AGENT_CANNOT_EXECUTE_CLAIM_SCRIPT, - CLAIM_AGENT_CLAIM_SCRIPT_FAILED, - CLAIM_AGENT_CLAIM_SCRIPT_RETURNED_INVALID_CODE, - CLAIM_AGENT_FAILED_WITH_MESSAGE, -} CLAIM_AGENT_RESPONSE; - -CLAIM_AGENT_RESPONSE claim_agent(const char *claiming_arguments, bool force, const char **msg); -char *get_agent_claimid(void); -void load_claiming_state(void); -void load_cloud_conf(int silent); -void claim_reload_all(void); - -bool netdata_random_session_id_generate(void); -const char *netdata_random_session_id_get_filename(void); -bool netdata_random_session_id_matches(const char *guid); -int api_v2_claim(struct web_client *w, char *url); +bool claim_agent(const char *url, const char *token, const char *rooms, const char *proxy, bool insecure); +bool claim_agent_automatically(void); + +bool claimed_id_save_to_file(const char *claimed_id_str); + +bool is_agent_claimed(void); +bool claim_id_matches(const char *claim_id); +bool claim_id_matches_any(const char *claim_id); +bool load_claiming_state(void); +void cloud_conf_load(int silent); +void cloud_conf_init_after_registry(void); +bool cloud_conf_save(void); +bool cloud_conf_regenerate(const char *claimed_id_str, const char *machine_guid, const char *hostname, const char *token, const char *rooms, const char *url, const char *proxy, int insecure); +CLOUD_STATUS claim_reload_and_wait_online(void); + +const char *cloud_config_url_get(void); +void cloud_config_url_set(const char *url); +const char *cloud_config_proxy_get(void); +bool cloud_config_insecure_get(void); #endif //NETDATA_CLAIM_H diff --git a/src/claim/claim_id.c b/src/claim/claim_id.c new file mode 100644 index 000000000..dd79eb640 --- /dev/null +++ b/src/claim/claim_id.c @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "claim_id.h" + +static struct { + SPINLOCK spinlock; + ND_UUID claim_uuid; + ND_UUID claim_uuid_saved; +} claim = { + .spinlock = NETDATA_SPINLOCK_INITIALIZER, +}; + +void claim_id_clear_previous_working(void) { + spinlock_lock(&claim.spinlock); + claim.claim_uuid_saved = UUID_ZERO; + spinlock_unlock(&claim.spinlock); +} + +void claim_id_set(ND_UUID new_claim_id) { + spinlock_lock(&claim.spinlock); + + if(!UUIDiszero(claim.claim_uuid)) { + if(aclk_online()) + claim.claim_uuid_saved = claim.claim_uuid; + claim.claim_uuid = UUID_ZERO; + } + + claim.claim_uuid = new_claim_id; + if(localhost) + localhost->aclk.claim_id_of_origin = claim.claim_uuid; + + spinlock_unlock(&claim.spinlock); +} + +// returns true when the supplied str is a valid UUID. +// giving NULL, an empty string, or "NULL" is valid. +bool claim_id_set_str(const char *claim_id_str) { + bool rc; + + ND_UUID uuid; + if(!claim_id_str || !*claim_id_str || strcmp(claim_id_str, "NULL") == 0) { + uuid = UUID_ZERO, + rc = true; + } + else + rc = uuid_parse(claim_id_str, uuid.uuid) == 0; + + claim_id_set(uuid); + + return rc; +} + +ND_UUID claim_id_get_uuid(void) { + static ND_UUID uuid; + spinlock_lock(&claim.spinlock); + uuid = claim.claim_uuid; + spinlock_unlock(&claim.spinlock); + return uuid; +} + +void claim_id_get_str(char str[UUID_STR_LEN]) { + ND_UUID uuid = claim_id_get_uuid(); + + if(UUIDiszero(uuid)) + memset(str, 0, UUID_STR_LEN); + else + uuid_unparse_lower(uuid.uuid, str); +} + +const char *claim_id_get_str_mallocz(void) { + char *str = mallocz(UUID_STR_LEN); + claim_id_get_str(str); + return str; +} + +CLAIM_ID claim_id_get(void) { + CLAIM_ID ret = { + .uuid = claim_id_get_uuid(), + }; + + if(claim_id_is_set(ret)) + uuid_unparse_lower(ret.uuid.uuid, ret.str); + else + ret.str[0] = '\0'; + + return ret; +} + +CLAIM_ID claim_id_get_last_working(void) { + CLAIM_ID ret = { 0 }; + + spinlock_lock(&claim.spinlock); + ret.uuid = claim.claim_uuid_saved; + spinlock_unlock(&claim.spinlock); + + if(claim_id_is_set(ret)) + uuid_unparse_lower(ret.uuid.uuid, ret.str); + else + ret.str[0] = '\0'; + + return ret; +} + +CLAIM_ID rrdhost_claim_id_get(RRDHOST *host) { + CLAIM_ID ret = { 0 }; + + if(host == localhost) { + ret.uuid = claim_id_get_uuid(); + if(UUIDiszero(ret.uuid)) + ret.uuid = host->aclk.claim_id_of_parent; + } + else { + if (!UUIDiszero(host->aclk.claim_id_of_origin)) + ret.uuid = host->aclk.claim_id_of_origin; + else + ret.uuid = host->aclk.claim_id_of_parent; + } + + if(claim_id_is_set(ret)) + uuid_unparse_lower(ret.uuid.uuid, ret.str); + + return ret; +} diff --git a/src/claim/claim_id.h b/src/claim/claim_id.h new file mode 100644 index 000000000..95958d430 --- /dev/null +++ b/src/claim/claim_id.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_CLAIM_ID_H +#define NETDATA_CLAIM_ID_H + +#include "claim.h" + +void claim_id_keep_current(void); + +bool claim_id_set_str(const char *claim_id_str); +void claim_id_set(ND_UUID new_claim_id); +void claim_id_clear_previous_working(void); +ND_UUID claim_id_get_uuid(void); +void claim_id_get_str(char str[UUID_STR_LEN]); +const char *claim_id_get_str_mallocz(void); + +typedef struct { + ND_UUID uuid; + char str[UUID_STR_LEN]; +} CLAIM_ID; + +#define claim_id_is_set(claim_id) (!UUIDiszero(claim_id.uuid)) + +CLAIM_ID claim_id_get(void); +CLAIM_ID claim_id_get_last_working(void); +CLAIM_ID rrdhost_claim_id_get(RRDHOST *host); + +#endif //NETDATA_CLAIM_ID_H diff --git a/src/claim/cloud-conf.c b/src/claim/cloud-conf.c new file mode 100644 index 000000000..bfa971b99 --- /dev/null +++ b/src/claim/cloud-conf.c @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "claim.h" + +struct config cloud_config = APPCONFIG_INITIALIZER; + +const char *cloud_config_url_get(void) { + return appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "url", DEFAULT_CLOUD_BASE_URL); +} + +void cloud_config_url_set(const char *url) { + if(!url || *url) return; + + const char *existing = cloud_config_url_get(); + if(strcmp(existing, url) != 0) + appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "url", url); +} + +const char *cloud_config_proxy_get(void) { + // load cloud.conf or internal default + const char *proxy = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "proxy", "env"); + + // backwards compatibility, from when proxy was in netdata.conf + // netdata.conf has bigger priority + if (config_exists(CONFIG_SECTION_CLOUD, "proxy")) { + // get it from netdata.conf + proxy = config_get(CONFIG_SECTION_CLOUD, "proxy", proxy); + + // update cloud.conf + proxy = appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "proxy", proxy); + } + else { + // set in netdata.conf the proxy of cloud.conf + config_set(CONFIG_SECTION_CLOUD, "proxy", proxy); + } + + return proxy; +} + +bool cloud_config_insecure_get(void) { + // load it from cloud.conf or use internal default + return appconfig_get_boolean(&cloud_config, CONFIG_SECTION_GLOBAL, "insecure", CONFIG_BOOLEAN_NO); +} + +static void cloud_conf_load_defaults(void) { + appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "url", DEFAULT_CLOUD_BASE_URL); + appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "proxy", "env"); + appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "token", ""); + appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "rooms", ""); + appconfig_get_boolean(&cloud_config, CONFIG_SECTION_GLOBAL, "insecure", CONFIG_BOOLEAN_NO); + appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "machine_guid", ""); + appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "claimed_id", ""); + appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "hostname", ""); +} + +void cloud_conf_load(int silent) { + errno_clear(); + char *filename = filename_from_path_entry_strdupz(netdata_configured_cloud_dir, "cloud.conf"); + int ret = appconfig_load(&cloud_config, filename, 1, NULL); + + if(!ret && !silent) + nd_log(NDLS_DAEMON, NDLP_ERR, + "CLAIM: cannot load cloud config '%s'. Running with internal defaults.", filename); + + freez(filename); + + appconfig_move(&cloud_config, + CONFIG_SECTION_GLOBAL, "cloud base url", + CONFIG_SECTION_GLOBAL, "url"); + + cloud_conf_load_defaults(); +} + +void cloud_conf_init_after_registry(void) { + const char *machine_guid = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "machine_guid", ""); + const char *hostname = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "hostname", ""); + + // for machine guid and hostname we have to use appconfig_set() for that they will be saved uncommented + if(!machine_guid || !*machine_guid) + appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "machine_guid", registry_get_this_machine_guid()); + + if(!hostname || !*hostname) + appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "hostname", registry_get_this_machine_hostname()); +} + +bool cloud_conf_save(void) { + char filename[FILENAME_MAX + 1]; + + CLEAN_BUFFER *wb = buffer_create(0, NULL); + appconfig_generate(&cloud_config, wb, false, false); + snprintfz(filename, sizeof(filename), "%s/cloud.conf", netdata_configured_cloud_dir); + FILE *fp = fopen(filename, "w"); + if(!fp) { + nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot open file '%s' for writing.", filename); + return false; + } + + fprintf(fp, "%s", buffer_tostring(wb)); + fclose(fp); + return true; +} + +bool cloud_conf_regenerate(const char *claimed_id_str, const char *machine_guid, const char *hostname, const char *token, const char *rooms, const char *url, const char *proxy, int insecure) { + // for backwards compatibility (older agents), save the claimed_id to its file + claimed_id_save_to_file(claimed_id_str); + + appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "url", url); + appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "proxy", proxy ? proxy : ""); + appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "token", token ? token : ""); + appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "rooms", rooms ? rooms : ""); + appconfig_set_boolean(&cloud_config, CONFIG_SECTION_GLOBAL, "insecure", insecure); + appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "machine_guid", machine_guid ? machine_guid : ""); + appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "claimed_id", claimed_id_str ? claimed_id_str : ""); + appconfig_set(&cloud_config, CONFIG_SECTION_GLOBAL, "hostname", hostname ? hostname : ""); + + return cloud_conf_save(); +} diff --git a/src/claim/cloud-status.c b/src/claim/cloud-status.c new file mode 100644 index 000000000..45db177e9 --- /dev/null +++ b/src/claim/cloud-status.c @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "claim.h" + +const char *cloud_status_to_string(CLOUD_STATUS status) { + switch(status) { + default: + case CLOUD_STATUS_AVAILABLE: + return "available"; + + case CLOUD_STATUS_BANNED: + return "banned"; + + case CLOUD_STATUS_OFFLINE: + return "offline"; + + case CLOUD_STATUS_ONLINE: + return "online"; + + case CLOUD_STATUS_INDIRECT: + return "indirect"; + } +} + +CLOUD_STATUS cloud_status(void) { + if(unlikely(aclk_disable_runtime)) + return CLOUD_STATUS_BANNED; + + if(likely(aclk_online())) + return CLOUD_STATUS_ONLINE; + + if(localhost->sender && + rrdhost_flag_check(localhost, RRDHOST_FLAG_RRDPUSH_SENDER_READY_4_METRICS) && + stream_has_capability(localhost->sender, STREAM_CAP_NODE_ID) && + !UUIDiszero(localhost->node_id) && + !UUIDiszero(localhost->aclk.claim_id_of_parent)) + return CLOUD_STATUS_INDIRECT; + + if(is_agent_claimed()) + return CLOUD_STATUS_OFFLINE; + + return CLOUD_STATUS_AVAILABLE; +} + +time_t cloud_last_change(void) { + time_t ret = MAX(last_conn_time_mqtt, last_disconnect_time); + if(!ret) ret = netdata_start_time; + return ret; +} + +time_t cloud_next_connection_attempt(void) { + return next_connection_attempt; +} + +size_t cloud_connection_id(void) { + return aclk_connection_counter; +} + +const char *cloud_status_aclk_offline_reason() { + if(aclk_disable_runtime) + return "banned"; + + return aclk_status_to_string(); +} + +const char *cloud_status_aclk_base_url() { + return aclk_cloud_base_url; +} + +CLOUD_STATUS buffer_json_cloud_status(BUFFER *wb, time_t now_s) { + CLOUD_STATUS status = cloud_status(); + + buffer_json_member_add_object(wb, "cloud"); + { + size_t id = cloud_connection_id(); + time_t last_change = cloud_last_change(); + time_t next_connect = cloud_next_connection_attempt(); + buffer_json_member_add_uint64(wb, "id", id); + buffer_json_member_add_string(wb, "status", cloud_status_to_string(status)); + buffer_json_member_add_time_t(wb, "since", last_change); + buffer_json_member_add_time_t(wb, "age", now_s - last_change); + + switch(status) { + default: + case CLOUD_STATUS_AVAILABLE: + // the agent is not claimed + buffer_json_member_add_string(wb, "url", cloud_config_url_get()); + buffer_json_member_add_string(wb, "reason", claim_agent_failure_reason_get()); + break; + + case CLOUD_STATUS_BANNED: { + // the agent is claimed, but has been banned from NC + CLAIM_ID claim_id = claim_id_get(); + buffer_json_member_add_string(wb, "claim_id", claim_id.str); + buffer_json_member_add_string(wb, "url", cloud_status_aclk_base_url()); + buffer_json_member_add_string(wb, "reason", "Agent is banned from Netdata Cloud"); + buffer_json_member_add_string(wb, "url", cloud_config_url_get()); + break; + } + + case CLOUD_STATUS_OFFLINE: { + // the agent is claimed, but cannot get online + CLAIM_ID claim_id = claim_id_get(); + buffer_json_member_add_string(wb, "claim_id", claim_id.str); + buffer_json_member_add_string(wb, "url", cloud_status_aclk_base_url()); + buffer_json_member_add_string(wb, "reason", cloud_status_aclk_offline_reason()); + if (next_connect > now_s) { + buffer_json_member_add_time_t(wb, "next_check", next_connect); + buffer_json_member_add_time_t(wb, "next_in", next_connect - now_s); + } + break; + } + + case CLOUD_STATUS_ONLINE: { + // the agent is claimed and online + CLAIM_ID claim_id = claim_id_get(); + buffer_json_member_add_string(wb, "claim_id", claim_id.str); + buffer_json_member_add_string(wb, "url", cloud_status_aclk_base_url()); + buffer_json_member_add_string(wb, "reason", ""); + break; + } + + case CLOUD_STATUS_INDIRECT: { + CLAIM_ID claim_id = rrdhost_claim_id_get(localhost); + buffer_json_member_add_string(wb, "claim_id", claim_id.str); + buffer_json_member_add_string(wb, "url", cloud_config_url_get()); + break; + } + } + } + buffer_json_object_close(wb); // cloud + + return status; +} diff --git a/src/claim/cloud-status.h b/src/claim/cloud-status.h new file mode 100644 index 000000000..648c114f9 --- /dev/null +++ b/src/claim/cloud-status.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_CLOUD_STATUS_H +#define NETDATA_CLOUD_STATUS_H + +#include "daemon/common.h" + +typedef enum __attribute__((packed)) { + CLOUD_STATUS_AVAILABLE = 1, // cloud and aclk functionality is available, but the agent is not claimed + CLOUD_STATUS_BANNED, // the agent has been banned from cloud + CLOUD_STATUS_OFFLINE, // the agent tries to connect to cloud, but cannot do it + CLOUD_STATUS_INDIRECT, // the agent is connected to cloud via a parent + CLOUD_STATUS_ONLINE, // the agent is connected to cloud +} CLOUD_STATUS; + +const char *cloud_status_to_string(CLOUD_STATUS status); +CLOUD_STATUS cloud_status(void); + +time_t cloud_last_change(void); +time_t cloud_next_connection_attempt(void); +size_t cloud_connection_id(void); +const char *cloud_status_aclk_offline_reason(void); +const char *cloud_status_aclk_base_url(void); +CLOUD_STATUS buffer_json_cloud_status(BUFFER *wb, time_t now_s); + +#endif //NETDATA_CLOUD_STATUS_H diff --git a/src/claim/main.c b/src/claim/main.c new file mode 100644 index 000000000..8e3c4402c --- /dev/null +++ b/src/claim/main.c @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#define UNICODE +#define _UNICODE +#include <windows.h> +#include <shellapi.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <signal.h> + +#include "main.h" + +LPWSTR token = NULL; +LPWSTR room = NULL; +LPWSTR proxy = NULL; +LPWSTR url = NULL; +LPWSTR extPath = NULL; +LPWSTR *argv = NULL; + +char *aToken = NULL; +char *aRoom = NULL; +char *aProxy = NULL; +char *aURL = NULL; +int insecure = 0; + +LPWSTR netdata_claim_get_formatted_message(LPWSTR pMessage, ...) +{ + LPWSTR pBuffer = NULL; + + va_list args = NULL; + va_start(args, pMessage); + + FormatMessage(FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ALLOCATE_BUFFER, pMessage, 0, 0, (LPWSTR)&pBuffer, + 0, &args); + va_end(args); + + return pBuffer; +} + +// Common Functions +void netdata_claim_error_exit(wchar_t *function) +{ + DWORD error = GetLastError(); + LPWSTR pMessage = L"The function %1 failed with error %2."; + LPWSTR pBuffer = netdata_claim_get_formatted_message(pMessage, function, error); + + if (pBuffer) { + MessageBoxW(NULL, pBuffer, L"Error", MB_OK|MB_ICONERROR); + LocalFree(pBuffer); + } + + ExitProcess(error); +} + +/** + * Parse Args + * + * Parse arguments identifying necessity to make a window + * + * @param argc number of arguments + * @param argv A pointer for all arguments given + * + * @return it return the number of arguments parsed. + */ +int nd_claim_parse_args(int argc, LPWSTR *argv) +{ + int i; + for (i = 1 ; i < argc; i++) { + // We are working with Microsoft, thus it does not make sense wait for only smallcase + if(wcscasecmp(L"/T", argv[i]) == 0) { + if (argc <= i + 1) + continue; + i++; + token = argv[i]; + } + + if(wcscasecmp(L"/R", argv[i]) == 0) { + if (argc <= i + 1) + continue; + i++; + room = argv[i]; + } + + if(wcscasecmp(L"/P", argv[i]) == 0) { + if (argc <= i + 1) + continue; + i++; + // Minimum IPV4 + if(wcslen(argv[i]) >= 8) { + proxy = argv[i]; + } + } + + if(wcscasecmp(L"/F", argv[i]) == 0) { + if (argc <= i + 1) + continue; + i++; + extPath = argv[i]; + } + + if(wcscasecmp(L"/U", argv[i]) == 0) { + if (argc <= i + 1) + continue; + i++; + url = argv[i]; + } + + if(wcscasecmp(L"/I", argv[i]) == 0) { + if (argc <= i + 1) + continue; + + i++; + size_t length = wcslen(argv[i]); + char *tmp = calloc(sizeof(char), length); + if (!tmp) + ExitProcess(1); + + netdata_claim_convert_str(tmp, argv[i], length - 1); + if (i < argc) + insecure = atoi(tmp); + else + insecure = 1; + + free(tmp); + } + } + + if (!token || !room) + return 0; + + return argc; +} + +static int netdata_claim_prepare_strings() +{ + if (!token || !room) + return -1; + + size_t length = wcslen(token) + 1; + aToken = calloc(sizeof(char), length); + if (!aToken) + return -1; + + netdata_claim_convert_str(aToken, token, length - 1); + + length = wcslen(room) + 1; + aRoom = calloc(sizeof(char), length - 1); + if (!aRoom) + return -1; + + netdata_claim_convert_str(aRoom, room, length - 1); + + if (proxy) { + length = wcslen(proxy) + 1; + aProxy = calloc(sizeof(char), length - 1); + if (!aProxy) + return -1; + + netdata_claim_convert_str(aProxy, proxy, length - 1); + } + + if (url) { + length = wcslen(url) + 1; + aURL = calloc(sizeof(char), length - 1); + if (!aURL) + return -1; + + netdata_claim_convert_str(aURL, url, length - 1); + } + return 0; +} + +static void netdata_claim_exit_callback(int signal) +{ + (void)signal; + if (aToken) + free(aToken); + + if (aRoom) + free(aRoom); + + if (aProxy) + free(aProxy); + + if (aURL) + free(aURL); + + if (argv) + LocalFree(argv); + + if (extPath) + LocalFree(extPath); +} + +static inline int netdata_claim_prepare_data(char *out, size_t length) +{ + char *proxyLabel = (aProxy) ? "proxy = " : "# proxy = "; + char *proxyValue = (aProxy) ? aProxy : ""; + + char *urlValue = (aURL) ? aURL : "https://app.netdata.cloud"; + return snprintf(out, + length, + "[global]\n url = %s\n token = %s\n rooms = %s\n %s%s\n insecure = %s", + urlValue, + aToken, + aRoom, + proxyLabel, + proxyValue, + (insecure) ? "yes" : "no" + ); +} + +static int netdata_claim_get_path(char *path) +{ + if (extPath) { + size_t length = wcslen(extPath) + 1; + if (length >= WINDOWS_MAX_PATH) + return -1; + + netdata_claim_convert_str(path, extPath, length - 1); + return 0; + } + + char *usrPath = { "\\usr\\bin" }; + DWORD length = GetCurrentDirectoryA(WINDOWS_MAX_PATH, path); + if (!length) { + return -1; + } + + if (strstr(path, usrPath)) { + length -= 7; + path[length] = '\0'; + } + + return 0; +} + +static void netdata_claim_write_config(char *path) +{ +#define NETDATA_MIN_CLOUD_LENGTH 135 +#define NETDATA_MIN_ROOM_LENGTH 36 + if (strlen(aToken) != NETDATA_MIN_CLOUD_LENGTH || strlen(aRoom) < NETDATA_MIN_ROOM_LENGTH) + return; + + char configPath[WINDOWS_MAX_PATH + 1]; + char data[WINDOWS_MAX_PATH + 1]; + char *filename; + if (!extPath) { + snprintf(configPath, WINDOWS_MAX_PATH - 1, "%s\\etc\\netdata\\claim.conf", path); + filename = configPath; + } else { + filename = path; + } + + HANDLE hf = CreateFileA(filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hf == INVALID_HANDLE_VALUE) + netdata_claim_error_exit(L"CreateFileA"); + + DWORD length = netdata_claim_prepare_data(data, WINDOWS_MAX_PATH); + DWORD written = 0; + + BOOL ret = WriteFile(hf, data, length, &written, NULL); + if (!ret) { + CloseHandle(hf); + netdata_claim_error_exit(L"WriteFileA"); + } + + if (length != written) + MessageBoxW(NULL, L"Cannot write claim.conf.", L"Error", MB_OK|MB_ICONERROR); + + CloseHandle(hf); +} + +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) +{ + signal(SIGABRT, netdata_claim_exit_callback); + signal(SIGINT, netdata_claim_exit_callback); + signal(SIGTERM, netdata_claim_exit_callback); + + int argc; + LPWSTR *argv = CommandLineToArgvW(GetCommandLineW(), &argc); + if (argc) + argc = nd_claim_parse_args(argc, argv); + + // When no data is given, user must to use graphic mode + int ret = 0; + if (!argc) { + ret = netdata_claim_window_loop(hInstance, nCmdShow); + } else { + if (netdata_claim_prepare_strings()) { + goto exit_claim; + } + + char basePath[WINDOWS_MAX_PATH]; + if (!netdata_claim_get_path(basePath)) { + netdata_claim_write_config(basePath); + } + } + +exit_claim: + netdata_claim_exit_callback(0); + + return ret; +} diff --git a/src/claim/main.h b/src/claim/main.h new file mode 100644 index 000000000..b5d9e3f89 --- /dev/null +++ b/src/claim/main.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_CLAIM_H_ +# define NETDATA_CLAIM_H_ 1 + +#include <wchar.h> +#include "ui.h" + +extern LPWSTR token; +extern LPWSTR room; +extern LPWSTR proxy; + +void netdata_claim_error_exit(wchar_t *function); +static inline void netdata_claim_convert_str(char *dst, wchar_t *src, size_t len) { + size_t copied = wcstombs(dst, src, len); + dst[copied] = '\0'; +} + +#endif //NETDATA_CLAIM_H_ diff --git a/src/claim/netdata-claim.sh.in b/src/claim/netdata-claim.sh.in index f4fa382b6..15c166e3f 100755 --- a/src/claim/netdata-claim.sh.in +++ b/src/claim/netdata-claim.sh.in @@ -1,451 +1,111 @@ -#!/usr/bin/env bash -# netdata -# real-time performance and health monitoring, done right! -# (C) 2023 Netdata Inc. -# SPDX-License-Identifier: GPL-3.0-or-later - -# Exit code: 0 - Success -# Exit code: 1 - Unknown argument -# Exit code: 2 - Problems with claiming working directory -# Exit code: 3 - Missing dependencies -# Exit code: 4 - Failure to connect to endpoint -# Exit code: 5 - The CLI didn't work -# Exit code: 6 - Wrong user -# Exit code: 7 - Unknown HTTP error message -# -# OK: Agent claimed successfully -# HTTP Status code: 204 -# Exit code: 0 +#!/bin/sh # -# Unknown HTTP error message -# HTTP Status code: 422 -# Exit code: 7 -ERROR_KEYS[7]="None" -ERROR_MESSAGES[7]="Unknown HTTP error message" - -# Error: The agent id is invalid; it does not fulfill the constraints -# HTTP Status code: 422 -# Exit code: 8 -ERROR_KEYS[8]="ErrInvalidNodeID" -ERROR_MESSAGES[8]="invalid node id" - -# Error: The agent hostname is invalid; it does not fulfill the constraints -# HTTP Status code: 422 -# Exit code: 9 -ERROR_KEYS[9]="ErrInvalidNodeName" -ERROR_MESSAGES[9]="invalid node name" - -# Error: At least one of the given rooms ids is invalid; it does not fulfill the constraints -# HTTP Status code: 422 -# Exit code: 10 -ERROR_KEYS[10]="ErrInvalidRoomID" -ERROR_MESSAGES[10]="invalid room id" - -# Error: Invalid public key; the public key is empty or not present -# HTTP Status code: 422 -# Exit code: 11 -ERROR_KEYS[11]="ErrInvalidPublicKey" -ERROR_MESSAGES[11]="invalid public key" +# Copyright (c) 2024 Netdata Inc. +# SPDX-License-Identifier: GPL-3.0-or-later # -# Error: Expired, missing or invalid token -# HTTP Status code: 403 -# Exit code: 12 -ERROR_KEYS[12]="ErrForbidden" -ERROR_MESSAGES[12]="token expired/token not found/invalid token" - -# Error: Duplicate agent id; an agent with the same id is already registered in the cloud -# HTTP Status code: 409 -# Exit code: 13 -ERROR_KEYS[13]="ErrAlreadyClaimed" -ERROR_MESSAGES[13]="already claimed" - -# Error: The node claiming process is still in progress. -# HTTP Status code: 102 -# Exit code: 14 -ERROR_KEYS[14]="ErrProcessingClaim" -ERROR_MESSAGES[14]="processing claiming" +# %%NEW_CLAIMING_METHOD%% -# Error: Internal server error. Any other unexpected error (DB problems, etc.) -# HTTP Status code: 500 -# Exit code: 15 -ERROR_KEYS[15]="ErrInternalServerError" -ERROR_MESSAGES[15]="Internal Server Error" +set -e -# Error: There was a timeout processing the claim. -# HTTP Status code: 504 -# Exit code: 16 -ERROR_KEYS[16]="ErrGatewayTimeout" -ERROR_MESSAGES[16]="Gateway Timeout" - -# Error: The service cannot handle the claiming request at this time. -# HTTP Status code: 503 -# Exit code: 17 -ERROR_KEYS[17]="ErrServiceUnavailable" -ERROR_MESSAGES[17]="Service Unavailable" - -# Exit code: 18 - Agent unique id is not generated yet. - -NETDATA_RUNNING=1 - -get_config_value() { - conf_file="${1}" - section="${2}" - key_name="${3}" - if [ "${NETDATA_RUNNING}" -eq 1 ]; then - config_result=$(@sbindir_POST@/netdatacli 2>/dev/null read-config "$conf_file|$section|$key_name"; exit $?) - result="$?" - if [ "${result}" -ne 0 ]; then - echo >&2 "Unable to communicate with Netdata daemon, querying config from disk instead." - NETDATA_RUNNING=0 - fi - fi - if [ "${NETDATA_RUNNING}" -eq 0 ]; then - config_result=$(@sbindir_POST@/netdata 2>/dev/null -W get2 "$conf_file" "$section" "$key_name" unknown_default) - fi - echo "$config_result" +warning() { + printf "WARNING: %s\n" "${1}" 1>&2 } -if command -v curl >/dev/null 2>&1 ; then - URLTOOL="curl" -elif command -v wget >/dev/null 2>&1 ; then - URLTOOL="wget" -else - echo >&2 "I need curl or wget to proceed, but neither is available on this system." - exit 3 -fi -if ! command -v openssl >/dev/null 2>&1 ; then - echo >&2 "I need openssl to proceed, but it is not available on this system." - exit 3 -fi - -# shellcheck disable=SC2050 -if [ "@enable_cloud_POST@" = "no" ]; then - echo >&2 "This agent was built with --disable-cloud and cannot be claimed" - exit 3 -fi -# shellcheck disable=SC2050 -if [ "@enable_aclk_POST@" != "yes" ]; then - echo >&2 "This agent was built without the dependencies for Cloud and cannot be claimed" - exit 3 -fi - -# ----------------------------------------------------------------------------- -# defaults to allow running this script by hand - -[ -z "${NETDATA_VARLIB_DIR}" ] && NETDATA_VARLIB_DIR="@varlibdir_POST@" -MACHINE_GUID_FILE="@registrydir_POST@/netdata.public.unique.id" -CLAIMING_DIR="${NETDATA_VARLIB_DIR}/cloud.d" -TOKEN="unknown" -URL_BASE=$(get_config_value cloud global "cloud base url") -[ -z "$URL_BASE" ] && URL_BASE="https://app.netdata.cloud" # Cover post-install with --dont-start -ID="unknown" -ROOMS="" -[ -z "$HOSTNAME" ] && HOSTNAME=$(hostname) -CLOUD_CERTIFICATE_FILE="${CLAIMING_DIR}/cloud_fullchain.pem" -VERBOSE=0 -INSECURE=0 -RELOAD=1 -NETDATA_USER=$(get_config_value netdata global "run as user") -[ -z "$EUID" ] && EUID="$(id -u)" +error() { + printf "ERROR: %s\n" "${1}" 1>&2 + exit "${2}" +} -gen_id() { - local id - - if command -v uuidgen > /dev/null 2>&1; then - id="$(uuidgen | tr '[:upper:]' '[:lower:]')" - elif [ -r /proc/sys/kernel/random/uuid ]; then - id="$(cat /proc/sys/kernel/random/uuid)" - else - echo >&2 "Unable to generate machine ID." - exit 18 - fi - - if [ "${id}" = "8a795b0c-2311-11e6-8563-000c295076a6" ] || [ "${id}" = "4aed1458-1c3e-11e6-a53f-000c290fc8f5" ]; then - gen_id +get_templated_value() { + value="$1" + default="$2" + override="$3" + + if [ -n "${override}" ]; then + echo "${override}" + elif [ -z "${value}" ]; then + error "Expected templated value not present" + elif (echo "${value}" | grep -q '@'); then + echo "${default}" else - echo "${id}" + echo "${value}" fi } -# get the MACHINE_GUID by default -if [ -r "${MACHINE_GUID_FILE}" ]; then - ID="$(cat "${MACHINE_GUID_FILE}")" - MGUID=$ID -elif [ -f "${MACHINE_GUID_FILE}" ]; then - echo >&2 "netdata.public.unique.id is not readable. Please make sure you have rights to read it (Filename: ${MACHINE_GUID_FILE})." - exit 18 -else - if mkdir -p "${MACHINE_GUID_FILE%/*}" && echo -n "$(gen_id)" > "${MACHINE_GUID_FILE}"; then - ID="$(cat "${MACHINE_GUID_FILE}")" - MGUID=$ID - else - echo >&2 "Failed to write new machine GUID. Please make sure you have rights to write to ${MACHINE_GUID_FILE}." - exit 18 - fi -fi - -# get token from file -if [ -r "${CLAIMING_DIR}/token" ]; then - TOKEN="$(cat "${CLAIMING_DIR}/token")" -fi - -# get rooms from file -if [ -r "${CLAIMING_DIR}/rooms" ]; then - ROOMS="$(cat "${CLAIMING_DIR}/rooms")" -fi - -variable_to_set= -for arg in "$@" -do - if [ -z "$variable_to_set" ]; then - case $arg in - --claim-token) variable_to_set="TOKEN" ;; - --claim-rooms) variable_to_set="ROOMS" ;; - --claim-url) variable_to_set="URL_BASE" ;; - -token=*) TOKEN=${arg:7} ;; - -url=*) [ -n "${arg:5}" ] && URL_BASE=${arg:5} ;; - -id=*) ID=$(echo "${arg:4}" | tr '[:upper:]' '[:lower:]');; - -rooms=*) ROOMS=${arg:7} ;; - -hostname=*) HOSTNAME=${arg:10} ;; - -verbose) VERBOSE=1 ;; - -insecure) INSECURE=1 ;; - -proxy=*) PROXY=${arg:7} ;; - -noproxy) NOPROXY=yes ;; - -noreload) RELOAD=0 ;; - -user=*) NETDATA_USER=${arg:6} ;; - -daemon-not-running) NETDATA_RUNNING=0 ;; - *) echo >&2 "Unknown argument ${arg}" - exit 1 ;; - esac - else - case "$variable_to_set" in - TOKEN) TOKEN="$arg" ;; - ROOMS) ROOMS="$arg" ;; - URL_BASE) URL_BASE="$arg" ;; - esac - variable_to_set= - fi - shift 1 -done - -if [ "$EUID" != "0" ] && [ "$(whoami)" != "$NETDATA_USER" ]; then - echo >&2 "This script must be run by the $NETDATA_USER user account" - exit 6 -fi - -# if curl not installed give warning SOCKS can't be used -if [[ "${URLTOOL}" != "curl" && "${PROXY:0:5}" = socks ]] ; then - echo >&2 "wget doesn't support SOCKS. Please install curl or disable SOCKS proxy." - exit 1 -fi - -echo >&2 "Token: ****************" -echo >&2 "Base URL: $URL_BASE" -echo >&2 "Id: $ID" -echo >&2 "Rooms: $ROOMS" -echo >&2 "Hostname: $HOSTNAME" -echo >&2 "Proxy: $PROXY" -echo >&2 "Netdata user: $NETDATA_USER" - -# create the claiming directory for this user -if [ ! -d "${CLAIMING_DIR}" ] ; then - mkdir -p "${CLAIMING_DIR}" && chmod 0770 "${CLAIMING_DIR}" -# shellcheck disable=SC2181 - if [ $? -ne 0 ] ; then - echo >&2 "Failed to create claiming working directory ${CLAIMING_DIR}" - exit 2 - fi -fi -if [ ! -w "${CLAIMING_DIR}" ] ; then - echo >&2 "No write permission in claiming working directory ${CLAIMING_DIR}" - exit 2 -fi - -if [ ! -f "${CLAIMING_DIR}/private.pem" ] ; then - echo >&2 "Generating private/public key for the first time." - if ! openssl genrsa -out "${CLAIMING_DIR}/private.pem" 2048 ; then - echo >&2 "Failed to generate private/public key pair." - exit 2 - fi -fi -if [ ! -f "${CLAIMING_DIR}/public.pem" ] ; then - echo >&2 "Extracting public key from private key." - if ! openssl rsa -in "${CLAIMING_DIR}/private.pem" -outform PEM -pubout -out "${CLAIMING_DIR}/public.pem" ; then - echo >&2 "Failed to extract public key." - exit 2 - fi -fi - -TARGET_URL="${URL_BASE%/}/api/v1/spaces/nodes/${ID}" -# shellcheck disable=SC2002 -KEY=$(cat "${CLAIMING_DIR}/public.pem" | tr '\n' '!' | sed -e 's/!/\\n/g') -# shellcheck disable=SC2001 -[ -n "$ROOMS" ] && ROOMS=\"$(echo "$ROOMS" | sed s'/,/", "/g')\" +config_dir="$(get_templated_value "@configdir_POST@" "/etc/netdata" "${NETDATA_CLAIM_CONFIG_DIR}")" +claim_config="${config_dir}/claim.conf" +netdatacli="$(get_templated_value "@sbindir_POST@/netdatacli" "$(command -v netdatacli 2>/dev/null)" "${NETDATA_CLAIM_NETDATACLI_PATH}")" +netdata_group="$(get_templated_value "@netdata_group_POST@" "netdata" "${NETDATA_CLAIM_CONFIG_GROUP}")" + +write_config() { + config="[global]" + config="${config}\n url = ${NETDATA_CLAIM_URL}" + config="${config}\n token = ${NETDATA_CLAIM_TOKEN}" + if [ -n "${NETDATA_CLAIM_ROOMS}" ]; then + config="${config}\n rooms = ${NETDATA_CLAIM_ROOMS}" + fi + if [ -n "${NETDATA_CLAIM_PROXY}" ]; then + config="${config}\n proxy = ${NETDATA_CLAIM_PROXY}" + fi + if [ -n "${NETDATA_CLAIM_INSECURE}" ]; then + config="${config}\n insecure = ${NETDATA_CLAIM_INSECURE}" + fi -cat > "${CLAIMING_DIR}/tmpin.txt" <<EMBED_JSON -{ - "node": { - "id": "$ID", - "hostname": "$HOSTNAME" - }, - "token": "$TOKEN", - "rooms" : [ $ROOMS ], - "publicKey" : "$KEY", - "mGUID" : "$MGUID" + touch "${claim_config}.tmp" + chmod 0660 "${claim_config}.tmp" + chown "root:${netdata_group}" "${claim_config}.tmp" + echo "${config}" > "${claim_config}.tmp" + chmod 0640 "${claim_config}.tmp" + mv -f "${claim_config}.tmp" "${claim_config}" } -EMBED_JSON - -if [ "${VERBOSE}" == 1 ] ; then - echo "Request to server:" - cat "${CLAIMING_DIR}/tmpin.txt" -fi - -if [ "${URLTOOL}" = "curl" ] ; then - URLCOMMAND="curl --connect-timeout 30 --retry 0 -s -i -X PUT -d \"@${CLAIMING_DIR}/tmpin.txt\"" - if [ "${NOPROXY}" = "yes" ] ; then - URLCOMMAND="${URLCOMMAND} -x \"\"" - elif [ -n "${PROXY}" ] ; then - URLCOMMAND="${URLCOMMAND} -x \"${PROXY}\"" - fi -else - URLCOMMAND="wget -T 15 -O - -q --server-response --content-on-error=on --method=PUT \ - --body-file=\"${CLAIMING_DIR}/tmpin.txt\"" - if [ "${NOPROXY}" = "yes" ] ; then - URLCOMMAND="${URLCOMMAND} --no-proxy" - elif [ "${PROXY:0:4}" = http ] ; then - URLCOMMAND="export http_proxy=${PROXY}; ${URLCOMMAND}" - fi -fi - -if [ "${INSECURE}" == 1 ] ; then - if [ "${URLTOOL}" = "curl" ] ; then - URLCOMMAND="${URLCOMMAND} --insecure" - else - URLCOMMAND="${URLCOMMAND} --no-check-certificate" +reload_claiming() { + if [ -z "${NORELOAD}" ]; then + "${netdatacli}" reload-claiming-state fi -fi - -if [ -r "${CLOUD_CERTIFICATE_FILE}" ] ; then - if [ "${URLTOOL}" = "curl" ] ; then - URLCOMMAND="${URLCOMMAND} --cacert \"${CLOUD_CERTIFICATE_FILE}\"" - else - URLCOMMAND="${URLCOMMAND} --ca-certificate \"${CLOUD_CERTIFICATE_FILE}\"" - fi -fi - -if [ "${VERBOSE}" == 1 ]; then - echo "${URLCOMMAND} \"${TARGET_URL}\"" -fi - -attempt_contact () { - if [ "${URLTOOL}" = "curl" ] ; then - eval "${URLCOMMAND} \"${TARGET_URL}\"" >"${CLAIMING_DIR}/tmpout.txt" - else - eval "${URLCOMMAND} \"${TARGET_URL}\"" >"${CLAIMING_DIR}/tmpout.txt" 2>&1 - fi - URLCOMMAND_EXIT_CODE=$? - if [ "${URLTOOL}" = "wget" ] && [ "${URLCOMMAND_EXIT_CODE}" -eq 8 ] ; then - # We consider the server issuing an error response a successful attempt at communicating - URLCOMMAND_EXIT_CODE=0 - fi - - # Check if URLCOMMAND connected and received reply - if [ "${URLCOMMAND_EXIT_CODE}" -ne 0 ] ; then - echo >&2 "Failed to connect to ${URL_BASE}, return code ${URLCOMMAND_EXIT_CODE}" - rm -f "${CLAIMING_DIR}/tmpout.txt" - return 4 - fi - - if [ "${VERBOSE}" == 1 ] ; then - echo "Response from server:" - cat "${CLAIMING_DIR}/tmpout.txt" - fi - - return 0 } -for i in {1..3} -do - if attempt_contact ; then - echo "Connection attempt $i successful" - break - fi - echo "Connection attempt $i failed. Retry in ${i}s." - if [ "$i" -eq 5 ] ; then - rm -f "${CLAIMING_DIR}/tmpin.txt" - exit 4 - fi - sleep "$i" -done +parse_args() { + while [ -n "${1}" ]; do + case "${1}" in + --claim-token) NETDATA_CLAIM_TOKEN="${2}"; shift 1 ;; + -token=*) NETDATA_CLAIM_TOKEN="$(echo "${1}" | sed 's/^-token=//')" ;; + --claim-rooms) NETDATA_CLAIM_ROOMS="${2}"; shift 1 ;; + -rooms=*) NETDATA_CLAIM_ROOMS="$(echo "${1}" | sed 's/^-rooms=//')" ;; + --claim-url) NETDATA_CLAIM_URL="${2}"; shift 1 ;; + -url=*) NETDATA_CLAIM_URL="$(echo "${1}" | sed 's/^-url=//')" ;; + --claim-proxy) NETDATA_CLAIM_PROXY="${2}"; shift 1 ;; + -proxy=*) NETDATA_CLAIM_PROXY="$(echo "${1}" | sed 's/-proxy=//')" ;; + -noproxy|--noproxy) NETDATA_CLAIM_PROXY="none" ;; + -noreload|--noreload) NORELOAD=1 ;; + -insecure|--insecure) NETDATA_CLAIM_INSECURE=yes ;; + -verbose) true ;; + -daemon-not-running) true ;; + -id=*) warning "-id option is no longer supported. Remove the node ID file instead." ;; + -hostname=*) warning "-hostname option is no longer supported. Update the main netdata configuration manually instead." ;; + -user=*) warning "-user option is no longer supported." ;; + *) warning "Ignoring unrecognized option ${1}";; + esac -rm -f "${CLAIMING_DIR}/tmpin.txt" - -ERROR_KEY=$(grep "\"errorMsgKey\":" "${CLAIMING_DIR}/tmpout.txt" | awk -F "errorMsgKey\":\"" '{print $2}' | awk -F "\"" '{print $1}') -case ${ERROR_KEY} in - "ErrInvalidNodeID") EXIT_CODE=8 ;; - "ErrInvalidNodeName") EXIT_CODE=9 ;; - "ErrInvalidRoomID") EXIT_CODE=10 ;; - "ErrInvalidPublicKey") EXIT_CODE=11 ;; - "ErrForbidden") EXIT_CODE=12 ;; - "ErrAlreadyClaimed") EXIT_CODE=13 ;; - "ErrProcessingClaim") EXIT_CODE=14 ;; - "ErrInternalServerError") EXIT_CODE=15 ;; - "ErrGatewayTimeout") EXIT_CODE=16 ;; - "ErrServiceUnavailable") EXIT_CODE=17 ;; - *) EXIT_CODE=7 ;; -esac - -HTTP_STATUS_CODE=$(grep "HTTP" "${CLAIMING_DIR}/tmpout.txt" | tail -1 | awk -F " " '{print $2}') -if [ "${HTTP_STATUS_CODE}" = "204" ] ; then - EXIT_CODE=0 -fi + shift 1 + done -if [ "${HTTP_STATUS_CODE}" = "204" ] || [ "${ERROR_KEY}" = "ErrAlreadyClaimed" ] ; then - rm -f "${CLAIMING_DIR}/tmpout.txt" - if [ "${HTTP_STATUS_CODE}" = "204" ] ; then - echo -n "${ID}" >"${CLAIMING_DIR}/claimed_id" || (echo >&2 "Claiming failed"; set -e; exit 2) - fi - rm -f "${CLAIMING_DIR}/token" || (echo >&2 "Claiming failed"; set -e; exit 2) + if [ -z "${NETDATA_CLAIM_TOKEN}" ]; then + error "Claim token must be specified" 1 + fi - # Rewrite the cloud.conf on the disk - cat > "$CLAIMING_DIR/cloud.conf" <<HERE_DOC -[global] - enabled = yes - cloud base url = $URL_BASE -${PROXY:+ proxy = $PROXY} -HERE_DOC - if [ "$EUID" == "0" ]; then - chown -R "${NETDATA_USER}:${NETDATA_USER}" "${CLAIMING_DIR}" || (echo >&2 "Claiming failed"; set -e; exit 2) - fi - if [ "${RELOAD}" == "0" ] ; then - exit $EXIT_CODE - fi + if [ -z "${NETDATA_CLAIM_URL}" ]; then + NETDATA_CLAIM_URL="https://app.netdata.cloud/" + fi +} - # Update cloud.conf in the agent memory - @sbindir_POST@/netdatacli write-config 'cloud|global|enabled|yes' && \ - @sbindir_POST@/netdatacli write-config "cloud|global|cloud base url|$URL_BASE" && \ - @sbindir_POST@/netdatacli reload-claiming-state && \ - if [ "${HTTP_STATUS_CODE}" = "204" ] ; then - echo >&2 "Node was successfully claimed." - else - echo >&2 "The agent cloud base url is set to the url provided." - echo >&2 "The cloud may have different credentials already registered for this agent ID and it cannot be reclaimed under different credentials for security reasons. If you are unable to connect use -id=\$(uuidgen) to overwrite this agent ID with a fresh value if the original credentials cannot be restored." - echo >&2 "Failed to claim node with the following error message:\"${ERROR_MESSAGES[$EXIT_CODE]}\"" - fi && exit $EXIT_CODE - if [ "${ERROR_KEY}" = "ErrAlreadyClaimed" ] ; then - echo >&2 "The cloud may have different credentials already registered for this agent ID and it cannot be reclaimed under different credentials for security reasons. If you are unable to connect use -id=\$(uuidgen) to overwrite this agent ID with a fresh value if the original credentials cannot be restored." - echo >&2 "Failed to claim node with the following error message:\"${ERROR_MESSAGES[$EXIT_CODE]}\"" - exit $EXIT_CODE - fi - echo >&2 "The claim was successful but the agent could not be notified ($?)- it requires a restart to connect to the cloud." - [ "$NETDATA_RUNNING" -eq 0 ] && exit 0 || exit 5 +[ -z "$EUID" ] && EUID="$(id -u)" +if [ "${EUID}" != "0" ] && [ ! -w "${config_dir}" ]; then + error "Script must be run by a user with write access to ${config_dir}." 32 fi -echo >&2 "Failed to claim node with the following error message:\"${ERROR_MESSAGES[$EXIT_CODE]}\"" -if [ "${VERBOSE}" == 1 ]; then - echo >&2 "Error key was:\"${ERROR_KEYS[$EXIT_CODE]}\"" -fi -rm -f "${CLAIMING_DIR}/tmpout.txt" -exit $EXIT_CODE +warning "This script is deprecated and will be officially unsupported in the near future. Please either use the kickstart script with the appropriate '--claim-*' options, or directly write out the claiming configuration instead." +parse_args "${@}" +write_config +reload_claiming diff --git a/src/claim/ui.c b/src/claim/ui.c new file mode 100644 index 000000000..30a001d0b --- /dev/null +++ b/src/claim/ui.c @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#define UNICODE +#define _UNICODE +#include <windows.h> +#include "richedit.h" +#include "tchar.h" +#include "main.h" + +static LPCTSTR szWindowClass = _T("DesktopApp"); + +static HINSTANCE hInst; +static HWND hToken; +static HWND hRoom; + +LRESULT CALLBACK WndProc(HWND hNetdatawnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + PAINTSTRUCT ps; + HDC hdc; + LPCTSTR topMsg[] = { L" Help", + L" ", + L"In this initial version of the software, there are no fields for data", + L" entry. To claim your agent, you must use the following options:", + L" ", + L"/T TOKEN: The cloud token;", + L"/R ROOMS: A list of rooms to claim;", + L"/P PROXY: The proxy information;", + L"/U URL : The cloud URL;", + L"/I : Use insecure connection;", + L"/F File : file to store cloud info;" + }; + + switch (message) + { + case WM_PAINT: { + hdc = BeginPaint(hNetdatawnd, &ps); + + int i; + for (i = 0; i < sizeof(topMsg) / sizeof(LPCTSTR); i++) { + TextOut(hdc, 5, 5 + 15*i, topMsg[i], wcslen(topMsg[i])); + } + EndPaint(hNetdatawnd, &ps); + break; + } + case WM_COMMAND: + case WM_DESTROY: { + PostQuitMessage(0); + break; + } + default: { + return DefWindowProc(hNetdatawnd, message, wParam, lParam); + break; + } + } + + return 0; +} + +int netdata_claim_window_loop(HINSTANCE hInstance, int nCmdShow) +{ + WNDCLASSEX wcex; + + wcex.cbSize = sizeof(WNDCLASSEX); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = hInstance; + wcex.hIcon = LoadIcon(wcex.hInstance, MAKEINTRESOURCEW(11)); + wcex.hCursor = LoadCursor(NULL, IDC_ARROW); + wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); + wcex.lpszMenuName = NULL; + wcex.lpszClassName = szWindowClass; + wcex.hIconSm = LoadIcon(wcex.hInstance, IDI_APPLICATION); + + if (!RegisterClassEx(&wcex)) { + MessageBoxW(NULL, L"Call to RegisterClassEx failed!", L"Error", 0); + return 1; + } + + hInst = hInstance; + + HWND hNetdatawnd = CreateWindowExW(WS_EX_OVERLAPPEDWINDOW, + szWindowClass, + L"Netdata Claim", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, + 460, 240, + NULL, + NULL, + hInstance, + NULL + ); + + if (!hNetdatawnd) { + MessageBoxW(NULL, L"Call to CreateWindow failed!", L"Error", 0); + return 1; + } + + ShowWindow(hNetdatawnd, nCmdShow); + UpdateWindow(hNetdatawnd); + + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return (int) msg.wParam; +} diff --git a/src/claim/ui.h b/src/claim/ui.h new file mode 100644 index 000000000..583ed1cda --- /dev/null +++ b/src/claim/ui.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_CLAIM_WINDOW_H_ +# define NETDATA_CLAIM_WINDOW_H_ 1 + +// https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/command-line-string-limitation +// https://sourceforge.net/p/mingw/mailman/mingw-users/thread/4C8FD4EB.4050503@xs4all.nl/ +#define WINDOWS_MAX_PATH 8191 + +int netdata_claim_window_loop(HINSTANCE hInstance, int nCmdShow); + +#endif //NETDATA_CLAIM_WINDOW_H_ |