"""
This module exposes device configuration.
Endpoints for these functions can be found under /api/v1/config.
"""
import json
from klein import Klein
from twisted.internet import reactor
from twisted.internet.defer import DeferredList, inlineCallbacks, returnValue
from paradrop.base import constants, nexus, settings
from paradrop.base.output import out
from paradrop.base.pdutils import timeint
from paradrop.core.config import hostconfig
from paradrop.core.agent.http import PDServerRequest
from paradrop.core.agent.provisioning import read_provisioning_result
from paradrop.core.agent.reporting import sendNodeIdentity, sendStateReport
from paradrop.core.agent.wamp_session import WampSession
from paradrop.confd import client as pdconf_client
from paradrop.lib.misc import ssh_keys
from paradrop.lib.misc.governor import GovernorClient
from . import cors
[docs]class ConfigApi(object):
"""
Configuration API.
This class handles HTTP API calls related to router configuration.
"""
routes = Klein()
def __init__(self, update_manager, update_fetcher):
self.update_manager = update_manager
self.update_fetcher = update_fetcher
[docs] @routes.route('/hostconfig', methods=['PUT'])
def update_hostconfig(self, request):
"""
Replace the device's host configuration.
**Example request**:
.. sourcecode:: http
PUT /api/v1/config/hostconfig
Content-Type: application/json
{
"firewall": {
"defaults": {
"forward": "ACCEPT",
"input": "ACCEPT",
"output": "ACCEPT"
}
},
...
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
change_id: 1
}
For a complete example, please see the Host Configuration section.
"""
cors.config_cors(request)
body = json.loads(request.content.read().decode('utf-8'))
config = body['config']
update = dict(updateClass='ROUTER',
updateType='sethostconfig',
name=constants.RESERVED_CHUTE_NAME,
tok=timeint(),
hostconfig=config)
# We will return the change ID to the caller for tracking and log
# retrieval.
update['change_id'] = self.update_manager.assign_change_id()
self.update_manager.add_update(**update)
result = {
'change_id': update['change_id']
}
request.setHeader('Content-Type', 'application/json')
return json.dumps(result)
[docs] @routes.route('/hostconfig', methods=['GET'])
def get_hostconfig(self, request):
"""
Get the device's current host configuration.
**Example request**:
.. sourcecode:: http
GET /api/v1/config/hostconfig
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"firewall": {
"defaults": {
"forward": "ACCEPT",
"input": "ACCEPT",
"output": "ACCEPT"
}
},
...
}
For a complete example, please see the Host Configuration section.
"""
cors.config_cors(request)
config = hostconfig.prepareHostConfig()
request.setHeader('Content-Type', 'application/json')
return json.dumps(config, separators=(',',':'))
[docs] @routes.route('/new-config', methods=['GET'])
def new_config(self, request):
"""
Generate a new node configuration based on the hardware.
**Example request**:
.. sourcecode:: http
GET /api/v1/config/new_config
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
"firewall": {
"defaults": {
"forward": "ACCEPT",
"input": "ACCEPT",
"output": "ACCEPT"
}
},
...
}
For a complete example, please see the Host Configuration section.
"""
cors.config_cors(request)
config = hostconfig.prepareHostConfig(hostConfigPath='/dev/null')
request.setHeader('Content-Type', 'application/json')
return json.dumps(config, separators=(',',':'))
[docs] @routes.route('/pdid', methods=['GET'])
def get_pdid(self, request):
"""
Get the device's current ParaDrop ID. This is the identifier assigned
by the cloud controller.
**Example request**:
.. sourcecode:: http
GET /api/v1/config/pdid
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Content-Type: application/json
{
pdid: "5890e1e5ab7e317e6c6e049f"
}
"""
cors.config_cors(request)
pdid = nexus.core.info.pdid
if pdid is None:
pdid = ""
request.setHeader('Content-Type', 'application/json')
return json.dumps({'pdid': pdid})
[docs] @routes.route('/provision', methods=['POST'])
def provision(self, request):
"""
Provision the device with credentials from a cloud controller.
"""
cors.config_cors(request)
body = json.loads(request.content.read().decode('utf-8'))
routerId = body['routerId']
apitoken = body['apitoken']
pdserver = body['pdserver']
wampRouter = body['wampRouter']
changed = False
if routerId != nexus.core.info.pdid \
or pdserver != nexus.core.info.pdserver \
or wampRouter != nexus.core.info.wampRouter:
if pdserver and wampRouter:
nexus.core.provision(routerId, pdserver, wampRouter)
else:
nexus.core.provision(routerId)
changed = True
if apitoken != nexus.core.getKey('apitoken'):
nexus.core.saveKey(apitoken, 'apitoken')
changed = True
if changed:
PDServerRequest.resetToken()
nexus.core.jwt_valid = False
def set_update_fetcher(session):
session.set_update_fetcher(self.update_fetcher)
@inlineCallbacks
def start_polling(result):
yield self.update_fetcher.start_polling()
def send_response(result):
response = dict()
response['provisioned'] = True
response['httpConnected'] = nexus.core.jwt_valid
response['wampConnected'] = nexus.core.wamp_connected
request.setHeader('Content-Type', 'application/json')
return json.dumps(response)
wampDeferred = nexus.core.connect(WampSession)
wampDeferred.addCallback(set_update_fetcher)
httpDeferred = sendStateReport()
httpDeferred.addCallback(start_polling)
identDeferred = sendNodeIdentity()
dl = DeferredList([wampDeferred, httpDeferred, identDeferred],
consumeErrors=True)
dl.addBoth(send_response)
reactor.callLater(6, dl.cancel)
return dl
else:
return json.dumps({'success': False,
'message': 'No change on the provision parameters'})
[docs] @routes.route('/provision', methods=['GET'])
def get_provision(self, request):
"""
Get the provision status of the device.
"""
cors.config_cors(request)
request.setHeader('Content-Type', 'application/json')
result = read_provisioning_result()
result['routerId'] = nexus.core.info.pdid
result['pdserver'] = nexus.core.info.pdserver
result['wampRouter'] = nexus.core.info.wampRouter
apitoken = nexus.core.getKey('apitoken')
result['provisioned'] = (result['routerId'] is not None and \
apitoken is not None)
result['httpConnected'] = nexus.core.jwt_valid
result['wampConnected'] = nexus.core.wamp_connected
return json.dumps(result)
[docs] @routes.route('/settings', methods=['GET'])
def get_settings(self, request):
"""
Get current values of system settings.
These are the values from paradrop.base.settings. Settings are loaded
at system initialization from the settings.ini file and environment
variables. They are intended to be read-only after initialization.
This endpoint returns the settings as a dictionary with lowercase field
names.
Example:
{
"portal_server_port": 8080,
...
}
"""
cors.config_cors(request)
request.setHeader('Content-Type', 'application/json')
result = {}
for name, value in settings.iterate_module_attributes(settings):
result[name.lower()] = value
return json.dumps(result)
[docs] @routes.route('/startUpdate', methods=['POST'])
def start_update(self, request):
cors.config_cors(request)
self.update_manager.startUpdate()
request.setHeader('Content-Type', 'application/json')
return json.dumps({'success': True})
[docs] @routes.route('/factoryReset', methods=['POST'])
@inlineCallbacks
def factory_reset(self, request):
"""
Initiate the factory reset process.
"""
cors.config_cors(request)
update = dict(updateClass='ROUTER',
updateType='factoryreset',
name='PARADROP',
tok=timeint())
update = yield self.update_manager.add_update(**update)
returnValue(json.dumps(update.result))
[docs] @routes.route('/pdconf', methods=['GET'])
def pdconf(self, request):
"""
Get configuration sections from pdconf.
This returns a list of configuration sections and whether they were
successfully applied. This is intended for debugging purposes.
"""
cors.config_cors(request)
request.setHeader('Content-Type', 'application/json')
return pdconf_client.systemStatus()
[docs] @routes.route('/pdconf', methods=['PUT'])
def pdconf_reload(self, request):
"""
Trigger pdconf to reload UCI configuration files.
Trigger pdconf to reload UCI configuration files and return the status.
This function is intended for low-level debugging of the paradrop
pdconf module.
"""
cors.config_cors(request)
request.setHeader('Content-Type', 'application/json')
return pdconf_client.reloadAll()
[docs] @routes.route('/sshKeys/<user>', methods=['GET', 'POST'])
def sshKeys(self, request, user):
"""
Manage list of authorized keys for SSH access.
"""
cors.config_cors(request)
request.setHeader('Content-Type', 'application/json')
if request.method == "GET":
try:
if GovernorClient.isAvailable():
client = GovernorClient()
keys = client.listAuthorizedKeys(user)
# Move keys to the root because the API consumer is
# expecting a list, not an object.
if 'keys' in keys:
keys = keys['keys']
else:
keys = ssh_keys.getAuthorizedKeys(user)
return json.dumps(keys)
except Exception as e:
out.warn(str(e))
request.setResponseCode(404)
return json.dumps({'message': str(e)})
else:
body = json.loads(request.content.read().decode('utf-8'))
key = body['key'].strip()
try:
if GovernorClient.isAvailable():
client = GovernorClient()
client.addAuthorizedKey(key, user)
else:
ssh_keys.addAuthorizedKey(key, user)
return json.dumps(body)
except Exception as e:
out.warn(str(e))
request.setResponseCode(404)
return json.dumps({'message': str(e)})