Projet

Général

Profil

Révision 17e0fce8

ID17e0fce840ad78921f6751724f28f351a6894cd8
Parent 914180fe
Enfant 407adbd6

Ajouté par heeplr il y a plus de 6 ans

DOCSIS status monitoring

MUNIN Plugin to monitor status of Arris TG3442 / TG2492LG-85 and compatible cable modems

Voir les différences:

plugins/router/arris-tg3442
1
#!/usr/bin/env python3
2

  
3
"""
4
# MUNIN Plugin to monitor status of Arris TG3442 / TG2492LG-85
5
# and compatible cable modems
6
#
7
# Connect to the web-frontend and get current DOCSIS status of upstream and
8
# downstream channels. (Signal Power, SNR, Lock Status)
9
#
10
#
11
# Requirements:
12
#   - BeautifulSoup
13
#   - pycryptodome
14
#
15
# Configuration:
16
#   [arris]
17
#   env.url http://192.168.100.1
18
#   env.username admin
19
#   env.password yourpassword
20
#
21
# Parameters:
22
#   url      - URL to web-frontend
23
#   username - defaults to "admin"
24
#   password - valid password
25
#
26
#
27
# References: https://www.arris.com/products/touchstone-tg3442-cable-voice-gateway/
28
#
29
#
30
#
31
# Copyright (c) 2019 Daniel Hiepler <d-munin@coderdu.de>
32
#
33
# Permission to use, copy, and modify this software with or without fee
34
# is hereby granted, provided that this entire notice is included in
35
# all source code copies of any software which is or includes a copy or
36
# modification of this software.
37
#
38
# THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR
39
# IMPLIED WARRANTY. IN PARTICULAR, NONE OF THE AUTHORS MAKES ANY
40
# REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE
41
# MERCHANTABILITY OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR
42
# PURPOSE.
43
#
44
#
45
# Magic markers
46
#  #%# family=contrib
47
"""
48

  
49
import binascii
50
from bs4 import BeautifulSoup
51
from Crypto.Cipher import AES
52
import hashlib
53
import json
54
import re
55
import requests
56
import sys
57
import os
58

  
59

  
60
def login(session, url, username, password):
61
    """login to """
62
    # get login page
63
    r = session.get(f"{url}")
64
    # parse HTML
65
    h = BeautifulSoup(r.text, "lxml")
66
    # get session id from javascript in head
67
    current_session_id = re.search(r".*var currentSessionId = '(.+)';.*", h.head.text)[1]
68

  
69
    # encrypt password
70
    salt = os.urandom(8)
71
    iv = os.urandom(8)
72
    key = hashlib.pbkdf2_hmac(
73
        'sha256',
74
        bytes(password.encode("ascii")),
75
        salt,
76
        iterations=1000,
77
        dklen=128/8
78
    )
79
    secret = { "Password": password, "Nonce": current_session_id }
80
    plaintext = bytes(json.dumps(secret).encode("ascii"))
81
    associated_data = "loginPassword"
82
    # initialize cipher
83
    cipher = AES.new(key, AES.MODE_CCM, iv)
84
    # set associated data
85
    cipher.update(bytes(associated_data.encode("ascii")))
86
    # encrypt plaintext
87
    encrypt_data = cipher.encrypt(plaintext)
88
    # append digest
89
    encrypt_data += cipher.digest()
90
    # return
91
    login_data = {
92
        'EncryptData': binascii.hexlify(encrypt_data).decode("ascii"),
93
        'Name': username,
94
        'Salt': binascii.hexlify(salt).decode("ascii"),
95
        'Iv': binascii.hexlify(iv).decode("ascii"),
96
        'AuthData': associated_data
97
    }
98

  
99
    # login
100
    r = session.put(
101
        f"{url}/php/ajaxSet_Password.php",
102
        headers={
103
            "Content-Type": "application/json",
104
            "csrfNonce": "undefined"
105
        },
106
        data=json.dumps(login_data)
107
    )
108

  
109
    # parse result
110
    result = json.loads(r.text)
111
    # success?
112
    if result['p_status'] == "Fail":
113
        print("login failure", file=sys.stderr)
114
        exit(-1)
115
    # remember CSRF nonce
116
    csrf_nonce = result['nonce']
117

  
118
    # prepare headers
119
    session.headers.update({
120
        "X-Requested-With": "XMLHttpRequest",
121
        "csrfNonce": csrf_nonce,
122
        "Origin": f"{url}/",
123
        "Referer": f"{url}/"
124
    })
125
    # set credentials cookie
126
    session.cookies.set(
127
        "credential",
128
        "eyAidW5pcXVlIjoiMjgwb2FQU0xpRiIsICJmYW1pbHkiOiI4NTIiLCAibW9kZWxuYW1lIjoiV"
129
        "EcyNDkyTEctODUiLCAibmFtZSI6InRlY2huaWNpYW4iLCAidGVjaCI6dHJ1ZSwgIm1vY2EiOj"
130
        "AsICJ3aWZpIjo1LCAiY29uVHlwZSI6IldBTiIsICJnd1dhbiI6ImYiLCAiRGVmUGFzc3dkQ2h"
131
        "hbmdlZCI6IllFUyIgfQ=="
132
    )
133

  
134
    # set session
135
    r = session.post(f"{url}/php/ajaxSet_Session.php")
136

  
137
def docsis_status(session):
138
    """get current DOCSIS status page, parse and return channel data"""
139
    r = session.get(f"{url}/php/status_docsis_data.php")
140
    # extract json from javascript
141
    json_downstream_data = re.search(r".*json_dsData = (.+);.*", r.text)[1]
142
    json_upstream_data = re.search(r".*json_usData = (.+);.*", r.text)[1]
143
    # parse json
144
    downstream_data = json.loads(json_downstream_data)
145
    upstream_data = json.loads(json_upstream_data)
146
    # convert lock status to numeric values
147
    for d in [ upstream_data, downstream_data ]:
148
        for c in d:
149
            if c['LockStatus'] == "ACTIVE" or c['LockStatus'] == "Locked":
150
                c['LockStatus'] = 1
151
            else:
152
                c['LockStatus'] = 0
153
    return downstream_data, upstream_data
154

  
155

  
156
# -----------------------------------------------------------------------------
157
if __name__ == "__main__":
158
    # get config
159
    url = os.getenv("url")
160
    username = os.getenv("username")
161
    password = os.getenv("password")
162
    # validate config
163
    if not url or not username or not password:
164
        print("Set url, username and password first.", file=sys.stderr)
165
        exit(1)
166
    # create session
167
    session = requests.Session()
168
    # login with username and password
169
    login(session, url, username, password)
170
    # get DOCSIS status
171
    downstream, upstream = docsis_status(session)
172
    # prepare munin graph info
173
    graph_descriptions = [
174
        {
175
            "name": "up_signal",
176
            "title": "DOCSIS Upstream signal strength",
177
            "vlabel": "dBmV",
178
            "info": "DOCSIS upstream signal strength by channel",
179
            "data": upstream,
180
            "key": "PowerLevel"
181
        },
182
        {
183
            "name": "up_lock",
184
            "title": "DOCSIS Upstream lock",
185
            "vlabel": "locked",
186
            "info": "DOCSIS upstream channel lock status",
187
            "data": upstream,
188
            "key": "LockStatus"
189
        },
190
        {
191
            "name": "down_signal",
192
            "title": "DOCSIS Downstream signal strength",
193
            "vlabel": "dBmV",
194
            "info": "DOCSIS downstream signal strength by channel",
195
            "data": downstream,
196
            "key": "PowerLevel"
197
        },
198
        {
199
            "name": "down_lock",
200
            "title": "DOCSIS Downstream lock",
201
            "vlabel": "locked",
202
            "info": "DOCSIS downstream channel lock status",
203
            "data": downstream,
204
            "key": "LockStatus"
205
        },
206
        {
207
            "name": "down_snr",
208
            "title": "DOCSIS Downstream signal/noise ratio",
209
            "vlabel": "dB",
210
            "info": "SNR/MER",
211
            "data": downstream,
212
            "key": "SNRLevel"
213
        }
214
    ]
215

  
216
    # configure ?
217
    if len(sys.argv) > 1 and "config" in sys.argv[1]:
218
        # process all graphs
219
        for g in graph_descriptions:
220
            # graph config
221
            print(
222
                f"multigraph docsis_{g['name']}\n"
223
                f"graph_title {g['title']}\n" \
224
                f"graph_category network\n" \
225
                f"graph_vlabel {g['vlabel']}\n" \
226
                f"graph_info {g['info']}\n" \
227
                f"graph_scale no\n"
228
            )
229

  
230
            # channels
231
            for c in g['data']:
232
                # only use channels with PowerLevel
233
                if not c['PowerLevel']:
234
                    continue
235
                print(
236
                    f"channel_{c['ChannelID']}.label {c['ChannelID']} ({c['Frequency']} MHz)\n"
237
                    f"channel_{c['ChannelID']}.info Channel type: {c['ChannelType']}, Modulation: {c['Modulation']}"
238
                )
239

  
240
    # output values ?
241
    else:
242
        # process all graphs
243
        for g in graph_descriptions:
244
            print(f"multigraph docsis_{g['name']}")
245
            # channels
246
            for c in g['data']:
247
                # only use channels with PowerLevel
248
                if not c['PowerLevel']:
249
                    continue
250
                print(f"channel_{c['ChannelID']}.value {c[g['key']]}")

Formats disponibles : Unified diff