| 1 | # Copyright (C) 2006 The University of Waikato |
|---|
| 2 | # |
|---|
| 3 | # This file is part of crcnetd - CRCnet Configuration System Daemon |
|---|
| 4 | # |
|---|
| 5 | # CRCnet Monitor Base Website Functionality |
|---|
| 6 | # |
|---|
| 7 | # Author: Matt Brown <matt@crc.net.nz> |
|---|
| 8 | # Version: $Id$ |
|---|
| 9 | # |
|---|
| 10 | # crcnetd is free software; you can redistribute it and/or modify it under the |
|---|
| 11 | # terms of the GNU General Public License version 2 as published by the Free |
|---|
| 12 | # Software Foundation. |
|---|
| 13 | # |
|---|
| 14 | # crcnetd is distributed in the hope that it will be useful, but WITHOUT ANY |
|---|
| 15 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|---|
| 16 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
|---|
| 17 | # details. |
|---|
| 18 | # |
|---|
| 19 | # You should have received a copy of the GNU General Public License along with |
|---|
| 20 | # crcnetd; if not, write to the Free Software Foundation, Inc., |
|---|
| 21 | # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
|---|
| 22 | import sys |
|---|
| 23 | import time |
|---|
| 24 | import urllib |
|---|
| 25 | |
|---|
| 26 | from crcnetd._utils.ccsd_common import * |
|---|
| 27 | from crcnetd._utils.ccsd_log import * |
|---|
| 28 | from crcnetd._utils.ccsd_clientserver import registerPage, registerDir, \ |
|---|
| 29 | registerRealm |
|---|
| 30 | from crcnetd._utils.ccsd_config import config_get, init_pref_store, pref_get, \ |
|---|
| 31 | config_getboolean |
|---|
| 32 | from crcnetd.version import ccsd_version, ccsd_revision |
|---|
| 33 | |
|---|
| 34 | class ccs_monitor_error(ccsd_error): |
|---|
| 35 | pass |
|---|
| 36 | |
|---|
| 37 | ccs_mod_type = CCSD_CLIENT |
|---|
| 38 | |
|---|
| 39 | DEFAULT_RESOURCE_DIR = "/usr/share/ccsd/resources" |
|---|
| 40 | DEFAULT_MOTD_REFRESH = 60 * 60 |
|---|
| 41 | DEFAULT_MOTD_URL = "http://www.crc.net.nz/motd" |
|---|
| 42 | DEFAULT_HELP_EMAIL = "help@crc.net.nz" |
|---|
| 43 | DEFAULT_URGENT_DETAILS = """<b>Matt Brown</b><br /><a href="mailto:""" \ |
|---|
| 44 | """matt@crc.net.nz">matt@crc.net.nz</a><br />+64 21 611 544""" \ |
|---|
| 45 | """<br /><br /><b>Jamie Curtis</b><br /><a href="mailto:""" \ |
|---|
| 46 | """jamie@wand.net.nz">jamie@wand.net.nz</a><br />+64 21 392 102""" |
|---|
| 47 | DEFAULT_PREF_FILE = "/var/lib/ccsd/preferences" |
|---|
| 48 | DEFAULT_ADMIN_PASS = "$1$pbz0c5l4$qPtttsQzkg3BHDQrKoKCK0" # 'admin' |
|---|
| 49 | |
|---|
| 50 | menu = {} |
|---|
| 51 | MENU_TOP = "top" |
|---|
| 52 | MENU_BOTTOM = "bottom" |
|---|
| 53 | MENU_GROUP_HOME = "Ahome" |
|---|
| 54 | MENU_GROUP_GENERAL = "general" |
|---|
| 55 | MENU_GROUP_CONTACT = "zcontact" |
|---|
| 56 | |
|---|
| 57 | CPE_ADMIN_REALM = "admin" |
|---|
| 58 | ADMIN_USERNAME = "admin" |
|---|
| 59 | |
|---|
| 60 | monitor_prefs = None |
|---|
| 61 | realm = None |
|---|
| 62 | |
|---|
| 63 | ############################################################################## |
|---|
| 64 | # Module Helper Functions |
|---|
| 65 | ############################################################################## |
|---|
| 66 | def returnErrorPage(request, message): |
|---|
| 67 | """Displays an error message""" |
|---|
| 68 | |
|---|
| 69 | output = """<div class="content"> |
|---|
| 70 | <h2>An Error Occurred!</h2><br /> |
|---|
| 71 | <br /> |
|---|
| 72 | <span class="error">%s</span><br /><br /></div>""" % message |
|---|
| 73 | |
|---|
| 74 | returnPage(request, "Error", output) |
|---|
| 75 | |
|---|
| 76 | def returnPage(request, title, content, menuadd=None, scripts=[], styles=[]): |
|---|
| 77 | """Returns a basic HTML page using the default template. |
|---|
| 78 | |
|---|
| 79 | title - The desired page title |
|---|
| 80 | content - An HTML blob to populate the content area of the page |
|---|
| 81 | menuadd - An HTML blob to append to the end of the generated menus |
|---|
| 82 | |
|---|
| 83 | The template must allow substitution of the tokens %TITLE%, |
|---|
| 84 | %CONTENT%, %MENU%, %SCRIPTS%, %STYLES% in the appropriate places. |
|---|
| 85 | """ |
|---|
| 86 | |
|---|
| 87 | # Get the template |
|---|
| 88 | resourcedir = config_get("www", "resourcedir", DEFAULT_RESOURCE_DIR) |
|---|
| 89 | tfile = "%s/page.html" % resourcedir |
|---|
| 90 | try: |
|---|
| 91 | fd = open(tfile, "r") |
|---|
| 92 | template = fd.read() |
|---|
| 93 | fd.close() |
|---|
| 94 | except: |
|---|
| 95 | log_error("Page template not available!", sys.exc_info()) |
|---|
| 96 | # Return a very cruddy basic page |
|---|
| 97 | template = "<html><head><title>%TITLE%</title></head>" \ |
|---|
| 98 | "<body><h2>Menu</h2><br />%MENU%<br /><hr />" \ |
|---|
| 99 | "<h2>%TITLE%</h2><br />%CONTENT%</body></html>" |
|---|
| 100 | |
|---|
| 101 | # Substitute as necessary |
|---|
| 102 | menustr = buildMenu() |
|---|
| 103 | if menuadd is not None: |
|---|
| 104 | menustr += menuadd |
|---|
| 105 | scriptstr = stylestr = "" |
|---|
| 106 | for script in scripts: |
|---|
| 107 | scriptstr += """<script type="text/javascript" src="%s">""" \ |
|---|
| 108 | "</script>\n""" % script |
|---|
| 109 | for style in styles: |
|---|
| 110 | stylestr += """<link rel="stylesheet" type="text/css" src="%s""" \ |
|---|
| 111 | " />\n""" % style |
|---|
| 112 | footer = "Page generated at: %s" % time.ctime() |
|---|
| 113 | ip = getIfaceIPForIP(request.client_address[0]) |
|---|
| 114 | if ip == "": |
|---|
| 115 | ip = request.headers.get("Host").strip() |
|---|
| 116 | host = "http://%s" % ip |
|---|
| 117 | output = template.replace("%SCRIPTS%", scriptstr).replace("%STYLES%", \ |
|---|
| 118 | stylestr).replace("%TITLE%", title).replace("%MENU%", \ |
|---|
| 119 | menustr).replace("%CONTENT%", content).replace("%FOOTER%", \ |
|---|
| 120 | footer).replace("%HOST%", host) |
|---|
| 121 | |
|---|
| 122 | # Send it away |
|---|
| 123 | request.send_response(200) |
|---|
| 124 | request.end_headers() |
|---|
| 125 | request.wfile.write(output) |
|---|
| 126 | request.finish() |
|---|
| 127 | |
|---|
| 128 | def loginForm(request, method, username=""): |
|---|
| 129 | """Request that the user enter their username / password""" |
|---|
| 130 | global users |
|---|
| 131 | |
|---|
| 132 | # Display the form to get a new password |
|---|
| 133 | output = """<div class="content"><h2>Authentication Required</h2> |
|---|
| 134 | <br /> |
|---|
| 135 | Please enter your username and password below<br /> |
|---|
| 136 | <br /> |
|---|
| 137 | <form method="POST" action="%s"> |
|---|
| 138 | <table> |
|---|
| 139 | <tr> |
|---|
| 140 | <th width="20%%">Username</th> |
|---|
| 141 | <td><input type="input" value="%s" name="username"></td> |
|---|
| 142 | </tr> |
|---|
| 143 | <tr> |
|---|
| 144 | <th width="20%%">Password</th> |
|---|
| 145 | <td><input type="password" value="" name="password" id="password"></td> |
|---|
| 146 | </tr> |
|---|
| 147 | <tr> |
|---|
| 148 | <td> </td> |
|---|
| 149 | <td><input type="submit" value="Login >>"></td> |
|---|
| 150 | </tr> |
|---|
| 151 | </table> |
|---|
| 152 | """ % (request.path, username) |
|---|
| 153 | if username != "": |
|---|
| 154 | output += """<script type="text/javascript" language="javascript"> |
|---|
| 155 | $("password").focus(); |
|---|
| 156 | </script>""" |
|---|
| 157 | |
|---|
| 158 | returnPage(request, "Authentication Required", output) |
|---|
| 159 | |
|---|
| 160 | def buildMenu(): |
|---|
| 161 | """Builds the HTML to display a menu from the registered items""" |
|---|
| 162 | global menu |
|---|
| 163 | |
|---|
| 164 | output = "" |
|---|
| 165 | for tmenu,contents in menu.items(): |
|---|
| 166 | output += """<div class="menu">\n""" |
|---|
| 167 | title = contents["title"].replace("%HOSTNAME%", socket.gethostname()) |
|---|
| 168 | output += "<h2>%s</h2>\n" % title |
|---|
| 169 | groups = contents["groups"].keys() |
|---|
| 170 | groups.sort() |
|---|
| 171 | for group in groups: |
|---|
| 172 | items = contents["groups"][group] |
|---|
| 173 | output += "<ul>\n" |
|---|
| 174 | for url,caption in items.items(): |
|---|
| 175 | if not url.startswith("http://"): |
|---|
| 176 | url = "%%HOST%%%s" % url |
|---|
| 177 | output += """<li><a href="%s">%s</a></li>\n""" % (url, caption) |
|---|
| 178 | output += "</ul>\n" |
|---|
| 179 | output += "</div><br />\n" |
|---|
| 180 | |
|---|
| 181 | return output |
|---|
| 182 | |
|---|
| 183 | def registerMenu(menuName, title, reset=False): |
|---|
| 184 | """Creates a new top level menu""" |
|---|
| 185 | global menu |
|---|
| 186 | |
|---|
| 187 | if menuName in menu.keys(): |
|---|
| 188 | if not reset: |
|---|
| 189 | menu[menuName]["title"] = title |
|---|
| 190 | return |
|---|
| 191 | |
|---|
| 192 | menu[menuName] = {"title":title, "groups":{}} |
|---|
| 193 | |
|---|
| 194 | def registerMenuItem(menuName, groupName, url, caption): |
|---|
| 195 | """Register an item to be shown in the menu |
|---|
| 196 | |
|---|
| 197 | This can be called as a normal function or as a decorator. |
|---|
| 198 | """ |
|---|
| 199 | global menu |
|---|
| 200 | |
|---|
| 201 | if menuName not in menu.keys(): |
|---|
| 202 | registerMenu(menuName, menuName) |
|---|
| 203 | |
|---|
| 204 | tmenu = menu[menuName] |
|---|
| 205 | if groupName not in tmenu["groups"].keys(): |
|---|
| 206 | tmenu["groups"][groupName] = {} |
|---|
| 207 | |
|---|
| 208 | tmenu["groups"][groupName][url] = caption |
|---|
| 209 | |
|---|
| 210 | menu[menuName] = tmenu |
|---|
| 211 | |
|---|
| 212 | # Continue on, returning a no-op dcorator |
|---|
| 213 | return lambda f:f |
|---|
| 214 | |
|---|
| 215 | def HTable(data, columns, captions, tableid=""): |
|---|
| 216 | """Creates a horizontal table of the data |
|---|
| 217 | |
|---|
| 218 | Columns is a list where each entry specifies the index of an item |
|---|
| 219 | to display from the data dictionary. |
|---|
| 220 | Captions specifies a caption for each column |
|---|
| 221 | """ |
|---|
| 222 | |
|---|
| 223 | if tableid=="": |
|---|
| 224 | output = "<table>" |
|---|
| 225 | else: |
|---|
| 226 | output = """<table id="%s">""" % tableid |
|---|
| 227 | |
|---|
| 228 | # Output column headers |
|---|
| 229 | output += "<thead>" |
|---|
| 230 | for i in range(0,len(columns)): |
|---|
| 231 | output += "<th>%s</th>" % captions[i] |
|---|
| 232 | output += "</thead><tbody>" |
|---|
| 233 | |
|---|
| 234 | # Handle a dictionary or list of rows |
|---|
| 235 | if type(data) == type({}): |
|---|
| 236 | rows = data.values() |
|---|
| 237 | else: |
|---|
| 238 | rows = data |
|---|
| 239 | |
|---|
| 240 | # Output table body |
|---|
| 241 | for rdata in rows: |
|---|
| 242 | output += "<tr>" |
|---|
| 243 | for item in columns: |
|---|
| 244 | val = rdata[item] |
|---|
| 245 | if item=="status": |
|---|
| 246 | # Status column, do colouring |
|---|
| 247 | cclass=""" class="status%s" """ % rdata[item].lower() |
|---|
| 248 | if "statusdesc" in rdata: |
|---|
| 249 | val = rdata["statusdesc"] |
|---|
| 250 | else: |
|---|
| 251 | cclass="" |
|---|
| 252 | output += "<td%s>%s</td>" % (cclass, val) |
|---|
| 253 | output += "</tr>" |
|---|
| 254 | |
|---|
| 255 | # Finish table |
|---|
| 256 | output += "</tbody></table>" |
|---|
| 257 | |
|---|
| 258 | return output |
|---|
| 259 | |
|---|
| 260 | def generateSelect(name, value, options): |
|---|
| 261 | """Generates a select box |
|---|
| 262 | |
|---|
| 263 | options should be a dictionary of key:value for the options of the box |
|---|
| 264 | value should be the current value |
|---|
| 265 | name should be the ID the select box should take in the document |
|---|
| 266 | """ |
|---|
| 267 | |
|---|
| 268 | # Convert lists to dictionaries |
|---|
| 269 | if type(options) == type([]): |
|---|
| 270 | o2 = {} |
|---|
| 271 | for opt in options: |
|---|
| 272 | o2[opt] = opt |
|---|
| 273 | options = o2 |
|---|
| 274 | |
|---|
| 275 | output = """<select id="%s" name="%s" value="%s">\n""" % \ |
|---|
| 276 | (name, name, value) |
|---|
| 277 | keys = options.keys() |
|---|
| 278 | keys.sort() |
|---|
| 279 | for key in keys: |
|---|
| 280 | label = options[key] |
|---|
| 281 | sel = key == value and " selected" or "" |
|---|
| 282 | output += """<option value="%s"%s>%s</option>\n""" % (key, sel, label) |
|---|
| 283 | output += "</select>\n" |
|---|
| 284 | |
|---|
| 285 | return output |
|---|
| 286 | |
|---|
| 287 | def getMonitorPrefs(): |
|---|
| 288 | global monitor_prefs |
|---|
| 289 | return monitor_prefs |
|---|
| 290 | |
|---|
| 291 | def getRealm(): |
|---|
| 292 | global realm |
|---|
| 293 | return realm |
|---|
| 294 | |
|---|
| 295 | ############################################################################## |
|---|
| 296 | # Initialisation |
|---|
| 297 | ############################################################################## |
|---|
| 298 | def ccs_init(): |
|---|
| 299 | global monitor_prefs, realm |
|---|
| 300 | |
|---|
| 301 | registerMenu(MENU_TOP, "%HOSTNAME%") |
|---|
| 302 | registerMenu(MENU_BOTTOM, "External Links") |
|---|
| 303 | registerMenuItem(MENU_TOP, MENU_GROUP_HOME, "/", "CPE Homepage") |
|---|
| 304 | registerMenuItem(MENU_TOP, MENU_GROUP_CONTACT, "/contact", \ |
|---|
| 305 | "Contact Details") |
|---|
| 306 | |
|---|
| 307 | # Other modules can override these as necessary in ccs_init |
|---|
| 308 | registerMenuItem(MENU_BOTTOM, MENU_GROUP_GENERAL, \ |
|---|
| 309 | "http://www.crc.net.nz/", "CRCnet Homepage") |
|---|
| 310 | registerMenuItem(MENU_BOTTOM, MENU_GROUP_GENERAL, \ |
|---|
| 311 | "http://www.google.com/", "Google") |
|---|
| 312 | |
|---|
| 313 | resourcedir = config_get("www", "resourcedir", DEFAULT_RESOURCE_DIR) |
|---|
| 314 | registerDir("/resources", resourcedir) |
|---|
| 315 | |
|---|
| 316 | # Initialise preferences store |
|---|
| 317 | preffile = config_get("www", "preferences", DEFAULT_PREF_FILE) |
|---|
| 318 | try: |
|---|
| 319 | ensureFileExists(preffile) |
|---|
| 320 | monitor_prefs = init_pref_store(preffile) |
|---|
| 321 | except: |
|---|
| 322 | log_fatal("Unable to initialise preference store: %s" % preffile, \ |
|---|
| 323 | sys.exc_info()) |
|---|
| 324 | |
|---|
| 325 | realm = {"authenticator":loginForm, "users":{}, \ |
|---|
| 326 | "default_username":ADMIN_USERNAME} |
|---|
| 327 | adminpass = pref_get(None, "admin_password", monitor_prefs, \ |
|---|
| 328 | DEFAULT_ADMIN_PASS) |
|---|
| 329 | realm["users"][ADMIN_USERNAME] = adminpass |
|---|
| 330 | registerRealm(CPE_ADMIN_REALM, realm) |
|---|
| 331 | |
|---|
| 332 | ############################################################################## |
|---|
| 333 | # Content Pages |
|---|
| 334 | ############################################################################## |
|---|
| 335 | @registerPage("/motd") |
|---|
| 336 | def getMOTD(request, method): |
|---|
| 337 | """Returns HTML to display the Message of the Day""" |
|---|
| 338 | |
|---|
| 339 | tmpdir = config_get(None, "tmpdir", DEFAULT_TMPDIR) |
|---|
| 340 | motdFile = "%s/motd" % tmpdir |
|---|
| 341 | motdRefreshInterval = config_get("www", "motd_refresh", \ |
|---|
| 342 | DEFAULT_MOTD_REFRESH) |
|---|
| 343 | motdURL = config_get("www", "motdURL", DEFAULT_MOTD_URL) |
|---|
| 344 | fetchMotd = config_getboolean("www", "fetch_motd", False) |
|---|
| 345 | updateNote = "" |
|---|
| 346 | |
|---|
| 347 | try: |
|---|
| 348 | mtime = os.stat(motdFile)[8] |
|---|
| 349 | except: |
|---|
| 350 | mtime = -1 |
|---|
| 351 | |
|---|
| 352 | if (mtime == -1 or time.time()-mtime > motdRefreshInterval or \ |
|---|
| 353 | request.query.find("refreshMotd=true") != -1) and fetchMotd: |
|---|
| 354 | # Get new MOTD |
|---|
| 355 | try: |
|---|
| 356 | o = urllib.URLopener() |
|---|
| 357 | o.addheaders = [("User-agent", "crcnet-monitor/%s (r%s)" % \ |
|---|
| 358 | (ccsd_version, ccsd_revision))] |
|---|
| 359 | wfd = o.open(motdURL) |
|---|
| 360 | fd = open(motdFile, "w") |
|---|
| 361 | motd = wfd.read() |
|---|
| 362 | fd.write(motd) |
|---|
| 363 | fd.close |
|---|
| 364 | wfd.close() |
|---|
| 365 | mtime = time.time() |
|---|
| 366 | except: |
|---|
| 367 | log_error("Unable to fetch MOTD", sys.exc_info()) |
|---|
| 368 | motd = "Unable to retrieve latest news." |
|---|
| 369 | mtime = -1 |
|---|
| 370 | else: |
|---|
| 371 | try: |
|---|
| 372 | fd = open(motdFile, "r") |
|---|
| 373 | motd = fd.read() |
|---|
| 374 | fd.close() |
|---|
| 375 | except: |
|---|
| 376 | motd = "No news available" |
|---|
| 377 | |
|---|
| 378 | # Calculate how long till next update |
|---|
| 379 | if mtime != -1: |
|---|
| 380 | updateAtSecs = (mtime + motdRefreshInterval) - time.time() |
|---|
| 381 | updateAt = formatTime(updateAtSecs) |
|---|
| 382 | retrieved = time.ctime(mtime) |
|---|
| 383 | updateNote = "Retrieved at %s, next update in %s" % (retrieved, updateAt) |
|---|
| 384 | |
|---|
| 385 | # Generate the output |
|---|
| 386 | output = """<h2>Latest News <span class="note">%s |
|---|
| 387 | <a href="/?refreshMotd=true">[Refresh Now]</a> |
|---|
| 388 | </span> |
|---|
| 389 | </h2><br /> |
|---|
| 390 | %s |
|---|
| 391 | """ % (updateNote, motd.replace("\n", "<br />")) |
|---|
| 392 | |
|---|
| 393 | length = len(output) |
|---|
| 394 | request.send_response(200) |
|---|
| 395 | request.send_header("Length", length) |
|---|
| 396 | request.end_headers() |
|---|
| 397 | request.wfile.write(output) |
|---|
| 398 | request.finish() |
|---|
| 399 | return |
|---|
| 400 | |
|---|
| 401 | @registerPage("/") |
|---|
| 402 | def homepage(request, method): |
|---|
| 403 | |
|---|
| 404 | output = """<div class="content" id="motd">""" |
|---|
| 405 | |
|---|
| 406 | # MOTD at the top |
|---|
| 407 | output += "<h2>Loading Latest News...</h2><br />" |
|---|
| 408 | output += "Please wait while the latest news is retrieved." |
|---|
| 409 | output += "</div>" |
|---|
| 410 | |
|---|
| 411 | # Try and load the status summary from the status module |
|---|
| 412 | try: |
|---|
| 413 | from crcnetd.modules.ccs_monitor_status import getStatusSummary |
|---|
| 414 | status = getStatusSummary() |
|---|
| 415 | except: |
|---|
| 416 | log_warn("Could not retrieve status summary", sys.exc_info()) |
|---|
| 417 | status = "CPE Status not available" |
|---|
| 418 | |
|---|
| 419 | # CPE Status |
|---|
| 420 | output += """<div class="content"> |
|---|
| 421 | <h2>CPE Status</h2><br /> |
|---|
| 422 | %s |
|---|
| 423 | </div> |
|---|
| 424 | """ % status |
|---|
| 425 | |
|---|
| 426 | returnPage(request, "CPE Navigation", output, |
|---|
| 427 | scripts=["/resources/homepage.js"]) |
|---|
| 428 | |
|---|
| 429 | @registerPage("/contact") |
|---|
| 430 | def contactpage(request, method): |
|---|
| 431 | |
|---|
| 432 | output = "" |
|---|
| 433 | |
|---|
| 434 | # Get the template |
|---|
| 435 | resourcedir = config_get("www", "resourcedir", DEFAULT_RESOURCE_DIR) |
|---|
| 436 | tfile = "%s/contact.html" % resourcedir |
|---|
| 437 | try: |
|---|
| 438 | fd = open(tfile, "r") |
|---|
| 439 | details = fd.read() |
|---|
| 440 | fd.close() |
|---|
| 441 | except: |
|---|
| 442 | log_error("Contact HTML not available!", sys.exc_info()) |
|---|
| 443 | # Return a very cruddy basic page |
|---|
| 444 | details = "No contact details available!" |
|---|
| 445 | |
|---|
| 446 | output += """<div class="content">%s</div>""" % details |
|---|
| 447 | |
|---|
| 448 | returnPage(request, "Contact Details", output) |
|---|
| 449 | |
|---|