diff options
Diffstat (limited to 'meta-facebook/meta-wedge/recipes-wedge/rest-api')
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" |