Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clone instance configuration only #357

Open
wants to merge 16 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion app/api/controllers/container.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import Blueprint, request
from flask_jwt_extended import jwt_required
from app.api.schemas.container_schema import doValidate, doValidateCloneMove, doValidateImageExport
from app.api.schemas.container_schema import doValidate, doValidateCloneMove, doValidateImageExport, doValidateInstanceConfigurationClone

from app.api.models.LXCContainer import LXCContainer
from app.api.models.LXDModule import LXDModule
Expand Down Expand Up @@ -130,6 +130,21 @@ def cloneContainer(name):
except ValueError as e:
return response.replyFailed(message=e.__str__())

@container_api.route('/cloneConfiguration/<string:name>', methods=['POST'])
@jwt_required
def cloneInstanceConfiguration(name):
input = request.get_json(silent=True)
validation = doValidateInstanceConfigurationClone(input)
if validation:
return response.reply(message=validation.message, status=403)

input['name'] = name
try:
container = LXCContainer(input)
return response.replySuccess(container.cloneConfiguration(), message='Instance {} cloned successfully.'.format(name))
except ValueError as e:
return response.replyFailed(message=e.__str__())

@container_api.route('/move/<string:name>', methods=['POST'])
@jwt_required
def moveContainer(name):
Expand Down
44 changes: 43 additions & 1 deletion app/api/models/LXCContainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ def __init__(self, input):
if input.get('devices'):
self.setDevices(input.get('devices'))

if input.get('newInstance'):
self.setNewInstance(input.get('newInstance'))

def setImageType(self, input):
# Detect image type (alias or fingerprint)
logging.debug('Checking if image {} exists'.format(input))
Expand Down Expand Up @@ -150,6 +153,9 @@ def setConfig(self, input):
for key in input.keys():
self.data['config'][key] = input[key]

def setNewInstance(self, input):
self.data['newInstance'] = input

def info(self):
try:
logging.info('Reading container {} information'.format(self.data.get('name')))
Expand Down Expand Up @@ -275,6 +281,31 @@ def clone(self):
logging.exception(e)
raise ValueError(e)

def cloneConfiguration(self):
try:
logging.info('Cloning instance {} configuration'.format(self.data.get('name')))
instance = self.client.instances.get(self.data.get('name'))
newInstanceData = {}
image_fingerprint = instance.config['volatile.base_image']
newInstanceConfig = extractConfig(instance.config)

# To avoid calling the 'generate_migration_data' method from pylxd we manually set the data fields
# The 'generate_migration_data' method requires the original instance to be stopped which should not be necessary
# when cloning only the configuration options
newInstanceData['config'] = newInstanceConfig
newInstanceData['profiles'] = instance.profiles
newInstanceData['devices'] = instance.devices
newInstanceData['name'] = self.data.get('newInstance')
newInstanceData['source'] = {'type': 'image', 'fingerprint': image_fingerprint}

newInstance = self.client.instances.create(newInstanceData, wait=True)
newInstance.start(wait=True)
return self.client.api.instances[self.data.get('newInstance')].get().json()['metadata']
except Exception as e:
logging.error('Failed to clone instance {}'.format(self.data.get('name')))
logging.exception(e)
raise ValueError(e)

def move(self):
try:
logging.info('Moving container {}'.format(self.data.get('name')))
Expand Down Expand Up @@ -408,4 +439,15 @@ def removeProxy(self, name):
container.save()
return self.info()
except Exception as e:
raise ValueError(e)
raise ValueError(e)

def extractConfig(config):
config_options = ['boot.autostart', 'boot.autostart.delay', 'boot.autostart.priority', 'boot.host_shutdown_timeout', 'boot.stop.priority', 'environment.*', 'limits.cpu', 'limits.cpu.allowance', 'limits.cpu.priority', 'limits.disk.priority', 'limits.kernel.*', 'limits.memory', 'limits.memory.enforce', 'limits.memory.swap', 'limits.memory.swap.priority', 'limits.network.priority', 'limits.processes','linux.kernel_modules', 'migration.incremental.memory', 'migration.incremental.memory.goal', 'migration.incremental.memory.iterations', 'nvidia.runtime', 'raw.apparmor', 'raw.idmap', 'raw.lxc', 'raw.seccomp', 'security.devkxd', 'security.devlxd.images', 'security.idmap.base','security.idmap.isolated', 'security.idmap.size', 'security.nesting', 'security.privileged', 'security.secureboot', 'security.syscalls.blacklist', 'security.syscalls.blacklist_compat', 'security.syscalls.blacklist_default', 'security.syscalls.whitelist']

# remove all instance specific config options leaving only the configurable configuration options
newInstanceConfig = config
for option in list(newInstanceConfig):
if option not in config_options:
newInstanceConfig.pop(option, None)

return newInstanceConfig
1 change: 0 additions & 1 deletion app/api/models/LXDModule.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ def __init__(self, remoteHost='127.0.0.1'):
logging.info('using local socket')
self.client = Client()


def listContainers(self):
try:
logging.info('Reading container list')
Expand Down
27 changes: 26 additions & 1 deletion app/api/schemas/container_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@
}
}

instanceConfigurationSchema = {
"oneOf": [
{"$ref": "#/definitions/singleObject"}, # plain object
],
"definitions": {
"singleObject": {
'type':'object',
'required': ['newInstance'],
'properties':{
'newInstance':{
'type':'string',
'description':'newInstance (name)'
}
}
}
}
}

copyMoveSchema = {
"oneOf": [
{"$ref": "#/definitions/singleObject"}, # plain object
Expand Down Expand Up @@ -141,9 +159,16 @@ def doValidateCloneMove(input):
except ValidationError as e:
return e

def doValidateInstanceConfigurationClone(input):
try:
validate(input, instanceConfigurationSchema)
return None
except ValidationError as e:
return e

def doValidate(input):
try:
validate(input, schema)
return None
except ValidationError as e:
return e
return e
29 changes: 29 additions & 0 deletions app/ui/static/js/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ App.containers = App.containers || {
$('#buttonCloneContainer').on('click', $.proxy(this.cloneContainer, this));
$('#cloneForm').on('submit', $.proxy(this.cloneContainer, this));

$('#buttonCloneInstanceConfiguration').on('click', $.proxy(this.cloneInstanceConfiguration, this));
$('#cloneConfigurationForm').on('submit', $.proxy(this.cloneInstanceConfiguration, this));


$('#buttonMoveContainer').on('click', $.proxy(this.moveContainer, this));
$('#moveForm').on('submit', $.proxy(this.moveContainer, this));

Expand Down Expand Up @@ -279,6 +283,7 @@ App.containers = App.containers || {
},
switchView: function(view){
$('#cloneContainerForm').hide();
$('#cloneInstanceConfigurationForm').hide();
$('#moveContainerForm').hide();
$('#snapshotContainerForm').hide();
$('#exportContainerForm').hide();
Expand Down Expand Up @@ -358,6 +363,13 @@ App.containers = App.containers || {
$("#cloneContainerModal").modal("show");
this.selectedContainer = name;
},
showCloneInstanceConfiguration: function(name) {
$('#cloneInstanceConfigurationModal .modal-title').text('');
$('#newInstanceConfigurationClone').val('');
$('#cloneInstanceConfigurationModal .modal-title').text('Clone Instance Configuration: ' + name);
$("#cloneInstanceConfigurationModal").modal("show");
this.selectedContainer = name;
},
showMoveContainer: function(name) {
$('#moveContainerModal .modal-title').text('');
$('#moveContainerModal .modal-title').text('Move Container: ' + name);
Expand Down Expand Up @@ -421,6 +433,23 @@ App.containers = App.containers || {
$("#cloneContainerModal").modal("hide");
location.reload();
},
cloneInstanceConfiguration: function() {
$.ajax({
url:App.baseAPI+'container/cloneConfiguration/'+this.selectedContainer,
type: 'POST',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({
newInstance: $('#newInstanceConfigurationClone').val()
}),
success: $.proxy(this.onCloneConfigurationSuccess, this)
});
},
onCloneConfigurationSuccess: function(response){
console.log(response);
$("#cloneInstanceConfigurationModal").modal("hide");
location.reload();
},
moveContainer: function() {
$.ajax({
url:App.baseAPI+'container/move/'+this.selectedContainer,
Expand Down
39 changes: 39 additions & 0 deletions app/ui/templates/containers.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@
title="Clone Container">
<i class="glyphicon glyphicon-duplicate"></i>Clone</a>
</li>
<li>
<a id="cloneInstanceConfiguration"
onClick="$.proxy(App.containers.showCloneInstanceConfiguration('{{container.name}}'));"
title="Clone Instance Configuration">
<i class="glyphicon glyphicon-duplicate"></i>Clone Configuration</a>
</li>
<li>
<a id="moveContainer"
onClick="$.proxy(App.containers.showMoveContainer('{{container.name}}'));"
Expand Down Expand Up @@ -354,6 +360,39 @@ <h4 class="modal-title"></h4>
</div>
</div>

<div class="modal fade" id="cloneInstanceConfigurationModal" role="dialog" tabindex="-1">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title"></h4>
</div>
<div class="modal-body">
<form id="cloneConfigurationForm" class="form-horizontal">
<div class="form-group">
<label for="newInstanceConfigurationClone" class="col-sm-3 control-label">Clone Name</label>
<div class="col-sm-8">
<input id="newInstanceConfigurationClone" name="newInstance" class="form-control" type="text"
placeholder="Clone Name" title="Clone Name" required/>
</div>
</div>
<div class="form-group">
<div class="form-inline col-md-offset-3 col-md-3 mgtop-10">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="submit" id="buttonCloneInstanceConfiguration" class="btn btn-primary">Clone Instance Configuration <span
class="glyphicon glyphicon-refresh spinning loader"></span></button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>


<div class="modal fade" id="moveContainerModal" role="dialog" tabindex="-1">
<div class="modal-dialog">
<!-- Modal content-->
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ tornado-xstatic
XStatic==1.0.1
XStatic-term.js==0.0.7.0
bcrypt==3.2.0
pyyaml==5.4
pyyaml==3.11