diff options
author | Dixsie Wolmers <dixsie@ibm.com> | 2020-02-27 04:52:28 +0300 |
---|---|---|
committer | Derick Montague <derick.montague@ibm.com> | 2020-06-05 20:39:25 +0300 |
commit | bb81d55c5cd9db01ad4f7949bc3fb80426570914 (patch) | |
tree | 015aeda354f407a0ce1e1d054baa6c6790e8c093 | |
parent | 304095d60a270a311d7e081313ccada27cc2d3cd (diff) | |
download | webui-vue-bb81d55c5cd9db01ad4f7949bc3fb80426570914.tar.xz |
Add network settings page
- Adds ability to configure newtowrk settings by selected ethernet interface
- Default gateway is currently unavailable in redfish,
to work around, grabbed gateway from first static ipv4 configuration
and assigned to new static ipv4 configurations
- Adds ability to add, modify and delete static ipv4 configs
- Adds ability to add, modify and delete static dns
- Adds ability to edit gateway, hostname and mac address
- Form validations include regex for ip, mac address, and hostname
- Language translations to be addressed in separate commit
- Enabling DHCP and configuring DHCP settings to be addressed in separate commit
Signed-off-by: Dixsie Wolmers <dixsie@ibm.com>
Change-Id: I122034ae0ef3a8c08e5599ee3eca66e8d0d59f67
-rw-r--r-- | src/components/AppNavigation/AppNavigation.vue | 2 | ||||
-rw-r--r-- | src/router/index.js | 8 | ||||
-rw-r--r-- | src/store/modules/Configuration/NetworkSettingsStore.js | 71 | ||||
-rw-r--r-- | src/views/Configuration/NetworkSettings/NetworkSettings.vue | 492 | ||||
-rw-r--r-- | src/views/Configuration/NetworkSettings/index.js | 2 |
5 files changed, 571 insertions, 4 deletions
diff --git a/src/components/AppNavigation/AppNavigation.vue b/src/components/AppNavigation/AppNavigation.vue index b7a3e071..8103558e 100644 --- a/src/components/AppNavigation/AppNavigation.vue +++ b/src/components/AppNavigation/AppNavigation.vue @@ -59,7 +59,7 @@ <b-nav-item href="javascript:void(0)"> {{ $t('appNavigation.firmware') }} </b-nav-item> - <b-nav-item href="javascript:void(0)"> + <b-nav-item to="/configuration/network-settings"> {{ $t('appNavigation.networkSettings') }} </b-nav-item> <b-nav-item href="javascript:void(0)"> diff --git a/src/router/index.js b/src/router/index.js index 30532a5f..e35e0f59 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -72,6 +72,14 @@ const routes = [ } }, { + path: '/configuration/network-settings', + name: 'network-settings', + component: () => import('@/views/Configuration/NetworkSettings'), + meta: { + title: 'appPageTitle.networkSettings' + } + }, + { path: '/control/reboot-bmc', name: 'reboot-bmc', component: () => import('@/views/Control/RebootBmc'), diff --git a/src/store/modules/Configuration/NetworkSettingsStore.js b/src/store/modules/Configuration/NetworkSettingsStore.js index f6912c87..524ad342 100644 --- a/src/store/modules/Configuration/NetworkSettingsStore.js +++ b/src/store/modules/Configuration/NetworkSettingsStore.js @@ -1,16 +1,25 @@ import api from '../../api'; +import { find, remove } from 'lodash'; const NetworkSettingsStore = { namespaced: true, state: { - ethernetData: [] + defaultGateway: '', + ethernetData: [], + interfaceOptions: [] }, getters: { - ethernetData: state => state.ethernetData + defaultGateway: state => state.defaultGateway, + ethernetData: state => state.ethernetData, + interfaceOptions: state => state.interfaceOptions }, mutations: { + setDefaultGateway: (state, defaultGateway) => + (state.defaultGateway = defaultGateway), setEthernetData: (state, ethernetData) => - (state.ethernetData = ethernetData) + (state.ethernetData = ethernetData), + setInterfaceOptions: (state, interfaceOptions) => + (state.interfaceOptions = interfaceOptions) }, actions: { async getEthernetData({ commit }) { @@ -32,11 +41,67 @@ const NetworkSettingsStore = { const ethernetData = ethernetInterfaces.map( ethernetInterface => ethernetInterface.data ); + const interfaceOptions = ethernetInterfaces.map( + ethernetName => ethernetName.data.Id + ); + const addresses = ethernetData[0].IPv4StaticAddresses; + + // Default gateway manually set to first gateway saved on the first interface. Default gateway property is WIP on backend + const defaultGateway = addresses.map(ipv4 => { + return ipv4.Gateway; + }); + + commit('setDefaultGateway', defaultGateway[0]); commit('setEthernetData', ethernetData); + commit('setInterfaceOptions', interfaceOptions); }) .catch(error => { console.log('Network Data:', error); }); + }, + + async updateInterfaceSettings({ dispatch, state }, networkSettingsForm) { + const updatedAddresses = networkSettingsForm.staticIpv4; + const originalAddresses = + state.ethernetData[networkSettingsForm.selectedInterfaceIndex] + .IPv4StaticAddresses; + + const addressArray = originalAddresses.map(item => { + const address = item.Address; + if (find(updatedAddresses, { Address: address })) { + remove(updatedAddresses, item => { + return item.Address === address; + }); + return {}; + } else { + return null; + } + }); + + const data = { + HostName: networkSettingsForm.hostname, + MACAddress: networkSettingsForm.macAddress + }; + + // If DHCP disabled, update static DNS or static ipv4 + if (!networkSettingsForm.isDhcpEnabled) { + data.IPv4StaticAddresses = [...addressArray, ...updatedAddresses]; + data.StaticNameServers = networkSettingsForm.staticNameServers; + } + + return await api + .patch( + `/redfish/v1/Managers/bmc/EthernetInterfaces/${networkSettingsForm.interfaceId}`, + data + ) + .then(() => dispatch('getEthernetData')) + .then(() => { + return 'Successfully configured network settings.'; + }) + .catch(error => { + console.log(error); + throw new Error('Error configuring network settings.'); + }); } } }; diff --git a/src/views/Configuration/NetworkSettings/NetworkSettings.vue b/src/views/Configuration/NetworkSettings/NetworkSettings.vue new file mode 100644 index 00000000..18e73a04 --- /dev/null +++ b/src/views/Configuration/NetworkSettings/NetworkSettings.vue @@ -0,0 +1,492 @@ +<template> + <b-container fluid="xl"> + <page-title + description="Configure network settings for the BMC and the Virtualization management interface" + /> + <page-section section-title="Interface"> + <b-row> + <b-col lg="3"> + <b-form-group label-for="interface-select" label="Network interface"> + <b-form-select + id="interface-select" + v-model="selectedInterfaceIndex" + :options="interfaceSelectOptions" + @change="selectInterface" + > + </b-form-select> + </b-form-group> + </b-col> + </b-row> + </page-section> + <b-form novalidate @submit.prevent="submitForm"> + <page-section section-title="System"> + <b-row> + <b-col lg="3"> + <b-form-group label="Default gateway" label-for="default-gateway"> + <b-form-input + id="default-gateway" + v-model.trim="form.gateway" + type="text" + :readonly="dhcpEnabled" + :state="getValidationState($v.form.gateway)" + @change="$v.form.gateway.$touch()" + /> + <b-form-invalid-feedback role="alert"> + <div v-if="!$v.form.gateway.required">Field required</div> + <div v-if="!$v.form.gateway.validateAddress">Invalid</div> + </b-form-invalid-feedback> + </b-form-group> + </b-col> + <b-col lg="3"> + <b-form-group label="Hostname" label-for="hostname-field"> + <b-form-input + id="hostname-field" + v-model.trim="form.hostname" + type="text" + :state="getValidationState($v.form.hostname)" + @change="$v.form.hostname.$touch()" + /> + <b-form-invalid-feedback role="alert"> + <div v-if="!$v.form.hostname.required">Field required</div> + <div v-if="!$v.form.hostname.validateHostname"> + Must be less than 64 characters + </div> + </b-form-invalid-feedback> + </b-form-group> + </b-col> + <b-col lg="3"> + <b-form-group label="MAC address" label-for="mac-address"> + <b-form-input + id="mac-address" + v-model.trim="form.macAddress" + type="text" + :state="getValidationState($v.form.macAddress)" + @change="$v.form.macAddress.$touch()" + /> + <b-form-invalid-feedback role="alert"> + <div v-if="!$v.form.macAddress.required">Field required</div> + <div v-if="!$v.form.macAddress.validateMacAddress">Invalid</div> + </b-form-invalid-feedback> + </b-form-group> + </b-col> + </b-row> + </page-section> + <page-section section-title="Static IPv4"> + <b-row> + <b-col lg="9" class="mb-3"> + <b-table + :fields="ipv4StaticTableFields" + :items="form.ipv4StaticTableItems" + class="mb-0" + > + <template v-slot:cell(Address)="{ item, index }"> + <b-form-input + v-model.trim="item.Address" + :aria-label="'Static IPV4 address ' + (index + 1)" + :readonly="dhcpEnabled" + :state=" + getValidationState( + $v.form.ipv4StaticTableItems.$each.$iter[index].Address + ) + " + @change=" + $v.form.ipv4StaticTableItems.$each.$iter[ + index + ].Address.$touch() + " + /> + <b-form-invalid-feedback role="alert"> + <div + v-if=" + !$v.form.ipv4StaticTableItems.$each.$iter[index].Address + .required + " + > + Field required + </div> + <div + v-if=" + !$v.form.ipv4StaticTableItems.$each.$iter[index].Address + .validateAddress + " + > + Invalid + </div> + </b-form-invalid-feedback> + </template> + <template v-slot:cell(SubnetMask)="{ item, index }"> + <b-form-input + v-model.trim="item.SubnetMask" + :aria-label="'Static IPV4 Subnet mask ' + (index + 1)" + :readonly="dhcpEnabled" + :state=" + getValidationState( + $v.form.ipv4StaticTableItems.$each.$iter[index].SubnetMask + ) + " + @change=" + $v.form.ipv4StaticTableItems.$each.$iter[ + index + ].SubnetMask.$touch() + " + /> + <b-form-invalid-feedback role="alert"> + <div + v-if=" + !$v.form.ipv4StaticTableItems.$each.$iter[index] + .SubnetMask.required + " + > + Field required + </div> + <div + v-if=" + !$v.form.ipv4StaticTableItems.$each.$iter[index] + .SubnetMask.validateAddress + " + > + Invalid + </div> + </b-form-invalid-feedback> + </template> + <template v-slot:cell(actions)="{ item, index }"> + <table-row-action + v-for="(action, actionIndex) in item.actions" + :key="actionIndex" + :value="action.value" + :title="action.title" + @click:tableAction="onDeleteIpv4StaticTableRow($event, index)" + > + <template v-slot:icon> + <icon-trashcan v-if="action.value === 'delete'" /> + </template> + </table-row-action> + </template> + </b-table> + <b-button variant="link" @click="addIpv4StaticTableRow"> + <icon-add /> Add static IP + </b-button> + </b-col> + </b-row> + </page-section> + <page-section section-title="Static DNS"> + <b-row> + <b-col lg="4" class="mb-3"> + <b-table + :fields="dnsTableFields" + :items="form.dnsStaticTableItems" + class="mb-0" + > + <template v-slot:cell(address)="{ item, index }"> + <b-form-input + v-model.trim="item.address" + :aria-label="'Static DNS ' + (index + 1)" + :readonly="dhcpEnabled" + :state=" + getValidationState( + $v.form.dnsStaticTableItems.$each.$iter[index].address + ) + " + @change=" + $v.form.dnsStaticTableItems.$each.$iter[ + index + ].address.$touch() + " + /> + <b-form-invalid-feedback role="alert"> + <div + v-if=" + !$v.form.dnsStaticTableItems.$each.$iter[index].address + .required + " + > + Field required + </div> + <div + v-if=" + !$v.form.dnsStaticTableItems.$each.$iter[index].address + .validateAddress + " + > + Invalid + </div> + </b-form-invalid-feedback> + </template> + <template v-slot:cell(actions)="{ item, index }"> + <table-row-action + v-for="(action, actionIndex) in item.actions" + :key="actionIndex" + :value="action.value" + :title="action.title" + @click:tableAction="onDeleteDnsTableRow($event, index)" + > + <template v-slot:icon> + <icon-trashcan v-if="action.value === 'delete'" /> + </template> + </table-row-action> + </template> + </b-table> + <b-button variant="link" @click="addDnsTableRow"> + <icon-add /> Add DNS server + </b-button> + </b-col> + </b-row> + </page-section> + <b-button + variant="primary" + type="submit" + :disabled="!$v.form.$anyDirty || $v.form.$invalid" + > + Save settings + </b-button> + </b-form> + </b-container> +</template> + +<script> +import IconTrashcan from '@carbon/icons-vue/es/trash-can/20'; +import IconAdd from '@carbon/icons-vue/es/add--alt/20'; +import BVToastMixin from '@/components/Mixins/BVToastMixin'; +import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin'; +import PageSection from '@/components/Global/PageSection'; +import PageTitle from '@/components/Global/PageTitle'; +import TableRowAction from '@/components/Global/TableRowAction'; +import VuelidateMixin from '@/components/Mixins/VuelidateMixin'; +import { mapState } from 'vuex'; +import { required, helpers } from 'vuelidate/lib/validators'; + +// IP address, gateway and subnet pattern +const validateAddress = helpers.regex( + 'validateAddress', + /^(?=.*[^.]$)((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.?){4}$/ +); +// Hostname pattern +const validateHostname = helpers.regex('validateHostname', /^\S{0,64}$/); +// MAC address pattern +const validateMacAddress = helpers.regex( + 'validateMacAddress', + /^(?:[0-9A-Fa-f]{2}([:-]?)[0-9A-Fa-f]{2})(?:(?:\1|\.)(?:[0-9A-Fa-f]{2}([:-]?)[0-9A-Fa-f]{2})){2}$/ +); + +export default { + name: 'NetworkSettings', + components: { + PageTitle, + PageSection, + TableRowAction, + IconTrashcan, + IconAdd + }, + mixins: [BVToastMixin, VuelidateMixin, LoadingBarMixin], + data() { + return { + dhcpEnabled: null, + ipv4Configuration: '', + ipv4StaticTableFields: [ + { key: 'Address', label: 'IP address' }, + { key: 'SubnetMask', label: 'Subnet mask' }, + { key: 'actions', label: '', tdClass: 'text-right' } + ], + dnsTableFields: [ + { key: 'address', label: 'IP address' }, + { key: 'actions', label: '', tdClass: 'text-right' } + ], + selectedInterfaceIndex: 0, + selectedInterface: {}, + form: { + gateway: '', + hostname: '', + macAddress: '', + ipv4StaticTableItems: [], + dnsStaticTableItems: [] + } + }; + }, + validations() { + return { + form: { + gateway: { required, validateAddress }, + hostname: { required, validateHostname }, + ipv4StaticTableItems: { + $each: { + Address: { + required, + validateAddress + }, + SubnetMask: { + required, + validateAddress + } + } + }, + macAddress: { required, validateMacAddress }, + dnsStaticTableItems: { + $each: { + address: { + required, + validateAddress + } + } + } + } + }; + }, + computed: { + ...mapState('networkSettings', [ + 'ethernetData', + 'interfaceOptions', + 'defaultGateway' + ]), + interfaceSelectOptions() { + return this.interfaceOptions.map((option, index) => { + return { + text: option, + value: index + }; + }); + } + }, + watch: { + ethernetData: function() { + this.selectInterface(); + } + }, + created() { + this.startLoader(); + this.$store + .dispatch('networkSettings/getEthernetData') + .finally(() => this.endLoader()); + }, + beforeRouteLeave(to, from, next) { + this.hideLoader(); + next(); + }, + methods: { + selectInterface() { + this.selectedInterface = this.ethernetData[this.selectedInterfaceIndex]; + this.getIpv4StaticTableItems(); + this.getDnsStaticTableItems(); + this.getInterfaceSettings(); + }, + getInterfaceSettings() { + this.form.gateway = this.defaultGateway; + this.form.hostname = this.selectedInterface.HostName; + this.form.macAddress = this.selectedInterface.MACAddress; + this.dhcpEnabled = this.selectedInterface.DHCPv4.DHCPEnabled; + }, + getDnsStaticTableItems() { + const dns = this.selectedInterface.StaticNameServers || []; + this.form.dnsStaticTableItems = dns.map(server => { + return { + address: server, + actions: [ + { + value: 'delete', + enabled: this.dhcpEnabled, + title: 'delete static dns row' + } + ] + }; + }); + }, + addDnsTableRow() { + this.$v.form.dnsStaticTableItems.$touch(); + this.form.dnsStaticTableItems.push({ + address: '', + actions: [ + { + value: 'delete', + enabled: this.dhcpEnabled, + title: 'delete static dns row' + } + ] + }); + }, + deleteDnsTableRow(index) { + this.$v.form.dnsStaticTableItems.$touch(); + this.form.dnsStaticTableItems.splice(index, 1); + }, + onDeleteDnsTableRow(action, row) { + this.deleteDnsTableRow(row); + }, + getIpv4StaticTableItems() { + const addresses = this.selectedInterface.IPv4StaticAddresses || []; + this.form.ipv4StaticTableItems = addresses.map(ipv4 => { + return { + Address: ipv4.Address, + SubnetMask: ipv4.SubnetMask, + actions: [ + { + value: 'delete', + enabled: this.dhcpEnabled, + title: 'delete static ipv4 row' + } + ] + }; + }); + }, + addIpv4StaticTableRow() { + this.$v.form.ipv4StaticTableItems.$touch(); + this.form.ipv4StaticTableItems.push({ + Address: '', + SubnetMask: '', + actions: [ + { + value: 'delete', + enabled: this.dhcpEnabled, + title: 'delete static ipv4 row' + } + ] + }); + }, + deleteIpv4StaticTableRow(index) { + this.$v.form.ipv4StaticTableItems.$touch(); + this.form.ipv4StaticTableItems.splice(index, 1); + }, + onDeleteIpv4StaticTableRow(action, row) { + this.deleteIpv4StaticTableRow(row); + }, + submitForm() { + this.startLoader(); + let networkInterfaceSelected = this.selectedInterface; + let selectedInterfaceIndex = this.selectedInterfaceIndex; + let interfaceId = networkInterfaceSelected.Id; + let isDhcpEnabled = networkInterfaceSelected.DHCPv4.DHCPEnabled; + let macAddress = this.form.macAddress; + let hostname = this.form.hostname; + let networkSettingsForm = { + interfaceId, + hostname, + macAddress, + selectedInterfaceIndex, + isDhcpEnabled + }; + networkSettingsForm.staticIpv4 = this.form.ipv4StaticTableItems.map( + updateIpv4 => { + delete updateIpv4.actions; + updateIpv4.Gateway = this.form.gateway; + return updateIpv4; + } + ); + networkSettingsForm.staticNameServers = this.form.dnsStaticTableItems.map( + updateDns => { + return updateDns.address; + } + ); + this.$store + .dispatch( + 'networkSettings/updateInterfaceSettings', + networkSettingsForm + ) + .then(success => { + this.successToast(success); + }) + .catch(({ message }) => this.errorToast(message)) + .finally(() => { + this.$v.form.$reset(); + this.endLoader(); + }); + } + } +}; +</script> diff --git a/src/views/Configuration/NetworkSettings/index.js b/src/views/Configuration/NetworkSettings/index.js new file mode 100644 index 00000000..1215e1c1 --- /dev/null +++ b/src/views/Configuration/NetworkSettings/index.js @@ -0,0 +1,2 @@ +import NetworkSettings from './NetworkSettings.vue'; +export default NetworkSettings; |