source: ccsd/trunk/crcnetd/modules/ccs_monitor_web.py @ 1060

Last change on this file since 1060 was 1060, checked in by mglb1, 8 years ago

Fixed bugs with fetching the MOTD

  • Fetch the MOTD via an Ajax call
  • Add a configuration parameter to completely disable fetching the MOTD

Until we get a multi-threaded server fetching the MOTD from a server
that is unavailable has the potential to block the entire server for
up to 2 minutes, which is rather unpleasant.

Fixes #30

  • Property svn:keywords set to Id
File size: 14.1 KB
Line 
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
22import sys
23import time
24import urllib
25
26from crcnetd._utils.ccsd_common import *
27from crcnetd._utils.ccsd_log import *
28from crcnetd._utils.ccsd_clientserver import registerPage, registerDir, \
29        registerRealm
30from crcnetd._utils.ccsd_config import config_get, init_pref_store, pref_get, \
31        config_getboolean
32from crcnetd.version import ccsd_version, ccsd_revision
33
34class ccs_monitor_error(ccsd_error):
35    pass
36
37ccs_mod_type = CCSD_CLIENT
38
39DEFAULT_RESOURCE_DIR = "/usr/share/ccsd/resources"
40DEFAULT_MOTD_REFRESH = 60 * 60
41DEFAULT_MOTD_URL = "http://www.crc.net.nz/motd"
42DEFAULT_HELP_EMAIL = "help@crc.net.nz"
43DEFAULT_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"""
47DEFAULT_PREF_FILE = "/var/lib/ccsd/preferences"
48DEFAULT_ADMIN_PASS = "$1$pbz0c5l4$qPtttsQzkg3BHDQrKoKCK0" # 'admin'
49
50menu = {}
51MENU_TOP = "top"
52MENU_BOTTOM = "bottom"
53MENU_GROUP_HOME = "Ahome"
54MENU_GROUP_GENERAL = "general"
55MENU_GROUP_CONTACT = "zcontact"
56
57CPE_ADMIN_REALM = "admin"
58ADMIN_USERNAME = "admin"
59
60monitor_prefs = None
61realm = None
62
63##############################################################################
64# Module Helper Functions
65##############################################################################
66def 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   
76def 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
128def 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 />
135Please 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>&nbsp;</td>
149<td><input type="submit" value="Login &gt;&gt;"></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
160def 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 
183def 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   
194def 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
215def 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
260def 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
287def getMonitorPrefs():
288    global monitor_prefs
289    return monitor_prefs
290
291def getRealm():
292    global realm
293    return realm
294
295##############################################################################
296# Initialisation
297##############################################################################
298def 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")
336def 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&nbsp;
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("/")
402def 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")
430def 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
Note: See TracBrowser for help on using the repository browser.