summaryrefslogtreecommitdiffstats
path: root/meta-facebook/meta-wedge/recipes-wedge/rest-api
diff options
context:
space:
mode:
Diffstat (limited to 'meta-facebook/meta-wedge/recipes-wedge/rest-api')
-rw-r--r--meta-facebook/meta-wedge/recipes-wedge/rest-api/files/bmc_command.py112
-rw-r--r--meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest.py155
-rw-r--r--meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_fruid.py10
-rw-r--r--meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_psu_update.py81
-rw-r--r--meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_sensors.py15
-rw-r--r--meta-facebook/meta-wedge/recipes-wedge/rest-api/files/setup-rest-api.sh53
-rw-r--r--meta-facebook/meta-wedge/recipes-wedge/rest-api/rest-api_0.1.bb4
7 files changed, 376 insertions, 54 deletions
diff --git a/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/bmc_command.py b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/bmc_command.py
new file mode 100644
index 0000000..d4dc877
--- /dev/null
+++ b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/bmc_command.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python
+#
+# Copyright 2014-present Facebook. All Rights Reserved.
+#
+# This program file is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program in a file named COPYING; if not, write to the
+# Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301 USA
+#
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+
+import os
+import subprocess
+import select
+import sys
+import time
+
+DEFAULT_TIMEOUT = 10 #sec
+
+# Note: Python 3.0 supports communicate() with a timeout option.
+# If we upgrade to this version we will no longer need timed_communicate
+
+class TimeoutError(Exception):
+ def __init__(self, output, error):
+ super(TimeoutError, self).__init__('process timed out')
+ self.output = output
+ self.error = error
+
+class WaitTimeoutError(Exception):
+ pass
+
+def kill_process(proc):
+ proc.terminate()
+ try:
+ timed_wait(proc, 0.1)
+ except WaitTimeoutError:
+ proc.kill()
+ try:
+ timed_wait(proc, 0.1)
+ except WaitTimeoutError:
+ # This can happen if the process is stuck waiting inside a system
+ # call for a long time. There isn't much we can do unless we want
+ # to keep waiting forever. Just give up. The child process will
+ # remain around as a zombie until we exit.
+ pass
+
+def timed_wait(proc, timeout):
+ # There unfortunately isn't a great way to wait for a process with a
+ # timeout, other than polling. (Registering for SIGCHLD and sleeping might
+ # be one option, but that's fragile and not thread-safe.)
+ poll_interval = 0.1
+ end_time = time.time() + timeout
+ while True:
+ if proc.poll() is not None:
+ return
+ time_left = max(end_time - time.time(), 0)
+ if time_left <= 0:
+ raise WaitTimeoutError()
+ time.sleep(min(time_left, poll_interval))
+
+def timed_communicate(proc, timeout=DEFAULT_TIMEOUT):
+ end_time = time.time() + timeout
+
+ p = select.poll()
+ outfd = proc.stdout.fileno()
+ errfd = proc.stderr.fileno()
+ p.register(outfd, select.POLLIN)
+ p.register(errfd, select.POLLIN)
+ results = {outfd: [], errfd: []}
+ remaining_fds = set([outfd, errfd])
+
+ bufsize = 4096
+ while remaining_fds:
+ time_left = max(end_time - time.time(), 0)
+ r = p.poll(time_left * 1000) # poll() takes timeout in milliseconds
+ if not r:
+ kill_process(proc)
+ raise TimeoutError(b''.join(results[outfd]),
+ b''.join(results[errfd]))
+ for fd, flags in r:
+ # We didn't put the fds in non-blocking mode, but we know the fd
+ # has data to read, so os.read() will return immediately.
+ d = os.read(fd, bufsize)
+ if d:
+ results[fd].append(d)
+ else:
+ # EOF on this fd, stop listening on it
+ p.unregister(fd)
+ remaining_fds.remove(fd)
+
+ try:
+ timed_wait(proc, max(end_time - time.time(), 0))
+ except WaitTimeoutError:
+ kill_process(proc)
+ raise TimeoutError(b''.join(results[outfd]),
+ b''.join(results[errfd]))
+
+ return b''.join(results[outfd]), b''.join(results[errfd])
diff --git a/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest.py b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest.py
index 52c98d9..af3e75d 100644
--- a/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest.py
+++ b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest.py
@@ -18,11 +18,13 @@
# Boston, MA 02110-1301 USA
#
-
from ctypes import *
-from bottle import route, run, template, request, response, ServerAdapter
-from bottle import abort
-from wsgiref.simple_server import make_server, WSGIRequestHandler, WSGIServer
+import bottle
+from cherrypy.wsgiserver import CherryPyWSGIServer
+from cherrypy.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter
+import datetime
+import logging
+import logging.config
import json
import ssl
import socket
@@ -34,13 +36,43 @@ import rest_bmc
import rest_gpios
import rest_modbus
import rest_slotid
+import rest_psu_update
CONSTANTS = {
'certificate': '/usr/lib/ssl/certs/rest_server.pem',
+ 'key': '/usr/lib/ssl/private/rest_server_key.pem',
+}
+
+LOGGER_CONF = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'formatters': {
+ 'default': {
+ 'format': '%(message)s'
+ },
+ },
+ 'handlers': {
+ 'file_handler': {
+ 'level': 'INFO',
+ 'formatter':'default',
+ 'class': 'logging.handlers.RotatingFileHandler',
+ 'filename':'/tmp/rest.log',
+ 'maxBytes': 1048576,
+ 'backupCount': 3,
+ 'encoding': 'utf8'
+ },
+ },
+ 'loggers': {
+ '': {
+ 'handlers': ['file_handler'],
+ 'level': 'DEBUG',
+ 'propagate': True,
+ },
+ }
}
# Handler for root resource endpoint
-@route('/api')
+@bottle.route('/api')
def rest_api():
result = {
"Information": {
@@ -53,7 +85,7 @@ def rest_api():
return result
# Handler for sys resource endpoint
-@route('/api/sys')
+@bottle.route('/api/sys')
def rest_sys():
result = {
"Information": {
@@ -67,7 +99,7 @@ def rest_sys():
return result
# Handler for sys/mb resource endpoint
-@route('/api/sys/mb')
+@bottle.route('/api/sys/mb')
def rest_sys():
result = {
"Information": {
@@ -80,73 +112,110 @@ def rest_sys():
return result
# Handler for sys/mb/fruid resource endpoint
-@route('/api/sys/mb/fruid')
+@bottle.route('/api/sys/mb/fruid')
def rest_fruid_hdl():
return rest_fruid.get_fruid()
# Handler for sys/bmc resource endpoint
-@route('/api/sys/bmc')
+@bottle.route('/api/sys/bmc')
def rest_bmc_hdl():
return rest_bmc.get_bmc()
# Handler for sys/server resource endpoint
-@route('/api/sys/server')
+@bottle.route('/api/sys/server')
def rest_server_hdl():
return rest_server.get_server()
# Handler for uServer resource endpoint
-@route('/api/sys/server', method='POST')
+@bottle.route('/api/sys/server', method='POST')
def rest_server_act_hdl():
data = json.load(request.body)
return rest_server.server_action(data)
# Handler for sensors resource endpoint
-@route('/api/sys/sensors')
+@bottle.route('/api/sys/sensors')
def rest_sensors_hdl():
return rest_sensors.get_sensors()
# Handler for sensors resource endpoint
-@route('/api/sys/gpios')
+@bottle.route('/api/sys/gpios')
def rest_gpios_hdl():
return rest_gpios.get_gpios()
-@route('/api/sys/modbus_registers')
+@bottle.route('/api/sys/modbus_registers')
def modbus_registers_hdl():
return rest_modbus.get_modbus_registers()
+@bottle.route('/api/sys/psu_update')
+def psu_update_hdl():
+ return rest_psu_update.get_jobs()
+
+@bottle.route('/api/sys/psu_update', method='POST')
+def psu_update_hdl():
+ data = json.load(request.body)
+ return rest_psu_update.begin_job(data)
+
# Handler for sensors resource endpoint
-@route('/api/sys/slotid')
+@bottle.route('/api/sys/slotid')
def rest_slotid_hdl():
return rest_slotid.get_slotid()
-run(host = "::", port = 8080)
-
# SSL Wrapper for Rest API
-class SSLWSGIRefServer(ServerAdapter):
+class SSLCherryPyServer(bottle.ServerAdapter):
def run(self, handler):
- if self.quiet:
- class QuietHandler(WSGIRequestHandler):
- def log_request(*args, **kw): pass
- self.options['handler_class'] = QuietHandler
-
- # IPv6 Support
- server_cls = self.options.get('server_class', WSGIServer)
-
- if ':' in self.host:
- if getattr(server_cls, 'address_family') == socket.AF_INET:
- class server_cls(server_cls):
- address_family = socket.AF_INET6
-
- srv = make_server(self.host, self.port, handler,
- server_class=server_cls, **self.options)
- srv.socket = ssl.wrap_socket (
- srv.socket,
- certfile=CONSTANTS['certificate'],
- server_side=True)
- srv.serve_forever()
-
-# Use SSL if the certificate exists. Otherwise, run without SSL.
-if os.access(CONSTANTS['certificate'], os.R_OK):
- run(server=SSLWSGIRefServer(host="::", port=8443))
+ server = CherryPyWSGIServer((self.host, self.port), handler)
+ server.ssl_adapter = pyOpenSSLAdapter(CONSTANTS['certificate'], CONSTANTS['key'])
+ try:
+ server.start()
+ finally:
+ server.stop()
+
+
+def log_after_request():
+ try:
+ length = bottle.response.content_length
+ except:
+ try:
+ length = len(bottle.response.body)
+ except:
+ length = 0
+
+ logging.info('{} - - [{}] "{} {} {}" {} {}'.format(
+ bottle.request.environ.get('REMOTE_ADDR'),
+ datetime.datetime.now().strftime('%d/%b/%Y %H:%M:%S'),
+ bottle.request.environ.get('REQUEST_METHOD'),
+ bottle.request.environ.get('REQUEST_URI'),
+ bottle.request.environ.get('SERVER_PROTOCOL'),
+ bottle.response.status_code,
+ length))
+
+
+# Error logging to log file
+class ErrorLogging(object):
+ def write(self, err):
+ logging.error(err)
+
+
+# Middleware to log the requests
+class LogMiddleware(object):
+ def __init__(self, app):
+ self.app = app
+
+ def __call__(self, e, h):
+ e['wsgi.errors'] = ErrorLogging()
+ ret_val = self.app(e, h)
+ log_after_request()
+ return ret_val
+
+# overwrite the stderr and stdout to log to the file
+bottle._stderr = logging.error
+bottle._stdout = logging.info
+logging.config.dictConfig(LOGGER_CONF)
+
+bottle_app = LogMiddleware(bottle.app())
+# Use SSL if the certificate and key exists. Otherwise, run without SSL.
+if (os.access(CONSTANTS['key'], os.R_OK) and
+ os.access(CONSTANTS['certificate'], os.R_OK)):
+ bottle.run(host = "::", port= 8443, server=SSLCherryPyServer, app=bottle_app)
else:
- run(host = "::", port = 8080)
+ bottle.run(host = "::", port = 8080, server='cherrypy', app=bottle_app)
diff --git a/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_fruid.py b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_fruid.py
index 167e1fa..3248e92 100644
--- a/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_fruid.py
+++ b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_fruid.py
@@ -29,9 +29,10 @@ class FRU(Structure):
("fbw_product_name", c_char * 13),
("fbw_product_number", c_char * 10),
("fbw_assembly_number", c_char * 15),
+ ("fbw_facebook_pcba_number", c_char * 15),
("fbw_facebook_pcb_number", c_char * 15),
- ("fbw_odm_pcb_number", c_char * 14),
- ("fbw_odm_pcb_serial", c_char * 13),
+ ("fbw_odm_pcba_number", c_char * 14),
+ ("fbw_odm_pcba_serial", c_char * 13),
("fbw_production_state", c_ubyte),
("fbw_product_version", c_ubyte),
("fbw_product_subversion", c_ubyte),
@@ -59,9 +60,10 @@ def get_fruid():
"Product Name": myfru.fbw_product_name,
"Product Part Number": myfru.fbw_product_number,
"System Assembly Part Number": myfru.fbw_assembly_number,
+ "Facebook PCBA Part Number": myfru.fbw_facebook_pcba_number,
"Facebook PCB Part Number": myfru.fbw_facebook_pcb_number,
- "ODM PCB Part Number": myfru.fbw_odm_pcb_number,
- "ODM PCB Serial Number": myfru.fbw_odm_pcb_serial,
+ "ODM PCBA Part Number": myfru.fbw_odm_pcba_number,
+ "ODM PCBA Serial Number": myfru.fbw_odm_pcba_serial,
"Product Production State": myfru.fbw_production_state,
"Product Version": myfru.fbw_product_version,
"Product Sub-Version": myfru.fbw_product_subversion,
diff --git a/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_psu_update.py b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_psu_update.py
new file mode 100644
index 0000000..d0e57c7
--- /dev/null
+++ b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_psu_update.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+#
+# Copyright 2014-present Facebook. All Rights Reserved.
+#
+# This program file is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program in a file named COPYING; if not, write to the
+# Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor,
+# Boston, MA 02110-1301 USA
+#
+
+import subprocess
+from subprocess import Popen
+import os
+import os.path
+import json
+import uuid
+import urllib2
+from tempfile import mkstemp
+from bottle import HTTPError
+
+UPDATE_JOB_DIR = '/var/rackmond/update_jobs'
+UPDATERS = {'delta': '/usr/local/bin/psu-update-delta.py'}
+
+def get_jobs():
+ jobs = []
+ if not os.path.exists(UPDATE_JOB_DIR):
+ os.makedirs(UPDATE_JOB_DIR)
+ for f in os.listdir(UPDATE_JOB_DIR):
+ fullpath = os.path.join(UPDATE_JOB_DIR, f)
+ if f.endswith('.json') and os.path.isfile(fullpath):
+ with open(fullpath, 'r') as fh:
+ jdata = json.load(fh)
+ jdata['job_id'] = os.path.splitext(f)[0]
+ jobs.append(jdata)
+ return {'jobs': jobs}
+
+updater_process = None
+def begin_job(jobdesc):
+ global updater_process
+ if updater_process is not None:
+ if updater_process.poll() is not None:
+ # Update complete
+ updater_process = None
+ else:
+ body = {'error': 'update_already_running',
+ 'pid': updater_process.pid }
+ raise HTTPError(409, body)
+ job_id = str(uuid.uuid1())
+ (fwfd, fwfilepath) = mkstemp()
+ if not os.path.exists(UPDATE_JOB_DIR):
+ os.makedirs(UPDATE_JOB_DIR)
+ statusfilepath = os.path.join(UPDATE_JOB_DIR, str(job_id) + '.json')
+ status = {'pid': 0,
+ 'state': 'fetching' }
+ with open(statusfilepath, 'wb') as sfh:
+ sfh.write(json.dumps(status))
+ fwdata = urllib2.urlopen(jobdesc['fw_url'])
+ with os.fdopen(fwfd, 'wb') as fwfile:
+ fwfile.write(fwdata.read())
+ fwfile.flush()
+ updater = UPDATERS[jobdesc.get('updater', 'delta')]
+ updater_process = Popen([updater,
+ '--addr', str(jobdesc['address']),
+ '--statusfile', statusfilepath,
+ '--rmfwfile',
+ fwfilepath])
+ status = {'pid': updater_process.pid,
+ 'state': 'starting' }
+ with open(statusfilepath, 'wb') as sfh:
+ sfh.write(json.dumps(status))
+ return {'job_id': job_id, 'pid': updater_process.pid}
diff --git a/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_sensors.py b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_sensors.py
index fa65372..382513b 100644
--- a/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_sensors.py
+++ b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/rest_sensors.py
@@ -18,15 +18,23 @@
# Boston, MA 02110-1301 USA
#
-
import json
-import os
import re
+import subprocess
+import bmc_command
# Handler for sensors resource endpoint
def get_sensors():
result = []
- data = os.popen('sensors').read()
+ proc = subprocess.Popen(['sensors'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ try:
+ data, err = bmc_command.timed_communicate(proc)
+ except bmc_command.TimeoutError as ex:
+ data = ex.output
+ err = ex.error
+
data = re.sub(r'\(.+?\)', '', data)
for edata in data.split('\n\n'):
adata = edata.split('\n', 1)
@@ -40,6 +48,7 @@ def get_sensors():
continue
sresult[tdata[0].strip()] = tdata[1].strip()
result.append(sresult)
+
fresult = {
"Information": result,
"Actions": [],
diff --git a/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/setup-rest-api.sh b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/setup-rest-api.sh
index bdd79b6..2b274dc 100644
--- a/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/setup-rest-api.sh
+++ b/meta-facebook/meta-wedge/recipes-wedge/rest-api/files/setup-rest-api.sh
@@ -27,6 +27,53 @@
# Short-Description: Set REST API handler
### END INIT INFO
-echo -n "Setup REST API handler... "
-/usr/local/bin/rest.py > /tmp/rest.log 2>&1 &
-echo "done."
+# source function library
+. /etc/init.d/functions
+
+ACTION="$1"
+CMD="/usr/local/bin/rest.py"
+case "$ACTION" in
+ start)
+ echo -n "Setting up REST API handler: "
+ pid=$(ps | grep -v grep | grep $CMD | awk '{print $1}')
+ if [ $pid ]; then
+ echo "already running"
+ else
+ $CMD > /tmp/rest_start.log 2>&1 &
+ echo "done."
+ fi
+ ;;
+ stop)
+ echo -n "Stopping REST API handler: "
+ pid=$(ps | grep -v grep | grep $CMD | awk '{print $1}')
+ if [ $pid ]; then
+ kill $pid
+ fi
+ echo "done."
+ ;;
+ restart)
+ echo -n "Restarting REST API handler: "
+ pid=$(ps | grep -v grep | grep $CMD | awk '{print $1}')
+ if [ $pid ]; then
+ kill $pid
+ fi
+ sleep 1
+ $CMD > /tmp/rest_start.log 2>&1 &
+ echo "done."
+ ;;
+ status)
+ if [[ -n $(ps | grep -v grep | grep $CMD | awk '{print $1}') ]]; then
+ echo "REST API handler is running"
+ else
+ echo "REST API is stopped"
+ fi
+ ;;
+ *)
+ N=${0##*/}
+ N=${N#[SK]??}
+ echo "Usage: $N {start|stop|status|restart}" >&2
+ exit 1
+ ;;
+esac
+
+exit 0
diff --git a/meta-facebook/meta-wedge/recipes-wedge/rest-api/rest-api_0.1.bb b/meta-facebook/meta-wedge/recipes-wedge/rest-api/rest-api_0.1.bb
index 5dec4bf..2ed3f7f 100644
--- a/meta-facebook/meta-wedge/recipes-wedge/rest-api/rest-api_0.1.bb
+++ b/meta-facebook/meta-wedge/recipes-wedge/rest-api/rest-api_0.1.bb
@@ -33,11 +33,13 @@ SRC_URI = "file://setup-rest-api.sh \
file://rest_sensors.py \
file://rest_modbus.py \
file://rest_slotid.py \
+ file://rest_psu_update.py \
+ file://bmc_command.py \
"
S = "${WORKDIR}"
-binfiles = "rest.py rest_bmc.py rest_fruid.py rest_gpios.py rest_server.py rest_sensors.py rest_modbus.py rest_slotid.py setup-rest-api.sh"
+binfiles = "rest.py rest_bmc.py rest_fruid.py rest_gpios.py rest_server.py rest_sensors.py bmc_command.py rest_modbus.py rest_slotid.py rest_psu_update.py setup-rest-api.sh"
pkgdir = "rest-api"
OpenPOWER on IntegriCloud