first commit
This commit is contained in:
36
.github/workflows/amd64.yml
vendored
Normal file
36
.github/workflows/amd64.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: amd64
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile-amd64
|
||||
push: true
|
||||
tags: saphoooo/freebox-exporter
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
36
.github/workflows/armv7.yml
vendored
Normal file
36
.github/workflows/armv7.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: armv7
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile-armv7
|
||||
push: true
|
||||
tags: saphoooo/freebox-exporter:armv7
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '44 5 * * 3'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
**/freebox_exporter
|
19
Dockerfile-armv7
Normal file
19
Dockerfile-armv7
Normal file
@ -0,0 +1,19 @@
|
||||
FROM golang:1.14
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN set -x && \
|
||||
go get -d -v . && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARM=7 GOARCH=arm go build -ldflags "-w -s" -a -installsuffix cgo -o app .
|
||||
|
||||
FROM scratch
|
||||
|
||||
LABEL maintainer="stephane.beuret@gmail.com"
|
||||
|
||||
COPY --from=0 app /
|
||||
|
||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
ENTRYPOINT ["/app"]
|
28
Dockerfile.amd64
Normal file
28
Dockerfile.amd64
Normal file
@ -0,0 +1,28 @@
|
||||
FROM golang:1.14
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY . .
|
||||
|
||||
ADD https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz /usr/local
|
||||
|
||||
RUN set -x && \
|
||||
apt update && \
|
||||
apt install -y xz-utils && \
|
||||
xz -d -c /usr/local/upx-3.95-amd64_linux.tar.xz | \
|
||||
tar -xOf - upx-3.95-amd64_linux/upx > /bin/upx && \
|
||||
chmod a+x /bin/upx && \
|
||||
go get -d -v . && \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o app . && \
|
||||
strip --strip-unneeded app && \
|
||||
upx app
|
||||
|
||||
FROM scratch
|
||||
|
||||
LABEL maintainer="stephane.beuret@gmail.com"
|
||||
|
||||
COPY --from=0 app /
|
||||
|
||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
ENTRYPOINT ["/app"]
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
91
README.md
Normal file
91
README.md
Normal file
@ -0,0 +1,91 @@
|
||||
# freebox_exporter
|
||||
|
||||
A Prometheus exporter for Freebox stats
|
||||
|
||||
## Cmds
|
||||
|
||||
`freebox_exporter`
|
||||
|
||||
## Flags
|
||||
|
||||
- `-endpoint`: Freebox API url (default http://mafreebox.freebox.fr)
|
||||
- `-listen`: port for Prometheus metrics (default :10001)
|
||||
- `-debug`: turn on debug mode
|
||||
- `-fiber`: turn off DSL metric for fiber Freebox
|
||||
|
||||
## Preview
|
||||
|
||||
Here's what you can get in Prometheus / Grafana with freebox_exporter:
|
||||
|
||||

|
||||
|
||||
# How to use it
|
||||
|
||||
## Compiled binary
|
||||
|
||||
If you want to compile the binary, you can refer to [this document](https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63) which explains how to do it, depending on your OS and architecture. Alternatively, you can use `./build.sh`.
|
||||
|
||||
You can also find the compiled binaries for MacOS, Linux (x86_64, arm64 and arm) and Windows in the release tab.
|
||||
|
||||
### Quick start
|
||||
|
||||
```
|
||||
./freebox_exporter
|
||||
```
|
||||
|
||||
### The following parameters are optional and can be overridden:
|
||||
|
||||
- Freebox API endpoint
|
||||
|
||||
```
|
||||
./freebox_exporter -endpoint "http://mafreebox.freebox.fr"
|
||||
```
|
||||
|
||||
- Port
|
||||
|
||||
```
|
||||
./freebox_exporter -listen ":10001"
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
### Quick start
|
||||
|
||||
```
|
||||
docker run -d --name freebox-exporter --restart on-failure -p 10001:10001 \
|
||||
saphoooo/freebox-exporter
|
||||
```
|
||||
|
||||
### The following parameters are optional and can be overridden:
|
||||
|
||||
- Local token
|
||||
|
||||
Volume allows to save the access token outside of the container to reuse authentication upon an update of the container.
|
||||
|
||||
```
|
||||
docker run -d --name freebox-exporter --restart on-failure -p 10001:10001 \
|
||||
-e HOME=token -v /path/to/token:/token saphoooo/freebox-exporter
|
||||
```
|
||||
|
||||
- Freebox API endpoint
|
||||
|
||||
```
|
||||
docker run -d --name freebox-exporter --restart on-failure -p 10001:10001
|
||||
saphoooo/freebox-exporter -endpoint "http://mafreebox.freebox.fr"
|
||||
```
|
||||
|
||||
- Port
|
||||
|
||||
```
|
||||
docker run -d --name freebox-exporter --restart on-failure -p 8080:10001 \
|
||||
saphoooo/freebox-exporter
|
||||
```
|
||||
|
||||
## Caution on first run
|
||||
|
||||
If you launch the application for the first time, you must allow it to access the freebox API.
|
||||
- The application must be launched from the local network.
|
||||
- You have to authorize the application from the freebox front panel.
|
||||
- You have to modify the rights of the application to give it "Modification des réglages de la Freebox"
|
||||
|
||||
Source: https://dev.freebox.fr/sdk/os/
|
227
authz.go
Normal file
227
authz.go
Normal file
@ -0,0 +1,227 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// storeToken stores app_token in ~/.freebox_token
|
||||
func storeToken(token string, authInf *authInfo) error {
|
||||
err := os.Setenv("FREEBOX_TOKEN", token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) {
|
||||
err := ioutil.WriteFile(authInf.myStore.location, []byte(token), 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// retreiveToken gets the token from file and
|
||||
// load it in environment variable
|
||||
func retreiveToken(authInf *authInfo) (string, error) {
|
||||
if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
data, err := ioutil.ReadFile(authInf.myStore.location)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = os.Setenv("FREEBOX_TOKEN", string(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// getTrackID is the initial request to freebox API
|
||||
// get app_token and track_id
|
||||
func getTrackID(authInf *authInfo) (*track, error) {
|
||||
req, _ := json.Marshal(authInf.myApp)
|
||||
buf := bytes.NewReader(req)
|
||||
resp, err := http.Post(authInf.myAPI.authz, "application/json", buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trackID := track{}
|
||||
err = json.Unmarshal(body, &trackID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = storeToken(trackID.Result.AppToken, authInf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &trackID, nil
|
||||
}
|
||||
|
||||
// getGranted waits for user to validate on the freebox front panel
|
||||
// with a timeout of 15 seconds
|
||||
func getGranted(authInf *authInfo) error {
|
||||
trackID, err := getTrackID(authInf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := authInf.myAPI.authz + strconv.Itoa(trackID.Result.TrackID)
|
||||
for i := 0; i < 15; i++ {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
granted := grant{}
|
||||
err = json.Unmarshal(body, &granted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch granted.Result.Status {
|
||||
case "unknown":
|
||||
return errors.New("the app_token is invalid or has been revoked")
|
||||
case "pending":
|
||||
log.Println("the user has not confirmed the authorization request yet")
|
||||
case "timeout":
|
||||
return errors.New("the user did not confirmed the authorization within the given time")
|
||||
case "granted":
|
||||
log.Println("the app_token is valid and can be used to open a session")
|
||||
i = 15
|
||||
case "denied":
|
||||
return errors.New("the user denied the authorization request")
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getChallenge makes sure the app always has a valid challenge
|
||||
func getChallenge(authInf *authInfo) (*challenge, error) {
|
||||
resp, err := http.Get(authInf.myAPI.login)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
challenged := challenge{}
|
||||
err = json.Unmarshal(body, &challenged)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &challenged, nil
|
||||
}
|
||||
|
||||
// hmacSha1 encodes app_token in hmac-sha1 and stores it in password
|
||||
func hmacSha1(appToken, challenge string) string {
|
||||
hash := hmac.New(sha1.New, []byte(appToken))
|
||||
hash.Write([]byte(challenge))
|
||||
return hex.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
// getSession gets a session with freeebox API
|
||||
func getSession(authInf *authInfo, passwd string) (*sessionToken, error) {
|
||||
s := session{
|
||||
AppID: authInf.myApp.AppID,
|
||||
Password: passwd,
|
||||
}
|
||||
req, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := bytes.NewReader(req)
|
||||
resp, err := http.Post(authInf.myAPI.loginSession, "application/json", buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := sessionToken{}
|
||||
err = json.Unmarshal(body, &token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &token, nil
|
||||
}
|
||||
|
||||
// getToken gets a valid session_token and asks for user to change
|
||||
// the set of permissions on the API
|
||||
func getToken(authInf *authInfo, xSessionToken *string) (string, error) {
|
||||
if _, err := os.Stat(authInf.myStore.location); os.IsNotExist(err) {
|
||||
err = getGranted(authInf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
reader := authInf.myReader
|
||||
log.Println("check \"Modification des réglages de la Freebox\" and press enter")
|
||||
_, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
_, err := retreiveToken(authInf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
token, err := getSessToken(os.Getenv("FREEBOX_TOKEN"), authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
*xSessionToken = token
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// getSessToken gets a new token session when the old one has expired
|
||||
func getSessToken(token string, authInf *authInfo, xSessionToken *string) (string, error) {
|
||||
challenge, err := getChallenge(authInf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
password := hmacSha1(token, challenge.Result.Challenge)
|
||||
t, err := getSession(authInf, password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if t.Success == false {
|
||||
return "", errors.New(t.Msg)
|
||||
}
|
||||
*xSessionToken = t.Result.SessionToken
|
||||
return t.Result.SessionToken, nil
|
||||
}
|
413
authz_test.go
Normal file
413
authz_test.go
Normal file
@ -0,0 +1,413 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRetreiveToken(t *testing.T) {
|
||||
ai := &authInfo{
|
||||
myStore: store{location: "/tmp/token"},
|
||||
}
|
||||
|
||||
_, err := retreiveToken(ai)
|
||||
if err.Error() != "stat /tmp/token: no such file or directory" {
|
||||
t.Error("Expected bla, but got", err)
|
||||
}
|
||||
|
||||
ioutil.WriteFile(ai.myStore.location, []byte("IOI"), 0600)
|
||||
defer os.Remove(ai.myStore.location)
|
||||
|
||||
token, err := retreiveToken(ai)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
newToken := os.Getenv("FREEBOX_TOKEN")
|
||||
|
||||
if newToken != "IOI" {
|
||||
t.Error("Expected IOI, but got", newToken)
|
||||
}
|
||||
|
||||
if token != "IOI" {
|
||||
t.Error("Expected IOI, but got", newToken)
|
||||
}
|
||||
|
||||
os.Unsetenv("FREEBOX_TOKEN")
|
||||
}
|
||||
|
||||
func TestStoreToken(t *testing.T) {
|
||||
var token string
|
||||
|
||||
ai := &authInfo{}
|
||||
token = "IOI"
|
||||
err := storeToken(token, ai)
|
||||
if err.Error() != "open : no such file or directory" {
|
||||
t.Error("Expected open : no such file or directory, but got", err)
|
||||
}
|
||||
|
||||
ai.myStore.location = "/tmp/token"
|
||||
err = storeToken(token, ai)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
defer os.Remove(ai.myStore.location)
|
||||
|
||||
token = os.Getenv("FREEBOX_TOKEN")
|
||||
if token != "IOI" {
|
||||
t.Error("Expected IOI, but got", token)
|
||||
}
|
||||
os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
data, err := ioutil.ReadFile(ai.myStore.location)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if string(data) != "IOI" {
|
||||
t.Error("Expected IOI, but got", string(data))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetTrackID(t *testing.T) {
|
||||
ai := &authInfo{
|
||||
myStore: store{location: "/tmp/token"},
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
myTrack := track{
|
||||
Success: true,
|
||||
}
|
||||
myTrack.Result.AppToken = "IOI"
|
||||
myTrack.Result.TrackID = 101
|
||||
result, _ := json.Marshal(myTrack)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
ai.myAPI.authz = ts.URL
|
||||
trackID, err := getTrackID(ai)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
defer os.Remove(ai.myStore.location)
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
if trackID.Result.TrackID != 101 {
|
||||
t.Error("Expected 101, but got", trackID.Result.TrackID)
|
||||
}
|
||||
|
||||
// as getTrackID have no return value
|
||||
// the result of storeToken func is checked instead
|
||||
token := os.Getenv("FREEBOX_TOKEN")
|
||||
if token != "IOI" {
|
||||
t.Error("Expected IOI, but got", token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGranted(t *testing.T) {
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.RequestURI {
|
||||
case "/unknown/":
|
||||
myTrack := track{
|
||||
Success: true,
|
||||
}
|
||||
myTrack.Result.TrackID = 101
|
||||
result, _ := json.Marshal(myTrack)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/unknown/101":
|
||||
myGrant := grant{
|
||||
Success: true,
|
||||
}
|
||||
myGrant.Result.Status = "unknown"
|
||||
result, _ := json.Marshal(myGrant)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/timeout/":
|
||||
myTrack := track{
|
||||
Success: true,
|
||||
}
|
||||
myTrack.Result.TrackID = 101
|
||||
result, _ := json.Marshal(myTrack)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/timeout/101":
|
||||
myGrant := grant{
|
||||
Success: true,
|
||||
}
|
||||
myGrant.Result.Status = "timeout"
|
||||
result, _ := json.Marshal(myGrant)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/denied/":
|
||||
myTrack := track{
|
||||
Success: true,
|
||||
}
|
||||
myTrack.Result.TrackID = 101
|
||||
result, _ := json.Marshal(myTrack)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/denied/101":
|
||||
myGrant := grant{
|
||||
Success: true,
|
||||
}
|
||||
myGrant.Result.Status = "denied"
|
||||
result, _ := json.Marshal(myGrant)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/granted/":
|
||||
myTrack := track{
|
||||
Success: true,
|
||||
}
|
||||
myTrack.Result.TrackID = 101
|
||||
result, _ := json.Marshal(myTrack)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/granted/101":
|
||||
myGrant := grant{
|
||||
Success: true,
|
||||
}
|
||||
myGrant.Result.Status = "granted"
|
||||
result, _ := json.Marshal(myGrant)
|
||||
fmt.Fprintln(w, string(result))
|
||||
default:
|
||||
fmt.Fprintln(w, http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
ai := authInfo{}
|
||||
ai.myAPI.authz = ts.URL + "/unknown/"
|
||||
ai.myStore.location = "/tmp/token"
|
||||
|
||||
err := getGranted(&ai)
|
||||
if err.Error() != "the app_token is invalid or has been revoked" {
|
||||
t.Error("Expected the app_token is invalid or has been revoked, but got", err)
|
||||
}
|
||||
defer os.Remove(ai.myStore.location)
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
ai.myAPI.authz = ts.URL + "/timeout/"
|
||||
err = getGranted(&ai)
|
||||
if err.Error() != "the user did not confirmed the authorization within the given time" {
|
||||
t.Error("Expected the user did not confirmed the authorization within the given time, but got", err)
|
||||
}
|
||||
|
||||
ai.myAPI.authz = ts.URL + "/denied/"
|
||||
err = getGranted(&ai)
|
||||
if err.Error() != "the user denied the authorization request" {
|
||||
t.Error("Expected the user denied the authorization request, but got", err)
|
||||
}
|
||||
|
||||
ai.myAPI.authz = ts.URL + "/granted/"
|
||||
err = getGranted(&ai)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChallenge(t *testing.T) {
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
myChall := &challenge{
|
||||
Success: true,
|
||||
}
|
||||
myChall.Result.Challenge = "foobar"
|
||||
result, _ := json.Marshal(myChall)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
ai := &authInfo{
|
||||
myAPI: api{
|
||||
login: ts.URL,
|
||||
},
|
||||
}
|
||||
|
||||
challenged, err := getChallenge(ai)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if challenged.Success != true {
|
||||
t.Error("Expected true, but got", challenged.Success)
|
||||
}
|
||||
|
||||
if challenged.Result.Challenge != "foobar" {
|
||||
t.Error("Expected foobar, but got", challenged.Result.Challenge)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHmacSha1(t *testing.T) {
|
||||
hmac := hmacSha1("IOI", "foobar")
|
||||
if hmac != "02fb876a39b64eddcfee3eaa69465cb3e8d53cde" {
|
||||
t.Error("Expected 02fb876a39b64eddcfee3eaa69465cb3e8d53cde, but got", hmac)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSession(t *testing.T) {
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
myToken := &sessionToken{
|
||||
Success: true,
|
||||
}
|
||||
myToken.Result.Challenge = "foobar"
|
||||
result, _ := json.Marshal(myToken)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
ai := &authInfo{
|
||||
myAPI: api{
|
||||
loginSession: ts.URL,
|
||||
},
|
||||
}
|
||||
|
||||
token, err := getSession(ai, "")
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
if token.Success != true {
|
||||
t.Error("Expected true, but got", token.Success)
|
||||
}
|
||||
|
||||
if token.Result.Challenge != "foobar" {
|
||||
t.Error("Expected foobar, but got", token.Result.Challenge)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetToken(t *testing.T) {
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.RequestURI {
|
||||
case "/login":
|
||||
myChall := &challenge{
|
||||
Success: true,
|
||||
}
|
||||
myChall.Result.Challenge = "foobar"
|
||||
result, _ := json.Marshal(myChall)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/session":
|
||||
myToken := sessionToken{
|
||||
Success: true,
|
||||
}
|
||||
myToken.Result.SessionToken = "foobar"
|
||||
result, _ := json.Marshal(myToken)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/granted/":
|
||||
myTrack := track{
|
||||
Success: true,
|
||||
}
|
||||
myTrack.Result.TrackID = 101
|
||||
result, _ := json.Marshal(myTrack)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/granted/101":
|
||||
myGrant := grant{
|
||||
Success: true,
|
||||
}
|
||||
myGrant.Result.Status = "granted"
|
||||
result, _ := json.Marshal(myGrant)
|
||||
fmt.Fprintln(w, string(result))
|
||||
default:
|
||||
fmt.Fprintln(w, http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
ai := authInfo{}
|
||||
ai.myStore.location = "/tmp/token"
|
||||
ai.myAPI.login = ts.URL + "/login"
|
||||
ai.myAPI.loginSession = ts.URL + "/session"
|
||||
ai.myAPI.authz = ts.URL + "/granted/"
|
||||
ai.myReader = bufio.NewReader(strings.NewReader("\n"))
|
||||
|
||||
var mySessionToken string
|
||||
|
||||
// the first pass valide getToken without a token stored in a file
|
||||
tk, err := getToken(&ai, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
defer os.Remove(ai.myStore.location)
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
if mySessionToken != "foobar" {
|
||||
t.Error("Expected foobar, but got", mySessionToken)
|
||||
}
|
||||
|
||||
if tk != "foobar" {
|
||||
t.Error("Expected foobar, but got", tk)
|
||||
}
|
||||
|
||||
// the second pass validate getToken with a token stored in a file:
|
||||
// the first pass creates a file at ai.myStore.location
|
||||
tk, err = getToken(&ai, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if mySessionToken != "foobar" {
|
||||
t.Error("Expected foobar, but got", mySessionToken)
|
||||
}
|
||||
|
||||
if tk != "foobar" {
|
||||
t.Error("Expected foobar, but got", tk)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetSessToken(t *testing.T) {
|
||||
|
||||
myToken := &sessionToken{}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.RequestURI {
|
||||
case "/login":
|
||||
myChall := &challenge{
|
||||
Success: true,
|
||||
}
|
||||
myChall.Result.Challenge = "foobar"
|
||||
result, _ := json.Marshal(myChall)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/session":
|
||||
myToken.Success = true
|
||||
myToken.Result.SessionToken = "foobar"
|
||||
result, _ := json.Marshal(myToken)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/session2":
|
||||
myToken.Msg = "failed to get a session"
|
||||
myToken.Success = false
|
||||
result, _ := json.Marshal(myToken)
|
||||
fmt.Fprintln(w, string(result))
|
||||
default:
|
||||
fmt.Fprintln(w, http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
ai := authInfo{}
|
||||
ai.myAPI.login = ts.URL + "/login"
|
||||
ai.myAPI.loginSession = ts.URL + "/session"
|
||||
var mySessionToken string
|
||||
|
||||
st, err := getSessToken("token", &ai, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if st != "foobar" {
|
||||
t.Error("Expected foobar, but got", st)
|
||||
}
|
||||
|
||||
ai.myAPI.loginSession = ts.URL + "/session2"
|
||||
|
||||
_, err = getSessToken("token", &ai, &mySessionToken)
|
||||
if err.Error() != "failed to get a session" {
|
||||
t.Error("Expected but got failed to get a session, but got", err)
|
||||
}
|
||||
}
|
8
build.sh
Executable file
8
build.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
ARCH=${1:-arm}
|
||||
GOARCH=$ARCH go build -ldflags "-s -w"
|
||||
ls -lh freebox_exporter
|
||||
file freebox_exporter
|
59
changelog.md
Normal file
59
changelog.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Changelog
|
||||
|
||||
## [1.3] - 2020-10-04
|
||||
|
||||
- Add VPN server metrics, mainly tx and rx for a user on a vpn with scr and local ip as labels
|
||||
|
||||
## [1.2.2] - 2020-10-03
|
||||
|
||||
- Change variable type int to int64 for RRD metrics, causing an error "constant overflow" on arm chipset
|
||||
|
||||
## [1.2] - 2020-05-27
|
||||
|
||||
- Add build script
|
||||
- Log Freebox Server uptime and firmware version
|
||||
- Log G.INP data
|
||||
- Log connection status, protocol and modulation
|
||||
- Log XDSL stats
|
||||
- Don't log incorrect values (previously logged as zero)
|
||||
- Remove dead code
|
||||
|
||||
## [1.1.9] - 2020-04-24
|
||||
|
||||
- Log freeplug speeds and connectivity
|
||||
- Go 1.14
|
||||
- Remove Godeps and vendored files
|
||||
|
||||
## [1.1.7] - 2019-09-04
|
||||
|
||||
- Go 1.13
|
||||
|
||||
## [1.1.6] - 2019-09-04
|
||||
|
||||
- There is no more uncomfortable error message when the application renews its token
|
||||
- Adding a `-fiber` flag so that Freebox fiber users do not capture DSL metrics, which are empty on this type of Freebox
|
||||
|
||||
## [1.1.4] - 2019-09-03
|
||||
|
||||
- Adding a `-debug` flag to have more verbose error logs
|
||||
|
||||
## [1.1.2] - 2019-08-25
|
||||
|
||||
- Improve error messages
|
||||
|
||||
## [1.1.1] - 2019-07-31
|
||||
|
||||
- New Dockerfile for amd64 arch: reduce the image size to 3mb
|
||||
|
||||
## [1.1.0] - 2019-07-31
|
||||
|
||||
- Fix temp metrics
|
||||
- Add Godeps
|
||||
|
||||
## [1.0.1] - 2019-07-30
|
||||
|
||||
- Change error catching
|
||||
|
||||
## [1.0.0] - 2019-07-29
|
||||
|
||||
- Rewriting the application by adding a ton of unit tests
|
9
contrib/README.md
Normal file
9
contrib/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
Here you'll find related contributions to the project, such as ready-to-use Grafana dashboards (this will save you from having to reinvent the wheel when using the Freebox Exporter).
|
||||
|
||||
Thanks to [mcanevet](https://gist.github.com/mcanevet) for his contribution:
|
||||
|
||||
<img width="600" alt="mcanevet Grafana dashbord" src="https://user-images.githubusercontent.com/13923756/57589143-de912180-751f-11e9-8bad-7842b35776d9.png">
|
||||
|
||||
Thanks to [Pichon](https://github.com/lepichon) for his contribution:
|
||||
|
||||
<img width="600" alt="lepichon Grafana dashbord" src="https://user-images.githubusercontent.com/13923756/74086200-e9fa8480-4a80-11ea-92d4-a4aca6f8c159.png">
|
544
contrib/lepichon_freebox_grafana_dashboard.json
Normal file
544
contrib/lepichon_freebox_grafana_dashboard.json
Normal file
@ -0,0 +1,544 @@
|
||||
{
|
||||
"__inputs": [
|
||||
{
|
||||
"name": "DS_PROMETHEUS",
|
||||
"label": "Prometheus",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "prometheus",
|
||||
"pluginName": "Prometheus"
|
||||
}
|
||||
],
|
||||
"__requires": [
|
||||
{
|
||||
"type": "grafana",
|
||||
"id": "grafana",
|
||||
"name": "Grafana",
|
||||
"version": "6.6.0"
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"id": "graph",
|
||||
"name": "Graph",
|
||||
"version": ""
|
||||
},
|
||||
{
|
||||
"type": "datasource",
|
||||
"id": "prometheus",
|
||||
"name": "Prometheus",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"id": "stat",
|
||||
"name": "Stat",
|
||||
"version": ""
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"gridPos": {
|
||||
"h": 3,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 11,
|
||||
"links": [],
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"fieldOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"max": 1,
|
||||
"min": 1,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "rgb(33, 33, 33)",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [],
|
||||
"values": false
|
||||
},
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "vertical"
|
||||
},
|
||||
"pluginVersion": "6.6.0",
|
||||
"repeat": null,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "freebox_lan_reachable == 1",
|
||||
"instant": true,
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Host on LAN",
|
||||
"transparent": true,
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 3
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 2,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"hideEmpty": false,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "connected",
|
||||
"options": {
|
||||
"dataLinks": []
|
||||
},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"alias": "/.*Trans.*/",
|
||||
"transform": "negative-Y"
|
||||
}
|
||||
],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "irate(freebox_net_down_bytes[5m])",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "Download Usage",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "freebox_net_bw_down_bytes / 10",
|
||||
"legendFormat": "Download Bandwidth",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Network Traffic Download (bytes/sec)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"decimals": null,
|
||||
"format": "Bps",
|
||||
"label": "Bytes out (-) / in (+)",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": false
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 11
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 13,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"options": {
|
||||
"dataLinks": []
|
||||
},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"alias": "/.*Trans.*/",
|
||||
"transform": "negative-Y"
|
||||
}
|
||||
],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "irate(freebox_net_up_bytes[5m])",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "Upload Usage",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "freebox_net_bw_up_bytes / 10",
|
||||
"legendFormat": "Upload Bandwidth",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Network Traffic Upload (bytes/sec)",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"decimals": null,
|
||||
"format": "Bps",
|
||||
"label": "Bytes out (-) / in (+)",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": false
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"description": "",
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 19
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 9,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"options": {
|
||||
"dataLinks": []
|
||||
},
|
||||
"percentage": false,
|
||||
"pluginVersion": "6.6.0",
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "freebox_system_fan_rpm",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Fan Speed",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"decimals": 3,
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "${DS_PROMETHEUS}",
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 19
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 6,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"options": {
|
||||
"dataLinks": []
|
||||
},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "freebox_system_temp_celsius",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "System Temperature",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "celsius",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 22,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"prometheus",
|
||||
"freebox"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Freebox",
|
||||
"uid": "TVsfEYmZk",
|
||||
"version": 14
|
||||
}
|
418
contrib/mcanevet_freebox_grafana_dashboard.json
Normal file
418
contrib/mcanevet_freebox_grafana_dashboard.json
Normal file
@ -0,0 +1,418 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": 5,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "Prometheus",
|
||||
"fill": 2,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"alias": "/.*Trans.*/",
|
||||
"transform": "negative-Y"
|
||||
}
|
||||
],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "irate(freebox_net_down_bytes[5m])",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "Receive",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "irate(freebox_net_up_bytes[5m])",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 2,
|
||||
"legendFormat": "Transmit",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Network Traffic by bytes",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"decimals": null,
|
||||
"format": "Bps",
|
||||
"label": "Bytes out (-) / in (+)",
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": false
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 4,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"rightSide": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "freebox_system_fan_rpm",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Fan RPM",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"decimals": null,
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": "0",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 6,
|
||||
"legend": {
|
||||
"alignAsTable": true,
|
||||
"avg": true,
|
||||
"current": true,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "freebox_system_temp_celsius",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "System Temperature",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "celsius",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"cacheTimeout": null,
|
||||
"colorBackground": false,
|
||||
"colorValue": false,
|
||||
"colors": [
|
||||
"#299c46",
|
||||
"rgba(237, 129, 40, 0.89)",
|
||||
"#d44a3a"
|
||||
],
|
||||
"format": "none",
|
||||
"gauge": {
|
||||
"maxValue": 100,
|
||||
"minValue": 0,
|
||||
"show": false,
|
||||
"thresholdLabels": false,
|
||||
"thresholdMarkers": true
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"id": 8,
|
||||
"interval": null,
|
||||
"links": [],
|
||||
"mappingType": 1,
|
||||
"mappingTypes": [
|
||||
{
|
||||
"name": "value to text",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"name": "range to text",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"maxDataPoints": 100,
|
||||
"nullPointMode": "connected",
|
||||
"nullText": null,
|
||||
"postfix": "",
|
||||
"postfixFontSize": "50%",
|
||||
"prefix": "",
|
||||
"prefixFontSize": "50%",
|
||||
"rangeMaps": [
|
||||
{
|
||||
"from": "null",
|
||||
"text": "N/A",
|
||||
"to": "null"
|
||||
}
|
||||
],
|
||||
"sparkline": {
|
||||
"fillColor": "rgba(31, 118, 189, 0.18)",
|
||||
"full": false,
|
||||
"lineColor": "rgb(31, 120, 193)",
|
||||
"show": true
|
||||
},
|
||||
"tableColumn": "",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(freebox_lan_reachable)",
|
||||
"format": "time_series",
|
||||
"intervalFactor": 1,
|
||||
"legendFormat": "",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": "",
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Hosts on LAN",
|
||||
"type": "singlestat",
|
||||
"valueFontSize": "80%",
|
||||
"valueMaps": [
|
||||
{
|
||||
"op": "=",
|
||||
"text": "N/A",
|
||||
"value": "null"
|
||||
}
|
||||
],
|
||||
"valueName": "current"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 18,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"prometheus",
|
||||
"freebox"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Freebox",
|
||||
"uid": "TVsfEYmZk",
|
||||
"version": 14
|
||||
}
|
255
gauges.go
Normal file
255
gauges.go
Normal file
@ -0,0 +1,255 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
// XXX: see https://dev.freebox.fr/sdk/os/ for API documentation
|
||||
// XXX: see https://prometheus.io/docs/practices/naming/ for metric names
|
||||
|
||||
// connectionXdsl
|
||||
connectionXdslStatusUptimeGauges = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "freebox_connection_xdsl_status_uptime_seconds_total",
|
||||
},
|
||||
[]string{
|
||||
"status",
|
||||
"protocol",
|
||||
"modulation",
|
||||
},
|
||||
)
|
||||
|
||||
connectionXdslDownAttnGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_connection_xdsl_down_attn_decibels",
|
||||
})
|
||||
connectionXdslUpAttnGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_connection_xdsl_up_attn_decibels",
|
||||
})
|
||||
connectionXdslDownSnrGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_connection_xdsl_down_snr_decibels",
|
||||
})
|
||||
connectionXdslUpSnrGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_connection_xdsl_up_snr_decibels",
|
||||
})
|
||||
|
||||
connectionXdslErrorGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_connection_xdsl_errors_total",
|
||||
Help: "Error counts",
|
||||
},
|
||||
[]string{
|
||||
"direction", // up|down
|
||||
"name", // crc|es|fec|hec
|
||||
},
|
||||
)
|
||||
|
||||
connectionXdslGinpGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_connection_xdsl_ginp",
|
||||
},
|
||||
[]string{
|
||||
"direction", // up|down
|
||||
"name", // enabled|rtx_(tx|c|uc)
|
||||
},
|
||||
)
|
||||
|
||||
connectionXdslNitroGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_connection_xdsl_nitro",
|
||||
},
|
||||
[]string{
|
||||
"direction", // up|down
|
||||
},
|
||||
)
|
||||
|
||||
// RRD dsl [unstable]
|
||||
rateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_dsl_up_bytes",
|
||||
Help: "Available upload bandwidth (in byte/s)",
|
||||
})
|
||||
rateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_dsl_down_bytes",
|
||||
Help: "Available download bandwidth (in byte/s)",
|
||||
})
|
||||
snrUpGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_dsl_snr_up_decibel",
|
||||
Help: "Upload signal/noise ratio (in 1/10 dB)",
|
||||
})
|
||||
snrDownGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_dsl_snr_down_decibel",
|
||||
Help: "Download signal/noise ratio (in 1/10 dB)",
|
||||
})
|
||||
|
||||
// freeplug
|
||||
freeplugRxRateGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "freebox_freeplug_rx_rate_bits",
|
||||
Help: "rx rate (from the freeplugs to the \"cco\" freeplug) (in bits/s) -1 if not available",
|
||||
},
|
||||
[]string{
|
||||
"id",
|
||||
},
|
||||
)
|
||||
freeplugTxRateGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "freebox_freeplug_tx_rate_bits",
|
||||
Help: "tx rate (from the \"cco\" freeplug to the freeplugs) (in bits/s) -1 if not available",
|
||||
},
|
||||
[]string{
|
||||
"id",
|
||||
},
|
||||
)
|
||||
freeplugHasNetworkGauge = promauto.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "freebox_freeplug_has_network",
|
||||
Help: "is connected to the network",
|
||||
},
|
||||
[]string{
|
||||
"id",
|
||||
},
|
||||
)
|
||||
|
||||
// RRD Net [unstable]
|
||||
bwUpGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_net_bw_up_bytes",
|
||||
Help: "Upload available bandwidth (in byte/s)",
|
||||
})
|
||||
bwDownGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_net_bw_down_bytes",
|
||||
Help: "Download available bandwidth (in byte/s)",
|
||||
})
|
||||
netRateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_net_up_bytes",
|
||||
Help: "Upload rate (in byte/s)",
|
||||
})
|
||||
netRateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_net_down_bytes",
|
||||
Help: "Download rate (in byte/s)",
|
||||
})
|
||||
vpnRateUpGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_net_vpn_up_bytes",
|
||||
Help: "Vpn client upload rate (in byte/s)",
|
||||
})
|
||||
vpnRateDownGauge = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "freebox_net_vpn_down_bytes",
|
||||
Help: "Vpn client download rate (in byte/s)",
|
||||
})
|
||||
|
||||
// Lan
|
||||
lanReachableGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_lan_reachable",
|
||||
Help: "Hosts reachable on LAN",
|
||||
},
|
||||
[]string{
|
||||
"name", // hostname
|
||||
"vendor",
|
||||
"ip",
|
||||
},
|
||||
)
|
||||
|
||||
systemTempGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_system_temp_celsius",
|
||||
Help: "Temperature sensors reported by system (in °C)",
|
||||
},
|
||||
[]string{
|
||||
"name",
|
||||
},
|
||||
)
|
||||
|
||||
systemFanGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_system_fan_rpm",
|
||||
Help: "Fan speed reported by system (in RPM)",
|
||||
},
|
||||
[]string{
|
||||
"name",
|
||||
},
|
||||
)
|
||||
|
||||
systemUptimeGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_system_uptime_seconds_total",
|
||||
},
|
||||
[]string{
|
||||
"firmware_version",
|
||||
},
|
||||
)
|
||||
|
||||
// wifi
|
||||
wifiLabels = []string{
|
||||
"access_point",
|
||||
"hostname",
|
||||
"state",
|
||||
}
|
||||
|
||||
wifiSignalGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_wifi_signal_attenuation_db",
|
||||
Help: "Wifi signal attenuation in decibel",
|
||||
},
|
||||
wifiLabels,
|
||||
)
|
||||
|
||||
wifiInactiveGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_wifi_inactive_duration_seconds",
|
||||
Help: "Wifi inactive duration in seconds",
|
||||
},
|
||||
wifiLabels,
|
||||
)
|
||||
|
||||
wifiConnectionDurationGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_wifi_connection_duration_seconds",
|
||||
Help: "Wifi connection duration in seconds",
|
||||
},
|
||||
wifiLabels,
|
||||
)
|
||||
|
||||
wifiRXBytesGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_wifi_rx_bytes",
|
||||
Help: "Wifi received data (from station to Freebox) in bytes",
|
||||
},
|
||||
wifiLabels,
|
||||
)
|
||||
|
||||
wifiTXBytesGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_wifi_tx_bytes",
|
||||
Help: "Wifi transmitted data (from Freebox to station) in bytes",
|
||||
},
|
||||
wifiLabels,
|
||||
)
|
||||
|
||||
wifiRXRateGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_wifi_rx_rate",
|
||||
Help: "Wifi reception data rate (from station to Freebox) in bytes/seconds",
|
||||
},
|
||||
wifiLabels,
|
||||
)
|
||||
|
||||
wifiTXRateGauges = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "freebox_wifi_tx_rate",
|
||||
Help: "Wifi transmission data rate (from Freebox to station) in bytes/seconds",
|
||||
},
|
||||
wifiLabels,
|
||||
)
|
||||
|
||||
// vpn server connections list [unstable]
|
||||
vpnServerConnectionsList = promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "vpn_server_connections_list",
|
||||
Help: "VPN server connections list",
|
||||
},
|
||||
[]string{
|
||||
"user",
|
||||
"vpn",
|
||||
"src_ip",
|
||||
"local_ip",
|
||||
"name", // rx_bytes|tx_bytes
|
||||
},
|
||||
)
|
||||
)
|
573
getters.go
Normal file
573
getters.go
Normal file
@ -0,0 +1,573 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
apiErrors = map[string]error{
|
||||
"invalid_token": errors.New("The app token you are trying to use is invalid or has been revoked"),
|
||||
"insufficient_rights": errors.New("Your app permissions does not allow accessing this API"),
|
||||
"denied_from_external_ip": errors.New("You are trying to get an app_token from a remote IP"),
|
||||
"invalid_request": errors.New("Your request is invalid"),
|
||||
"ratelimited": errors.New("Too many auth error have been made from your IP"),
|
||||
"new_apps_denied": errors.New("New application token request has been disabled"),
|
||||
"apps_denied": errors.New("API access from apps has been disabled"),
|
||||
"internal_error": errors.New("Internal error"),
|
||||
"db_error": errors.New("Oops, the database you are trying to access doesn't seem to exist"),
|
||||
"nodev": errors.New("Invalid interface"),
|
||||
}
|
||||
)
|
||||
|
||||
func (r *rrd) status() error {
|
||||
if apiErrors[r.ErrorCode] == nil {
|
||||
return errors.New("RRD: The API returns an unknown error_code: " + r.ErrorCode)
|
||||
}
|
||||
return apiErrors[r.ErrorCode]
|
||||
}
|
||||
|
||||
func (l *lan) status() error {
|
||||
if apiErrors[l.ErrorCode] == nil {
|
||||
return errors.New("LAN: The API returns an unknown error_code: " + l.ErrorCode)
|
||||
}
|
||||
return apiErrors[l.ErrorCode]
|
||||
}
|
||||
|
||||
func setFreeboxToken(authInf *authInfo, xSessionToken *string) (string, error) {
|
||||
token := os.Getenv("FREEBOX_TOKEN")
|
||||
|
||||
if token == "" {
|
||||
var err error
|
||||
*xSessionToken, err = getToken(authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token = *xSessionToken
|
||||
}
|
||||
|
||||
if *xSessionToken == "" {
|
||||
var err error
|
||||
*xSessionToken, err = getSessToken(token, authInf, xSessionToken)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
token = *xSessionToken
|
||||
}
|
||||
|
||||
return token, nil
|
||||
|
||||
}
|
||||
|
||||
func newPostRequest() *postRequest {
|
||||
return &postRequest{
|
||||
method: "POST",
|
||||
url: mafreebox + "api/v4/rrd/",
|
||||
header: "X-Fbx-App-Auth",
|
||||
}
|
||||
}
|
||||
|
||||
func getConnectionXdsl(authInf *authInfo, pr *postRequest, xSessionToken *string) (connectionXdsl, error) {
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(pr.method, pr.url, nil)
|
||||
if err != nil {
|
||||
return connectionXdsl{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return connectionXdsl{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return connectionXdsl{}, errors.New(resp.Status)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return connectionXdsl{}, err
|
||||
}
|
||||
|
||||
connectionXdslResp := connectionXdsl{}
|
||||
err = json.Unmarshal(body, &connectionXdslResp)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return connectionXdsl{}, err
|
||||
}
|
||||
|
||||
return connectionXdslResp, nil
|
||||
}
|
||||
|
||||
func getDsl(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) {
|
||||
d := &database{
|
||||
DB: "dsl",
|
||||
Fields: []string{"rate_up", "rate_down", "snr_up", "snr_down"},
|
||||
Precision: 10,
|
||||
DateStart: int(time.Now().Unix() - 10),
|
||||
}
|
||||
|
||||
freeboxToken, err := setFreeboxToken(authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
client := http.Client{}
|
||||
r, err := json.Marshal(*d)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
buf := bytes.NewReader(r)
|
||||
req, err := http.NewRequest(pr.method, pr.url, buf)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return []int64{}, errors.New(resp.Status)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
rrdTest := rrd{}
|
||||
err = json.Unmarshal(body, &rrdTest)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return []int64{}, err
|
||||
}
|
||||
|
||||
if rrdTest.ErrorCode == "auth_required" {
|
||||
*xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" {
|
||||
if rrdTest.status().Error() == "Unknown return code from the API" {
|
||||
fmt.Println("getDsl")
|
||||
}
|
||||
return []int64{}, rrdTest.status()
|
||||
}
|
||||
|
||||
if len(rrdTest.Result.Data) == 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
|
||||
result := []int64{rrdTest.Result.Data[0]["rate_up"], rrdTest.Result.Data[0]["rate_down"], rrdTest.Result.Data[0]["snr_up"], rrdTest.Result.Data[0]["snr_down"]}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getTemp(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) {
|
||||
d := &database{
|
||||
DB: "temp",
|
||||
Fields: []string{"cpum", "cpub", "sw", "hdd", "fan_speed"},
|
||||
Precision: 10,
|
||||
DateStart: int(time.Now().Unix() - 10),
|
||||
}
|
||||
|
||||
freeboxToken, err := setFreeboxToken(authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
|
||||
client := http.Client{}
|
||||
r, err := json.Marshal(*d)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
buf := bytes.NewReader(r)
|
||||
req, err := http.NewRequest(pr.method, fmt.Sprintf(pr.url), buf)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return []int64{}, errors.New(resp.Status)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
rrdTest := rrd{}
|
||||
err = json.Unmarshal(body, &rrdTest)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return []int64{}, err
|
||||
}
|
||||
|
||||
if rrdTest.ErrorCode == "auth_required" {
|
||||
*xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" {
|
||||
if rrdTest.status().Error() == "Unknown return code from the API" {
|
||||
fmt.Println("getTemp")
|
||||
}
|
||||
return []int64{}, rrdTest.status()
|
||||
}
|
||||
|
||||
if len(rrdTest.Result.Data) == 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
|
||||
return []int64{rrdTest.Result.Data[0]["cpum"], rrdTest.Result.Data[0]["cpub"], rrdTest.Result.Data[0]["sw"], rrdTest.Result.Data[0]["hdd"], rrdTest.Result.Data[0]["fan_speed"]}, nil
|
||||
}
|
||||
|
||||
func getNet(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) {
|
||||
d := &database{
|
||||
DB: "net",
|
||||
Fields: []string{"bw_up", "bw_down", "rate_up", "rate_down", "vpn_rate_up", "vpn_rate_down"},
|
||||
Precision: 10,
|
||||
DateStart: int(time.Now().Unix() - 10),
|
||||
}
|
||||
|
||||
freeboxToken, err := setFreeboxToken(authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
|
||||
client := http.Client{}
|
||||
r, err := json.Marshal(*d)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
buf := bytes.NewReader(r)
|
||||
req, err := http.NewRequest(pr.method, pr.url, buf)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return []int64{}, errors.New(resp.Status)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
rrdTest := rrd{}
|
||||
err = json.Unmarshal(body, &rrdTest)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return []int64{}, err
|
||||
}
|
||||
|
||||
if rrdTest.ErrorCode == "auth_required" {
|
||||
*xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" {
|
||||
if rrdTest.status().Error() == "Unknown return code from the API" {
|
||||
fmt.Println("getNet")
|
||||
}
|
||||
return []int64{}, rrdTest.status()
|
||||
}
|
||||
|
||||
if len(rrdTest.Result.Data) == 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
|
||||
return []int64{rrdTest.Result.Data[0]["bw_up"], rrdTest.Result.Data[0]["bw_down"], rrdTest.Result.Data[0]["rate_up"], rrdTest.Result.Data[0]["rate_down"], rrdTest.Result.Data[0]["vpn_rate_up"], rrdTest.Result.Data[0]["vpn_rate_down"]}, nil
|
||||
}
|
||||
|
||||
func getSwitch(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]int64, error) {
|
||||
d := &database{
|
||||
DB: "switch",
|
||||
Fields: []string{"rx_1", "tx_1", "rx_2", "tx_2", "rx_3", "tx_3", "rx_4", "tx_4"},
|
||||
Precision: 10,
|
||||
DateStart: int(time.Now().Unix() - 10),
|
||||
}
|
||||
|
||||
freeboxToken, err := setFreeboxToken(authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
|
||||
client := http.Client{}
|
||||
r, err := json.Marshal(*d)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
buf := bytes.NewReader(r)
|
||||
req, err := http.NewRequest(pr.method, pr.url, buf)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return []int64{}, errors.New(resp.Status)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
rrdTest := rrd{}
|
||||
err = json.Unmarshal(body, &rrdTest)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return []int64{}, err
|
||||
}
|
||||
|
||||
if rrdTest.ErrorCode == "auth_required" {
|
||||
*xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return []int64{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if rrdTest.ErrorCode != "" && rrdTest.ErrorCode != "auth_required" {
|
||||
if rrdTest.status().Error() == "Unknown return code from the API" {
|
||||
fmt.Println("getSwitch")
|
||||
}
|
||||
return []int64{}, rrdTest.status()
|
||||
}
|
||||
|
||||
if len(rrdTest.Result.Data) == 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
|
||||
return []int64{rrdTest.Result.Data[0]["rx_1"], rrdTest.Result.Data[0]["tx_1"], rrdTest.Result.Data[0]["rx_2"], rrdTest.Result.Data[0]["tx_2"], rrdTest.Result.Data[0]["rx_3"], rrdTest.Result.Data[0]["tx_3"], rrdTest.Result.Data[0]["rx_4"], rrdTest.Result.Data[0]["tx_4"]}, nil
|
||||
}
|
||||
|
||||
func getLan(authInf *authInfo, pr *postRequest, xSessionToken *string) ([]lanHost, error) {
|
||||
freeboxToken, err := setFreeboxToken(authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return []lanHost{}, err
|
||||
}
|
||||
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(pr.method, pr.url, nil)
|
||||
if err != nil {
|
||||
return []lanHost{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return []lanHost{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return []lanHost{}, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return []lanHost{}, err
|
||||
}
|
||||
|
||||
lanResp := lan{}
|
||||
err = json.Unmarshal(body, &lanResp)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return []lanHost{}, err
|
||||
}
|
||||
|
||||
if lanResp.ErrorCode == "auth_required" {
|
||||
*xSessionToken, err = getSessToken(freeboxToken, authInf, xSessionToken)
|
||||
if err != nil {
|
||||
return []lanHost{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if lanResp.ErrorCode != "" && lanResp.ErrorCode != "auth_required" {
|
||||
return []lanHost{}, lanResp.status()
|
||||
}
|
||||
|
||||
return lanResp.Result, nil
|
||||
}
|
||||
|
||||
func getFreeplug(authInf *authInfo, pr *postRequest, xSessionToken *string) (freeplug, error) {
|
||||
if _, err := setFreeboxToken(authInf, xSessionToken); err != nil {
|
||||
return freeplug{}, err
|
||||
}
|
||||
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(pr.method, pr.url, nil)
|
||||
if err != nil {
|
||||
return freeplug{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return freeplug{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return freeplug{}, errors.New(resp.Status)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return freeplug{}, err
|
||||
}
|
||||
|
||||
freeplugResp := freeplug{}
|
||||
err = json.Unmarshal(body, &freeplugResp)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return freeplug{}, err
|
||||
}
|
||||
|
||||
return freeplugResp, nil
|
||||
}
|
||||
|
||||
func getSystem(authInf *authInfo, pr *postRequest, xSessionToken *string) (system, error) {
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(pr.method, pr.url, nil)
|
||||
if err != nil {
|
||||
return system{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return system{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return system{}, errors.New(resp.Status)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return system{}, err
|
||||
}
|
||||
|
||||
systemResp := system{}
|
||||
err = json.Unmarshal(body, &systemResp)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return system{}, err
|
||||
}
|
||||
|
||||
return systemResp, nil
|
||||
}
|
||||
|
||||
func getWifi(authInf *authInfo, pr *postRequest, xSessionToken *string) (wifi, error) {
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(pr.method, pr.url, nil)
|
||||
if err != nil {
|
||||
return wifi{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return wifi{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return wifi{}, errors.New(resp.Status)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return wifi{}, err
|
||||
}
|
||||
|
||||
wifiResp := wifi{}
|
||||
err = json.Unmarshal(body, &wifiResp)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return wifi{}, err
|
||||
}
|
||||
|
||||
return wifiResp, nil
|
||||
}
|
||||
|
||||
func getWifiStations(authInf *authInfo, pr *postRequest, xSessionToken *string) (wifiStations, error) {
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(pr.method, pr.url, nil)
|
||||
if err != nil {
|
||||
return wifiStations{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return wifiStations{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return wifiStations{}, errors.New(resp.Status)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return wifiStations{}, err
|
||||
}
|
||||
|
||||
wifiStationResp := wifiStations{}
|
||||
err = json.Unmarshal(body, &wifiStationResp)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return wifiStations{}, err
|
||||
}
|
||||
|
||||
return wifiStationResp, nil
|
||||
}
|
||||
|
||||
func getVpnServer(authInf *authInfo, pr *postRequest, xSessionToken *string) (vpnServer, error) {
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(pr.method, pr.url, nil)
|
||||
if err != nil {
|
||||
return vpnServer{}, err
|
||||
}
|
||||
req.Header.Add(pr.header, *xSessionToken)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return vpnServer{}, err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return vpnServer{}, errors.New(resp.Status)
|
||||
}
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return vpnServer{}, err
|
||||
}
|
||||
|
||||
vpnServerResp := vpnServer{}
|
||||
err = json.Unmarshal(body, &vpnServerResp)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println(string(body))
|
||||
}
|
||||
return vpnServer{}, err
|
||||
}
|
||||
|
||||
return vpnServerResp, nil
|
||||
}
|
727
getters_test.go
Normal file
727
getters_test.go
Normal file
@ -0,0 +1,727 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetFreeboxToken(t *testing.T) {
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.RequestURI {
|
||||
case "/login":
|
||||
myChall := &challenge{
|
||||
Success: true,
|
||||
}
|
||||
myChall.Result.Challenge = "foobar"
|
||||
result, _ := json.Marshal(myChall)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/session":
|
||||
myToken := sessionToken{
|
||||
Success: true,
|
||||
}
|
||||
myToken.Result.SessionToken = "foobar"
|
||||
result, _ := json.Marshal(myToken)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/granted/":
|
||||
myTrack := track{
|
||||
Success: true,
|
||||
}
|
||||
myTrack.Result.TrackID = 101
|
||||
result, _ := json.Marshal(myTrack)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/granted/101":
|
||||
myGrant := grant{
|
||||
Success: true,
|
||||
}
|
||||
myGrant.Result.Status = "granted"
|
||||
result, _ := json.Marshal(myGrant)
|
||||
fmt.Fprintln(w, string(result))
|
||||
default:
|
||||
fmt.Fprintln(w, http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
ai := &authInfo{}
|
||||
ai.myStore.location = "/tmp/token"
|
||||
ai.myAPI.login = ts.URL + "/login"
|
||||
ai.myAPI.loginSession = ts.URL + "/session"
|
||||
ai.myAPI.authz = ts.URL + "/granted/"
|
||||
ai.myReader = bufio.NewReader(strings.NewReader("\n"))
|
||||
|
||||
var mySessionToken string
|
||||
|
||||
token, err := setFreeboxToken(ai, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
defer os.Remove(ai.myStore.location)
|
||||
|
||||
if token != "foobar" {
|
||||
t.Error("Expected foobar, but got", token)
|
||||
}
|
||||
|
||||
os.Setenv("FREEBOX_TOKEN", "barfoo")
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
token, err = setFreeboxToken(ai, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if token != "barfoo" {
|
||||
t.Error("Expected barfoo, but got", token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDsl(t *testing.T) {
|
||||
os.Setenv("FREEBOX_TOKEN", "IOI")
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.RequestURI {
|
||||
case "/good":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
}
|
||||
myRRD.Result.Data = []map[string]int64{
|
||||
{
|
||||
"rate_up": 12,
|
||||
"rate_down": 34,
|
||||
"snr_up": 56,
|
||||
"snr_down": 78,
|
||||
},
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/error":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
ErrorCode: "insufficient_rights",
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/null":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
goodPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/good",
|
||||
}
|
||||
|
||||
errorPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/error",
|
||||
}
|
||||
|
||||
nullPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/null",
|
||||
}
|
||||
|
||||
ai := &authInfo{}
|
||||
mySessionToken := "foobar"
|
||||
|
||||
getDslResult, err := getDsl(ai, goodPR, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if getDslResult[0] != 12 || getDslResult[1] != 34 || getDslResult[2] != 56 || getDslResult[3] != 78 {
|
||||
t.Errorf("Expected 12 34 56 78, but got %v %v %v %v\n", getDslResult[0], getDslResult[1], getDslResult[2], getDslResult[3])
|
||||
}
|
||||
|
||||
getDslResult, err = getDsl(ai, errorPR, &mySessionToken)
|
||||
if err.Error() != "Your app permissions does not allow accessing this API" {
|
||||
t.Error("Expected Your app permissions does not allow accessing this API, but go", err)
|
||||
}
|
||||
|
||||
if len(getDslResult) != 0 {
|
||||
t.Error("Expected 0, but got", len(getDslResult))
|
||||
}
|
||||
|
||||
getDslResult, err = getDsl(ai, nullPR, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if len(getDslResult) != 0 {
|
||||
t.Error("Expected 0, but got", len(getDslResult))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetTemp(t *testing.T) {
|
||||
os.Setenv("FREEBOX_TOKEN", "IOI")
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.RequestURI {
|
||||
case "/good":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
}
|
||||
myRRD.Result.Data = []map[string]int64{
|
||||
{
|
||||
"cpum": 01,
|
||||
"cpub": 02,
|
||||
"sw": 03,
|
||||
"hdd": 04,
|
||||
"fan_speed": 05,
|
||||
},
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/error":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
ErrorCode: "denied_from_external_ip",
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/null":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
goodPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/good",
|
||||
}
|
||||
|
||||
errorPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/error",
|
||||
}
|
||||
|
||||
nullPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/null",
|
||||
}
|
||||
|
||||
ai := &authInfo{}
|
||||
mySessionToken := "foobar"
|
||||
|
||||
getTempResult, err := getTemp(ai, goodPR, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if getTempResult[0] != 01 || getTempResult[1] != 02 || getTempResult[2] != 03 || getTempResult[3] != 04 || getTempResult[4] != 05 {
|
||||
t.Errorf("Expected 01 02 03 04 05, but got %v %v %v %v %v\n", getTempResult[0], getTempResult[1], getTempResult[2], getTempResult[3], getTempResult[4])
|
||||
}
|
||||
|
||||
getTempResult, err = getTemp(ai, errorPR, &mySessionToken)
|
||||
if err.Error() != "You are trying to get an app_token from a remote IP" {
|
||||
t.Error("Expected You are trying to get an app_token from a remote IP, but go", err)
|
||||
}
|
||||
|
||||
if len(getTempResult) != 0 {
|
||||
t.Error("Expected 0, but got", len(getTempResult))
|
||||
}
|
||||
|
||||
getTempResult, err = getTemp(ai, nullPR, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if len(getTempResult) != 0 {
|
||||
t.Error("Expected 0, but got", len(getTempResult))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetNet(t *testing.T) {
|
||||
os.Setenv("FREEBOX_TOKEN", "IOI")
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.RequestURI {
|
||||
case "/good":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
}
|
||||
myRRD.Result.Data = []map[string]int64{
|
||||
{
|
||||
"bw_up": 12500000000,
|
||||
"bw_down": 12500000000,
|
||||
"rate_up": 12500000000,
|
||||
"rate_down": 12500000000,
|
||||
"vpn_rate_up": 12500000000,
|
||||
"vpn_rate_down": 12500000000,
|
||||
},
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/error":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
ErrorCode: "new_apps_denied",
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/null":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
goodPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/good",
|
||||
}
|
||||
|
||||
errorPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/error",
|
||||
}
|
||||
|
||||
nullPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/null",
|
||||
}
|
||||
|
||||
ai := &authInfo{}
|
||||
mySessionToken := "foobar"
|
||||
|
||||
getNetResult, err := getNet(ai, goodPR, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but go", err)
|
||||
}
|
||||
|
||||
if getNetResult[0] != 12500000000 || getNetResult[1] != 12500000000 || getNetResult[2] != 12500000000 || getNetResult[3] != 12500000000 || getNetResult[4] != 12500000000 || getNetResult[5] != 12500000000 {
|
||||
t.Errorf("Expected 01 02 03 04 05 06, but got %v %v %v %v %v %v\n", getNetResult[0], getNetResult[1], getNetResult[2], getNetResult[3], getNetResult[4], getNetResult[5])
|
||||
}
|
||||
|
||||
getNetResult, err = getNet(ai, errorPR, &mySessionToken)
|
||||
if err.Error() != "New application token request has been disabled" {
|
||||
t.Error("Expected New application token request has been disabled, but got", err)
|
||||
}
|
||||
|
||||
if len(getNetResult) != 0 {
|
||||
t.Error("Expected 0, but got", len(getNetResult))
|
||||
}
|
||||
|
||||
getNetResult, err = getNet(ai, nullPR, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if len(getNetResult) != 0 {
|
||||
t.Error("Expected 0, but got", len(getNetResult))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetSwitch(t *testing.T) {
|
||||
os.Setenv("FREEBOX_TOKEN", "IOI")
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.RequestURI {
|
||||
case "/good":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
}
|
||||
myRRD.Result.Data = []map[string]int64{
|
||||
{
|
||||
"rx_1": 01,
|
||||
"tx_1": 11,
|
||||
"rx_2": 02,
|
||||
"tx_2": 12,
|
||||
"rx_3": 03,
|
||||
"tx_3": 13,
|
||||
"rx_4": 04,
|
||||
"tx_4": 14,
|
||||
},
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/error":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
ErrorCode: "apps_denied",
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/null":
|
||||
myRRD := rrd{
|
||||
Success: true,
|
||||
}
|
||||
result, _ := json.Marshal(myRRD)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
goodPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/good",
|
||||
}
|
||||
|
||||
errorPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/error",
|
||||
}
|
||||
|
||||
nullPR := &postRequest{
|
||||
method: "POST",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/null",
|
||||
}
|
||||
|
||||
ai := &authInfo{}
|
||||
mySessionToken := "foobar"
|
||||
|
||||
getSwitchResult, err := getSwitch(ai, goodPR, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if getSwitchResult[0] != 01 || getSwitchResult[1] != 11 || getSwitchResult[2] != 02 || getSwitchResult[3] != 12 || getSwitchResult[4] != 03 || getSwitchResult[5] != 13 || getSwitchResult[6] != 04 || getSwitchResult[7] != 14 {
|
||||
t.Errorf("Expected 01 11 02 12 03 13 04 14, but got %v %v %v %v %v %v %v %v\n", getSwitchResult[0], getSwitchResult[1], getSwitchResult[2], getSwitchResult[3], getSwitchResult[4], getSwitchResult[5], getSwitchResult[6], getSwitchResult[7])
|
||||
}
|
||||
|
||||
getSwitchResult, err = getSwitch(ai, errorPR, &mySessionToken)
|
||||
if err.Error() != "API access from apps has been disabled" {
|
||||
t.Error("Expected API access from apps has been disabled, but got", err)
|
||||
}
|
||||
|
||||
if len(getSwitchResult) != 0 {
|
||||
t.Error("Expected 0, but got", len(getSwitchResult))
|
||||
}
|
||||
|
||||
getSwitchResult, err = getSwitch(ai, nullPR, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if len(getSwitchResult) != 0 {
|
||||
t.Error("Expected 0, but got", len(getSwitchResult))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetLan(t *testing.T) {
|
||||
os.Setenv("FREEBOX_TOKEN", "IOI")
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.RequestURI {
|
||||
case "/good":
|
||||
myLan := lan{
|
||||
Success: true,
|
||||
}
|
||||
myLan.Result = []lanHost{
|
||||
{
|
||||
Reachable: true,
|
||||
PrimaryName: "Reachable host",
|
||||
},
|
||||
{
|
||||
Reachable: false,
|
||||
PrimaryName: "Unreachable host",
|
||||
},
|
||||
}
|
||||
result, _ := json.Marshal(myLan)
|
||||
fmt.Fprintln(w, string(result))
|
||||
case "/error":
|
||||
myLan := lan{
|
||||
Success: true,
|
||||
ErrorCode: "ratelimited",
|
||||
}
|
||||
result, _ := json.Marshal(myLan)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
goodPR := &postRequest{
|
||||
method: "GET",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/good",
|
||||
}
|
||||
|
||||
errorPR := &postRequest{
|
||||
method: "GET",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL + "/error",
|
||||
}
|
||||
|
||||
ai := &authInfo{}
|
||||
mySessionToken := "foobar"
|
||||
|
||||
lanAvailable, err := getLan(ai, goodPR, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
for _, v := range lanAvailable {
|
||||
if v.Reachable && v.PrimaryName != "Reachable host" {
|
||||
t.Errorf("Expected Reachable: true, Host: Reachable host, but go Reachable: %v, Host: %v", v.Reachable, v.PrimaryName)
|
||||
}
|
||||
|
||||
if !v.Reachable && v.PrimaryName != "Unreachable host" {
|
||||
t.Errorf("Expected Reachable: false, Host: Unreachable host, but go Reachable: %v, Host: %v", !v.Reachable, v.PrimaryName)
|
||||
}
|
||||
}
|
||||
|
||||
lanAvailable, err = getLan(ai, errorPR, &mySessionToken)
|
||||
if err.Error() != "Too many auth error have been made from your IP" {
|
||||
t.Error("Expected Too many auth error have been made from your IP, but got", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetSystem(t *testing.T) {
|
||||
os.Setenv("FREEBOX_TOKEN", "IOI")
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mySys := system{
|
||||
Success: true,
|
||||
}
|
||||
mySys.Result.FanRPM = 666
|
||||
mySys.Result.TempCpub = 81
|
||||
mySys.Result.TempCpum = 89
|
||||
mySys.Result.TempHDD = 30
|
||||
mySys.Result.TempSW = 54
|
||||
|
||||
/*
|
||||
mySys.Result {
|
||||
FanRPM: 666,
|
||||
TempCpub: 81,
|
||||
TempCpum: 89,
|
||||
TempHDD: 30,
|
||||
TempSW: 54,
|
||||
}
|
||||
*/
|
||||
result, _ := json.Marshal(mySys)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
pr := &postRequest{
|
||||
method: "GET",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL,
|
||||
}
|
||||
|
||||
ai := &authInfo{}
|
||||
mySessionToken := "foobar"
|
||||
|
||||
systemStats, err := getSystem(ai, pr, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if systemStats.Result.FanRPM != 666 {
|
||||
t.Error("Expected 666, but got", systemStats.Result.FanRPM)
|
||||
}
|
||||
|
||||
if systemStats.Result.TempCpub != 81 {
|
||||
t.Error("Expected 81, but got", systemStats.Result.TempCpub)
|
||||
}
|
||||
|
||||
if systemStats.Result.TempCpum != 89 {
|
||||
t.Error("Expected 89, but got", systemStats.Result.TempCpum)
|
||||
}
|
||||
|
||||
if systemStats.Result.TempHDD != 30 {
|
||||
t.Error("Expected 30, but got", systemStats.Result.TempHDD)
|
||||
}
|
||||
|
||||
if systemStats.Result.TempSW != 54 {
|
||||
t.Error("Expected 54, but got", systemStats.Result.TempSW)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetWifi(t *testing.T) {
|
||||
os.Setenv("FREEBOX_TOKEN", "IOI")
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
myWifi := wifi{
|
||||
Success: true,
|
||||
}
|
||||
myAP := wifiAccessPoint{
|
||||
Name: "AP1",
|
||||
ID: 0,
|
||||
}
|
||||
myWifi.Result = []wifiAccessPoint{myAP}
|
||||
|
||||
result, _ := json.Marshal(myWifi)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
pr := &postRequest{
|
||||
method: "GET",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL,
|
||||
}
|
||||
|
||||
ai := &authInfo{}
|
||||
mySessionToken := "foobar"
|
||||
|
||||
wifiStats, err := getWifi(ai, pr, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if wifiStats.Result[0].Name != "AP1" {
|
||||
t.Error("Expected AP1, but got", wifiStats.Result[0].Name)
|
||||
}
|
||||
|
||||
if wifiStats.Result[0].ID != 0 {
|
||||
t.Error("Expected 0, but got", wifiStats.Result[0].ID)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestGetWifiStations(t *testing.T) {
|
||||
os.Setenv("FREEBOX_TOKEN", "IOI")
|
||||
defer os.Unsetenv("FREEBOX_TOKEN")
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
myWifiStations := wifiStations{
|
||||
Success: true,
|
||||
}
|
||||
|
||||
myStation := wifiStation{
|
||||
Hostname: "station_host",
|
||||
MAC: "AA:BB:CC:DD:EE:FF",
|
||||
State: "authorized",
|
||||
Inactive: 60,
|
||||
RXBytes: 500,
|
||||
TXBytes: 2280000000,
|
||||
ConnectionDuration: 600,
|
||||
TXRate: 4260000000,
|
||||
RXRate: 5,
|
||||
Signal: -20,
|
||||
}
|
||||
myWifiStations.Result = []wifiStation{myStation}
|
||||
|
||||
result, _ := json.Marshal(myWifiStations)
|
||||
fmt.Fprintln(w, string(result))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
pr := &postRequest{
|
||||
method: "GET",
|
||||
header: "X-Fbx-App-Auth",
|
||||
url: ts.URL,
|
||||
}
|
||||
|
||||
ai := &authInfo{}
|
||||
mySessionToken := "foobar"
|
||||
|
||||
wifiStationsStats, err := getWifiStations(ai, pr, &mySessionToken)
|
||||
if err != nil {
|
||||
t.Error("Expected no err, but got", err)
|
||||
}
|
||||
|
||||
if wifiStationsStats.Result[0].Hostname != "station_host" {
|
||||
t.Error("Expected station_host, but got", wifiStationsStats.Result[0].Hostname)
|
||||
}
|
||||
|
||||
if wifiStationsStats.Result[0].MAC != "AA:BB:CC:DD:EE:FF" {
|
||||
t.Error("Expected AA:BB:CC:DD:EE:FF, but got", wifiStationsStats.Result[0].MAC)
|
||||
}
|
||||
|
||||
if wifiStationsStats.Result[0].State != "authorized" {
|
||||
t.Error("Expected authorized, but got", wifiStationsStats.Result[0].State)
|
||||
}
|
||||
|
||||
if wifiStationsStats.Result[0].Inactive != 60 {
|
||||
t.Error("Expected 60, but got", wifiStationsStats.Result[0].Inactive)
|
||||
}
|
||||
|
||||
if wifiStationsStats.Result[0].RXBytes != 500 {
|
||||
t.Error("Expected 500, but got", wifiStationsStats.Result[0].RXBytes)
|
||||
}
|
||||
|
||||
if wifiStationsStats.Result[0].TXBytes != 10000 {
|
||||
t.Error("Expected 10000, but got", wifiStationsStats.Result[0].TXBytes)
|
||||
}
|
||||
|
||||
if wifiStationsStats.Result[0].ConnectionDuration != 600 {
|
||||
t.Error("Expected 600, but got", wifiStationsStats.Result[0].ConnectionDuration)
|
||||
}
|
||||
|
||||
if wifiStationsStats.Result[0].TXRate != 20 {
|
||||
t.Error("Expected 20, but got", wifiStationsStats.Result[0].TXRate)
|
||||
}
|
||||
|
||||
if wifiStationsStats.Result[0].RXRate != 5 {
|
||||
t.Error("Expected 5, but got", wifiStationsStats.Result[0].RXRate)
|
||||
}
|
||||
|
||||
if wifiStationsStats.Result[0].Signal != -20 {
|
||||
t.Error("Expected -20, but got", wifiStationsStats.Result[0].Signal)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_getNet(t *testing.T) {
|
||||
type args struct {
|
||||
authInf *authInfo
|
||||
pr *postRequest
|
||||
xSessionToken *string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []int
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := getNet(tt.args.authInf, tt.args.pr, tt.args.xSessionToken)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getNet() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getNet() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
9
go.mod
Normal file
9
go.mod
Normal file
@ -0,0 +1,9 @@
|
||||
module freebox_exporter
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c // indirect
|
||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
|
||||
github.com/prometheus/client_golang v0.9.2
|
||||
)
|
23
go.sum
Normal file
23
go.sum
Normal file
@ -0,0 +1,23 @@
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c h1:fQ4P1oAipLwec/j5tfZTYV/e5i9ICSk23uVL+TK9III=
|
||||
github.com/golang/protobuf v1.2.1-0.20190109072247-347cf4a86c1c/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
|
||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
|
||||
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8=
|
||||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
310
main.go
Normal file
310
main.go
Normal file
@ -0,0 +1,310 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
var (
|
||||
mafreebox string
|
||||
listen string
|
||||
debug bool
|
||||
fiber bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&mafreebox, "endpoint", "http://mafreebox.freebox.fr/", "Endpoint for freebox API")
|
||||
flag.StringVar(&listen, "listen", ":10001", "Prometheus metrics port")
|
||||
flag.BoolVar(&debug, "debug", false, "Debug mode")
|
||||
flag.BoolVar(&fiber, "fiber", false, "Turn on if you're using a fiber Freebox")
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if !strings.HasSuffix(mafreebox, "/") {
|
||||
mafreebox = mafreebox + "/"
|
||||
}
|
||||
|
||||
endpoint := mafreebox + "api/v4/login/"
|
||||
myAuthInfo := &authInfo{
|
||||
myAPI: api{
|
||||
login: endpoint,
|
||||
authz: endpoint + "authorize/",
|
||||
loginSession: endpoint + "session/",
|
||||
},
|
||||
myStore: store{location: os.Getenv("HOME") + "/.freebox_token"},
|
||||
myApp: app{
|
||||
AppID: "fr.freebox.exporter",
|
||||
AppName: "prometheus-exporter",
|
||||
AppVersion: "0.4",
|
||||
DeviceName: "local",
|
||||
},
|
||||
myReader: bufio.NewReader(os.Stdin),
|
||||
}
|
||||
|
||||
myPostRequest := newPostRequest()
|
||||
|
||||
myConnectionXdslRequest := &postRequest{
|
||||
method: "GET",
|
||||
url: mafreebox + "api/v4/connection/xdsl/",
|
||||
header: "X-Fbx-App-Auth",
|
||||
}
|
||||
|
||||
myFreeplugRequest := &postRequest{
|
||||
method: "GET",
|
||||
url: mafreebox + "api/v4/freeplug/",
|
||||
header: "X-Fbx-App-Auth",
|
||||
}
|
||||
|
||||
myLanRequest := &postRequest{
|
||||
method: "GET",
|
||||
url: mafreebox + "api/v4/lan/browser/pub/",
|
||||
header: "X-Fbx-App-Auth",
|
||||
}
|
||||
|
||||
mySystemRequest := &postRequest{
|
||||
method: "GET",
|
||||
url: mafreebox + "api/v4/system/",
|
||||
header: "X-Fbx-App-Auth",
|
||||
}
|
||||
|
||||
myWifiRequest := &postRequest{
|
||||
method: "GET",
|
||||
url: mafreebox + "api/v2/wifi/ap/",
|
||||
header: "X-Fbx-App-Auth",
|
||||
}
|
||||
|
||||
myVpnRequest := &postRequest{
|
||||
method: "GET",
|
||||
url: mafreebox + "api/v4/vpn/connection/",
|
||||
header: "X-Fbx-App-Auth",
|
||||
}
|
||||
|
||||
var mySessionToken string
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// There is no DSL metric on fiber Freebox
|
||||
// If you use a fiber Freebox, use -fiber flag to turn off this metric
|
||||
if !fiber {
|
||||
// connectionXdsl metrics
|
||||
connectionXdslStats, err := getConnectionXdsl(myAuthInfo, myConnectionXdslRequest, &mySessionToken)
|
||||
if err != nil {
|
||||
log.Printf("An error occured with connectionXdsl metrics: %v", err)
|
||||
}
|
||||
|
||||
if connectionXdslStats.Success {
|
||||
status := connectionXdslStats.Result.Status
|
||||
result := connectionXdslStats.Result
|
||||
down := result.Down
|
||||
up := result.Up
|
||||
|
||||
connectionXdslStatusUptimeGauges.
|
||||
WithLabelValues(status.Status, status.Protocol, status.Modulation).
|
||||
Set(float64(status.Uptime))
|
||||
|
||||
connectionXdslDownAttnGauge.Set(float64(down.Attn10) / 10)
|
||||
connectionXdslUpAttnGauge.Set(float64(up.Attn10) / 10)
|
||||
|
||||
// XXX: sometimes the Freebox is reporting zero as SNR which
|
||||
// does not make sense so we don't log these
|
||||
if down.Snr10 > 0 {
|
||||
connectionXdslDownSnrGauge.Set(float64(down.Snr10) / 10)
|
||||
}
|
||||
if up.Snr10 > 0 {
|
||||
connectionXdslUpSnrGauge.Set(float64(up.Snr10) / 10)
|
||||
}
|
||||
|
||||
connectionXdslNitroGauges.WithLabelValues("down").
|
||||
Set(bool2float(down.Nitro))
|
||||
connectionXdslNitroGauges.WithLabelValues("up").
|
||||
Set(bool2float(up.Nitro))
|
||||
|
||||
connectionXdslGinpGauges.WithLabelValues("down", "enabled").
|
||||
Set(bool2float(down.Ginp))
|
||||
connectionXdslGinpGauges.WithLabelValues("up", "enabled").
|
||||
Set(bool2float(up.Ginp))
|
||||
|
||||
logFields(result, connectionXdslGinpGauges,
|
||||
[]string{"rtx_tx", "rtx_c", "rtx_uc"})
|
||||
|
||||
logFields(result, connectionXdslErrorGauges,
|
||||
[]string{"crc", "es", "fec", "hec", "ses"})
|
||||
}
|
||||
|
||||
// dsl metrics
|
||||
getDslResult, err := getDsl(myAuthInfo, myPostRequest, &mySessionToken)
|
||||
if err != nil {
|
||||
log.Printf("An error occured with DSL metrics: %v", err)
|
||||
}
|
||||
|
||||
if len(getDslResult) > 0 {
|
||||
rateUpGauge.Set(float64(getDslResult[0]))
|
||||
rateDownGauge.Set(float64(getDslResult[1]))
|
||||
snrUpGauge.Set(float64(getDslResult[2]))
|
||||
snrDownGauge.Set(float64(getDslResult[3]))
|
||||
}
|
||||
}
|
||||
|
||||
// freeplug metrics
|
||||
freeplugStats, err := getFreeplug(myAuthInfo, myFreeplugRequest, &mySessionToken)
|
||||
if err != nil {
|
||||
log.Printf("An error occured with freeplug metrics: %v", err)
|
||||
}
|
||||
|
||||
for _, freeplugNetwork := range freeplugStats.Result {
|
||||
for _, freeplugMember := range freeplugNetwork.Members {
|
||||
if freeplugMember.HasNetwork {
|
||||
freeplugHasNetworkGauge.WithLabelValues(freeplugMember.ID).Set(float64(1))
|
||||
} else {
|
||||
freeplugHasNetworkGauge.WithLabelValues(freeplugMember.ID).Set(float64(0))
|
||||
}
|
||||
|
||||
Mb := 1e6
|
||||
rxRate := float64(freeplugMember.RxRate) * Mb
|
||||
txRate := float64(freeplugMember.TxRate) * Mb
|
||||
|
||||
if rxRate >= 0 { // -1 if not unavailable
|
||||
freeplugRxRateGauge.WithLabelValues(freeplugMember.ID).Set(rxRate)
|
||||
}
|
||||
|
||||
if txRate >= 0 { // -1 if not unavailable
|
||||
freeplugTxRateGauge.WithLabelValues(freeplugMember.ID).Set(txRate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// net metrics
|
||||
getNetResult, err := getNet(myAuthInfo, myPostRequest, &mySessionToken)
|
||||
if err != nil {
|
||||
log.Printf("An error occured with NET metrics: %v", err)
|
||||
}
|
||||
|
||||
if len(getNetResult) > 0 {
|
||||
bwUpGauge.Set(float64(getNetResult[0]))
|
||||
bwDownGauge.Set(float64(getNetResult[1]))
|
||||
netRateUpGauge.Set(float64(getNetResult[2]))
|
||||
netRateDownGauge.Set(float64(getNetResult[3]))
|
||||
vpnRateUpGauge.Set(float64(getNetResult[4]))
|
||||
vpnRateDownGauge.Set(float64(getNetResult[5]))
|
||||
}
|
||||
|
||||
// lan metrics
|
||||
lanAvailable, err := getLan(myAuthInfo, myLanRequest, &mySessionToken)
|
||||
if err != nil {
|
||||
log.Printf("An error occured with LAN metrics: %v", err)
|
||||
}
|
||||
for _, v := range lanAvailable {
|
||||
var Ip string
|
||||
if len(v.L3c) > 0 {
|
||||
Ip = v.L3c[0].Addr
|
||||
} else {
|
||||
Ip = ""
|
||||
}
|
||||
if v.Reachable {
|
||||
lanReachableGauges.With(prometheus.Labels{"name": v.PrimaryName, "vendor":v.Vendor_name, "ip": Ip}).Set(float64(1))
|
||||
} else {
|
||||
lanReachableGauges.With(prometheus.Labels{"name": v.PrimaryName, "vendor":v.Vendor_name, "ip": Ip}).Set(float64(0))
|
||||
}
|
||||
}
|
||||
|
||||
// system metrics
|
||||
systemStats, err := getSystem(myAuthInfo, mySystemRequest, &mySessionToken)
|
||||
if err != nil {
|
||||
log.Printf("An error occured with System metrics: %v", err)
|
||||
}
|
||||
|
||||
systemTempGauges.WithLabelValues("Température CPU B").Set(float64(systemStats.Result.TempCpub))
|
||||
systemTempGauges.WithLabelValues("Température CPU M").Set(float64(systemStats.Result.TempCpum))
|
||||
systemTempGauges.WithLabelValues("Température Switch").Set(float64(systemStats.Result.TempSW))
|
||||
systemTempGauges.WithLabelValues("Disque dur").Set(float64(systemStats.Result.TempHDD))
|
||||
systemFanGauges.WithLabelValues("Ventilateur 1").Set(float64(systemStats.Result.FanRPM))
|
||||
|
||||
systemUptimeGauges.
|
||||
WithLabelValues(systemStats.Result.FirmwareVersion).
|
||||
Set(float64(systemStats.Result.UptimeVal))
|
||||
|
||||
// wifi metrics
|
||||
wifiStats, err := getWifi(myAuthInfo, myWifiRequest, &mySessionToken)
|
||||
if err != nil {
|
||||
log.Printf("An error occured with Wifi metrics: %v", err)
|
||||
}
|
||||
for _, accessPoint := range wifiStats.Result {
|
||||
myWifiStationRequest := &postRequest{
|
||||
method: "GET",
|
||||
url: mafreebox + "api/v2/wifi/ap/" + strconv.Itoa(accessPoint.ID) + "/stations",
|
||||
header: "X-Fbx-App-Auth",
|
||||
}
|
||||
wifiStationsStats, err := getWifiStations(myAuthInfo, myWifiStationRequest, &mySessionToken)
|
||||
if err != nil {
|
||||
log.Printf("An error occured with Wifi station metrics: %v", err)
|
||||
}
|
||||
for _, station := range wifiStationsStats.Result {
|
||||
wifiSignalGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.Signal))
|
||||
wifiInactiveGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.Inactive))
|
||||
wifiConnectionDurationGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.ConnectionDuration))
|
||||
wifiRXBytesGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.RXBytes))
|
||||
wifiTXBytesGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.TXBytes))
|
||||
wifiRXRateGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.RXRate))
|
||||
wifiTXRateGauges.With(prometheus.Labels{"access_point": accessPoint.Name, "hostname": station.Hostname, "state": station.State}).Set(float64(station.TXRate))
|
||||
}
|
||||
}
|
||||
|
||||
// VPN Server Connections List
|
||||
getVpnServerResult, err := getVpnServer(myAuthInfo, myVpnRequest, &mySessionToken)
|
||||
if err != nil {
|
||||
log.Printf("An error occured with VPN station metrics: %v", err)
|
||||
}
|
||||
for _, connection := range getVpnServerResult.Result {
|
||||
vpnServerConnectionsList.With(prometheus.Labels{"user": connection.User, "vpn": connection.Vpn, "src_ip": connection.SrcIP, "local_ip": connection.LocalIP, "name": "rx_bytes"}).Set(float64(connection.RxBytes))
|
||||
vpnServerConnectionsList.With(prometheus.Labels{"user": connection.User, "vpn": connection.Vpn, "src_ip": connection.SrcIP, "local_ip": connection.LocalIP, "name": "tx_bytes"}).Set(float64(connection.TxBytes))
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Println("freebox_exporter started on port", listen)
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
log.Fatal(http.ListenAndServe(listen, nil))
|
||||
}
|
||||
|
||||
func logFields(result interface{}, gauge *prometheus.GaugeVec, fields []string) error {
|
||||
resultReflect := reflect.ValueOf(result)
|
||||
|
||||
for _, direction := range []string{"down", "up"} {
|
||||
for _, field := range fields {
|
||||
value := reflect.Indirect(resultReflect).
|
||||
FieldByName(strcase.ToCamel(direction)).
|
||||
FieldByName(strcase.ToCamel(field))
|
||||
|
||||
if value.IsZero() {
|
||||
continue
|
||||
}
|
||||
|
||||
gauge.WithLabelValues(direction, field).
|
||||
Set(float64(value.Int()))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bool2float(b bool) float64 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
276
structs.go
Normal file
276
structs.go
Normal file
@ -0,0 +1,276 @@
|
||||
package main
|
||||
|
||||
import "bufio"
|
||||
|
||||
type track struct {
|
||||
Success bool `json:"success"`
|
||||
Result struct {
|
||||
AppToken string `json:"app_token"`
|
||||
TrackID int `json:"track_id"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type grant struct {
|
||||
Success bool `json:"success"`
|
||||
Result struct {
|
||||
Status string `json:"status"`
|
||||
Challenge string `json:"challenge"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type challenge struct {
|
||||
Success bool `json:"success"`
|
||||
Result struct {
|
||||
LoggedIN bool `json:"logged_in,omitempty"`
|
||||
Challenge string `json:"challenge"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type session struct {
|
||||
AppID string `json:"app_id"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type sessionToken struct {
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
UID string `json:"uid,omitempty"`
|
||||
ErrorCode string `json:"error_code,omitempty"`
|
||||
Result struct {
|
||||
SessionToken string `json:"session_token,omitempty"`
|
||||
Challenge string `json:"challenge"`
|
||||
Permissions struct {
|
||||
Settings bool `json:"settings,omitempty"`
|
||||
Contacts bool `json:"contacts,omitempty"`
|
||||
Calls bool `json:"calls,omitempty"`
|
||||
Explorer bool `json:"explorer,omitempty"`
|
||||
Downloader bool `json:"downloader,omitempty"`
|
||||
Parental bool `json:"parental,omitempty"`
|
||||
Pvr bool `json:"pvr,omitempty"`
|
||||
Home bool `json:"home,omitempty"`
|
||||
Camera bool `json:"camera,omitempty"`
|
||||
} `json:"permissions,omitempty"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type rrd struct {
|
||||
UID string `json:"uid,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
Msg string `json:"msg,omitempty"`
|
||||
Result struct {
|
||||
DateStart int `json:"date_start,omitempty"`
|
||||
DateEnd int `json:"date_end,omitempty"`
|
||||
Data []map[string]int64 `json:"data,omitempty"`
|
||||
} `json:"result"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
}
|
||||
|
||||
// https://dev.freebox.fr/sdk/os/connection/
|
||||
type connectionXdsl struct {
|
||||
Success bool `json:"success"`
|
||||
Result struct {
|
||||
Status struct {
|
||||
Status string `json:"status"`
|
||||
Modulation string `json:"modulation"`
|
||||
Protocol string `json:"protocol"`
|
||||
Uptime int `json:"uptime"`
|
||||
} `json:"status"`
|
||||
Down struct {
|
||||
Attn int `json:"attn"`
|
||||
Attn10 int `json:"attn_10"`
|
||||
Crc int `json:"crc"`
|
||||
Es int `json:"es"`
|
||||
Fec int `json:"fec"`
|
||||
Ginp bool `json:"ginp"`
|
||||
Hec int `json:"hec"`
|
||||
Maxrate uint64 `json:"maxrate"`
|
||||
Nitro bool `json:"nitro"`
|
||||
Phyr bool `json:"phyr"`
|
||||
Rate int `json:"rate"`
|
||||
RtxC int `json:"rtx_c,omitempty"`
|
||||
RtxTx int `json:"rtx_tx,omitempty"`
|
||||
RtxUc int `json:"rtx_uc,omitempty"`
|
||||
Rxmt int `json:"rxmt"`
|
||||
RxmtCorr int `json:"rxmt_corr"`
|
||||
RxmtUncorr int `json:"rxmt_uncorr"`
|
||||
Ses int `json:"ses"`
|
||||
Snr int `json:"snr"`
|
||||
Snr10 int `json:"snr_10"`
|
||||
} `json:"down"`
|
||||
Up struct {
|
||||
Attn int `json:"attn"`
|
||||
Attn10 int `json:"attn_10"`
|
||||
Crc int `json:"crc"`
|
||||
Es int `json:"es"`
|
||||
Fec int `json:"fec"`
|
||||
Ginp bool `json:"ginp"`
|
||||
Hec int `json:"hec"`
|
||||
Maxrate uint64 `json:"maxrate"`
|
||||
Nitro bool `json:"nitro"`
|
||||
Phyr bool `json:"phyr"`
|
||||
Rate uint64 `json:"rate"`
|
||||
RtxC int `json:"rtx_c,omitempty"`
|
||||
RtxTx int `json:"rtx_tx,omitempty"`
|
||||
RtxUc int `json:"rtx_uc,omitempty"`
|
||||
Rxmt int `json:"rxmt"`
|
||||
RxmtCorr int `json:"rxmt_corr"`
|
||||
RxmtUncorr int `json:"rxmt_uncorr"`
|
||||
Ses int `json:"ses"`
|
||||
Snr int `json:"snr"`
|
||||
Snr10 int `json:"snr_10"`
|
||||
} `json:"up"`
|
||||
}
|
||||
}
|
||||
|
||||
type database struct {
|
||||
DB string `json:"db"`
|
||||
DateStart int `json:"date_start,omitempty"`
|
||||
DateEnd int `json:"date_end,omitempty"`
|
||||
Precision int `json:"precision,omitempty"`
|
||||
Fields []string `json:"fields"`
|
||||
}
|
||||
|
||||
// https://dev.freebox.fr/sdk/os/freeplug/
|
||||
type freeplug struct {
|
||||
Success bool `json:"success"`
|
||||
Result []freeplugNetwork `json:"result"`
|
||||
}
|
||||
|
||||
type freeplugNetwork struct {
|
||||
ID string `json:"id"`
|
||||
Members []freeplugMember `json:"members"`
|
||||
}
|
||||
|
||||
type freeplugMember struct {
|
||||
ID string `json:"id"`
|
||||
Local bool `json:"local"`
|
||||
NetRole string `json:"net_role"`
|
||||
EthPortStatus string `json:"eth_port_status"`
|
||||
EthFullDuplex bool `json:"eth_full_duplex"`
|
||||
HasNetwork bool `json:"has_network"`
|
||||
EthSpeed int `json:"eth_speed"`
|
||||
Inative int `json:"inactive"`
|
||||
NetID string `json:"net_id"`
|
||||
RxRate int64 `json:"rx_rate"`
|
||||
TxRate int64 `json:"tx_rate"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
// https://dev.freebox.fr/sdk/os/lan/
|
||||
type l3c struct {
|
||||
Addr string `json:"addr,omitempty"`
|
||||
}
|
||||
|
||||
type lanHost struct {
|
||||
Reachable bool `json:"reachable,omitempty"`
|
||||
PrimaryName string `json:"primary_name,omitempty"`
|
||||
Vendor_name string `json:"vendor_name,omitempty"`
|
||||
L3c []l3c `json:"l3connectivities,omitempty"`
|
||||
}
|
||||
|
||||
type lan struct {
|
||||
Success bool `json:"success"`
|
||||
Result []lanHost `json:"result"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
}
|
||||
|
||||
type idNameValue struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Value int `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// https://dev.freebox.fr/sdk/os/system/
|
||||
type system struct {
|
||||
Success bool `json:"success"`
|
||||
Result struct {
|
||||
Mac string `json:"mac,omitempty"`
|
||||
FanRPM int `json:"fan_rpm,omitempty"`
|
||||
BoxFlavor string `json:"box_flavor,omitempty"`
|
||||
TempCpub int `json:"temp_cpub,omitempty"`
|
||||
TempCpum int `json:"temp_cpum,omitempty"`
|
||||
DiskStatus string `json:"disk_status,omitempty"`
|
||||
TempHDD int `json:"temp_hdd,omitempty"`
|
||||
BoardName string `json:"board_name,omitempty"`
|
||||
TempSW int `json:"temp_sw,omitempty"`
|
||||
Uptime string `json:"uptime,omitempty"`
|
||||
UptimeVal int `json:"uptime_val,omitempty"`
|
||||
UserMainStorage string `json:"user_main_storage,omitempty"`
|
||||
BoxAuthenticated bool `json:"box_authenticated,omitempty"`
|
||||
Serial string `json:"serial,omitempty"`
|
||||
FirmwareVersion string `json:"firmware_version,omitempty"`
|
||||
}
|
||||
}
|
||||
|
||||
// https://dev.freebox.fr/sdk/os/wifi/
|
||||
type wifiAccessPoint struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
ID int `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type wifi struct {
|
||||
Success bool `json:"success"`
|
||||
Result []wifiAccessPoint `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
type wifiStation struct {
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
MAC string `json:"mac,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Inactive int `json:"inactive,omitempty"`
|
||||
RXBytes int64 `json:"rx_bytes,omitempty"`
|
||||
TXBytes int64 `json:"tx_bytes,omitempty"`
|
||||
ConnectionDuration int `json:"conn_duration,omitempty"`
|
||||
TXRate int64 `json:"tx_rate,omitempty"`
|
||||
RXRate int64 `json:"rx_rate,omitempty"`
|
||||
Signal int `json:"signal,omitempty"`
|
||||
}
|
||||
|
||||
type wifiStations struct {
|
||||
Success bool `json:"success"`
|
||||
Result []wifiStation `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
type app struct {
|
||||
AppID string `json:"app_id"`
|
||||
AppName string `json:"app_name"`
|
||||
AppVersion string `json:"app_version"`
|
||||
DeviceName string `json:"device_name"`
|
||||
}
|
||||
|
||||
type api struct {
|
||||
authz string
|
||||
login string
|
||||
loginSession string
|
||||
}
|
||||
|
||||
type store struct {
|
||||
location string
|
||||
}
|
||||
|
||||
type authInfo struct {
|
||||
myApp app
|
||||
myAPI api
|
||||
myStore store
|
||||
myReader *bufio.Reader
|
||||
}
|
||||
|
||||
type postRequest struct {
|
||||
method, url, header string
|
||||
}
|
||||
|
||||
// https://dev.freebox.fr/sdk/os/vpn/
|
||||
type vpnServer struct {
|
||||
Success bool `json:"success"`
|
||||
Result []struct {
|
||||
RxBytes int64 `json:"rx_bytes,omitempty"`
|
||||
Authenticated bool `json:"authenticated,omitempty"`
|
||||
TxBytes int64 `json:"tx_bytes,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Vpn string `json:"vpn,omitempty"`
|
||||
SrcIP string `json:"src_ip,omitempty"`
|
||||
AuthTime int32 `json:"auth_time,omitempty"`
|
||||
LocalIP string `json:"local_ip,omitempty"`
|
||||
} `json:"result,omitempty"`
|
||||
}
|
Reference in New Issue
Block a user