Cancel
diff --git a/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/edit_ports.jst b/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/edit_ports.jst
new file mode 100644
index 000000000..177b7266e
--- /dev/null
+++ b/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/edit_ports.jst
@@ -0,0 +1 @@
+
diff --git a/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/edit_ports_form.jst b/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/edit_ports_form.jst
new file mode 100644
index 000000000..700acb7b2
--- /dev/null
+++ b/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/edit_ports_form.jst
@@ -0,0 +1,66 @@
+
+
+
+
+ You can choose to unpublish any port currently exported by this rock-on. Be aware,
+ however, that this is likely to interfere with its proper function, so change these settings
+ only if you know what it implies.
+
+
+
+
Rocknets (optional)
+
Select one or more rocknet(s) to join for each container of this rock-on:
+
+
+
diff --git a/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/settings_summary_table.jst b/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/settings_summary_table.jst
index 41005bfaa..08c863831 100644
--- a/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/settings_summary_table.jst
+++ b/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/settings_summary_table.jst
@@ -19,9 +19,21 @@
Port
{{this.hostp}}
- {{this.containerp}}
+ {{this.containerp}}{{isPublished this.id this.publish}}
+
{{/each}}
+{{#if new_cnets}}
+ {{display_newCnets}}
+{{else}}
+{{#each rocknets}}
+
+ Network
+ {{this.container_name}}
+ {{this.docker_name}}
+
+{{/each}}
+{{/if}}
{{#each cc}}
Custom
diff --git a/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/wizard_summary.jst b/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/wizard_summary.jst
index 179eb018a..3eee69dc2 100644
--- a/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/wizard_summary.jst
+++ b/src/rockstor/storageadmin/static/storageadmin/js/templates/rockons/wizard_summary.jst
@@ -29,6 +29,7 @@
Next
Label
+ Networking
Back
diff --git a/src/rockstor/storageadmin/static/storageadmin/js/templates/share/shares_table.jst b/src/rockstor/storageadmin/static/storageadmin/js/templates/share/shares_table.jst
index e6f08a5c5..deb6d3e06 100644
--- a/src/rockstor/storageadmin/static/storageadmin/js/templates/share/shares_table.jst
+++ b/src/rockstor/storageadmin/static/storageadmin/js/templates/share/shares_table.jst
@@ -76,7 +76,7 @@
-
Deleting will destroy all of it's data ( ). Are you sure?
+
Deleting will destroy all of its data ( ). Are you sure?
Force Delete
diff --git a/src/rockstor/storageadmin/static/storageadmin/js/views/dashboard/network_utilization.js b/src/rockstor/storageadmin/static/storageadmin/js/views/dashboard/network_utilization.js
index faaf99fea..49abdefb7 100644
--- a/src/rockstor/storageadmin/static/storageadmin/js/views/dashboard/network_utilization.js
+++ b/src/rockstor/storageadmin/static/storageadmin/js/views/dashboard/network_utilization.js
@@ -168,7 +168,7 @@ NetworkUtilizationWidget = RockStorWidgetView.extend({
this.networkInterfaces.each(function(ni, i) {
var opt = $(' ');
opt.val(ni.get('name'));
- opt.text(ni.get('name'));
+ opt.text(ni.get('dev_name'));
if (i == 0) {
opt.attr({
selected: 'selected'
diff --git a/src/rockstor/storageadmin/static/storageadmin/js/views/edit_network_connection.js b/src/rockstor/storageadmin/static/storageadmin/js/views/edit_network_connection.js
index 81192e60b..0ac3e9dc7 100644
--- a/src/rockstor/storageadmin/static/storageadmin/js/views/edit_network_connection.js
+++ b/src/rockstor/storageadmin/static/storageadmin/js/views/edit_network_connection.js
@@ -64,7 +64,7 @@ NetworkConnectionView = RockstorLayoutView.extend({
$(this.el).append(this.template({
connection: connection,
devices: this.devices.toJSON(),
- ctypes: ['ethernet', 'team', 'bond'],
+ ctypes: ['ethernet', 'team', 'bond', 'docker'],
teamProfiles: ['broadcast', 'roundrobin', 'activebackup', 'loadbalance', 'lacp'],
bondProfiles: ['balance-rr', 'active-backup', 'balance-xor', 'broadcast',
'802.3ad', 'balance-tlb', 'balance-alb'
@@ -73,13 +73,28 @@ NetworkConnectionView = RockstorLayoutView.extend({
if (this.connection) {
this.renderCTypeOptionalFields();
+ this.renderMethodOptionalFields();
}
+ $.validator.addMethod('isAlphanumeric', function(value, element) {
+ var regExp = new RegExp(/^[A-Za-z0-9]+$/);
+ return this.optional(element) || regExp.test(value);
+ }, 'The connection name can only contain alphanumeric characters.')
+
+ $.validator.addMethod('isNotExcluded', function(value, element) {
+ var excluded = ['host', 'bridge', 'null'];
+ return this.optional(element) || excluded.indexOf(value) === -1;
+ }, 'This connection name is reserved for system use.')
+
this.validator = this.$('#new-connection-form').validate({
onfocusout: false,
onkeyup: false,
rules: {
- name: 'required',
+ name: {
+ required: true,
+ isAlphanumeric: true,
+ isNotExcluded: true
+ },
ipaddr: {
required: {
depends: function(element) {
@@ -120,6 +135,9 @@ NetworkConnectionView = RockstorLayoutView.extend({
disableButton(cancelButton);
var data = _this.$('#new-connection-form').getJSON();
var conn = _this.connection;
+ if (conn) {
+ var data = $.extend(data, {'docker_name':conn.get('docker_name')});
+ }
if (!_this.connection) {
conn = new NetworkConnection();
}
@@ -184,15 +202,63 @@ NetworkConnectionView = RockstorLayoutView.extend({
placement: 'right',
title: 'Enter a value in [1500-9000] range. Defaults to 1500.'
});
+ this.$('#aux_address').tooltip({
+ html: true,
+ placement: 'right',
+ title: 'Comma-separated list of auxiliary IPv4 addresses used by Network driver ("my-router=192.168.1.5"). This can prove useful to reserve IP addresses already attributed on the network.'
+ });
+ this.$('#dgateway').tooltip({
+ html: true,
+ placement: 'right',
+ title: 'IP address of your gateway.'
+ });
+ this.$('#ip_range').tooltip({
+ html: true,
+ placement: 'right',
+ title: 'Enter an IP range in CIDR notation (172.28.5.0/24).'
+ });
+ this.$('#subnet').tooltip({
+ html: true,
+ placement: 'right',
+ title: 'Enter a subnet value in CIDR notation (172.28.0.0/16).'
+ });
+ this.$('#host_binding').tooltip({
+ html: true,
+ placement: 'right',
+ title: 'Enter a default IP address when binding container ports (172.23.0.1).'
+ });
+ this.$('#internal').tooltip({
+ html: true,
+ placement: 'right',
+ title: 'Restrict external access to the network.'
+ });
+ this.$('#ip_masquerade').tooltip({
+ html: true,
+ placement: 'right',
+ title: 'Enable IP masquerading.'
+ });
+ this.$('#icc').tooltip({
+ html: true,
+ placement: 'right',
+ title: 'Enable or Disable Inter-Containers Connectivity.'
+ });
+
},
// hide fields when selected method is auto
renderMethodOptionalFields: function() {
var selection = this.$('#method').val();
+ var ctype = this.$('#ctype').val();
if (selection == 'auto') {
- $('#methodOptionalFields').hide();
+ $('#methodOptionalFields, #methodOptionalFieldsDocker').hide();
} else {
- $('#methodOptionalFields').show();
+ if (ctype == 'docker') {
+ $('#methodOptionalFields').hide();
+ $('#methodOptionalFieldsDocker').show();
+ } else {
+ $('#methodOptionalFields').show();
+ $('#methodOptionalFieldsDocker').hide();
+ }
}
},
@@ -206,12 +272,14 @@ NetworkConnectionView = RockstorLayoutView.extend({
$('#teamProfiles, #multiDevice').show();
$('#bondProfiles, #singleDevice').hide();
} else if (selection == 'ethernet') {
- $('#teamProfiles, #multiDevice #bondProfiles').hide();
+ $('#teamProfiles, #multiDevice, #bondProfiles').hide();
$('#singleDevice').show();
- } else {
- //bond
+ } else if (selection == 'bond') {
$('#teamProfiles, #singleDevice').hide();
$('#bondProfiles, #multiDevice').show();
+ } else if (selection == 'docker') {
+ // show docker-specific config options
+ $('#teamProfiles, #singleDevice, #bondProfiles, #multiDevice').hide();
}
},
@@ -219,7 +287,8 @@ NetworkConnectionView = RockstorLayoutView.extend({
var _this = this;
Handlebars.registerHelper('selectedCtype', function(ctype) {
var html = '';
- if (ctype == _this.connection.get('ctype')) {
+ if ((ctype == _this.connection.get('ctype')) ||
+ ((ctype == 'docker') && (_this.connection.get('ctype') == 'bridge'))) {
html = 'selected="selected"';
}
return new Handlebars.SafeString(html);
diff --git a/src/rockstor/storageadmin/static/storageadmin/js/views/rockons.js b/src/rockstor/storageadmin/static/storageadmin/js/views/rockons.js
index f9f5558d5..bd02abd8f 100644
--- a/src/rockstor/storageadmin/static/storageadmin/js/views/rockons.js
+++ b/src/rockstor/storageadmin/static/storageadmin/js/views/rockons.js
@@ -234,7 +234,6 @@ RockonsView = RockstorLayoutView.extend({
_this.updateStatus();
},
error: function(data, status, xhr) {
- console.log('error while starting rockon');
}
});
},
@@ -251,7 +250,6 @@ RockonsView = RockstorLayoutView.extend({
_this.updateStatus();
},
error: function(data, status, xhr) {
- console.log('error while stopping rockon');
}
});
},
@@ -353,9 +351,13 @@ RockonsView = RockstorLayoutView.extend({
html += ' ';
if (_this.ui_map[rockon.get('id')]) {
if (rockon.get('status') == 'started') {
- html += '' + rockon.get('name') + ' UI ';
+ if (rockon.get('ui_publish')) {
+ html += '' + rockon.get('name') + ' UI ';
+ } else {
+ html += '' + rockon.get('name') + ' UI ';
+ }
} else {
- html += '' + rockon.get('name') + ' UI ';
+ html += '' + rockon.get('name') + ' UI ';
}
}
if (rockon.get('status') != 'started') {
@@ -885,7 +887,8 @@ RockonSettingsWizardView = WizardView.extend({
events: {
'click #next-page': 'nextPage',
'click #prev-page': 'prevPage',
- 'click #add-label': 'addLabels'
+ 'click #add-label': 'addLabels',
+ 'click #edit-ports': 'editPorts'
},
initialize: function() {
@@ -914,11 +917,16 @@ RockonSettingsWizardView = WizardView.extend({
this.containers = new ContainerCollection(null, {
rid: this.rockon.id
});
+ this.rocknets = new RockOnNetworkCollection(null, {
+ rid: this.rockon.id
+ });
this.shares = {};
this.model.set('shares', this.shares);
this.new_labels = {};
this.model.set('new_labels', this.new_labels);
+ this.networks = new NetworkConnectionCollection();
+ this.model.set('networks', this.networks);
this.evAgg.bind('addLabels', this.addLabels, this);
},
@@ -987,6 +995,16 @@ RockonSettingsWizardView = WizardView.extend({
this.labels.fetch({
success: function() {
_this.model.set('labels', _this.labels);
+ _this.fetchRocknets();
+ }
+ });
+ },
+
+ fetchRocknets: function() {
+ var _this = this;
+ this.rocknets.fetch({
+ success: function() {
+ _this.model.set('rocknets', _this.rocknets);
_this.addPages();
}
});
@@ -1000,6 +1018,15 @@ RockonSettingsWizardView = WizardView.extend({
return this;
},
+ editPorts: function() {
+ this.pages[1] = RockonEditPorts;
+ this.pages[2] = RockonSettingsSummary;
+ this.pages[3] = RockonSettingsComplete;
+ WizardView.prototype.render.apply(this, arguments);
+ return this;
+ },
+
+
render: function() {
this.fetchVolumes();
return this;
@@ -1028,18 +1055,25 @@ RockonSettingsWizardView = WizardView.extend({
this.$('#prev-page').hide();
this.$('#add-label').html('Add Label');
this.$('#add-label').css({'display': 'inline'});
+ this.$('#edit-ports').show();
this.$('#next-page').html('Add Storage');
if (!this.rockon.get('volume_add_support')) {
this.$('#next-page').hide();
- } else {
- if (this.rockon.get('status') == 'started') {
- var _this = this;
- this.$('.wizard-btn').click(function() {
- //disabling the button so that the backbone event is not triggered after the alert click.
- _this.$('.wizard-btn').prop('disabled', true);
- alert('Rock-on must be turned off to change its settings.');
- });
- }
+ }
+ if (this.rockon.get('status') == 'started') {
+ var _this = this;
+ this.$('.wizard-btn').click(function () {
+ //disabling the button so that the backbone event is not triggered after the alert click.
+ _this.$('.wizard-btn').prop('disabled', true);
+ alert('Rock-on must be turned off to change its settings.');
+ });
+ } else if (this.rockon.get('host_network')) {
+ var _this = this;
+ this.$('#edit-ports').click(function () {
+ //disabling the button so that the backbone event is not triggered after the alert click.
+ _this.$('#edit-ports').prop('disabled', true);
+ alert('Network settings cannot be altered for this rock-on as it uses host networking.');
+ });
}
} else if (this.currentPageNum == (this.pages.length - 2)) {
this.$('#prev-page').show();
@@ -1047,10 +1081,12 @@ RockonSettingsWizardView = WizardView.extend({
} else if (this.currentPageNum == (this.pages.length - 1)) {
this.$('#prev-page').show();
this.$('#add-label').hide();
+ this.$('#edit-ports').hide();
this.$('#next-page').html('Submit');
} else {
this.$('#prev-page').show();
this.$('#add-label').hide();
+ this.$('#edit-ports').hide();
this.$('#next-page').html('Next');
this.$('#ph-wizard-buttons').show();
}
@@ -1230,7 +1266,7 @@ RockonAddLabel = RockstorWizardPage.extend({
if (this.rockon.get('volume_add_support')) {
this.parent.pages[1] = RockonAddShare;
} else {
- this.parent.pages[1] = RockonAddLabel;
+ this.parent.pages[1] = RockonAddLabel;
}
return this;
},
@@ -1241,12 +1277,12 @@ RockonAddLabel = RockstorWizardPage.extend({
return $.Deferred().reject();
}
var field_data = $('input[name^=labels]').map(function(idx, elem) {
- if ($(elem).val() != "") {
+ if ($(elem).val() != '') {
return $(elem).val();
}
}).get();
var new_labels = {};
- field_data.forEach(function(prop) {
+ field_data.forEach(function (prop) {
new_labels[prop] = this.$('#container').val();
});
this.new_labels = new_labels;
@@ -1255,6 +1291,232 @@ RockonAddLabel = RockstorWizardPage.extend({
}
});
+RockonEditPorts = RockstorWizardPage.extend({
+ initialize: function() {
+ this.template = window.JST.rockons_edit_ports;
+ this.sub_template = window.JST.rockons_edit_ports_form;
+ this.rockon = this.model.get('rockon');
+ this.ports = new RockOnPortCollection(null, {
+ rid: this.rockon.id
+ });
+ this.networks = new NetworkConnectionCollection();
+ this.containers = new ContainerCollection(null, {
+ rid: this.rockon.id
+ });
+ RockstorWizardPage.prototype.initialize.apply(this, arguments);
+ },
+
+ render: function() {
+ RockstorWizardPage.prototype.render.apply(this, arguments);
+ this.fetchPorts();
+ return this;
+ },
+
+ fetchPorts: function() {
+ var _this = this;
+ this.ports.fetch({
+ success: function() {
+ _this.model.set('ports', _this.ports);
+ _this.fetchContainers();
+ }
+ });
+ },
+
+ fetchContainers: function() {
+ var _this = this;
+ this.containers.fetch({
+ success: function() {
+ _this.model.set('containers', _this.containers);
+ _this.fetchNetworks();
+ }
+ });
+ },
+
+ fetchNetworks: function() {
+ var _this = this;
+ this.networks.fetch({
+ success: function() {
+ _this.model.set('networks', _this.networks);
+ _this.renderPorts();
+ }
+ });
+ },
+
+ renderPorts: function() {
+ var _this = this;
+
+ // Fetch list of docker networks available to be used as rocknets
+ this.user_dnets = [];
+ for (var i = 0; i < this.networks.length; i++) {
+ var n = this.networks.at(i);
+ if (n.get('user_dnet')) {
+ this.user_dnets.push(n.toJSON());
+ }
+ }
+
+ this.$('#ph-edit-ports-form').html(this.sub_template({
+ ports: this.model.get('ports').toJSON(),
+ user_dnets: this.user_dnets,
+ containers: this.containers.map(function(c) {
+ return c.toJSON();
+ })
+ }));
+
+ // Initialize and configure Rocknets choice form
+ this.$('.form-control').each(function(index, element) {
+ $(this).select2({
+ dropdownParent: $('#install-rockon-overlay'),
+ width: '80%',
+ createTag: function (params) {
+ var excluded = ['host', 'bridge', 'null'];
+ if (
+ params.term.match(/^[a-zA-Z0-9]+$/g)
+ && excluded.indexOf(params.term) === -1
+ ) {
+ return {
+ id: params.term,
+ text: params.term
+ };
+ return null;
+ }
+ },
+ tags: true // allows creating rocknet(s) on-the-fly
+ });
+ });
+
+ // Preselect networks fields with currently attached rocknets
+ if (this.model.get('rocknets').length > 0) {
+ this.rocknets = this.model.get('rocknets');
+ this.rocknets_map = {};
+ for (var i = 0; i < this.containers.length; i++) {
+ var c = this.containers.at(i);
+ var cname = c.get('name');
+ this.rocknets_map[cname] = [];
+ }
+ for (var i = 0; i < this.rocknets.length; i++) {
+ var r = this.rocknets.at(i);
+ var rcname = r.get('container_name');
+ var rdname = r.get('docker_name');
+ this.rocknets_map[rcname].push(rdname);
+ }
+ for (var k in this.rocknets_map) {
+ if (this.rocknets_map.hasOwnProperty(k)) {
+ this.$('#' + k).val(this.rocknets_map[k]);
+ this.$('#' + k).trigger('change');
+ }
+ }
+ }
+
+ // Ensure previous page is correct
+ if (this.rockon.get('volume_add_support')) {
+ this.parent.pages[1] = RockonEditPorts;
+ } else {
+ this.parent.pages[1] = RockonEditPorts;
+ }
+ return this;
+ },
+
+ save: function() {
+ // // Verify that settings were altered
+ // 1. change in ports' publish state?
+ // Get ports publish states from form
+ var edit_ports = {};
+ $('.ports-group').map(function(idx, elem) {
+ var pstatus = 'unchecked';
+ if ($(elem).attr('checked') === 'checked') {
+ pstatus = 'checked';
+ }
+ return edit_ports[$(elem).attr('id')] = pstatus;
+ }).get();
+
+ // Get current published ports from database
+ var psDb = {};
+ this.ports.each(function (port) {
+ var state = "unchecked";
+ if (port.attributes.publish) {
+ state = "checked";
+ }
+ psDb[port.id] = state;
+ });
+
+ // 2. change in rocknets?
+ // Get join-networks data
+ var _this = this;
+ var rocknets_data = _this.$('#join-networks').getJSON();
+ Object.keys(rocknets_data).forEach((key) =>
+ (rocknets_data[key] === null) && delete rocknets_data[key]);
+
+ // Compare to rocknets in database
+ var differentRocknets = function (new_nets, reference) {
+ /**
+ * Compare the newly-defined rocknets attached to one
+ * or more containers in a rock-on, to those already
+ * existing in the database (if any).
+ * It returns 'true' if the two differ.
+ * @param {string} new_nets Rocknets object newly-defined
+ * in the form
+ * @param {string} reference Rocknets object already defined
+ * in the database
+ * @return {string} true if both objects differ,
+ * false otherwise
+ */
+ if (typeof reference === 'undefined') {
+ return Object.keys(new_nets).length > 0;
+ } else {
+ // get keys of each object
+ var new_keys = Object.keys(new_nets);
+ if (!new_keys.length > 0) {
+ return true;
+ } else {
+ var ref_keys = Object.keys(reference);
+ // Compare how many containers have rocknets
+ if (new_keys.length != ref_keys.length) {
+ return true;
+ }
+ // for each key, compare arrays
+ var new_entries = Object.entries(new_nets);
+ var ref_entries = Object.entries(reference);
+ for (var [key, nets] of new_entries) {
+ // Get all needed values
+ if (nets !== null) {
+ var ref_val = ref_entries[ref_keys.indexOf(key)][1];
+ var new_len = nets.length;
+ var ref_len = ref_val.length;
+ // Compare lengths
+ if (new_len != ref_len) {
+ return true;
+ }
+ // Compare individual values
+ if (!nets.every(e => ref_val.includes(e))) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ };
+
+ if (JSON.stringify(edit_ports) !== JSON.stringify(psDb)) {
+ var update_mode = 'normal';
+ } else if (differentRocknets(rocknets_data, this.rocknets_map)) {
+ update_mode = 'live';
+ } else {
+ alert('Please customize either ports or rocknets before proceeding further.')
+ return $.Deferred().reject();
+ }
+
+ // Save to model
+ this.edit_ports = edit_ports;
+ this.model.set('edit_ports', this.edit_ports);
+ this.new_cnets = rocknets_data;
+ this.model.set('new_cnets', this.new_cnets);
+ this.model.set('update_mode', update_mode);
+
+ return $.Deferred().resolve();
+ }
+});
+
RockonInfoSummary = RockstorWizardPage.extend({
initialize: function() {
this.template = window.JST.rockons_settings_summary;
@@ -1293,11 +1555,16 @@ RockonSettingsSummary = RockstorWizardPage.extend({
env: this.model.get('environment').toJSON(),
labels: this.model.get('labels').toJSON(),
new_labels: this.model.get('new_labels'),
+ rocknets: this.model.get('rocknets').toJSON(),
+ new_cnets: this.model.get('new_cnets'),
rockon: this.model.get('rockon')
}));
// Ensure previous page is correct
if (!$.isEmptyObject(this.model.get('new_labels'))) {
this.parent.pages[1] = RockonAddLabel;
+ } else if (!$.isEmptyObject(this.model.get('new_cnets')) ||
+ typeof this.model.get('update_mode') !== 'undefined') {
+ this.parent.pages[1] = RockonEditPorts;
} else {
if (this.rockon.get('volume_add_support')) {
this.parent.pages[1] = RockonAddShare;
@@ -1335,6 +1602,36 @@ RockonSettingsSummary = RockstorWizardPage.extend({
}
return new Handlebars.SafeString(html);
});
+ Handlebars.registerHelper('display_newCnets', function() {
+ // Display newly-defined rocknets and their corresponding container
+ // for confirmation before submit in settings_summary_table.jst
+ var html = '';
+ for (new_cnet in this.new_cnets) {
+ html += ' ';
+ html += 'Network ';
+ html += '' + new_cnet + ' ';
+ html += '' + this.new_cnets[new_cnet] + ' ';
+ html += ' ';
+ }
+ return new Handlebars.SafeString(html);
+ });
+ var _this = this;
+ Handlebars.registerHelper('isPublished', function(port, state) {
+ // Adds "(Unpublished)" text next to the port number
+ // if it is (or set to be) unpublished
+ var html = '';
+ if (_this.model.get('edit_ports')){
+ var edit_ports = _this.model.get('edit_ports');
+ if (edit_ports[port] == 'unchecked') {
+ html += ' (Unpublished)';
+ }
+ } else {
+ if(state != true) {
+ html += ' (Unpublished)';
+ }
+ }
+ return new Handlebars.SafeString(html);
+ });
}
});
@@ -1344,6 +1641,9 @@ RockonSettingsComplete = RockstorWizardPage.extend({
this.rockon = this.model.get('rockon');
this.shares = this.model.get('shares');
this.new_labels = this.model.get('new_labels');
+ this.edit_ports = this.model.get('edit_ports');
+ this.new_cnets = this.model.get('new_cnets');
+ this.update_mode = this.model.get('update_mode');
RockstorWizardPage.prototype.initialize.apply(this, arguments);
},
@@ -1358,15 +1658,21 @@ RockonSettingsComplete = RockstorWizardPage.extend({
var _this = this;
if (document.getElementById('next-page').disabled) return false;
document.getElementById('next-page').disabled = true;
+
+ // Collect all data to be sent during the API call
+ var dataObj = {};
+ if (!_.isEmpty(this.shares)) dataObj['shares'] = this.shares;
+ if (!_.isEmpty(this.new_labels)) dataObj['labels'] = this.new_labels;
+ if (!_.isEmpty(this.edit_ports)) dataObj['edit_ports'] = this.edit_ports;
+ if (!_.isEmpty(this.new_cnets)) dataObj['cnets'] = this.new_cnets;
+ if (!_.isEmpty(this.update_mode)) dataObj['update_mode'] = this.update_mode;
+
return $.ajax({
url: '/api/rockons/' + this.rockon.id + '/update',
type: 'POST',
dataType: 'json',
contentType: 'application/json',
- data: JSON.stringify({
- 'shares': this.shares,
- 'labels': this.new_labels
- }),
+ data: JSON.stringify(dataObj),
success: function() {}
});
}
diff --git a/src/rockstor/storageadmin/tests/test_network.py b/src/rockstor/storageadmin/tests/test_network.py
index b9b0f563e..034f5fad9 100644
--- a/src/rockstor/storageadmin/tests/test_network.py
+++ b/src/rockstor/storageadmin/tests/test_network.py
@@ -16,19 +16,21 @@
along with this program. If not, see
.
"""
+import mock
from rest_framework import status
from rest_framework.test import APITestCase
from mock import patch
from storageadmin.tests.test_api import APITestMixin
+from storageadmin.models import NetworkConnection, NetworkDevice
class NetworkTests(APITestMixin, APITestCase):
# Fixture from single ethernet KVM instance for now to start off new
# mocking required after recent api change.
- fixtures = ['test_network.json']
+ # fixtures = ["test_network2.json"]
# TODO: Needs changing as API url different ie connection|devices|refresh
# see referenced pr in setUpClass
- BASE_URL = '/api/network'
+ BASE_URL = "/api/network"
@classmethod
def setUpClass(cls):
@@ -43,199 +45,441 @@ def setUpClass(cls):
# post mocks
# devices map dictionary
- cls.patch_devices = patch('system.network.get_dev_config')
+ cls.patch_devices = patch("system.network.get_dev_config")
cls.mock_devices = cls.patch_devices.start()
cls.mock_devices.return_value = {
- 'lo': {'dtype': 'loopback', 'mac': '00:00:00:00:00:00',
- 'state': '10 (unmanaged)', 'mtu': '65536'},
- 'eth0': {'dtype': 'ethernet', 'mac': '52:54:00:58:5D:66',
- 'connection': 'eth0', 'state': '100 (connected)',
- 'mtu': '1500'}}
+ "lo": {
+ "dtype": "loopback",
+ "mac": "00:00:00:00:00:00",
+ "state": "10 (unmanaged)",
+ "mtu": "65536",
+ },
+ "eth0": {
+ "dtype": "ethernet",
+ "mac": "52:54:00:58:5D:66",
+ "connection": "eth0",
+ "state": "100 (connected)",
+ "mtu": "1500",
+ },
+ }
# connections map dictionary
- cls.patch_connections = patch('system.network.get_con_config')
+ cls.patch_connections = patch("system.network.get_con_config")
cls.mock_connections = cls.patch_connections.start()
cls.mock_connections.return_value = {
- '8dca3630-8c54-4ad7-8421-327cc2d3d14a':
- {'ctype': '802-3-ethernet',
- 'ipv6_addresses': None,
- 'ipv4_method': 'auto',
- 'ipv6_method': None,
- 'ipv6_dns': None,
- 'name': 'eth0',
- 'ipv4_addresses': '192.168.124.235/24',
- 'ipv6_gw': None,
- 'ipv4_dns': '192.168.124.1',
- 'state': 'activated',
- 'ipv6_dns_search': None,
- '802-3-ethernet': {
- 'mac': '52:54:00:58:5D:66',
- 'mtu': 'auto',
- 'cloned_mac': None},
- 'ipv4_gw': '192.168.124.1',
- 'ipv4_dns_search': None}}
-
+ "8dca3630-8c54-4ad7-8421-327cc2d3d14a": {
+ "ctype": "802-3-ethernet",
+ "ipv6_addresses": None,
+ "ipv4_method": "auto",
+ "ipv6_method": None,
+ "ipv6_dns": None,
+ "name": "eth0",
+ "ipv4_addresses": "192.168.124.235/24",
+ "ipv6_gw": None,
+ "ipv4_dns": "192.168.124.1",
+ "state": "activated",
+ "ipv6_dns_search": None,
+ "802-3-ethernet": {
+ "mac": "52:54:00:58:5D:66",
+ "mtu": "auto",
+ "cloned_mac": None,
+ },
+ "ipv4_gw": "192.168.124.1",
+ "ipv4_dns_search": None,
+ }
+ }
# valid_connection
- cls.patch_valid_connection = patch('system.network.valid_connection')
+ cls.patch_valid_connection = patch("system.network.valid_connection")
cls.mock_valid_connection = cls.patch_valid_connection.start()
cls.mock_valid_connection.return_value = True
# toggle_connection
- cls.patch_toggle_connection = patch('system.network.toggle_connection')
+ cls.patch_toggle_connection = patch("system.network.toggle_connection")
cls.mock_toggle_connection = cls.patch_toggle_connection.start()
- cls.mock_toggle_connection.return_value = [''], [''], 0
+ cls.mock_toggle_connection.return_value = [""], [""], 0
# delete_connection
- cls.patch_delete_connection = patch('system.network.delete_connection')
+ cls.patch_delete_connection = patch("system.network.delete_connection")
cls.mock_delete_connection = cls.patch_delete_connection.start()
- cls.mock_delete_connection.return_value = [''], [''], 0
+ cls.mock_delete_connection.return_value = [""], [""], 0
# reload_connection
- cls.patch_reload_connection = patch('system.network.reload_connection')
+ cls.patch_reload_connection = patch("system.network.reload_connection")
cls.mock_reload_connection = cls.patch_reload_connection.start()
- cls.mock_reload_connection.return_value = [''], [''], 0
+ cls.mock_reload_connection.return_value = [""], [""], 0
# new_connection_helper
- cls.patch_new_con_helper = patch(
- 'system.network.new_connection_helper')
+ cls.patch_new_con_helper = patch("system.network.new_connection_helper")
cls.mock_new_con_helper = cls.patch_new_con_helper.start()
- cls.mock_new_con_helper.return_value = [''], [''], 0
+ cls.mock_new_con_helper.return_value = [""], [""], 0
# new_ethernet_connection
- cls.patch_new_eth_conn = patch(
- 'system.network.new_ethernet_connection')
+ cls.patch_new_eth_conn = patch("system.network.new_ethernet_connection")
cls.mock_new_eth_conn = cls.patch_new_eth_conn.start()
- cls.mock_new_eth_conn.return_value = [''], [''], 0
+ cls.mock_new_eth_conn.return_value = [""], [""], 0
# new_member_helper
- cls.patch_new_mem_helper = patch('system.network.new_member_helper')
+ cls.patch_new_mem_helper = patch("system.network.new_member_helper")
cls.mock_new_mem_helper = cls.patch_new_mem_helper.start()
- cls.mock_new_mem_helper.return_value = [''], [''], 0
+ cls.mock_new_mem_helper.return_value = [""], [""], 0
# TODO: Also need to mock
# system.network.new_team_connection
# and
# system.network.new_bond_connection
+ # Set temp models entries as per fixtures
+ cls.temp_ethernet = NetworkConnection(id=1, name="Wired connection 1")
+ cls.temp_device_eth0 = NetworkDevice(id=2, name="eth0")
+ cls.temp_rocknet = NetworkConnection(id=17, name="br-6088a34098e0")
+ cls.temp_device = NetworkDevice(id=12, name="br-6088a34098e0")
+
@classmethod
def tearDownClass(cls):
super(NetworkTests, cls).tearDownClass()
- # TODO: Probably needs a re-write from here down due to API change.
# N.B. There are working and current system level unit tests in:
# src/rockstor/system/tests/test_system_network.py
# Added via pr: "Add unit testing for core network functions" #2045 on GitHub
# Fixture fix1.json has the test data. networks already exits in data are
# 'enp0s3' and 'enp0s8'
- # TODO: AttributeError: 'HttpResponseNotFound' object has no attribute 'data'
- # received on both below tests. Suspected as due to above referenced API change.
- # def test_get(self):
- # """
- # unauthorized access
- # """
- # # get base URL
- # response = self.client.get(self.BASE_URL)
- # self.assertEqual(response.status_code,
- # status.HTTP_200_OK, msg=response.data)
- #
- # # get with iname
- # response = self.client.get('%s/enp0s3' % self.BASE_URL)
- # self.assertEqual(response.status_code,
- # status.HTTP_200_OK, msg=response.data)
-
- # def test_put(self):
- # """
- # put, change itype
- # """
- # # TODO: test needs updating, interface now different.
- # # invalid network interface
- # data = {'itype': 'management'}
- # response = self.client.put('%s/invalid' % self.BASE_URL, data=data)
- # self.assertEqual(response.status_code,
- # status.HTTP_500_INTERNAL_SERVER_ERROR,
- # msg=response.data)
- # e_msg = 'Network connection (invalid) does not exist.'
- # self.assertEqual(response.data['detail'], e_msg)
- #
- # # edit configuration with out providing config method
- # data = {'method': '', 'itype': 'management'}
- # response = self.client.put('%s/enp0s3' % self.BASE_URL, data=data)
- # self.assertEqual(response.status_code,
- # status.HTTP_500_INTERNAL_SERVER_ERROR,
- # msg=response.data)
- # e_msg = 'Method must be auto(for dhcp) or manual(for static IP). not: '
- # self.assertEqual(response.data['detail'], e_msg)
- #
- # # happy path
- # data = {'method': 'auto', 'itype': 'management'}
- # response = self.client.put('%s/enp0s3' % self.BASE_URL, data=data)
- # self.assertEqual(response.status_code,
- # status.HTTP_200_OK, msg=response.data)
- # self.assertEqual(response.data['itype'], 'management')
- #
- # # netmask set to None
- # data = {'method': 'manual', 'ipaddr': '192.168.56.101',
- # 'netmask': None, 'gateway': '', 'dns_servers': '',
- # 'itype': 'io'}
- # response = self.client.put('%s/enp0s3' % self.BASE_URL, data=data)
- # self.assertEqual(response.status_code,
- # status.HTTP_500_INTERNAL_SERVER_ERROR,
- # msg=response.data)
- # e_msg = ('Provided netmask value(None) is invalid. You can provide '
- # 'it in a IP address format(eg: 255.255.255.0) or number of '
- # 'bits(eg: 24)')
- # self.assertEqual(response.data['detail'], e_msg)
- #
- # # Invalid netmask
- # data = {'method': 'manual', 'ipaddr': '192.168.56.101',
- # 'netmask': '111', 'gateway': '',
- # 'dns_servers': '', 'itype': 'io'}
- # response = self.client.put('%s/enp0s3' % self.BASE_URL, data=data)
- # self.assertEqual(response.status_code,
- # status.HTTP_500_INTERNAL_SERVER_ERROR,
- # msg=response.data)
- # e_msg = ('Provided netmask value(111) is invalid. Number of bits in '
- # 'netmask must be between 1-32')
- # self.assertEqual(response.data['detail'], e_msg)
- #
- # # happy path
- # data = {'method': 'manual', 'ipaddr': '192.168.56.101',
- # 'netmask': '225.225.225.0', 'gateway': '',
- # 'dns_servers': '', 'itype': 'io'}
- # response = self.client.put('%s/enp0s3' % self.BASE_URL, data=data)
- # self.assertEqual(response.status_code,
- # status.HTTP_200_OK, msg=response.data)
- # self.assertEqual(response.data['itype'], 'io')
- #
- # # happy path
- # data = {'method': 'auto', 'itype': 'management'}
- # response = self.client.put('%s/enp0s3' % self.BASE_URL, data=data)
- # self.assertEqual(response.status_code,
- # status.HTTP_200_OK, msg=response.data)
- # self.assertEqual(response.data['itype'], 'management')
- #
- # # Setting network interface itype to management when the othet network
- # # is already set to management
- # data = {'method': 'auto', 'itype': 'management'}
- # response = self.client.put('%s/enp0s8' % self.BASE_URL, data=data)
- # self.assertEqual(response.status_code,
- # status.HTTP_500_INTERNAL_SERVER_ERROR,
- # msg=response.data)
- # e_msg = ('Another interface(enp0s3) is already configured for '
- # 'management. You must disable it first before making this '
- # 'change.')
- # self.assertEqual(response.data['detail'], e_msg)
- #
- # # provide ipaddress thats already been used by another interface
- # data = {'method': 'manual', 'ipaddr': '10.0.3.15',
- # 'netmask': '225.225.225.0', 'gateway': '',
- # 'dns_servers': '', 'itype': 'io'}
- # response = self.client.put('%s/enp0s3' % self.BASE_URL, data=data)
- # self.assertEqual(response.status_code,
- # status.HTTP_500_INTERNAL_SERVER_ERROR,
- # msg=response.data)
- # e_msg = ('IP: 192.168.56.101 already in use by another '
- # 'interface: enp0s8')
- # self.assertEqual(response.data['detail'], e_msg)
+
+ # def session_login(self):
+ # self.client.login(username='admin', password='admin')
+
+ def test_get_base(self):
+ """
+ unauthorized access
+ """
+ # get base URL
+ response = self.client.get("{}/connections".format(self.BASE_URL))
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_200_OK,
+ msg="Un-expected get() result:\n"
+ "response.status_code = ({}).\n "
+ "response.data = ({}).\n ".format(response.status_code, response.data),
+ )
+
+ def test_put_invalid_id(self):
+ """
+ test with invalid connection id
+ :return:
+ """
+ data = {"id": 99}
+ response = self.client.put("{}/connections/99".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg=response.data,
+ )
+ e_msg = "Network connection (99) does not exist."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ @mock.patch("storageadmin.views.network.NetworkConnectionDetailView._nco")
+ def test_put(self, mock_nco):
+ """
+ test put with valid connection id
+ """
+ mock_nco.return_value = self.temp_rocknet
+
+ # Valid rocknet edit
+ data = {"id": 17}
+ response = self.client.put("{}/connections/17".format(self.BASE_URL), data=data)
+ self.assertEqual(response.status_code, status.HTTP_200_OK, msg=response.data)
+
+ # Invalid MTU
+ data = {"id": 17, "mtu": 10000}
+ response = self.client.put("{}/connections/17".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.status_code returned was {}".format(response.status_code),
+ )
+ e_msg = "The mtu must be an integer in 1500 - 9000 range."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ data = {"id": 17, "mtu": 100}
+ response = self.client.put("{}/connections/17".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.status_code returned was {}".format(response.status_code),
+ )
+ e_msg = "The mtu must be an integer in 1500 - 9000 range."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ @mock.patch("storageadmin.views.network.NetworkConnectionDetailView._nco")
+ def test_delete(self, mock_nco):
+ """
+ test put with valid connection id
+ """
+ mock_nco.return_value = self.temp_rocknet
+
+ data = {"id": 17}
+ response = self.client.put("{}/connections/17".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_200_OK,
+ msg="response.data = {}\n"
+ "reponse.status_code = {}".format(response.data, response.status_code),
+ )
+
+ @mock.patch("storageadmin.views.network.NetworkConnection.objects")
+ def test_nclistview_post_invalid(self, mock_networkconnection):
+ """
+ test NetworkConnectionListView.post with invalid settings
+ :return:
+ """
+ # A NetworkConnection object with the same name already exists
+ mock_networkconnection.filter.return_value = mock_networkconnection
+ mock_networkconnection.exists.return_value = True
+
+ data = {"id": 17, "name": "br-6088a34098e0"}
+ response = self.client.post("{}/connections".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.data = {}\n"
+ "response.status_code = {}".format(response.data, response.status_code),
+ )
+ e_msg = "Connection name (br-6088a34098e0) is already in use. Choose a different name."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ # No method is defined
+ mock_networkconnection.filter.return_value = mock_networkconnection
+ mock_networkconnection.exists.return_value = False
+
+ data = {"id": 17, "name": "br-6088a34098e0"}
+ response = self.client.post("{}/connections".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.data = {}\n"
+ "response.status_code = {}".format(response.data, response.status_code),
+ )
+ e_msg = "Unsupported config method (None). Supported ones include: (('auto', 'manual'))."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ # Invalid connection type
+ mock_networkconnection.filter.return_value = mock_networkconnection
+ mock_networkconnection.exists.return_value = False
+
+ data = {
+ "id": 17,
+ "name": "br-6088a34098e0",
+ "method": "auto",
+ "ctype": "invalid_ctype",
+ }
+ response = self.client.post("{}/connections".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.data = {}\n"
+ "response.status_code = {}".format(response.data, response.status_code),
+ )
+ e_msg = "Unsupported connection type (invalid_ctype). Supported ones include: (('ethernet', 'team', 'bond', 'docker'))."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ # Invalid team profile
+ mock_networkconnection.filter.return_value = mock_networkconnection
+ mock_networkconnection.exists.return_value = False
+
+ data = {
+ "id": 17,
+ "name": "br-6088a34098e0",
+ "method": "auto",
+ "ctype": "team",
+ "team_profile": "invalid_profile",
+ }
+ response = self.client.post("{}/connections".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.data = {}\n"
+ "response.status_code = {}".format(response.data, response.status_code),
+ )
+ e_msg = "Unsupported team profile (invalid_profile). Supported ones include: (('broadcast', 'roundrobin', 'activebackup', 'loadbalance', 'lacp'))."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ # Invalid bond profile
+ mock_networkconnection.filter.return_value = mock_networkconnection
+ mock_networkconnection.exists.return_value = False
+
+ data = {
+ "id": 17,
+ "name": "br-6088a34098e0",
+ "method": "auto",
+ "ctype": "bond",
+ "bond_profile": "invalid_profile",
+ }
+ response = self.client.post("{}/connections".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.data = {}\n"
+ "response.status_code = {}".format(response.data, response.status_code),
+ )
+ e_msg = "Unsupported bond profile (invalid_profile). Supported ones include: (('balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb'))."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ # TODO: write test for NetworkConnectionListView._validate_devices
+
+ def test_nclistview_post_devices(self):
+ """
+ test NetworkConnectionListView.post devices combinations
+ :return:
+ """
+ # Unknown device for ethernet connection
+ data = {
+ "id": 99,
+ "name": "Wired connection 99",
+ "device": "eth0",
+ "method": "auto",
+ "ctype": "ethernet",
+ }
+ response = self.client.post("{}/connections".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.data = {}\n"
+ "response.status_code = {}".format(response.data, response.status_code),
+ )
+ e_msg = "Unknown network device (eth0)."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ @mock.patch("storageadmin.views.network.NetworkDevice.objects")
+ def test_nclistview_post_devices_not_list(self, mock_networkdevice):
+
+ mock_networkdevice.get.return_value = self.temp_device_eth0
+ ## Team
+ # Devices not a list for team connection
+ data = {
+ "id": 99,
+ "name": "Wired connection 99",
+ "devices": "eth0",
+ "method": "auto",
+ "ctype": "team",
+ "team_profile": "broadcast",
+ }
+ response = self.client.post("{}/connections".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.data = {}\n"
+ "response.status_code = {}".format(response.data, response.status_code),
+ )
+ e_msg = "devices must be a list"
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ # Not enough devices for team connection
+ data = {
+ "id": 99,
+ "name": "Wired connection 99",
+ "devices": ["eth0",],
+ "method": "auto",
+ "ctype": "team",
+ "team_profile": "broadcast",
+ }
+ response = self.client.post("{}/connections".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.data = {}\n"
+ "response.status_code = {}".format(response.data, response.status_code),
+ )
+ e_msg = "A minimum of 2 devices are required."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ ## Bond
+ # Devices not a list for team connection
+ data = {
+ "id": 99,
+ "name": "Wired connection 99",
+ "devices": "eth0",
+ "method": "auto",
+ "ctype": "bond",
+ "bond_profile": "balance-rr",
+ }
+ response = self.client.post("{}/connections".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.data = {}\n"
+ "response.status_code = {}".format(response.data, response.status_code),
+ )
+ e_msg = "devices must be a list"
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
+
+ # Not enough devices for team connection
+ data = {
+ "id": 99,
+ "name": "Wired connection 99",
+ "devices": ["eth0",],
+ "method": "auto",
+ "ctype": "bond",
+ "bond_profile": "balance-rr",
+ }
+ response = self.client.post("{}/connections".format(self.BASE_URL), data=data)
+ self.assertEqual(
+ response.status_code,
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ msg="response.data = {}\n"
+ "response.status_code = {}".format(response.data, response.status_code),
+ )
+ e_msg = "A minimum of 2 devices are required."
+ self.assertEqual(
+ response.data[0],
+ e_msg,
+ msg="response.data[0] = {}".format(response.data[0]),
+ )
diff --git a/src/rockstor/storageadmin/urls/rockons.py b/src/rockstor/storageadmin/urls/rockons.py
index b49c316e9..8e58a0b22 100644
--- a/src/rockstor/storageadmin/urls/rockons.py
+++ b/src/rockstor/storageadmin/urls/rockons.py
@@ -27,6 +27,7 @@
RockOnDeviceView,
RockOnContainerView,
RockOnLabelView,
+ RockOnNetworkView,
)
urlpatterns = patterns(
@@ -39,6 +40,7 @@
url(r"^/environment/(?P
\d+)$", RockOnEnvironmentView.as_view(),),
url(r"^/devices/(?P\d+)$", RockOnDeviceView.as_view(),),
url(r"^/labels/(?P\d+)$", RockOnLabelView.as_view(),),
+ url(r"^/networks/(?P\d+)$", RockOnNetworkView.as_view(),),
url(r"^/(?Pupdate)$", RockOnView.as_view(),),
url(r"^/(?P\d+)$", RockOnIdView.as_view(),),
url(
diff --git a/src/rockstor/storageadmin/views/__init__.py b/src/rockstor/storageadmin/views/__init__.py
index 522728924..46f9b47e7 100644
--- a/src/rockstor/storageadmin/views/__init__.py
+++ b/src/rockstor/storageadmin/views/__init__.py
@@ -27,8 +27,8 @@
from login import LoginView # noqa F401
from user import UserListView, UserDetailView # noqa F401
from dashboardconfig import DashboardConfigView # noqa F401
-from network import (
- NetworkDeviceListView, # noqa F401
+from storageadmin.views.network import (
+ NetworkDeviceListView,
NetworkConnectionListView,
NetworkStateView,
NetworkConnectionDetailView,
@@ -57,6 +57,7 @@
from rockon_environment import RockOnEnvironmentView # noqa F401
from rockon_device import RockOnDeviceView # noqa F401
from rockon_labels import RockOnLabelView # noqa F401
+from rockon_networks import RockOnNetworkView # noqa F401
from disk_smart import DiskSMARTDetailView # noqa F401
from config_backup import (
ConfigBackupListView,
diff --git a/src/rockstor/storageadmin/views/network.py b/src/rockstor/storageadmin/views/network.py
index 334543435..d6f226b82 100644
--- a/src/rockstor/storageadmin/views/network.py
+++ b/src/rockstor/storageadmin/views/network.py
@@ -26,6 +26,8 @@
EthernetConnection,
TeamConnection,
BondConnection,
+ BridgeConnection,
+ DContainerLink,
)
from smart_manager.models import Service
from storageadmin.util import handle_exception
@@ -33,7 +35,15 @@
NetworkDeviceSerializer,
NetworkConnectionSerializer,
)
-from system import network
+from system.docker import (
+ docker_status,
+ probe_running_containers,
+ dnet_create,
+ dnet_connect,
+ dnet_disconnect,
+ dnet_remove,
+)
+import system.network as sysnet
import rest_framework_custom as rfc
import logging
@@ -46,6 +56,7 @@
class NetworkMixin(object):
+ logger.debug("The class NetworkMixin has been initialized")
# Runners for teams. @todo: support basic defaults + custom configuration.
# @todo: lacp doesn't seem to be activating
runners = {
@@ -69,6 +80,7 @@ class NetworkMixin(object):
@staticmethod
@transaction.atomic
def _update_or_create_ctype(co, ctype, config):
+ logger.debug("The function _update_or_create_ctype has been called")
if ctype == "802-3-ethernet":
try:
eco = EthernetConnection.objects.get(connection=co)
@@ -94,6 +106,30 @@ def _update_or_create_ctype(co, ctype, config):
bco.save()
except BondConnection.DoesNotExist:
BondConnection.objects.create(connection=co, **config)
+ elif (ctype == "bridge") and (docker_status()):
+ try:
+ brco = BridgeConnection.objects.get(connection=co)
+ brco.docker_name = config["docker_name"]
+ if (not DContainerLink.objects.filter(name=brco.docker_name)) and (
+ "docker0" not in brco.docker_name
+ ):
+ # The bridge connection is not the default docker bridge network
+ # and isn't a docker bridge network defined as a container_link
+ brco.usercon = True
+ brco.save()
+ except BridgeConnection.DoesNotExist:
+ usercon = False
+ docker_name = config["docker_name"]
+ if (not DContainerLink.objects.filter(name=docker_name)) and (
+ "docker0" not in docker_name
+ ):
+ # The bridge connection is not the default docker bridge network
+ # and isn't a docker bridge network defined as a container_link
+ usercon = True
+ BridgeConnection.objects.create(
+ connection=co, usercon=usercon, **config
+ )
+
else:
logger.error("Unknown ctype: {} config: {}".format(ctype, config))
@@ -116,7 +152,7 @@ def _update_master(co, config, defer_list=None):
@classmethod
@transaction.atomic
def _refresh_connections(cls):
- cmap = network.get_con_config(network.get_con_list())
+ cmap = sysnet.get_con_config(sysnet.get_con_list())
defer_master_updates = []
for nco in NetworkConnection.objects.all():
if nco.uuid not in cmap:
@@ -163,7 +199,7 @@ def _refresh_connections(cls):
@staticmethod
@transaction.atomic
def _refresh_devices():
- dmap = network.get_dev_config(network.get_dev_list())
+ dmap = sysnet.get_dev_config(sysnet.get_dev_list())
def update_connection(dconfig):
if "connection" in dconfig:
@@ -206,7 +242,7 @@ def get_queryset(self, *args, **kwargs):
class NetworkConnectionListView(rfc.GenericView, NetworkMixin):
serializer_class = NetworkConnectionSerializer
- ctypes = ("ethernet", "team", "bond")
+ ctypes = ("ethernet", "team", "bond", "docker")
# ethtool is the default link watcher.
@@ -235,7 +271,18 @@ def _validate_devices(devices, request, size=2):
@transaction.atomic
def post(self, request):
with self._handle_exception(request):
- ipaddr = gateway = dns_servers = search_domains = None
+ ipaddr = (
+ gateway
+ ) = (
+ dns_servers
+ ) = (
+ search_domains
+ ) = (
+ aux_address
+ ) = (
+ dgateway
+ ) = host_binding = icc = internal = ip_masquerade = ip_range = subnet = None
+ mtu = DEFAULT_MTU
name = request.data.get("name")
if NetworkConnection.objects.filter(name=name).exists():
e_msg = (
@@ -258,8 +305,17 @@ def post(self, request):
gateway = request.data.get("gateway", None)
dns_servers = request.data.get("dns_servers", None)
search_domains = request.data.get("search_domains", None)
-
- # connection type can be one of ethernet, team or bond
+ aux_address = request.data.get("aux_address", None)
+ dgateway = request.data.get("dgateway", None)
+ host_binding = request.data.get("host_binding", None)
+ icc = request.data.get("icc")
+ internal = request.data.get("internal")
+ ip_masquerade = request.data.get("ip_masquerade")
+ ip_range = request.data.get("ip_range", None)
+ mtu = request.data.get("mtu", 1500)
+ subnet = request.data.get("subnet", None)
+
+ # connection type can be one of ethernet, team, bond, or docker
ctype = request.data.get("ctype")
if ctype not in self.ctypes:
e_msg = (
@@ -277,7 +333,7 @@ def post(self, request):
).format(team_profile, self.team_profiles)
handle_exception(Exception(e_msg), request)
self._validate_devices(devices, request)
- network.new_team_connection(
+ sysnet.new_team_connection(
name,
self.runners[team_profile],
devices,
@@ -290,7 +346,7 @@ def post(self, request):
elif ctype == "ethernet":
device = request.data.get("device")
self._validate_devices([device], request, size=1)
- network.new_ethernet_connection(
+ sysnet.new_ethernet_connection(
name, device, ipaddr, gateway, dns_servers, search_domains
)
@@ -303,7 +359,7 @@ def post(self, request):
).format(bond_profile, self.bond_profiles)
handle_exception(Exception(e_msg), request)
self._validate_devices(devices, request)
- network.new_bond_connection(
+ sysnet.new_bond_connection(
name,
bond_profile,
devices,
@@ -313,6 +369,20 @@ def post(self, request):
search_domains,
)
+ elif ctype == "docker":
+ dnet_create(
+ name,
+ aux_address,
+ dgateway,
+ host_binding,
+ icc,
+ internal,
+ ip_masquerade,
+ ip_range,
+ mtu,
+ subnet,
+ )
+
return Response()
@@ -337,7 +407,6 @@ def _nco(request, id):
@transaction.atomic
def put(self, request, id):
-
with self._handle_exception(request):
nco = self._nco(request, id)
method = request.data.get("method")
@@ -351,17 +420,37 @@ def put(self, request, id):
handle_exception(Exception(e_msg), request)
except ValueError:
handle_exception(Exception(e_msg), request)
- ipaddr = gateway = dns_servers = search_domains = None
+ ipaddr = (
+ gateway
+ ) = (
+ dns_servers
+ ) = (
+ search_domains
+ ) = (
+ aux_address
+ ) = (
+ dgateway
+ ) = host_binding = icc = internal = ip_masquerade = ip_range = subnet = None
if method == "manual":
ipaddr = request.data.get("ipaddr", None)
gateway = request.data.get("gateway", None)
dns_servers = request.data.get("dns_servers", None)
search_domains = request.data.get("search_domains", None)
+ aux_address = request.data.get("aux_address", None)
+ dgateway = request.data.get("dgateway", None)
+ host_binding = request.data.get("host_binding", None)
+ icc = request.data.get("icc")
+ internal = request.data.get("internal")
+ ip_masquerade = request.data.get("ip_masquerade")
+ ip_range = request.data.get("ip_range", None)
+ mtu = request.data.get("mtu", 1500)
+ subnet = request.data.get("subnet", None)
+ ctype = request.data.get("ctype")
if nco.ctype == "ethernet":
device = nco.networkdevice_set.first().name
self._delete_connection(nco)
- network.new_ethernet_connection(
+ sysnet.new_ethernet_connection(
nco.name, device, ipaddr, gateway, dns_servers, search_domains, mtu
)
elif nco.ctype == "team":
@@ -371,7 +460,7 @@ def put(self, request, id):
devices.append(child_nco.networkdevice_set.first().name)
self._delete_connection(nco)
- network.new_team_connection(
+ sysnet.new_team_connection(
nco.name,
self.runners[team_profile],
devices,
@@ -381,37 +470,108 @@ def put(self, request, id):
search_domains,
mtu,
)
-
+ elif ctype == "docker":
+ docker_name = request.data.get("docker_name")
+ dname = request.data.get("dname")
+ # Get list of connected containers to re-connect them later
+ clist = probe_running_containers(network=docker_name, all=True)[:-1]
+ logger.debug("clist is {}".format(clist))
+ if len(clist) > 0:
+ for c in clist:
+ dnet_disconnect(c, docker_name)
+ # Remove docker network
+ dnet_remove(network=docker_name)
+ # Create the Docker network with new settings
+ try:
+ dnet_create(
+ dname,
+ aux_address,
+ dgateway,
+ host_binding,
+ icc,
+ internal,
+ ip_masquerade,
+ ip_range,
+ mtu,
+ subnet,
+ )
+ # Disconnect and reconnect all containers (if any)
+ if len(clist) > 0:
+ for c in clist:
+ dnet_connect(c, dname, all=True)
+ except Exception as e:
+ logger.debug(
+ "An error occurred while creating the docker network: {}".format(
+ e
+ )
+ )
+ # The creation of the new network has failed, so re-create the old one
+ dconf = BridgeConnection.objects.filter(
+ docker_name=docker_name
+ ).values()[0]
+ aux_address = dconf["aux_address"]
+ dgateway = dconf["dgateway"]
+ host_binding = dconf["host_binding"]
+ icc = dconf["icc"]
+ internal = dconf["internal"]
+ ip_masquerade = dconf["ip_masquerade"]
+ ip_range = dconf["ip_range"]
+ subnet = dconf["subnet"]
+ dnet_create(
+ docker_name,
+ aux_address,
+ dgateway,
+ host_binding,
+ icc,
+ internal,
+ ip_masquerade,
+ ip_range,
+ mtu,
+ subnet,
+ )
+ if len(clist) > 0:
+ for c in clist:
+ dnet_connect(c, docker_name, all=True)
+ raise e
return Response(NetworkConnectionSerializer(nco).data)
@staticmethod
def _delete_connection(nco):
for mnco in nco.networkconnection_set.all():
- network.delete_connection(mnco.uuid)
- network.delete_connection(nco.uuid)
+ sysnet.delete_connection(mnco.uuid)
+ sysnet.delete_connection(nco.uuid)
nco.delete()
@transaction.atomic
def delete(self, request, id):
with self._handle_exception(request):
nco = self._nco(request, id)
- restricted = False
- try:
- so = Service.objects.get(name="rockstor")
- config = json.loads(so.config)
- if config["network_interface"] == nco.name:
- restricted = True
- except Exception as e:
- logger.exception(e)
- if restricted:
- e_msg = (
- "This connection ({}) is designated for "
- "management and cannot be deleted. If you really "
- "need to delete it, change the Rockstor service "
- "configuration and try again."
- ).format(nco.name)
- handle_exception(Exception(e_msg), request)
- self._delete_connection(nco)
+ if nco.bridgeconnection_set.first() > 0: # If docker network
+ brco = nco.bridgeconnection_set.first()
+ # check for running containers and disconnect them first
+ clist = probe_running_containers(network=brco.docker_name)
+ if len(clist) > 1:
+ for c in clist[:-1]:
+ dnet_disconnect(c, brco.docker_name)
+ dnet_remove(network=brco.docker_name)
+ else:
+ restricted = False
+ try:
+ so = Service.objects.get(name="rockstor")
+ config = json.loads(so.config)
+ if config["network_interface"] == nco.name:
+ restricted = True
+ except Exception as e:
+ logger.exception(e)
+ if restricted:
+ e_msg = (
+ "This connection ({}) is designated for "
+ "management and cannot be deleted. If you really "
+ "need to delete it, change the Rockstor service "
+ "configuration and try again."
+ ).format(nco.name)
+ handle_exception(Exception(e_msg), request)
+ self._delete_connection(nco)
return Response()
@transaction.atomic
@@ -424,9 +584,9 @@ def post(self, request, id, switch):
# be brought up in order. eg: active-backup.
for mnco in nco.networkconnection_set.all().order_by("name"):
logger.debug("upping {} {}".format(mnco.name, mnco.uuid))
- network.toggle_connection(mnco.uuid, switch)
+ sysnet.toggle_connection(mnco.uuid, switch)
else:
- network.toggle_connection(nco.uuid, switch)
+ sysnet.toggle_connection(nco.uuid, switch)
return Response(NetworkConnectionSerializer(nco).data)
diff --git a/src/rockstor/storageadmin/views/rockon.py b/src/rockstor/storageadmin/views/rockon.py
index 4c4f53020..afa371805 100644
--- a/src/rockstor/storageadmin/views/rockon.py
+++ b/src/rockstor/storageadmin/views/rockon.py
@@ -23,13 +23,9 @@
import re
import requests
-from django.conf import settings
from django.db import transaction
-from django_ztask.models import Task
from rest_framework.response import Response
-import rest_framework_custom as rfc
-from rockon_helpers import docker_status, rockon_status
from smart_manager.models import Service
from storageadmin.models import (
RockOn,
@@ -46,6 +42,11 @@
)
from storageadmin.serializers import RockOnSerializer
from storageadmin.util import handle_exception
+import rest_framework_custom as rfc
+from rockon_helpers import rockon_status
+from system.docker import docker_status
+from django_ztask.models import Task
+from django.conf import settings
logger = logging.getLogger(__name__)
diff --git a/src/rockstor/storageadmin/views/rockon_helpers.py b/src/rockstor/storageadmin/views/rockon_helpers.py
index 1afc9a111..1e01c9466 100644
--- a/src/rockstor/storageadmin/views/rockon_helpers.py
+++ b/src/rockstor/storageadmin/views/rockon_helpers.py
@@ -24,8 +24,6 @@
from django_ztask.decorators import task
from cli.api_wrapper import APIWrapper
-from fs.btrfs import mount_share
-from rockon_utils import container_status
from storageadmin.models import (
RockOn,
DContainer,
@@ -38,9 +36,12 @@
DContainerDevice,
DContainerArgs,
DContainerLabel,
+ DContainerNetwork,
)
+from system.docker import dnet_create, dnet_connect
from system.osi import run_command
-from system.services import service_status
+from fs.btrfs import mount_share
+from rockon_utils import container_status
DOCKER = "/usr/bin/docker"
ROCKON_URL = "https://localhost/api/rockons"
@@ -52,19 +53,15 @@
"-d",
"--restart=unless-stopped",
]
-
+DNET = [
+ DOCKER,
+ "network",
+]
logger = logging.getLogger(__name__)
aw = APIWrapper()
-def docker_status():
- o, e, rc = service_status("docker")
- if rc != 0:
- return False
- return True
-
-
def rockon_status(name):
ro = RockOn.objects.get(name=name)
if globals().get("%s_status" % ro.name.lower()) is not None:
@@ -128,9 +125,42 @@ def generic_stop(rockon):
@task()
-def update(rid):
- uninstall(rid, new_state="pending_update")
- install(rid)
+def update(rid, live=False):
+ """
+ Guides general update procedure for a rock-on settings:
+ - if a live-update is possible, first start the rock-on and apply new settings
+ - if a live-update is impossible, first uninstall the rock-on and re-install
+ using the new settings
+ :param rid:
+ :param live:
+ :return:
+ """
+ if live:
+ new_state = "installed"
+ try:
+ rockon = RockOn.objects.get(id=rid)
+ # Ensure the rock-on is running before attempting network connection
+ start(rid)
+ dnet_create_connect(rockon)
+ except Exception as e:
+ logger.debug(
+ "Exception while live-updating the rock-on ({})".format(rockon)
+ )
+ logger.exception(e)
+ new_state = "install_failed"
+ finally:
+ url = "rockons/{}/state_update".format(rid)
+ logger.debug(
+ "Update rockon ({}) state to: {} ({})".format(
+ rockon.name, new_state, url
+ )
+ )
+ return aw.api_call(
+ url, data={"new_state": new_state,}, calltype="post", save_error=False
+ )
+ else:
+ uninstall(rid, new_state="pending_update")
+ install(rid)
@task()
@@ -184,13 +214,21 @@ def container_ops(container):
def port_ops(container):
ops_list = []
for po in DPort.objects.filter(container=container):
- pstr = "%s:%s" % (po.hostp, po.containerp)
+ # Skip the port if no set to be published
+ if po.publish is not True:
+ logger.debug(
+ "The port {} ({}) should not be published ({}), so skip it.".format(
+ po.id, po.description, po.publish
+ )
+ )
+ continue
+ pstr = "{}:{}".format(po.hostp, po.containerp)
if po.protocol is not None:
- pstr = "%s/%s" % (pstr, po.protocol)
+ pstr = "{}/{}".format(pstr, po.protocol)
ops_list.extend(["-p", pstr])
else:
- tcp = "%s/tcp" % pstr
- udp = "%s/udp" % pstr
+ tcp = "{}/tcp".format(pstr)
+ udp = "{}/udp".format(pstr)
ops_list.extend(
["-p", tcp, "-p", udp,]
)
@@ -211,7 +249,6 @@ def vol_ops(container):
def device_ops(container):
device_list = []
for d in DContainerDevice.objects.filter(container=container):
- # device_list.append(d.dev)
if len(d.val.strip()) > 0:
device_list.extend(["--device", "%s" % (d.val)])
return device_list
@@ -250,6 +287,29 @@ def labels_ops(container):
return labels_list
+def dnet_create_connect(rockon):
+ """
+ For a given rockon, apply all containers <-> docker networks connections
+ (container_link or rocknet) if any. If the docker network in question does
+ not yet exist, create it first.
+ :param rockon: RockOn object
+ :return:
+ """
+ for c in DContainer.objects.filter(rockon=rockon).order_by("launch_order"):
+ if DContainerLink.objects.filter(destination=c):
+ for lo in DContainerLink.objects.filter(destination=c):
+ dnet_create(lo.name)
+ dnet_connect(lo.destination.name, lo.name)
+ dnet_connect(lo.source.name, lo.name)
+ if DContainerNetwork.objects.filter(container=c):
+ for cno in DContainerNetwork.objects.filter(container=c):
+ dnet_create(cno.connection.docker_name)
+ dnet_connect(
+ container=cno.container.name, network=cno.connection.docker_name
+ )
+ # @todo: add detection of (or wait for) finished installed before creating networks?
+
+
def generic_install(rockon):
for c in DContainer.objects.filter(rockon=rockon).order_by("launch_order"):
rm_container(c.name)
@@ -277,6 +337,8 @@ def generic_install(rockon):
cmd.append(image_name_plus_tag)
cmd.extend(cargs(c))
run_command(cmd, log=True)
+ # Apply all network connections, if any
+ dnet_create_connect(rockon)
def openvpn_install(rockon):
diff --git a/src/rockstor/storageadmin/views/rockon_id.py b/src/rockstor/storageadmin/views/rockon_id.py
index 8f33f466c..eaf50c88e 100644
--- a/src/rockstor/storageadmin/views/rockon_id.py
+++ b/src/rockstor/storageadmin/views/rockon_id.py
@@ -21,9 +21,6 @@
from django.db import transaction
from rest_framework.response import Response
-
-import rest_framework_custom as rfc
-from rockon_helpers import docker_status, start, stop, install, uninstall, update
from storageadmin.models import (
RockOn,
DContainer,
@@ -33,16 +30,23 @@
DPort,
DCustomConfig,
DContainerEnv,
+ DContainerLink,
DContainerLabel,
+ DContainerNetwork,
+ BridgeConnection,
)
from storageadmin.serializers import RockOnSerializer
+import rest_framework_custom as rfc
from storageadmin.util import handle_exception
+from rockon_helpers import start, stop, install, uninstall, update
from system.services import superctl
+from system.docker import docker_status, dnet_create, dnet_disconnect, dnet_remove
+from storageadmin.views.network import NetworkMixin
logger = logging.getLogger(__name__)
-class RockOnIdView(rfc.GenericView):
+class RockOnIdView(rfc.GenericView, NetworkMixin):
serializer_class = RockOnSerializer
def get_queryset(self, *args, **kwargs):
@@ -204,6 +208,13 @@ def post(self, request, rid, command):
for co in DContainer.objects.filter(rockon=rockon):
DVolume.objects.filter(container=co, uservol=True).delete()
DContainerLabel.objects.filter(container=co).delete()
+ DContainerNetwork.objects.filter(container=co).delete()
+ for lo in DContainerLink.objects.filter(destination=co):
+ dnet_remove(network=lo.name)
+ # Reset all ports to a published state (if any)
+ for po in DPort.objects.filter(container=co):
+ po.publish = True
+ po.save()
elif command == "update":
self._pending_check(request)
if rockon.state != "installed":
@@ -220,6 +231,12 @@ def post(self, request, rid, command):
handle_exception(Exception(e_msg), request)
share_map = request.data.get("shares")
label_map = request.data.get("labels")
+ ports_publish = request.data.get("edit_ports")
+ cnets_map = request.data.get("cnets")
+ update_mode = request.data.get("update_mode")
+ live = False
+ if request.data.get("update_mode") == "live":
+ live = True
if bool(share_map):
for co in DContainer.objects.filter(rockon=rockon):
for s in share_map.keys():
@@ -261,9 +278,37 @@ def post(self, request, rid, command):
continue
lo = DContainerLabel(container=co, key=cname, val=c)
lo.save()
+ if (update_mode == "normal") and bool(ports_publish):
+ for p in ports_publish.keys():
+ po = DPort.objects.get(id=p)
+ pub = ports_publish[p]
+ po.publish = True
+ if pub == "unchecked":
+ po.publish = False
+ po.save()
+ if bool(update_mode):
+ # Reset all existing rocknets
+ for co in DContainer.objects.filter(rockon=rockon):
+ for cno in DContainerNetwork.objects.filter(container=co.id):
+ dnet_disconnect(co.name, cno.connection.docker_name)
+ DContainerNetwork.objects.filter(container=co).delete()
+ # Create new one(s)
+ if bool(cnets_map):
+ for c in cnets_map.keys():
+ # Create new entries for updated rocknets settings
+ for net in cnets_map[c]:
+ if not BridgeConnection.objects.filter(
+ docker_name=net
+ ).exists():
+ dnet_create(network=net)
+ self._refresh_connections()
+ brco = BridgeConnection.objects.get(docker_name=net)
+ co = DContainer.objects.get(rockon=rockon, name=c)
+ cno = DContainerNetwork(container=co, connection=brco)
+ cno.save()
rockon.state = "pending_update"
rockon.save()
- update.async(rockon.id)
+ update.async(rockon.id, live=live)
elif command == "stop":
stop.async(rockon.id)
rockon.status = "pending_stop"
diff --git a/src/rockstor/storageadmin/views/rockon_networks.py b/src/rockstor/storageadmin/views/rockon_networks.py
new file mode 100644
index 000000000..4d6161207
--- /dev/null
+++ b/src/rockstor/storageadmin/views/rockon_networks.py
@@ -0,0 +1,36 @@
+"""
+Copyright (c) 2012-2013 RockStor, Inc.
+This file is part of RockStor.
+
+RockStor 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; either version 2 of the License,
+or (at your option) any later version.
+
+RockStor 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. If not, see .
+"""
+
+from storageadmin.models import RockOn, DContainer, DContainerNetwork
+from storageadmin.serializers import RockOnNetworkSerializer
+import rest_framework_custom as rfc
+from storageadmin.util import handle_exception
+
+
+class RockOnNetworkView(rfc.GenericView):
+ serializer_class = RockOnNetworkSerializer
+
+ def get_queryset(self, *args, **kwargs):
+ try:
+ rockon = RockOn.objects.get(id=self.kwargs["rid"])
+ except:
+ e_msg = "Rock-on ({}) does not exist.".format(self.kwargs["rid"])
+ handle_exception(Exception(e_msg), self.request)
+
+ containers = DContainer.objects.filter(rockon=rockon)
+ return DContainerNetwork.objects.filter(container__in=containers).order_by("id")
diff --git a/src/rockstor/system/docker.py b/src/rockstor/system/docker.py
index 674d4738f..a70957dbd 100644
--- a/src/rockstor/system/docker.py
+++ b/src/rockstor/system/docker.py
@@ -16,8 +16,12 @@
along with this program. If not, see .
"""
-from osi import run_command
import collections
+import json
+import logging
+
+from system.osi import run_command
+from system.services import service_status
Image = collections.namedtuple("Image", "repository tag image_id created virt_size")
Container = collections.namedtuple(
@@ -25,11 +29,21 @@
)
DOCKER = "/usr/bin/docker"
+DNET = [
+ DOCKER,
+ "network",
+]
+
+logger = logging.getLogger(__name__)
def image_list():
+ """
+ Appears to be unused currently.
+ :return:
+ """
images = []
- o, e, rc = run_command([DOCKER, "images",])
+ o, e, rc = run_command([DOCKER, "images"])
for l in o[1:-1]:
cur_image = Image(
l[0:20].strip(),
@@ -43,8 +57,12 @@ def image_list():
def container_list():
+ """
+ Appears to be unused currently.
+ :return:
+ """
containers = []
- o, e, rc = run_command([DOCKER, "ps", "-a",])
+ o, e, rc = run_command([DOCKER, "ps", "-a"])
for l in o[1:-1]:
cur_con = Container(
l[0:20].strip(),
@@ -57,3 +75,203 @@ def container_list():
)
containers.append(cur_con)
return containers
+
+
+def docker_status():
+ o, e, rc = service_status("docker")
+ if rc != 0:
+ return False
+ return True
+
+
+def dnets(id=None, type=None):
+ """
+ List the docker names of all docker networks.
+ :param id: string, used to test for network presence.
+ :param type: string, either 'custom' or 'builtin'
+ :return: list
+ """
+ cmd = list(DNET) + [
+ "ls",
+ "--format",
+ "{{.Name}}",
+ ]
+ if id:
+ cmd.extend(["--filter", "id={}".format(id)])
+ if type is not None:
+ if type == "custom":
+ cmd.extend((["--filter", "type=custom"]))
+ elif type == "builtin":
+ cmd.extend((["--filter", "type=builtin"]))
+ else:
+ raise Exception("type must be custom or builtin")
+ o, e, rc = run_command(cmd)
+ return o[:-1]
+
+
+def dnet_inspect(dname):
+ """
+ This function takes the name of a docker network as argument
+ and returns a dict of its configuration.
+ :param dname: docker network name
+ :return: dict
+ """
+ cmd = list(DNET) + ["inspect", dname, "--format", "{{json .}}"]
+ o, _, _ = run_command(cmd)
+ return json.loads(o[0])
+
+
+def probe_running_containers(container=None, network=None, all=False):
+ """
+ List docker containers.
+ :param container: container name
+ :param network:
+ :param all:
+ :return:
+ """
+ cmd = [
+ DOCKER,
+ "ps",
+ "--format",
+ "{{.Names}}",
+ ]
+ running_filters = [
+ "--filter",
+ "status=created",
+ "--filter",
+ "status=restarting",
+ "--filter",
+ "status=running",
+ "--filter",
+ "status=paused",
+ ]
+ if all:
+ cmd.extend((["-a",]))
+ if network:
+ cmd.extend((["--filter", "network={}".format(network),]))
+ if container:
+ cmd.extend((running_filters + ["--filter", "name={}".format(container),]))
+ else:
+ cmd.extend((running_filters))
+ o, e, rc = run_command(cmd)
+ return o
+
+
+def dnet_create(
+ network,
+ aux_address=None,
+ dgateway=None,
+ host_binding=None,
+ icc=None,
+ internal=None,
+ ip_masquerade=None,
+ ip_range=None,
+ mtu=1500,
+ subnet=None,
+):
+ """
+ This method checks for an already existing docker network with the same name.
+ If none is found, it will be created using the different parameters given.
+ If no parameter is specified, the network will be created using docker's defaults.
+ :param network:
+ :param aux_address:
+ :param dgateway:
+ :param host_binding:
+ :param icc:
+ :param internal:
+ :param ip_masquerade:
+ :param ip_range:
+ :param mtu:
+ :param subnet:
+ :return:
+ """
+ o, e, rc = run_command(list(DNET) + ["list", "--format", "{{.Name}}",])
+ if network not in o:
+ logger.debug(
+ "the network {} was NOT detected, so create it now.".format(network)
+ )
+ cmd = list(DNET) + [
+ "create",
+ ]
+ if subnet is not None and len(subnet.strip()) > 0:
+ cmd.extend(
+ ["--subnet={}".format(subnet),]
+ )
+ if dgateway is not None and len(dgateway.strip()) > 0:
+ cmd.extend(
+ ["--gateway={}".format(dgateway),]
+ )
+ if aux_address is not None and len(aux_address.strip()) > 0:
+ for i in aux_address.split(","):
+ cmd.extend(
+ ['--aux-address="{}"'.format(i.strip()),]
+ )
+ if host_binding is not None and len(host_binding.strip()) > 0:
+ cmd.extend(
+ [
+ "--opt",
+ "com.docker.network.bridge.host_binding_ipv4={}".format(
+ host_binding
+ ),
+ ]
+ )
+ if icc is True:
+ cmd.extend(
+ ["--opt", "com.docker.network.bridge.enable_icc=true",]
+ )
+ if internal is True:
+ cmd.extend(
+ ["--internal",]
+ )
+ if ip_masquerade is True:
+ cmd.extend(
+ ["--opt", "com.docker.network.bridge.enable_ip_masquerade=true",]
+ )
+ if ip_range is not None and len(ip_range.strip()) > 0:
+ cmd.extend(
+ ["--ip-range={}".format(ip_range),]
+ )
+ if mtu != 1500:
+ cmd.extend(
+ ["--opt", "com.docker.network.driver.mtu={}".format(mtu),]
+ )
+ cmd.extend(
+ [network,]
+ )
+ run_command(cmd, log=True)
+ else:
+ logger.debug(
+ "the network {} was detected, so do NOT create it.".format(network)
+ )
+
+
+def dnet_connect(container, network, all=False):
+ """
+ Simple wrapper around docker connect with prior check for the existence of the container
+ and the lack of current connection to desired network.
+ :param container:
+ :param network:
+ :param all:
+ :return:
+ """
+ if (container in probe_running_containers(container=container, all=all)) and (
+ container not in probe_running_containers(network=network, all=all)
+ ):
+ run_command(list(DNET) + ["connect", network, container,], log=True)
+
+
+def dnet_disconnect(container, network):
+ run_command(list(DNET) + ["disconnect", network, container,], log=True)
+
+
+def dnet_remove(network=None):
+ """
+ This method uses the docker toolset to remove a docker network.
+ As this would throw an error if the network does not exist, we need
+ to first verify its presence.
+ :param network: string of network name as seen by `docker network ls`
+ :return:
+ """
+ # First, verify the network still exists
+ if network in dnets():
+ run_command(list(DNET) + ["rm", network,], log=True)
diff --git a/src/rockstor/system/network.py b/src/rockstor/system/network.py
index f94493584..79cc0bbd2 100644
--- a/src/rockstor/system/network.py
+++ b/src/rockstor/system/network.py
@@ -22,7 +22,7 @@
from .exceptions import CommandException
from .osi import run_command
-
+from .docker import dnets, docker_status, dnet_inspect
NMCLI = "/usr/bin/nmcli"
DEFAULT_MTU = 1500
@@ -116,6 +116,19 @@ def flatten(l):
return None
return s
+ def parse_aux_addresses(dtmap):
+ """
+ Parses auxilliary addresses of a docker network and
+ returns a flat list.
+ :param dtmap:
+ :return:
+ """
+ aux = dtmap["IPAM"]["Config"][0]["AuxiliaryAddresses"]
+ aux_list = []
+ for k, v in aux.items():
+ aux_list.append("{}={}".format(k, v))
+ return flatten(aux_list)
+
for uuid in con_list:
if len(uuid.strip()) == 0:
continue
@@ -176,6 +189,66 @@ def flatten(l):
}
elif tmap["ctype"] in ("team", "bond"):
tmap[tmap["ctype"]] = {"config": None}
+ elif tmap["ctype"] == "bridge":
+ cid = tmap["name"]
+ tmap[tmap["ctype"]] = {
+ "docker_name": None,
+ "aux_address": None,
+ "dgateway": None,
+ "host_binding": None,
+ "icc": False,
+ "internal": False,
+ "ip_masquerade": False,
+ "ip_range": None,
+ "subnet": None,
+ }
+ # Get docker_name
+ if docker_status():
+ # if (cid[0].startswith('br-')): # custom-type docker network
+ if cid.startswith("br-"): # custom-type docker network
+ docker_name = dname = dnets(cid[3:])[0]
+ else: # default docker0 bridge network
+ docker_name = cid
+ dname = "bridge"
+ # Fill custom information, if any.
+ dtmap = dnet_inspect(dname)
+ tmap[tmap["ctype"]]["docker_name"] = docker_name
+ if dtmap["IPAM"]["Config"][0].get("AuxiliaryAddresses"):
+ tmap[tmap["ctype"]]["aux_address"] = parse_aux_addresses(
+ dtmap
+ )
+ # In some case, DNET inspect does NOT return Gateway in Docker version 18.09.5, build e8ff056
+ # This is likely related to the following bug in which the 'Gateway' is not reported the first
+ # time the docker daemon is started. Upon reload of docker daemon, it IS correctly reported.
+ # https://github.com/moby/moby/issues/26799
+ if dtmap["IPAM"]["Config"][0].get("Gateway"):
+ tmap[tmap["ctype"]]["dgateway"] = dtmap["IPAM"]["Config"][
+ 0
+ ]["Gateway"]
+ if dtmap["Options"].get(
+ "com.docker.network.bridge.host_binding_ipv4"
+ ):
+ tmap[tmap["ctype"]]["host_binding"] = dtmap["Options"][
+ "com.docker.network.bridge.host_binding_ipv4"
+ ]
+ if dtmap["Options"].get("com.docker.network.bridge.enable_icc"):
+ tmap[tmap["ctype"]]["icc"] = dtmap["Options"][
+ "com.docker.network.bridge.enable_icc"
+ ]
+ tmap[tmap["ctype"]]["internal"] = dtmap["Internal"]
+ if dtmap["Options"].get(
+ "com.docker.network.bridge.ip_masquerade"
+ ):
+ tmap[tmap["ctype"]]["ip_masquerade"] = dtmap["Options"][
+ "com.docker.network.bridge.ip_masquerade"
+ ]
+ if dtmap["IPAM"]["Config"][0].get("IPRange"):
+ tmap[tmap["ctype"]]["ip_range"] = dtmap["IPAM"]["Config"][
+ 0
+ ]["IPRange"]
+ tmap[tmap["ctype"]]["subnet"] = dtmap["IPAM"]["Config"][0][
+ "Subnet"
+ ]
else:
tmap[tmap["ctype"]] = {}
diff --git a/src/rockstor/system/tests/test_system_network.py b/src/rockstor/system/tests/test_system_network.py
index b8c9f3015..09239ea1d 100644
--- a/src/rockstor/system/tests/test_system_network.py
+++ b/src/rockstor/system/tests/test_system_network.py
@@ -162,6 +162,12 @@ def test_get_con_config(self):
This tests for correct parsing of nmcli connection config by get_con_config(),
which should return a dict with detailed config for each network connection detected.
"""
+ # Mock and patch docker-specific calls
+ self.patch_dnets = patch("system.network.dnets")
+ self.mock_dnets = self.patch_dnets.start()
+ self.patch_dnet_inspect = patch("system.network.dnet_inspect")
+ self.mock_dnet_inspect = self.patch_dnet_inspect.start()
+
con_name = ["c54ea011-0e23-43fa-8f06-23429b9ce714"]
out = [
[
@@ -298,6 +304,8 @@ def test_get_con_config(self):
]
err = [[""]]
rc = [0]
+ dnets_out = [""]
+ dnet_inspect_out = [""]
expected_result = [
{
"c54ea011-0e23-43fa-8f06-23429b9ce714": {
@@ -319,19 +327,21 @@ def test_get_con_config(self):
}
]
- con_name.append("ecb5c4a6-05ed-4a29-bdd2-2023f691f096")
+ # Default docker bridge
+ con_name.append("7ffa9c37-4559-4608-80de-ea77a27876cd")
out.append(
[
"connection.id: docker0",
- "connection.uuid: ecb5c4a6-05ed-4a29-bdd2-2023f691f096",
+ "connection.uuid: 7ffa9c37-4559-4608-80de-ea77a27876cd",
"connection.stable-id: --",
"connection.type: bridge",
"connection.interface-name: docker0",
"connection.autoconnect: no",
"connection.autoconnect-priority: 0",
"connection.autoconnect-retries: -1 (default)",
+ "connection.multi-connect: 0 (default)",
"connection.auth-retries: -1",
- "connection.timestamp: 1557955026",
+ "connection.timestamp: 1593384718",
"connection.read-only: no",
"connection.permissions: --",
"connection.zone: --",
@@ -343,36 +353,42 @@ def test_get_con_config(self):
"connection.metered: unknown",
"connection.lldp: default",
"connection.mdns: -1 (default)",
+ "connection.llmnr: -1 (default)",
+ "connection.wait-device-timeout: -1",
"ipv4.method: manual",
"ipv4.dns: --",
"ipv4.dns-search: --",
- 'ipv4.dns-options: ""',
+ "ipv4.dns-options: --",
"ipv4.dns-priority: 100",
"ipv4.addresses: 172.17.0.1/16",
"ipv4.gateway: --",
"ipv4.routes: --",
"ipv4.route-metric: -1",
"ipv4.route-table: 0 (unspec)",
+ "ipv4.routing-rules: --",
"ipv4.ignore-auto-routes: no",
"ipv4.ignore-auto-dns: no",
"ipv4.dhcp-client-id: --",
+ "ipv4.dhcp-iaid: --",
"ipv4.dhcp-timeout: 0 (default)",
"ipv4.dhcp-send-hostname: yes",
"ipv4.dhcp-hostname: --",
"ipv4.dhcp-fqdn: --",
+ "ipv4.dhcp-hostname-flags: 0x0 (none)",
"ipv4.never-default: no",
"ipv4.may-fail: yes",
"ipv4.dad-timeout: -1 (default)",
"ipv6.method: ignore",
"ipv6.dns: --",
"ipv6.dns-search: --",
- 'ipv6.dns-options: ""',
+ "ipv6.dns-options: --",
"ipv6.dns-priority: 100",
"ipv6.addresses: --",
"ipv6.gateway: --",
"ipv6.routes: --",
"ipv6.route-metric: -1",
"ipv6.route-table: 0 (unspec)",
+ "ipv6.routing-rules: --",
"ipv6.ignore-auto-routes: no",
"ipv6.ignore-auto-dns: no",
"ipv6.never-default: no",
@@ -380,8 +396,10 @@ def test_get_con_config(self):
"ipv6.ip6-privacy: -1 (unknown)",
"ipv6.addr-gen-mode: stable-privacy",
"ipv6.dhcp-duid: --",
+ "ipv6.dhcp-iaid: --",
"ipv6.dhcp-send-hostname: yes",
"ipv6.dhcp-hostname: --",
+ "ipv6.dhcp-hostname-flags: 0x0 (none)",
"ipv6.token: --",
"bridge.mac-address: --",
"bridge.stp: no",
@@ -392,20 +410,24 @@ def test_get_con_config(self):
"bridge.ageing-time: 300",
"bridge.group-forward-mask: 0",
"bridge.multicast-snooping: yes",
+ "bridge.vlan-filtering: no",
+ "bridge.vlan-default-pvid: 1",
+ "bridge.vlans: --",
"proxy.method: none",
"proxy.browser-only: no",
"proxy.pac-url: --",
"proxy.pac-script: --",
"GENERAL.NAME: docker0",
- "GENERAL.UUID: ecb5c4a6-05ed-4a29-bdd2-2023f691f096",
+ "GENERAL.UUID: 7ffa9c37-4559-4608-80de-ea77a27876cd",
"GENERAL.DEVICES: docker0",
+ "GENERAL.IP-IFACE: docker0",
"GENERAL.STATE: activated",
"GENERAL.DEFAULT: no",
"GENERAL.DEFAULT6: no",
"GENERAL.SPEC-OBJECT: --",
"GENERAL.VPN: no",
- "GENERAL.DBUS-PATH: /org/freedesktop/NetworkManager/ActiveConnection/6",
- "GENERAL.CON-PATH: /org/freedesktop/NetworkManager/Settings/6",
+ "GENERAL.DBUS-PATH: /org/freedesktop/NetworkManager/ActiveConnection/2",
+ "GENERAL.CON-PATH: /org/freedesktop/NetworkManager/Settings/2",
"GENERAL.ZONE: --",
"GENERAL.MASTER-PATH: --",
"IP4.ADDRESS[1]: 172.17.0.1/16",
@@ -417,10 +439,51 @@ def test_get_con_config(self):
)
err.append([""])
rc.append(0)
+ dnets_out.append("")
+ dnet_inspect_out.append(
+ {
+ "Ingress": False,
+ "Name": "bridge",
+ "Created": "2020-08-11T11:06:06.107666148-04:00",
+ "EnableIPv6": False,
+ "Labels": {},
+ "Driver": "bridge",
+ "Attachable": False,
+ "ConfigOnly": False,
+ "Internal": False,
+ "ConfigFrom": {"Network": ""},
+ "Options": {
+ "com.docker.network.bridge.name": "docker0",
+ "com.docker.network.bridge.default_bridge": "true",
+ "com.docker.network.bridge.enable_ip_masquerade": "true",
+ "com.docker.network.driver.mt": "1500",
+ "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
+ "com.docker.network.bridge.enable_icc": "true",
+ },
+ "IPAM": {
+ "Config": [{"Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1"}],
+ "Driver": "default",
+ "Options": None,
+ },
+ "Scope": "local",
+ "Id": "fc57dc3d46f9c27bc9b094e58be282b7fca57db3480e0e27e8dd83e0241ad0c8",
+ "Containers": {},
+ }
+ )
expected_result.append(
{
- "ecb5c4a6-05ed-4a29-bdd2-2023f691f096": {
- "bridge": {},
+ "7ffa9c37-4559-4608-80de-ea77a27876cd": {
+ "bridge": {
+ "aux_address": None,
+ "subnet": u"172.17.0.0/16",
+ "internal": False,
+ "host_binding": u"0.0.0.0",
+ "docker_name": "docker0",
+ "icc": u"true",
+ "ip_masquerade": False,
+ "dgateway": u"172.17.0.1",
+ "ip_range": None,
+ },
"ctype": "bridge",
"ipv6_addresses": None,
"ipv4_method": "manual",
@@ -438,19 +501,193 @@ def test_get_con_config(self):
}
)
+ # User-created rocknet
+ con_name.append("c344cf82-783e-420e-9d02-30980e058ae5")
+ out.append(
+ [
+ "connection.id: br-6088a34098e0",
+ "connection.uuid: c344cf82-783e-420e-9d02-30980e058ae5",
+ "connection.stable-id: --",
+ "connection.type: bridge",
+ "connection.interface-name: br-6088a34098e0",
+ "connection.autoconnect: no",
+ "connection.autoconnect-priority: 0",
+ "connection.autoconnect-retries: -1 (default)",
+ "connection.multi-connect: 0 (default)",
+ "connection.auth-retries: -1",
+ "connection.timestamp: 1593385318",
+ "connection.read-only: no",
+ "connection.permissions: --",
+ "connection.zone: --",
+ "connection.master: --",
+ "connection.slave-type: --",
+ "connection.autoconnect-slaves: -1 (default)",
+ "connection.secondaries: --",
+ "connection.gateway-ping-timeout: 0",
+ "connection.metered: unknown",
+ "connection.lldp: default",
+ "connection.mdns: -1 (default)",
+ "connection.llmnr: -1 (default)",
+ "connection.wait-device-timeout: -1",
+ "ipv4.method: manual",
+ "ipv4.dns: --",
+ "ipv4.dns-search: --",
+ "ipv4.dns-options: --",
+ "ipv4.dns-priority: 100",
+ "ipv4.addresses: 172.20.0.1/16",
+ "ipv4.gateway: --",
+ "ipv4.routes: --",
+ "ipv4.route-metric: -1",
+ "ipv4.route-table: 0 (unspec)",
+ "ipv4.routing-rules: --",
+ "ipv4.ignore-auto-routes: no",
+ "ipv4.ignore-auto-dns: no",
+ "ipv4.dhcp-client-id: --",
+ "ipv4.dhcp-iaid: --",
+ "ipv4.dhcp-timeout: 0 (default)",
+ "ipv4.dhcp-send-hostname: yes",
+ "ipv4.dhcp-hostname: --",
+ "ipv4.dhcp-fqdn: --",
+ "ipv4.dhcp-hostname-flags: 0x0 (none)",
+ "ipv4.never-default: no",
+ "ipv4.may-fail: yes",
+ "ipv4.dad-timeout: -1 (default)",
+ "ipv6.method: ignore",
+ "ipv6.dns: --",
+ "ipv6.dns-search: --",
+ "ipv6.dns-options: --",
+ "ipv6.dns-priority: 100",
+ "ipv6.addresses: --",
+ "ipv6.gateway: --",
+ "ipv6.routes: --",
+ "ipv6.route-metric: -1",
+ "ipv6.route-table: 0 (unspec)",
+ "ipv6.routing-rules: --",
+ "ipv6.ignore-auto-routes: no",
+ "ipv6.ignore-auto-dns: no",
+ "ipv6.never-default: no",
+ "ipv6.may-fail: yes",
+ "ipv6.ip6-privacy: -1 (unknown)",
+ "ipv6.addr-gen-mode: stable-privacy",
+ "ipv6.dhcp-duid: --",
+ "ipv6.dhcp-iaid: --",
+ "ipv6.dhcp-send-hostname: yes",
+ "ipv6.dhcp-hostname: --",
+ "ipv6.dhcp-hostname-flags: 0x0 (none)",
+ "ipv6.token: --",
+ "bridge.mac-address: --",
+ "bridge.stp: no",
+ "bridge.priority: 32768",
+ "bridge.forward-delay: 15",
+ "bridge.hello-time: 2",
+ "bridge.max-age: 20",
+ "bridge.ageing-time: 300",
+ "bridge.group-forward-mask: 0",
+ "bridge.multicast-snooping: yes",
+ "bridge.vlan-filtering: no",
+ "bridge.vlan-default-pvid: 1",
+ "bridge.vlans: --",
+ "proxy.method: none",
+ "proxy.browser-only: no",
+ "proxy.pac-url: --",
+ "proxy.pac-script: --",
+ "GENERAL.NAME: br-6088a34098e0",
+ "GENERAL.UUID: c344cf82-783e-420e-9d02-30980e058ae5",
+ "GENERAL.DEVICES: br-6088a34098e0",
+ "GENERAL.IP-IFACE: br-6088a34098e0",
+ "GENERAL.STATE: activated",
+ "GENERAL.DEFAULT: no",
+ "GENERAL.DEFAULT6: no",
+ "GENERAL.SPEC-OBJECT: --",
+ "GENERAL.VPN: no",
+ "GENERAL.DBUS-PATH: /org/freedesktop/NetworkManager/ActiveConnection/3",
+ "GENERAL.CON-PATH: /org/freedesktop/NetworkManager/Settings/3",
+ "GENERAL.ZONE: --",
+ "GENERAL.MASTER-PATH: --",
+ "IP4.ADDRESS[1]: 172.20.0.1/16",
+ "IP4.GATEWAY: --",
+ "IP4.ROUTE[1]: dst = 172.20.0.0/16, nh = 0.0.0.0, mt = 0",
+ "IP6.GATEWAY: --",
+ "",
+ ]
+ )
+ err.append([""])
+ rc.append(0)
+ dnets_out.append(["rocknet01"])
+ dnet_inspect_out.append(
+ {
+ "Ingress": False,
+ "Name": "rocknet01",
+ "Created": "2020-08-07T16:43:14.04154885-04:00",
+ "EnableIPv6": False,
+ "Labels": {},
+ "Driver": "bridge",
+ "Attachable": False,
+ "ConfigOnly": False,
+ "Internal": False,
+ "ConfigFrom": {"Network": ""},
+ "Options": {
+ "com.docker.network.bridge.enable_icc": "true",
+ "com.docker.network.driver.mtu": "1500",
+ },
+ "IPAM": {
+ "Config": [{"Subnet": "172.20.0.0/16", "Gateway": "172.20.0.1"}],
+ "Driver": "default",
+ "Options": {},
+ },
+ "Scope": "local",
+ "Id": "2c7dfdeef407717c107041f6e1e53c3248cf688b78be1f346e4a1079e9da6570",
+ "Containers": {},
+ }
+ )
+ expected_result.append(
+ {
+ "c344cf82-783e-420e-9d02-30980e058ae5": {
+ "bridge": {
+ "aux_address": None,
+ "subnet": u"172.20.0.0/16",
+ "internal": False,
+ "host_binding": None,
+ "docker_name": "rocknet01",
+ "icc": u"true",
+ "ip_masquerade": False,
+ "dgateway": u"172.20.0.1",
+ "ip_range": None,
+ },
+ "ctype": "bridge",
+ "ipv6_addresses": None,
+ "ipv4_method": "manual",
+ "ipv6_method": None,
+ "ipv6_dns": None,
+ "name": "br-6088a34098e0",
+ "ipv4_addresses": "172.20.0.1/16",
+ "ipv6_gw": None,
+ "ipv4_dns": None,
+ "state": "activated",
+ "ipv6_dns_search": None,
+ "ipv4_gw": None,
+ "ipv4_dns_search": None,
+ }
+ }
+ )
+
# @todo: Add more types of connections, such as docker0, wifi, eth, veth, etc...
# Cycle through each of the above parameter / run_command data sets.
- for con, o, e, r, expected in zip(con_name, out, err, rc, expected_result):
+ for con, o, e, r, dnet_o, dnet_inspect_o, expected in zip(
+ con_name, out, err, rc, dnets_out, dnet_inspect_out, expected_result
+ ):
con_list = [con]
self.mock_run_command.return_value = (o, e, r)
+ self.mock_dnets.return_value = dnet_o
+ self.mock_dnet_inspect.return_value = dnet_inspect_o
returned = get_con_config(con_list)
self.assertEqual(
returned,
expected,
msg="Un-expected get_con_config() result:\n "
- "returned = ({}).\n "
- "expected = ({}).".format(returned, expected),
+ "returned = {}.\n "
+ "expected = {}.\n ".format(returned, expected),
)
def test_get_con_config_con_not_found(self):