Add more Nova API features for NG Instances

This patch adds more Nova API features for instances.  It establishes
several new proxy APIs and provides the various JavaScript libraries for
accessing those APIs as well.

Change-Id: I016be1d0598faf78b1fb02f4d0768efdaf6cb7bf
Partially-Implements: blueprint angularize-instances-table
This commit is contained in:
Matt Borland 2016-07-20 11:06:26 -06:00
parent f3f2ea1d62
commit 93b7edd694
5 changed files with 750 additions and 7 deletions

View File

@ -13,12 +13,16 @@
# limitations under the License.
"""API over the nova service.
"""
from collections import OrderedDict
from django.http import HttpResponse
from django.template.defaultfilters import slugify
from django.utils import http as utils_http
from django.utils.translation import ugettext_lazy as _
from django.views import generic
from horizon import exceptions as hz_exceptions
from novaclient import exceptions
from openstack_dashboard import api
@ -27,6 +31,24 @@ from openstack_dashboard.api.rest import urls
from openstack_dashboard.api.rest import utils as rest_utils
from openstack_dashboard.usage import quotas
import six
@urls.register
class Snapshots(generic.View):
"""API for nova snapshots.
"""
url_regex = r'nova/snapshots/$'
@rest_utils.ajax(data_required=True)
def post(self, request):
instance_id = request.DATA['instance_id']
name = request.DATA['name']
result = api.nova.snapshot_create(request,
instance_id=instance_id,
name=name)
return result
@urls.register
class Keypairs(generic.View):
@ -177,6 +199,146 @@ class Limits(generic.View):
return result
@urls.register
class ServerActions(generic.View):
"""API over all server actions.
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+)/actions/$'
@rest_utils.ajax()
def get(self, request, server_id):
"""Get a list of server actions.
The listing result is an object with property "items". Each item is
an action taken against the given server.
Example GET:
http://localhost/api/nova/servers/abcd/actions/
"""
actions = api.nova.instance_action_list(request, server_id)
return {'items': [s.to_dict() for s in actions]}
@urls.register
class SecurityGroups(generic.View):
"""API over all server security groups.
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+)/security-groups/$'
@rest_utils.ajax()
def get(self, request, server_id):
"""Get a list of server security groups.
The listing result is an object with property "items". Each item is
security group associated with this server.
Example GET:
http://localhost/api/nova/servers/abcd/security-groups/
"""
groups = api.network.server_security_groups(request, server_id)
return {'items': [s.to_dict() for s in groups]}
@urls.register
class Volumes(generic.View):
"""API over all server volumes.
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+)/volumes/$'
@rest_utils.ajax()
def get(self, request, server_id):
"""Get a list of server volumes.
The listing result is an object with property "items". Each item is
a volume.
Example GET:
http://localhost/api/nova/servers/abcd/volumes/
"""
volumes = api.nova.instance_volumes_list(request, server_id)
return {'items': [s.to_dict() for s in volumes]}
@urls.register
class RemoteConsoleInfo(generic.View):
"""API for remote console information.
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+)/console-info/$'
@rest_utils.ajax()
def post(self, request, server_id):
"""Gets information about an available remote console for the given
server.
Example POST:
http://localhost/api/nova/servers/abcd/console-info/
"""
console_type = request.DATA.get('console_type', 'AUTO')
CONSOLES = OrderedDict([('VNC', api.nova.server_vnc_console),
('SPICE', api.nova.server_spice_console),
('RDP', api.nova.server_rdp_console),
('SERIAL', api.nova.server_serial_console)])
"""Get a tuple of console url and console type."""
if console_type == 'AUTO':
check_consoles = CONSOLES
else:
try:
check_consoles = {console_type: CONSOLES[console_type]}
except KeyError:
msg = _('Console type "%s" not supported.') % console_type
raise hz_exceptions.NotAvailable(msg)
# Ugly workaround due novaclient API change from 2.17 to 2.18.
try:
httpnotimplemented = exceptions.HttpNotImplemented
except AttributeError:
httpnotimplemented = exceptions.HTTPNotImplemented
for con_type, api_call in six.iteritems(check_consoles):
try:
console = api_call(request, server_id)
# If not supported, don't log it to avoid lot of errors in case
# of AUTO.
except httpnotimplemented:
continue
except Exception:
continue
if con_type == 'SERIAL':
console_url = console.url
else:
console_url = "%s&%s(%s)" % (
console.url,
utils_http.urlencode({'title': _("Console")}),
server_id)
return {"type": con_type, "url": console_url}
raise hz_exceptions.NotAvailable(_('No available console found.'))
@urls.register
class ConsoleOutput(generic.View):
"""API for console output.
"""
url_regex = r'nova/servers/(?P<server_id>[^/]+)/console-output/$'
@rest_utils.ajax()
def post(self, request, server_id):
"""Get a list of lines of console output.
The listing result is an object with property "items". Each item is
a line of output from the server.
Example GET:
http://localhost/api/nova/servers/abcd/console-output/
"""
log_length = request.DATA.get('length', 100)
console_lines = api.nova.server_console_output(request, server_id,
tail_length=log_length)
return {"lines": [x for x in console_lines.split('\n')]}
@urls.register
class Servers(generic.View):
"""API over all servers.
@ -186,7 +348,7 @@ class Servers(generic.View):
_optional_create = [
'block_device_mapping', 'block_device_mapping_v2', 'nics', 'meta',
'availability_zone', 'instance_count', 'admin_pass', 'disk_config',
'config_drive', 'scheduler_hints'
'config_drive'
]
@rest_utils.ajax()
@ -267,6 +429,27 @@ class Server(generic.View):
"""
return api.nova.server_get(request, server_id).to_dict()
@rest_utils.ajax(data_required=True)
def post(self, request, server_id):
"""Perform a change to a server
"""
operation = request.DATA.get('operation', 'none')
operations = {
'stop': api.nova.server_stop,
'start': api.nova.server_start,
'pause': api.nova.server_pause,
'unpause': api.nova.server_unpause,
'suspend': api.nova.server_suspend,
'resume': api.nova.server_resume,
'hard_reboot': lambda r, s: api.nova.server_reboot(r, s, False),
'soft_reboot': lambda r, s: api.nova.server_reboot(r, s, True),
}
return operations[operation](request, server_id)
@rest_utils.ajax()
def delete(self, request, server_id):
api.nova.server_delete(request, server_id)
@urls.register
class ServerGroups(generic.View):

View File

@ -83,11 +83,13 @@
// The following retrieves the first argument of the first call to the
// error spy. This exposes the inner function that, when invoked,
// allows us to inspect the error call and the message given.
var innerFunc = promise.error.calls.argsFor(0)[0];
expect(innerFunc).toBeDefined();
spyOn(toastService, 'add');
innerFunc({status: 500});
expect(toastService.add).toHaveBeenCalledWith(config.messageType || 'error', config.error);
if (config.error) {
var innerFunc = promise.error.calls.argsFor(0)[0];
expect(innerFunc).toBeDefined();
spyOn(toastService, 'add');
innerFunc({status: 500});
expect(toastService.add).toHaveBeenCalledWith(config.messageType || 'error', config.error);
}
}
})();

View File

@ -39,6 +39,11 @@
function novaAPI(apiService, toastService, $window) {
var service = {
getActionList: getActionList,
getConsoleLog: getConsoleLog,
getConsoleInfo: getConsoleInfo,
getServerVolumes: getServerVolumes,
getServerSecurityGroups: getServerSecurityGroups,
getKeypairs: getKeypairs,
createKeypair: createKeypair,
getAvailabilityZones: getAvailabilityZones,
@ -47,6 +52,15 @@
getServer: getServer,
getServers: getServers,
getServerGroups: getServerGroups,
deleteServer: deleteServer,
pauseServer: pauseServer,
unpauseServer: unpauseServer,
suspendServer: suspendServer,
resumeServer: resumeServer,
softRebootServer: softRebootServer,
hardRebootServer: hardRebootServer,
startServer: startServer,
stopServer: stopServer,
getExtensions: getExtensions,
getFlavors: getFlavors,
getFlavor: getFlavor,
@ -65,7 +79,8 @@
getDefaultQuotaSets: getDefaultQuotaSets,
setDefaultQuotaSets: setDefaultQuotaSets,
getEditableQuotas: getEditableQuotas,
updateProjectQuota: updateProjectQuota
updateProjectQuota: updateProjectQuota,
createServerSnapshot: createServerSnapshot
};
return service;
@ -264,6 +279,146 @@
});
}
/*
* @name deleteServer
* @description
* Delete a single server by ID.
*
* @param {String} serverId
* Server to delete
* @returns {Object} The result of the API call
*/
function deleteServer(serverId, suppressError) {
var promise = apiService.delete('/api/nova/servers/' + serverId);
return suppressError ? promise : promise.error(function() {
var msg = gettext('Unable to delete the server with id: %(id)s');
toastService.add('error', interpolate(msg, { id: serverId }, true));
});
}
function serverStateOperation(operation, serverId, suppressError, errMsg) {
var instruction = {"operation": operation};
var promise = apiService.post('/api/nova/servers/' + serverId, instruction);
return suppressError ? promise : promise.error(function() {
toastService.add('error', interpolate(errMsg, { id: serverId }, true));
});
}
/**
* @name startServer
* @description
* Start a single server by ID.
*
* @param {String} serverId
* Server to start
* @returns {Object} The result of the API call
*/
function startServer(serverId, suppressError) {
return serverStateOperation('start', serverId, suppressError,
gettext('Unable to start the server with id: %(id)s'));
}
/**
* @name pauseServer
* @description
* Pause a single server by ID.
*
* @param {String} serverId
* Server to pause
* @returns {Object} The result of the API call
*/
function pauseServer(serverId, suppressError) {
return serverStateOperation('pause', serverId, suppressError,
gettext('Unable to pause the server with id: %(id)s'));
}
/**
* @name unpauseServer
* @description
* Un-Pause a single server by ID.
*
* @param {String} serverId
* Server to unpause
* @returns {Object} The result of the API call
*/
function unpauseServer(serverId, suppressError) {
return serverStateOperation('unpause', serverId, suppressError,
gettext('Unable to unpause the server with id: %(id)s'));
}
/**
* @name suspendServer
* @description
* Suspend a single server by ID.
*
* @param {String} serverId
* Server to suspend
* @returns {Object} The result of the API call
*/
function suspendServer(serverId, suppressError) {
return serverStateOperation('suspend', serverId, suppressError,
gettext('Unable to suspend the server with id: %(id)s'));
}
/**
* @name resumeServer
* @description
* Resumes a single server by ID.
*
* @param {String} serverId
* Server to resume
* @returns {Object} The result of the API call
*/
function resumeServer(serverId, suppressError) {
return serverStateOperation('resume', serverId, suppressError,
gettext('Unable to resume the server with id: %(id)s'));
}
/**
* @name softRebootServer
* @description
* Soft-reboots a single server by ID.
*
* @param {String} serverId
* Server to reboot
* @returns {Object} The result of the API call
*/
function softRebootServer(serverId, suppressError) {
return serverStateOperation('soft_reboot', serverId, suppressError,
gettext('Unable to soft-reboot the server with id: %(id)s'));
}
/**
* @name hardRebootServer
* @description
* Hard-reboots a single server by ID.
*
* @param {String} serverId
* Server to reboot
* @returns {Object} The result of the API call
*/
function hardRebootServer(serverId, suppressError) {
return serverStateOperation('hard_reboot', serverId, suppressError,
gettext('Unable to hard-reboot the server with id: %(id)s'));
}
/**
* @name stopServer
* @description
* Stop a single server by ID.
*
* @param {String} serverId
* Server to stop
* @returns {Object} The result of the API call
*/
function stopServer(serverId, suppressError) {
return serverStateOperation('stop', serverId, suppressError,
gettext('Unable to stop the server with id: %(id)s'));
}
/**
* @name getExtensions
* @param {Object} config - A configuration object
@ -636,5 +791,109 @@
return getCreateKeypairUrl(keyPairName) + "?regenerate=true";
}
/**
* @name createServerSnapshot
* @param {Object} newSnapshot - The new server snapshot
* @description
* Create a server snapshot using the parameters supplied in the
* newSnapshot. The required parameters:
*
* "name", "instance_id"
* All strings
*
* @returns {Object} The result of the API call
*/
function createServerSnapshot(newSnapshot) {
return apiService.post('/api/nova/snapshots/', newSnapshot)
.error(function () {
toastService.add('error', gettext('Unable to create the server snapshot.'));
});
}
/**
* @name getActionList
* @param {String} ID - The server ID
* @description
* Retrieves a list of actions performed on the server.
*
* @returns {Object} The result of the API call
*/
function getActionList(instanceId) {
return apiService.get('/api/nova/servers/' + instanceId + '/actions/')
.error(function () {
toastService.add('error', gettext('Unable to load the server actions.'));
});
}
/**
* @name getConsoleLog
* @param {String} instanceId - The server ID
* @param {Number} length - The number of lines to retrieve (optional)
* @description
* Retrieves a list of most recent console log lines from the server.
*
* @returns {Object} The result of the API call
*/
function getConsoleLog(instanceId, length) {
var config = {};
if (length) {
config.length = length;
}
return apiService.post('/api/nova/servers/' + instanceId + '/console-output/', config)
.error(function () {
toastService.add('error', gettext('Unable to load the server console log.'));
});
}
/**
* @name getConsoleInfo
* @param {String} instanceId - The server ID
* @param {String} type - The type of console to use (optional)
* @description
* Retrieves information used to get to a remote console for the given host.
*
* @returns {Object} The result of the API call
*/
function getConsoleInfo(instanceId, type) {
var config = {};
if (type) {
config.console_type = type;
}
return apiService.post('/api/nova/servers/' + instanceId + '/console-info/', config)
.error(function () {
toastService.add('error', gettext('Unable to load the server console info.'));
});
}
/**
* @name getServerVolumes
* @param {String} instanceId - The server ID
* @description
* Retrieves information about volumes associated with the server
*
* @returns {Object} The result of the API call
*/
function getServerVolumes(instanceId) {
return apiService.get('/api/nova/servers/' + instanceId + '/volumes/')
.error(function () {
toastService.add('error', gettext('Unable to load the server volumes.'));
});
}
/**
* @name getServerSecurityGroups
* @param {String} ID - The server ID
* @description
* Retrieves information about security groups associated with the server
*
* @returns {Object} The result of the API call
*/
function getServerSecurityGroups(instanceId) {
return apiService.get('/api/nova/servers/' + instanceId + '/security-groups/')
.error(function () {
toastService.add('error', gettext('Unable to load the server security groups.'));
});
}
}
}());

View File

@ -46,6 +46,63 @@
"path": "/api/nova/services/",
"error": "Unable to retrieve the nova services."
},
{
"func": "getConsoleLog",
"method": "post",
"path": "/api/nova/servers/123/console-output/",
"data": {
"length": 6
},
"error": "Unable to load the server console log.",
"testInput": [123, 6]
},
{
"func": "getActionList",
"method": "get",
"path": "/api/nova/servers/123/actions/",
"error": "Unable to load the server actions.",
"testInput": [123]
},
{
"func": "getConsoleLog",
"method": "post",
"path": "/api/nova/servers/123/console-output/",
"data": {},
"error": "Unable to load the server console log.",
"testInput": [123]
},
{
"func": "getConsoleInfo",
"method": "post",
"path": "/api/nova/servers/123/console-info/",
"data": {
"console_type": "VNC"
},
"error": "Unable to load the server console info.",
"testInput": [123, "VNC"]
},
{
"func": "getConsoleInfo",
"method": "post",
"path": "/api/nova/servers/123/console-info/",
"data": {},
"error": "Unable to load the server console info.",
"testInput": [123]
},
{
"func": "getServerVolumes",
"method": "get",
"path": "/api/nova/servers/123/volumes/",
"error": "Unable to load the server volumes.",
"testInput": [123]
},
{
"func": "getServerSecurityGroups",
"method": "get",
"path": "/api/nova/servers/123/security-groups/",
"error": "Unable to load the server security groups.",
"testInput": [123]
},
{
"func": "getKeypairs",
"method": "get",
@ -76,6 +133,98 @@
{}
]
},
{
"func": "deleteServer",
"method": "delete",
"path": "/api/nova/servers/12",
"error": "Unable to delete the server with id: 12",
"testInput": [12]
},
{
"func": "deleteServer",
"method": "delete",
"path": "/api/nova/servers/12",
"testInput": [12, true]
},
{
"func": "createServerSnapshot",
"method": "post",
"path": "/api/nova/snapshots/",
"data": {info: 12},
"error": "Unable to create the server snapshot.",
"testInput": [{info: 12}]
},
{
"func": "startServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'start' },
"error": "Unable to start the server with id: 12",
"testInput": [12]
},
{
"func": "stopServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'stop' },
"error": "Unable to stop the server with id: 12",
"testInput": [12]
},
{
"func": "stopServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'stop' },
"testInput": [12, true]
},
{
"func": "pauseServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'pause' },
"error": "Unable to pause the server with id: 12",
"testInput": [12]
},
{
"func": "unpauseServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'unpause' },
"error": "Unable to unpause the server with id: 12",
"testInput": [12]
},
{
"func": "suspendServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'suspend' },
"error": "Unable to suspend the server with id: 12",
"testInput": [12]
},
{
"func": "resumeServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'resume' },
"error": "Unable to resume the server with id: 12",
"testInput": [12]
},
{
"func": "softRebootServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'soft_reboot' },
"error": "Unable to soft-reboot the server with id: 12",
"testInput": [12]
},
{
"func": "hardRebootServer",
"method": "post",
"path": "/api/nova/servers/12",
"data": { operation: 'hard_reboot' },
"error": "Unable to hard-reboot the server with id: 12",
"testInput": [12]
},
{
"func": "getAvailabilityZones",
"method": "get",
@ -329,6 +478,12 @@
"error": "Unable to delete the flavor with id: 42",
"testInput": [42]
},
{
"func": "deleteFlavor",
"method": "delete",
"path": "/api/nova/flavors/42/",
"testInput": [42, true]
},
{
"func": "getDefaultQuotaSets",
"method": "get",

View File

@ -26,6 +26,150 @@ from novaclient import exceptions
class NovaRestTestCase(test.TestCase):
#
# Snapshots
#
@mock.patch.object(nova.api, 'nova')
def test_snapshots_create(self, nc):
body = '{"instance_id": "1234", "name": "foo"}'
request = self.mock_rest_request(body=body)
nc.snapshot_create.return_value = {'id': 'abcd', 'name': 'foo'}
response = nova.Snapshots().post(request)
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'id': 'abcd', 'name': 'foo'})
nc.snapshot_create.assert_called_once_with(request,
instance_id='1234',
name='foo')
#
# Server Actions
#
@mock.patch.object(nova.api, 'nova')
def test_serveractions_list(self, nc):
request = self.mock_rest_request()
nc.instance_action_list.return_value = [
mock.Mock(**{'to_dict.return_value': {'id': '1'}}),
mock.Mock(**{'to_dict.return_value': {'id': '2'}}),
]
response = nova.ServerActions().get(request, 'MegaMan')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'items': [{'id': '1'}, {'id': '2'}]})
nc.instance_action_list.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_start(self, nc):
request = self.mock_rest_request(body='{"operation": "start"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_start.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_stop(self, nc):
request = self.mock_rest_request(body='{"operation": "stop"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_stop.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_pause(self, nc):
request = self.mock_rest_request(body='{"operation": "pause"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_pause.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_unpause(self, nc):
request = self.mock_rest_request(body='{"operation": "unpause"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_unpause.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_suspend(self, nc):
request = self.mock_rest_request(body='{"operation": "suspend"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_suspend.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_resume(self, nc):
request = self.mock_rest_request(body='{"operation": "resume"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_resume.assert_called_once_with(request, 'MegaMan')
@mock.patch.object(nova.api, 'nova')
def test_server_hard_reboot(self, nc):
request = self.mock_rest_request(body='{"operation": "hard_reboot"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_reboot.assert_called_once_with(request, 'MegaMan', False)
@mock.patch.object(nova.api, 'nova')
def test_server_soft_reboot(self, nc):
request = self.mock_rest_request(body='{"operation": "soft_reboot"}')
response = nova.Server().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
nc.server_reboot.assert_called_once_with(request, 'MegaMan', True)
#
# Security Groups
#
@mock.patch.object(nova.api, 'network')
def test_securitygroups_list(self, nc):
request = self.mock_rest_request()
nc.server_security_groups.return_value = [
mock.Mock(**{'to_dict.return_value': {'id': '1'}}),
mock.Mock(**{'to_dict.return_value': {'id': '2'}}),
]
response = nova.SecurityGroups().get(request, 'MegaMan')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'items': [{'id': '1'}, {'id': '2'}]})
nc.server_security_groups.assert_called_once_with(request, 'MegaMan')
#
# Console Output
#
@mock.patch.object(nova.api, 'nova')
def test_console_output(self, nc):
request = self.mock_rest_request(body='{"length": 50}')
nc.server_console_output.return_value = "this\nis\ncool"
response = nova.ConsoleOutput().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'lines': ["this", "is", "cool"]})
nc.server_console_output.assert_called_once_with(request,
'MegaMan',
tail_length=50)
#
# Remote Console Info
#
@mock.patch.object(nova.api, 'nova')
def test_console_info(self, nc):
request = self.mock_rest_request(body='{"console_type": "SERIAL"}')
retval = mock.Mock(**{"url": "http://here.com"})
nc.server_serial_console.return_value = retval
response = nova.RemoteConsoleInfo().post(request, 'MegaMan')
self.assertStatusCode(response, 200)
self.assertEqual(response.json,
{"type": "SERIAL", "url": "http://here.com"})
nc.server_serial_console.assert_called_once_with(request, 'MegaMan')
#
# Volumes
#
@mock.patch.object(nova.api, 'nova')
def test_volumes_list(self, nc):
request = self.mock_rest_request()
nc.instance_volumes_list.return_value = [
mock.Mock(**{'to_dict.return_value': {'id': '1'}}),
mock.Mock(**{'to_dict.return_value': {'id': '2'}}),
]
response = nova.Volumes().get(request, 'MegaMan')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'items': [{'id': '1'}, {'id': '2'}]})
nc.instance_volumes_list.assert_called_once_with(request, 'MegaMan')
#
# Keypairs
#