Add a simple status webapp
Change-Id: I8f3cb28d9c5136b1da54637d5fec82baf2348a24
This commit is contained in:
parent
cc7e819672
commit
9aacffe24a
@ -21,6 +21,7 @@ import time
|
||||
|
||||
from nodepool import nodedb
|
||||
from nodepool import nodepool
|
||||
from nodepool import status
|
||||
from nodepool.cmd import NodepoolApp
|
||||
from nodepool.version import version_info as npc_version_info
|
||||
from config_validator import ConfigValidator
|
||||
@ -177,47 +178,13 @@ class NodePoolCmd(NodepoolApp):
|
||||
'%(message)s')
|
||||
|
||||
def list(self, node_id=None):
|
||||
t = PrettyTable(["ID", "Provider", "AZ", "Label", "Target", "Manager",
|
||||
"Hostname", "NodeName", "Server ID", "IP", "State",
|
||||
"Age", "Comment"])
|
||||
t.align = 'l'
|
||||
with self.pool.getDB().getSession() as session:
|
||||
for node in session.getNodes():
|
||||
if node_id and node.id != node_id:
|
||||
continue
|
||||
t.add_row([node.id, node.provider_name, node.az,
|
||||
node.label_name, node.target_name,
|
||||
node.manager_name, node.hostname,
|
||||
node.nodename, node.external_id, node.ip,
|
||||
nodedb.STATE_NAMES[node.state],
|
||||
NodePoolCmd._age(node.state_time),
|
||||
node.comment])
|
||||
print t
|
||||
print status.node_list(self.pool.getDB(), node_id)
|
||||
|
||||
def dib_image_list(self):
|
||||
t = PrettyTable(["ID", "Image", "Filename", "Version",
|
||||
"State", "Age"])
|
||||
t.align = 'l'
|
||||
with self.pool.getDB().getSession() as session:
|
||||
for image in session.getDibImages():
|
||||
t.add_row([image.id, image.image_name,
|
||||
image.filename, image.version,
|
||||
nodedb.STATE_NAMES[image.state],
|
||||
NodePoolCmd._age(image.state_time)])
|
||||
print t
|
||||
print status.dib_image_list(self.pool.getDB())
|
||||
|
||||
def image_list(self):
|
||||
t = PrettyTable(["ID", "Provider", "Image", "Hostname", "Version",
|
||||
"Image ID", "Server ID", "State", "Age"])
|
||||
t.align = 'l'
|
||||
with self.pool.getDB().getSession() as session:
|
||||
for image in session.getSnapshotImages():
|
||||
t.add_row([image.id, image.provider_name, image.image_name,
|
||||
image.hostname, image.version,
|
||||
image.external_id, image.server_external_id,
|
||||
nodedb.STATE_NAMES[image.state],
|
||||
NodePoolCmd._age(image.state_time)])
|
||||
print t
|
||||
print status.image_list(self.pool.getDB())
|
||||
|
||||
def image_update(self):
|
||||
threads = []
|
||||
|
@ -33,6 +33,7 @@ import threading
|
||||
import nodepool.builder
|
||||
import nodepool.cmd
|
||||
import nodepool.nodepool
|
||||
import nodepool.webapp
|
||||
|
||||
|
||||
def stack_dump_handler(signum, frame):
|
||||
@ -110,6 +111,7 @@ class NodePoolDaemon(nodepool.cmd.NodepoolApp):
|
||||
self.pool.stop()
|
||||
if self.args.builder:
|
||||
self.builder.stop()
|
||||
self.webapp.stop()
|
||||
sys.exit(0)
|
||||
|
||||
def term_handler(self, signum, frame):
|
||||
@ -124,6 +126,8 @@ class NodePoolDaemon(nodepool.cmd.NodepoolApp):
|
||||
self.args.config, self.args.build_workers,
|
||||
self.args.upload_workers)
|
||||
|
||||
self.webapp = nodepool.webapp.WebApp(self.pool)
|
||||
|
||||
signal.signal(signal.SIGINT, self.exit_handler)
|
||||
# For back compatibility:
|
||||
signal.signal(signal.SIGUSR1, self.exit_handler)
|
||||
@ -136,6 +140,8 @@ class NodePoolDaemon(nodepool.cmd.NodepoolApp):
|
||||
nb_thread = threading.Thread(target=self.builder.runForever)
|
||||
nb_thread.start()
|
||||
|
||||
self.webapp.start()
|
||||
|
||||
while True:
|
||||
signal.pause()
|
||||
|
||||
|
75
nodepool/status.py
Normal file
75
nodepool/status.py
Normal file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
import time
|
||||
|
||||
from nodepool import nodedb
|
||||
|
||||
from prettytable import PrettyTable
|
||||
|
||||
|
||||
def age(timestamp):
|
||||
now = time.time()
|
||||
dt = now - timestamp
|
||||
m, s = divmod(dt, 60)
|
||||
h, m = divmod(m, 60)
|
||||
d, h = divmod(h, 24)
|
||||
return '%02d:%02d:%02d:%02d' % (d, h, m, s)
|
||||
|
||||
|
||||
def node_list(db, node_id=None):
|
||||
t = PrettyTable(["ID", "Provider", "AZ", "Label", "Target",
|
||||
"Manager", "Hostname", "NodeName", "Server ID",
|
||||
"IP", "State", "Age", "Comment"])
|
||||
t.align = 'l'
|
||||
with db.getSession() as session:
|
||||
for node in session.getNodes():
|
||||
if node_id and node.id != node_id:
|
||||
continue
|
||||
t.add_row([node.id, node.provider_name, node.az,
|
||||
node.label_name, node.target_name,
|
||||
node.manager_name, node.hostname,
|
||||
node.nodename, node.external_id, node.ip,
|
||||
nodedb.STATE_NAMES[node.state],
|
||||
age(node.state_time), node.comment])
|
||||
return str(t)
|
||||
|
||||
|
||||
def dib_image_list(db):
|
||||
t = PrettyTable(["ID", "Image", "Filename", "Version",
|
||||
"State", "Age"])
|
||||
t.align = 'l'
|
||||
with db.getSession() as session:
|
||||
for image in session.getDibImages():
|
||||
t.add_row([image.id, image.image_name,
|
||||
image.filename, image.version,
|
||||
nodedb.STATE_NAMES[image.state],
|
||||
age(image.state_time)])
|
||||
return str(t)
|
||||
|
||||
|
||||
def image_list(db):
|
||||
t = PrettyTable(["ID", "Provider", "Image", "Hostname", "Version",
|
||||
"Image ID", "Server ID", "State", "Age"])
|
||||
t.align = 'l'
|
||||
with db.getSession() as session:
|
||||
for image in session.getSnapshotImages():
|
||||
t.add_row([image.id, image.provider_name, image.image_name,
|
||||
image.hostname, image.version,
|
||||
image.external_id, image.server_external_id,
|
||||
nodedb.STATE_NAMES[image.state],
|
||||
age(image.state_time)])
|
||||
return str(t)
|
@ -34,7 +34,7 @@ import kazoo.client
|
||||
import testresources
|
||||
import testtools
|
||||
|
||||
from nodepool import allocation, builder, fakeprovider, nodepool, nodedb
|
||||
from nodepool import allocation, builder, fakeprovider, nodepool, nodedb, webapp
|
||||
|
||||
TRUE_VALUES = ('true', '1', 'yes')
|
||||
|
||||
@ -338,6 +338,9 @@ class BaseTestCase(testtools.TestCase, testresources.ResourcedTestCase):
|
||||
if t.name.startswith("Thread-"):
|
||||
# apscheduler thread pool
|
||||
continue
|
||||
if t.name.startswith("worker "):
|
||||
# paste web server
|
||||
continue
|
||||
if t.name not in whitelist:
|
||||
done = False
|
||||
if done:
|
||||
@ -516,6 +519,11 @@ class DBTestCase(BaseTestCase):
|
||||
self.addCleanup(pool.stop)
|
||||
return pool
|
||||
|
||||
def useWebApp(self, *args, **kwargs):
|
||||
app = webapp.WebApp(*args, **kwargs)
|
||||
self.addCleanup(app.stop)
|
||||
return app
|
||||
|
||||
def _useBuilder(self, configfile):
|
||||
self.useFixture(BuilderFixture(configfile))
|
||||
|
||||
|
40
nodepool/tests/test_webapp.py
Normal file
40
nodepool/tests/test_webapp.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Copyright (C) 2014 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
import logging
|
||||
import urllib2
|
||||
|
||||
from nodepool import tests
|
||||
|
||||
|
||||
class TestWebApp(tests.DBTestCase):
|
||||
log = logging.getLogger("nodepool.TestWebApp")
|
||||
|
||||
def test_image_list(self):
|
||||
configfile = self.setup_config('node.yaml')
|
||||
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||
pool.start()
|
||||
webapp = self.useWebApp(pool, port=0)
|
||||
webapp.start()
|
||||
port = webapp.server.socket.getsockname()[1]
|
||||
|
||||
self.waitForImage(pool, 'fake-provider', 'fake-image')
|
||||
self.waitForNodes(pool)
|
||||
|
||||
req = urllib2.Request(
|
||||
"http://localhost:%s/image-list" % port)
|
||||
f = urllib2.urlopen(req)
|
||||
data = f.read()
|
||||
self.assertTrue('fake-image' in data)
|
100
nodepool/webapp.py
Normal file
100
nodepool/webapp.py
Normal file
@ -0,0 +1,100 @@
|
||||
# Copyright 2012 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from paste import httpserver
|
||||
import webob
|
||||
from webob import dec
|
||||
|
||||
import status
|
||||
|
||||
"""Nodepool main web app.
|
||||
|
||||
Nodepool supports HTTP requests directly against it for determining
|
||||
status. These responses are provided as preformatted text for now, but
|
||||
should be augmented or replaced with JSON data structures.
|
||||
"""
|
||||
|
||||
|
||||
class Cache(object):
|
||||
def __init__(self, expiry=1):
|
||||
self.cache = {}
|
||||
self.expiry = expiry
|
||||
|
||||
def get(self, key):
|
||||
now = time.time()
|
||||
if key in self.cache:
|
||||
lm, value = self.cache[key]
|
||||
if now > lm + self.expiry:
|
||||
del self.cache[key]
|
||||
return None
|
||||
return (lm, value)
|
||||
|
||||
def put(self, key, value):
|
||||
now = time.time()
|
||||
res = (now, value)
|
||||
self.cache[key] = res
|
||||
return res
|
||||
|
||||
|
||||
class WebApp(threading.Thread):
|
||||
log = logging.getLogger("nodepool.WebApp")
|
||||
|
||||
def __init__(self, nodepool, port=8001, cache_expiry=1):
|
||||
threading.Thread.__init__(self)
|
||||
self.nodepool = nodepool
|
||||
self.port = port
|
||||
self.cache = Cache(cache_expiry)
|
||||
self.cache_expiry = cache_expiry
|
||||
self.daemon = True
|
||||
self.server = httpserver.serve(dec.wsgify(self.app), host='0.0.0.0',
|
||||
port=self.port, start_loop=False)
|
||||
|
||||
def run(self):
|
||||
self.server.serve_forever()
|
||||
|
||||
def stop(self):
|
||||
self.server.server_close()
|
||||
|
||||
def get_cache(self, path):
|
||||
result = self.cache.get(path)
|
||||
if result:
|
||||
return result
|
||||
if path == '/image-list':
|
||||
table = status.image_list(self.nodepool.getDB())
|
||||
elif path == '/dib-image-list':
|
||||
table = status.dib_image_list(self.nodepool.getDB())
|
||||
else:
|
||||
return None
|
||||
return self.cache.put(path, table)
|
||||
|
||||
def app(self, request):
|
||||
result = self.get_cache(request.path)
|
||||
if result is None:
|
||||
raise webob.exc.HTTPNotFound()
|
||||
last_modified, table = result
|
||||
|
||||
response = webob.Response(body=table,
|
||||
content_type='text/plain')
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
|
||||
response.cache_control.public = True
|
||||
response.cache_control.max_age = self.cache_expiry
|
||||
response.last_modified = last_modified
|
||||
response.expires = last_modified + self.cache_expiry
|
||||
|
||||
return response.conditional_response_app
|
@ -19,3 +19,5 @@ shade>=1.8.0
|
||||
diskimage-builder
|
||||
voluptuous
|
||||
kazoo
|
||||
Paste
|
||||
WebOb>=1.2.3
|
||||
|
Loading…
Reference in New Issue
Block a user