Merge: + DHCP: Support statically configured leases
Close #687 * commit 'b1fbd7c40c640eef575e6c2babc7eab26a525cf8': * openapi: add new dhcp methods * client: fix page scrolling on adding/deleting leases + client: handle static leases form + client: add table to show static leases + doc: DHCP static leases * dhcpd: refactor: use separate objects for ServerConfig and RWMutex + dhcp: /dhcp/status: return static leases * dhcpd: minor improvements * control: refactor: move DHCP lease -> json convertor to a separate function + dhcp: /dhcp/add_static_lease, /dhcp/remove_static_lease: control static lease table + helpers: parseIPv4() * control: use new DHCP functions: CheckConfig, Init, Start * control,dhcp: use dhcpServerConfigJSON struct + dhcpd: CheckConfig() * dhcpd: move code from Start() to Init()
This commit is contained in:
commit
d9e70f5244
@ -19,9 +19,11 @@ Contents:
|
|||||||
* Update client
|
* Update client
|
||||||
* Delete client
|
* Delete client
|
||||||
* Enable DHCP server
|
* Enable DHCP server
|
||||||
|
* "Show DHCP status" command
|
||||||
* "Check DHCP" command
|
* "Check DHCP" command
|
||||||
* "Enable DHCP" command
|
* "Enable DHCP" command
|
||||||
* Static IP check/set
|
* Static IP check/set
|
||||||
|
* Add a static lease
|
||||||
|
|
||||||
|
|
||||||
## First startup
|
## First startup
|
||||||
@ -302,6 +304,38 @@ Algorithm:
|
|||||||
* UI shows the status
|
* UI shows the status
|
||||||
|
|
||||||
|
|
||||||
|
### "Show DHCP status" command
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
GET /control/dhcp/status
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
{
|
||||||
|
"config":{
|
||||||
|
"enabled":false,
|
||||||
|
"interface_name":"...",
|
||||||
|
"gateway_ip":"...",
|
||||||
|
"subnet_mask":"...",
|
||||||
|
"range_start":"...",
|
||||||
|
"range_end":"...",
|
||||||
|
"lease_duration":60,
|
||||||
|
"icmp_timeout_msec":0
|
||||||
|
},
|
||||||
|
"leases":[
|
||||||
|
{"ip":"...","mac":"...","hostname":"...","expires":"..."}
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"static_leases":[
|
||||||
|
{"ip":"...","mac":"...","hostname":"..."}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
### "Check DHCP" command
|
### "Check DHCP" command
|
||||||
|
|
||||||
Request:
|
Request:
|
||||||
@ -428,6 +462,40 @@ If we would set a different IP address, we'd need to replace the IP address for
|
|||||||
ip addr replace dev eth0 192.168.0.1/24
|
ip addr replace dev eth0 192.168.0.1/24
|
||||||
|
|
||||||
|
|
||||||
|
### Add a static lease
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/dhcp/add_static_lease
|
||||||
|
|
||||||
|
{
|
||||||
|
"mac":"...",
|
||||||
|
"ip":"...",
|
||||||
|
"hostname":"..."
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
|
||||||
|
### Remove a static lease
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
POST /control/dhcp/remove_static_lease
|
||||||
|
|
||||||
|
{
|
||||||
|
"mac":"...",
|
||||||
|
"ip":"...",
|
||||||
|
"hostname":"..."
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
200 OK
|
||||||
|
|
||||||
|
|
||||||
## Device Names and Per-client Settings
|
## Device Names and Per-client Settings
|
||||||
|
|
||||||
When a client requests information from DNS server, he's identified by IP address.
|
When a client requests information from DNS server, he's identified by IP address.
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"dhcp_not_found": "It is safe to enable the built-in DHCP server - we didn't find any active DHCP servers on the network. However, we encourage you to re-check it manually as our automatic test currently doesn't give 100% guarantee.",
|
"dhcp_not_found": "It is safe to enable the built-in DHCP server - we didn't find any active DHCP servers on the network. However, we encourage you to re-check it manually as our automatic test currently doesn't give 100% guarantee.",
|
||||||
"dhcp_found": "An active DHCP server is found on the network. It is not safe to enable the built-in DHCP server.",
|
"dhcp_found": "An active DHCP server is found on the network. It is not safe to enable the built-in DHCP server.",
|
||||||
"dhcp_leases": "DHCP leases",
|
"dhcp_leases": "DHCP leases",
|
||||||
|
"dhcp_static_leases": "DHCP static leases",
|
||||||
"dhcp_leases_not_found": "No DHCP leases found",
|
"dhcp_leases_not_found": "No DHCP leases found",
|
||||||
"dhcp_config_saved": "Saved DHCP server config",
|
"dhcp_config_saved": "Saved DHCP server config",
|
||||||
"form_error_required": "Required field",
|
"form_error_required": "Required field",
|
||||||
@ -37,6 +38,13 @@
|
|||||||
"dhcp_error": "We could not determine whether there is another DHCP server in the network.",
|
"dhcp_error": "We could not determine whether there is another DHCP server in the network.",
|
||||||
"dhcp_static_ip_error": "In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually.",
|
"dhcp_static_ip_error": "In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually.",
|
||||||
"dhcp_dynamic_ip_found": "Your system uses dynamic IP address configuration for interface <0>{{interfaceName}}</0>. In order to use DHCP server a static IP address must be set. Your current IP address is <0>{{ipAddress}}</0>. We will automatically set this IP address as static if you press Enable DHCP button.",
|
"dhcp_dynamic_ip_found": "Your system uses dynamic IP address configuration for interface <0>{{interfaceName}}</0>. In order to use DHCP server a static IP address must be set. Your current IP address is <0>{{ipAddress}}</0>. We will automatically set this IP address as static if you press Enable DHCP button.",
|
||||||
|
"dhcp_lease_added": "Static lease \"{{key}}\" successfully added",
|
||||||
|
"dhcp_lease_deleted": "Static lease \"{{key}}\" successfully deleted",
|
||||||
|
"dhcp_new_static_lease": "New static lease",
|
||||||
|
"dhcp_static_leases_not_found": "No DHCP static leases found",
|
||||||
|
"dhcp_add_static_lease": "Add static lease",
|
||||||
|
"delete_confirm": "Are you sure you want to delete \"{{key}}\"?",
|
||||||
|
"form_enter_hostname": "Enter hostname",
|
||||||
"error_details": "Error details",
|
"error_details": "Error details",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
|
@ -662,17 +662,11 @@ export const setDhcpConfigRequest = createAction('SET_DHCP_CONFIG_REQUEST');
|
|||||||
export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS');
|
export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS');
|
||||||
export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE');
|
export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE');
|
||||||
|
|
||||||
// TODO rewrite findActiveDhcp part
|
|
||||||
export const setDhcpConfig = values => async (dispatch, getState) => {
|
export const setDhcpConfig = values => async (dispatch, getState) => {
|
||||||
const { config } = getState().dhcp;
|
const { config } = getState().dhcp;
|
||||||
const updatedConfig = { ...config, ...values };
|
const updatedConfig = { ...config, ...values };
|
||||||
dispatch(setDhcpConfigRequest());
|
dispatch(setDhcpConfigRequest());
|
||||||
if (values.interface_name) {
|
dispatch(findActiveDhcp(values.interface_name));
|
||||||
dispatch(findActiveDhcpRequest());
|
|
||||||
try {
|
|
||||||
const activeDhcp = await apiClient.findActiveDhcp(values.interface_name);
|
|
||||||
dispatch(findActiveDhcpSuccess(activeDhcp));
|
|
||||||
if (!activeDhcp.found) {
|
|
||||||
try {
|
try {
|
||||||
await apiClient.setDhcpConfig(updatedConfig);
|
await apiClient.setDhcpConfig(updatedConfig);
|
||||||
dispatch(setDhcpConfigSuccess(updatedConfig));
|
dispatch(setDhcpConfigSuccess(updatedConfig));
|
||||||
@ -681,63 +675,66 @@ export const setDhcpConfig = values => async (dispatch, getState) => {
|
|||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(setDhcpConfigFailure());
|
dispatch(setDhcpConfigFailure());
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
dispatch(addErrorToast({ error: 'dhcp_found' }));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(addErrorToast({ error }));
|
|
||||||
dispatch(findActiveDhcpFailure());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await apiClient.setDhcpConfig(updatedConfig);
|
|
||||||
dispatch(setDhcpConfigSuccess(updatedConfig));
|
|
||||||
dispatch(addSuccessToast('dhcp_config_saved'));
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(addErrorToast({ error }));
|
|
||||||
dispatch(setDhcpConfigFailure());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST');
|
export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST');
|
||||||
export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE');
|
export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE');
|
||||||
export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS');
|
export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS');
|
||||||
|
|
||||||
// TODO rewrite findActiveDhcp part
|
export const toggleDhcp = values => async (dispatch) => {
|
||||||
export const toggleDhcp = config => async (dispatch) => {
|
|
||||||
dispatch(toggleDhcpRequest());
|
dispatch(toggleDhcpRequest());
|
||||||
|
let config = { ...values, enabled: false };
|
||||||
|
let successMessage = 'disabled_dhcp';
|
||||||
|
|
||||||
|
if (!values.enabled) {
|
||||||
|
config = { ...values, enabled: true };
|
||||||
|
successMessage = 'enabled_dhcp';
|
||||||
|
dispatch(findActiveDhcp(values.interface_name));
|
||||||
|
}
|
||||||
|
|
||||||
if (config.enabled) {
|
|
||||||
try {
|
try {
|
||||||
await apiClient.setDhcpConfig({ ...config, enabled: false });
|
await apiClient.setDhcpConfig(config);
|
||||||
dispatch(toggleDhcpSuccess());
|
dispatch(toggleDhcpSuccess());
|
||||||
dispatch(addSuccessToast('disabled_dhcp'));
|
dispatch(addSuccessToast(successMessage));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(toggleDhcpFailure());
|
dispatch(toggleDhcpFailure());
|
||||||
}
|
}
|
||||||
} else {
|
};
|
||||||
dispatch(findActiveDhcpRequest());
|
|
||||||
try {
|
export const toggleLeaseModal = createAction('TOGGLE_LEASE_MODAL');
|
||||||
const activeDhcp = await apiClient.findActiveDhcp(config.interface_name);
|
|
||||||
dispatch(findActiveDhcpSuccess(activeDhcp));
|
export const addStaticLeaseRequest = createAction('ADD_STATIC_LEASE_REQUEST');
|
||||||
|
export const addStaticLeaseFailure = createAction('ADD_STATIC_LEASE_FAILURE');
|
||||||
if (!activeDhcp.found) {
|
export const addStaticLeaseSuccess = createAction('ADD_STATIC_LEASE_SUCCESS');
|
||||||
try {
|
|
||||||
await apiClient.setDhcpConfig({ ...config, enabled: true });
|
export const addStaticLease = config => async (dispatch) => {
|
||||||
dispatch(toggleDhcpSuccess());
|
dispatch(addStaticLeaseRequest());
|
||||||
dispatch(addSuccessToast('enabled_dhcp'));
|
try {
|
||||||
} catch (error) {
|
const name = config.hostname || config.ip;
|
||||||
dispatch(addErrorToast({ error }));
|
await apiClient.addStaticLease(config);
|
||||||
dispatch(toggleDhcpFailure());
|
dispatch(addStaticLeaseSuccess(config));
|
||||||
}
|
dispatch(addSuccessToast(t('dhcp_lease_added', { key: name })));
|
||||||
} else {
|
dispatch(toggleLeaseModal());
|
||||||
dispatch(addErrorToast({ error: 'dhcp_found' }));
|
} catch (error) {
|
||||||
}
|
dispatch(addErrorToast({ error }));
|
||||||
} catch (error) {
|
dispatch(addStaticLeaseFailure());
|
||||||
dispatch(addErrorToast({ error }));
|
}
|
||||||
dispatch(findActiveDhcpFailure());
|
};
|
||||||
}
|
|
||||||
}
|
export const removeStaticLeaseRequest = createAction('REMOVE_STATIC_LEASE_REQUEST');
|
||||||
|
export const removeStaticLeaseFailure = createAction('REMOVE_STATIC_LEASE_FAILURE');
|
||||||
|
export const removeStaticLeaseSuccess = createAction('REMOVE_STATIC_LEASE_SUCCESS');
|
||||||
|
|
||||||
|
export const removeStaticLease = config => async (dispatch) => {
|
||||||
|
dispatch(removeStaticLeaseRequest());
|
||||||
|
try {
|
||||||
|
const name = config.hostname || config.ip;
|
||||||
|
await apiClient.removeStaticLease(config);
|
||||||
|
dispatch(removeStaticLeaseSuccess(config));
|
||||||
|
dispatch(addSuccessToast(t('dhcp_lease_deleted', { key: name })));
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(addErrorToast({ error }));
|
||||||
|
dispatch(removeStaticLeaseFailure());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -318,6 +318,8 @@ export default class Api {
|
|||||||
DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' };
|
DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' };
|
||||||
DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' };
|
DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' };
|
||||||
DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' };
|
DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' };
|
||||||
|
DHCP_ADD_STATIC_LEASE = { path: 'dhcp/add_static_lease', method: 'POST' };
|
||||||
|
DHCP_REMOVE_STATIC_LEASE = { path: 'dhcp/remove_static_lease', method: 'POST' };
|
||||||
|
|
||||||
getDhcpStatus() {
|
getDhcpStatus() {
|
||||||
const { path, method } = this.DHCP_STATUS;
|
const { path, method } = this.DHCP_STATUS;
|
||||||
@ -347,6 +349,24 @@ export default class Api {
|
|||||||
return this.makeRequest(path, method, parameters);
|
return this.makeRequest(path, method, parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addStaticLease(config) {
|
||||||
|
const { path, method } = this.DHCP_ADD_STATIC_LEASE;
|
||||||
|
const parameters = {
|
||||||
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeStaticLease(config) {
|
||||||
|
const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;
|
||||||
|
const parameters = {
|
||||||
|
data: config,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
return this.makeRequest(path, method, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
// Installation
|
// Installation
|
||||||
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
|
INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
|
||||||
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
|
INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
|
||||||
|
@ -1,22 +1,97 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Field, reduxForm } from 'redux-form';
|
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
||||||
import { withNamespaces } from 'react-i18next';
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
import flow from 'lodash/flow';
|
import flow from 'lodash/flow';
|
||||||
|
|
||||||
import { renderField, required, ipv4, isPositive, toNumber } from '../../../helpers/form';
|
import { renderField, required, ipv4, isPositive, toNumber } from '../../../helpers/form';
|
||||||
|
|
||||||
const Form = (props) => {
|
const renderInterfaces = (interfaces => (
|
||||||
|
Object.keys(interfaces).map((item) => {
|
||||||
|
const option = interfaces[item];
|
||||||
|
const { name } = option;
|
||||||
|
const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':'));
|
||||||
|
let interfaceIP = option.ip_addresses[0];
|
||||||
|
|
||||||
|
if (!onlyIPv6) {
|
||||||
|
option.ip_addresses.forEach((ip) => {
|
||||||
|
if (!ip.includes(':')) {
|
||||||
|
interfaceIP = ip;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<option value={name} key={name} disabled={onlyIPv6}>
|
||||||
|
{name} - {interfaceIP}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
const renderInterfaceValues = (interfaceValues => (
|
||||||
|
<ul className="list-unstyled mt-1 mb-0">
|
||||||
|
<li>
|
||||||
|
<span className="interface__title">MTU: </span>
|
||||||
|
{interfaceValues.mtu}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="interface__title"><Trans>dhcp_hardware_address</Trans>: </span>
|
||||||
|
{interfaceValues.hardware_address}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span className="interface__title"><Trans>dhcp_ip_addresses</Trans>: </span>
|
||||||
|
{
|
||||||
|
interfaceValues.ip_addresses
|
||||||
|
.map(ip => <span key={ip} className="interface__ip">{ip}</span>)
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
));
|
||||||
|
|
||||||
|
let Form = (props) => {
|
||||||
const {
|
const {
|
||||||
t,
|
t,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
submitting,
|
submitting,
|
||||||
invalid,
|
invalid,
|
||||||
|
enabled,
|
||||||
|
interfaces,
|
||||||
|
interfaceValue,
|
||||||
processingConfig,
|
processingConfig,
|
||||||
|
processingInterfaces,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
|
{!processingInterfaces && interfaces &&
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12 col-md-6">
|
||||||
|
<div className="form__group form__group--settings">
|
||||||
|
<label>{t('dhcp_interface_select')}</label>
|
||||||
|
<Field
|
||||||
|
name="interface_name"
|
||||||
|
component="select"
|
||||||
|
className="form-control custom-select"
|
||||||
|
validate={[required]}
|
||||||
|
>
|
||||||
|
<option value="" disabled={enabled}>
|
||||||
|
{t('dhcp_interface_select')}
|
||||||
|
</option>
|
||||||
|
{renderInterfaces(interfaces)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{interfaceValue &&
|
||||||
|
<div className="col-sm-12 col-md-6">
|
||||||
|
{interfaces[interfaceValue] &&
|
||||||
|
renderInterfaceValues(interfaces[interfaceValue])}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<hr/>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<div className="form__group form__group--settings">
|
<div className="form__group form__group--settings">
|
||||||
@ -101,11 +176,24 @@ Form.propTypes = {
|
|||||||
submitting: PropTypes.bool,
|
submitting: PropTypes.bool,
|
||||||
invalid: PropTypes.bool,
|
invalid: PropTypes.bool,
|
||||||
interfaces: PropTypes.object,
|
interfaces: PropTypes.object,
|
||||||
|
interfaceValue: PropTypes.string,
|
||||||
initialValues: PropTypes.object,
|
initialValues: PropTypes.object,
|
||||||
processingConfig: PropTypes.bool,
|
processingConfig: PropTypes.bool,
|
||||||
|
processingInterfaces: PropTypes.bool,
|
||||||
|
enabled: PropTypes.bool,
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const selector = formValueSelector('dhcpForm');
|
||||||
|
|
||||||
|
Form = connect((state) => {
|
||||||
|
const interfaceValue = selector(state, 'interface_name');
|
||||||
|
return {
|
||||||
|
interfaceValue,
|
||||||
|
};
|
||||||
|
})(Form);
|
||||||
|
|
||||||
export default flow([
|
export default flow([
|
||||||
withNamespaces(),
|
withNamespaces(),
|
||||||
reduxForm({ form: 'dhcpForm' }),
|
reduxForm({ form: 'dhcpForm' }),
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Field, reduxForm, formValueSelector } from 'redux-form';
|
|
||||||
import { withNamespaces, Trans } from 'react-i18next';
|
|
||||||
import flow from 'lodash/flow';
|
|
||||||
|
|
||||||
const renderInterfaces = (interfaces => (
|
|
||||||
Object.keys(interfaces).map((item) => {
|
|
||||||
const option = interfaces[item];
|
|
||||||
const { name } = option;
|
|
||||||
const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':'));
|
|
||||||
let interfaceIP = option.ip_addresses[0];
|
|
||||||
|
|
||||||
if (!onlyIPv6) {
|
|
||||||
option.ip_addresses.forEach((ip) => {
|
|
||||||
if (!ip.includes(':')) {
|
|
||||||
interfaceIP = ip;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<option value={name} key={name} disabled={onlyIPv6}>
|
|
||||||
{name} - {interfaceIP}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
));
|
|
||||||
|
|
||||||
const renderInterfaceValues = (interfaceValues => (
|
|
||||||
<ul className="list-unstyled mt-1 mb-0">
|
|
||||||
<li>
|
|
||||||
<span className="interface__title">MTU: </span>
|
|
||||||
{interfaceValues.mtu}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span className="interface__title"><Trans>dhcp_hardware_address</Trans>: </span>
|
|
||||||
{interfaceValues.hardware_address}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span className="interface__title"><Trans>dhcp_ip_addresses</Trans>: </span>
|
|
||||||
{
|
|
||||||
interfaceValues.ip_addresses
|
|
||||||
.map(ip => <span key={ip} className="interface__ip">{ip}</span>)
|
|
||||||
}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
));
|
|
||||||
|
|
||||||
let Interface = (props) => {
|
|
||||||
const {
|
|
||||||
t,
|
|
||||||
handleChange,
|
|
||||||
interfaces,
|
|
||||||
processing,
|
|
||||||
interfaceValue,
|
|
||||||
enabled,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form>
|
|
||||||
{!processing && interfaces &&
|
|
||||||
<div className="row">
|
|
||||||
<div className="col-sm-12 col-md-6">
|
|
||||||
<div className="form__group form__group--settings">
|
|
||||||
<label>{t('dhcp_interface_select')}</label>
|
|
||||||
<Field
|
|
||||||
name="interface_name"
|
|
||||||
component="select"
|
|
||||||
className="form-control custom-select"
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
<option value="" disabled={enabled}>{t('dhcp_interface_select')}</option>
|
|
||||||
{renderInterfaces(interfaces)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{interfaceValue &&
|
|
||||||
<div className="col-sm-12 col-md-6">
|
|
||||||
{interfaces[interfaceValue] &&
|
|
||||||
renderInterfaceValues(interfaces[interfaceValue])}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<hr/>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Interface.propTypes = {
|
|
||||||
handleChange: PropTypes.func,
|
|
||||||
interfaces: PropTypes.object,
|
|
||||||
processing: PropTypes.bool,
|
|
||||||
interfaceValue: PropTypes.string,
|
|
||||||
initialValues: PropTypes.object,
|
|
||||||
enabled: PropTypes.bool,
|
|
||||||
t: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
const selector = formValueSelector('dhcpInterface');
|
|
||||||
|
|
||||||
Interface = connect((state) => {
|
|
||||||
const interfaceValue = selector(state, 'interface_name');
|
|
||||||
return {
|
|
||||||
interfaceValue,
|
|
||||||
};
|
|
||||||
})(Interface);
|
|
||||||
|
|
||||||
export default flow([
|
|
||||||
withNamespaces(),
|
|
||||||
reduxForm({ form: 'dhcpInterface' }),
|
|
||||||
])(Interface);
|
|
96
client/src/components/Settings/Dhcp/StaticLeases/Form.js
Normal file
96
client/src/components/Settings/Dhcp/StaticLeases/Form.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Field, reduxForm } from 'redux-form';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
import flow from 'lodash/flow';
|
||||||
|
|
||||||
|
import { renderField, ipv4, mac, required } from '../../../../helpers/form';
|
||||||
|
|
||||||
|
const Form = (props) => {
|
||||||
|
const {
|
||||||
|
t,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
pristine,
|
||||||
|
submitting,
|
||||||
|
toggleLeaseModal,
|
||||||
|
processingAdding,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
id="mac"
|
||||||
|
name="mac"
|
||||||
|
component={renderField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('form_enter_mac')}
|
||||||
|
validate={[required, mac]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
id="ip"
|
||||||
|
name="ip"
|
||||||
|
component={renderField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('form_enter_ip')}
|
||||||
|
validate={[required, ipv4]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form__group">
|
||||||
|
<Field
|
||||||
|
id="hostname"
|
||||||
|
name="hostname"
|
||||||
|
component={renderField}
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('form_enter_hostname')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<div className="btn-list">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary btn-standard"
|
||||||
|
disabled={submitting}
|
||||||
|
onClick={() => {
|
||||||
|
reset();
|
||||||
|
toggleLeaseModal();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trans>cancel_btn</Trans>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-success btn-standard"
|
||||||
|
disabled={submitting || pristine || processingAdding}
|
||||||
|
>
|
||||||
|
<Trans>save_btn</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Form.propTypes = {
|
||||||
|
pristine: PropTypes.bool.isRequired,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
reset: PropTypes.func.isRequired,
|
||||||
|
submitting: PropTypes.bool.isRequired,
|
||||||
|
toggleLeaseModal: PropTypes.func.isRequired,
|
||||||
|
processingAdding: PropTypes.bool.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default flow([
|
||||||
|
withNamespaces(),
|
||||||
|
reduxForm({ form: 'leaseForm' }),
|
||||||
|
])(Form);
|
49
client/src/components/Settings/Dhcp/StaticLeases/Modal.js
Normal file
49
client/src/components/Settings/Dhcp/StaticLeases/Modal.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
import ReactModal from 'react-modal';
|
||||||
|
|
||||||
|
import Form from './Form';
|
||||||
|
|
||||||
|
const Modal = (props) => {
|
||||||
|
const {
|
||||||
|
isModalOpen,
|
||||||
|
handleSubmit,
|
||||||
|
toggleLeaseModal,
|
||||||
|
processingAdding,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactModal
|
||||||
|
className="Modal__Bootstrap modal-dialog modal-dialog-centered modal-dialog--clients"
|
||||||
|
closeTimeoutMS={0}
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onRequestClose={() => toggleLeaseModal()}
|
||||||
|
>
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h4 className="modal-title">
|
||||||
|
<Trans>dhcp_new_static_lease</Trans>
|
||||||
|
</h4>
|
||||||
|
<button type="button" className="close" onClick={() => toggleLeaseModal()}>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
toggleLeaseModal={toggleLeaseModal}
|
||||||
|
processingAdding={processingAdding}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ReactModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Modal.propTypes = {
|
||||||
|
isModalOpen: PropTypes.bool.isRequired,
|
||||||
|
handleSubmit: PropTypes.func.isRequired,
|
||||||
|
toggleLeaseModal: PropTypes.func.isRequired,
|
||||||
|
processingAdding: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(Modal);
|
112
client/src/components/Settings/Dhcp/StaticLeases/index.js
Normal file
112
client/src/components/Settings/Dhcp/StaticLeases/index.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ReactTable from 'react-table';
|
||||||
|
import { Trans, withNamespaces } from 'react-i18next';
|
||||||
|
|
||||||
|
import Modal from './Modal';
|
||||||
|
|
||||||
|
class StaticLeases extends Component {
|
||||||
|
cellWrap = ({ value }) => (
|
||||||
|
<div className="logs__row logs__row--overflow">
|
||||||
|
<span className="logs__text" title={value}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
handleSubmit = (data) => {
|
||||||
|
this.props.addStaticLease(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDelete = (ip, mac, hostname = '') => {
|
||||||
|
const name = hostname || ip;
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
if (window.confirm(this.props.t('delete_confirm', { key: name }))) {
|
||||||
|
this.props.removeStaticLease({ ip, mac, hostname });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isModalOpen,
|
||||||
|
toggleLeaseModal,
|
||||||
|
processingAdding,
|
||||||
|
processingDeleting,
|
||||||
|
staticLeases,
|
||||||
|
t,
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<ReactTable
|
||||||
|
data={staticLeases || []}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
Header: 'MAC',
|
||||||
|
accessor: 'mac',
|
||||||
|
Cell: this.cellWrap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'IP',
|
||||||
|
accessor: 'ip',
|
||||||
|
Cell: this.cellWrap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: <Trans>dhcp_table_hostname</Trans>,
|
||||||
|
accessor: 'hostname',
|
||||||
|
Cell: this.cellWrap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: <Trans>actions_table_header</Trans>,
|
||||||
|
accessor: 'actions',
|
||||||
|
maxWidth: 150,
|
||||||
|
Cell: (row) => {
|
||||||
|
const { ip, mac, hostname } = row.original;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="logs__row logs__row--center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-outline-secondary btn-sm"
|
||||||
|
title={t('delete_table_action')}
|
||||||
|
disabled={processingDeleting}
|
||||||
|
onClick={() =>
|
||||||
|
this.handleDelete(ip, mac, hostname)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg className="icons">
|
||||||
|
<use xlinkHref="#delete" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
showPagination={false}
|
||||||
|
noDataText={t('dhcp_static_leases_not_found')}
|
||||||
|
className="-striped -highlight card-table-overflow"
|
||||||
|
minRows={6}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
isModalOpen={isModalOpen}
|
||||||
|
toggleLeaseModal={toggleLeaseModal}
|
||||||
|
handleSubmit={this.handleSubmit}
|
||||||
|
processingAdding={processingAdding}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StaticLeases.propTypes = {
|
||||||
|
staticLeases: PropTypes.array.isRequired,
|
||||||
|
isModalOpen: PropTypes.bool.isRequired,
|
||||||
|
toggleLeaseModal: PropTypes.func.isRequired,
|
||||||
|
removeStaticLease: PropTypes.func.isRequired,
|
||||||
|
addStaticLease: PropTypes.func.isRequired,
|
||||||
|
processingAdding: PropTypes.bool.isRequired,
|
||||||
|
processingDeleting: PropTypes.bool.isRequired,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withNamespaces()(StaticLeases);
|
@ -6,13 +6,15 @@ import { Trans, withNamespaces } from 'react-i18next';
|
|||||||
import { DHCP_STATUS_RESPONSE } from '../../../helpers/constants';
|
import { DHCP_STATUS_RESPONSE } from '../../../helpers/constants';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
import Leases from './Leases';
|
import Leases from './Leases';
|
||||||
import Interface from './Interface';
|
import StaticLeases from './StaticLeases/index';
|
||||||
import Card from '../../ui/Card';
|
import Card from '../../ui/Card';
|
||||||
import Accordion from '../../ui/Accordion';
|
import Accordion from '../../ui/Accordion';
|
||||||
|
|
||||||
class Dhcp extends Component {
|
class Dhcp extends Component {
|
||||||
handleFormSubmit = (values) => {
|
handleFormSubmit = (values) => {
|
||||||
|
if (values.interface_name) {
|
||||||
this.props.setDhcpConfig(values);
|
this.props.setDhcpConfig(values);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleToggle = (config) => {
|
handleToggle = (config) => {
|
||||||
@ -168,18 +170,16 @@ class Dhcp extends Component {
|
|||||||
<div className="dhcp">
|
<div className="dhcp">
|
||||||
{!dhcp.processing &&
|
{!dhcp.processing &&
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Interface
|
|
||||||
onChange={this.handleFormSubmit}
|
|
||||||
initialValues={{ interface_name }}
|
|
||||||
interfaces={dhcp.interfaces}
|
|
||||||
processing={dhcp.processingInterfaces}
|
|
||||||
enabled={dhcp.config.enabled}
|
|
||||||
/>
|
|
||||||
<Form
|
<Form
|
||||||
onSubmit={this.handleFormSubmit}
|
onSubmit={this.handleFormSubmit}
|
||||||
initialValues={{ ...values }}
|
initialValues={{
|
||||||
|
interface_name,
|
||||||
|
...values,
|
||||||
|
}}
|
||||||
interfaces={dhcp.interfaces}
|
interfaces={dhcp.interfaces}
|
||||||
processingConfig={dhcp.processingConfig}
|
processingConfig={dhcp.processingConfig}
|
||||||
|
processingInterfaces={dhcp.processingInterfaces}
|
||||||
|
enabled={enabled}
|
||||||
/>
|
/>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div className="card-actions mb-3">
|
<div className="card-actions mb-3">
|
||||||
@ -188,11 +188,11 @@ class Dhcp extends Component {
|
|||||||
type="button"
|
type="button"
|
||||||
className={statusButtonClass}
|
className={statusButtonClass}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
this.props.findActiveDhcp(dhcp.config.interface_name)
|
this.props.findActiveDhcp(interface_name)
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
dhcp.config.enabled
|
enabled
|
||||||
|| !dhcp.config.interface_name
|
|| !interface_name
|
||||||
|| dhcp.processingConfig
|
|| dhcp.processingConfig
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -211,6 +211,7 @@ class Dhcp extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
{!dhcp.processing && dhcp.config.enabled &&
|
{!dhcp.processing && dhcp.config.enabled &&
|
||||||
|
<Fragment>
|
||||||
<Card title={ t('dhcp_leases') } bodyType="card-body box-body--settings">
|
<Card title={ t('dhcp_leases') } bodyType="card-body box-body--settings">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col">
|
<div className="col">
|
||||||
@ -218,6 +219,31 @@ class Dhcp extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card title={ t('dhcp_static_leases') } bodyType="card-body box-body--settings">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-12">
|
||||||
|
<StaticLeases
|
||||||
|
staticLeases={dhcp.staticLeases}
|
||||||
|
isModalOpen={dhcp.isModalOpen}
|
||||||
|
addStaticLease={this.props.addStaticLease}
|
||||||
|
removeStaticLease={this.props.removeStaticLease}
|
||||||
|
toggleLeaseModal={this.props.toggleLeaseModal}
|
||||||
|
processingAdding={dhcp.processingAdding}
|
||||||
|
processingDeleting={dhcp.processingDeleting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-success btn-standard mt-3"
|
||||||
|
onClick={() => this.props.toggleLeaseModal()}
|
||||||
|
>
|
||||||
|
<Trans>dhcp_add_static_lease</Trans>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Fragment>
|
||||||
}
|
}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@ -231,6 +257,9 @@ Dhcp.propTypes = {
|
|||||||
setDhcpConfig: PropTypes.func,
|
setDhcpConfig: PropTypes.func,
|
||||||
findActiveDhcp: PropTypes.func,
|
findActiveDhcp: PropTypes.func,
|
||||||
handleSubmit: PropTypes.func,
|
handleSubmit: PropTypes.func,
|
||||||
|
addStaticLease: PropTypes.func,
|
||||||
|
removeStaticLease: PropTypes.func,
|
||||||
|
toggleLeaseModal: PropTypes.func,
|
||||||
t: PropTypes.func,
|
t: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -128,6 +128,9 @@ class Settings extends Component {
|
|||||||
getDhcpStatus={this.props.getDhcpStatus}
|
getDhcpStatus={this.props.getDhcpStatus}
|
||||||
findActiveDhcp={this.props.findActiveDhcp}
|
findActiveDhcp={this.props.findActiveDhcp}
|
||||||
setDhcpConfig={this.props.setDhcpConfig}
|
setDhcpConfig={this.props.setDhcpConfig}
|
||||||
|
addStaticLease={this.props.addStaticLease}
|
||||||
|
removeStaticLease={this.props.removeStaticLease}
|
||||||
|
toggleLeaseModal={this.props.toggleLeaseModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,9 @@ import {
|
|||||||
getDhcpInterfaces,
|
getDhcpInterfaces,
|
||||||
setDhcpConfig,
|
setDhcpConfig,
|
||||||
findActiveDhcp,
|
findActiveDhcp,
|
||||||
|
addStaticLease,
|
||||||
|
removeStaticLease,
|
||||||
|
toggleLeaseModal,
|
||||||
} from '../actions';
|
} from '../actions';
|
||||||
import {
|
import {
|
||||||
getTlsStatus,
|
getTlsStatus,
|
||||||
@ -62,6 +65,9 @@ const mapDispatchToProps = {
|
|||||||
updateClient,
|
updateClient,
|
||||||
deleteClient,
|
deleteClient,
|
||||||
toggleClientModal,
|
toggleClientModal,
|
||||||
|
addStaticLease,
|
||||||
|
removeStaticLease,
|
||||||
|
toggleLeaseModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
@ -287,11 +287,18 @@ const dhcp = handleActions({
|
|||||||
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
|
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
|
||||||
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),
|
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),
|
||||||
[actions.getDhcpStatusSuccess]: (state, { payload }) => {
|
[actions.getDhcpStatusSuccess]: (state, { payload }) => {
|
||||||
|
const {
|
||||||
|
static_leases: staticLeases,
|
||||||
|
...values
|
||||||
|
} = payload;
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
...state,
|
...state,
|
||||||
...payload,
|
staticLeases,
|
||||||
processing: false,
|
processing: false,
|
||||||
|
...values,
|
||||||
};
|
};
|
||||||
|
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -344,17 +351,62 @@ const dhcp = handleActions({
|
|||||||
const newState = { ...state, config: newConfig, processingConfig: false };
|
const newState = { ...state, config: newConfig, processingConfig: false };
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[actions.toggleLeaseModal]: (state) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
isModalOpen: !state.isModalOpen,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.addStaticLeaseRequest]: state => ({ ...state, processingAdding: true }),
|
||||||
|
[actions.addStaticLeaseFailure]: state => ({ ...state, processingAdding: false }),
|
||||||
|
[actions.addStaticLeaseSuccess]: (state, { payload }) => {
|
||||||
|
const {
|
||||||
|
ip, mac, hostname,
|
||||||
|
} = payload;
|
||||||
|
const newLease = {
|
||||||
|
ip,
|
||||||
|
mac,
|
||||||
|
hostname: hostname || '',
|
||||||
|
};
|
||||||
|
const leases = [...state.staticLeases, newLease];
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
staticLeases: leases,
|
||||||
|
processingAdding: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
|
|
||||||
|
[actions.removeStaticLeaseRequest]: state => ({ ...state, processingDeleting: true }),
|
||||||
|
[actions.removeStaticLeaseFailure]: state => ({ ...state, processingDeleting: false }),
|
||||||
|
[actions.removeStaticLeaseSuccess]: (state, { payload }) => {
|
||||||
|
const leaseToRemove = payload.ip;
|
||||||
|
const leases = state.staticLeases.filter(item => item.ip !== leaseToRemove);
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
staticLeases: leases,
|
||||||
|
processingDeleting: false,
|
||||||
|
};
|
||||||
|
return newState;
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
processing: true,
|
processing: true,
|
||||||
processingStatus: false,
|
processingStatus: false,
|
||||||
processingInterfaces: false,
|
processingInterfaces: false,
|
||||||
processingDhcp: false,
|
processingDhcp: false,
|
||||||
processingConfig: false,
|
processingConfig: false,
|
||||||
|
processingAdding: false,
|
||||||
|
processingDeleting: false,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
check: null,
|
check: null,
|
||||||
leases: [],
|
leases: [],
|
||||||
|
staticLeases: [],
|
||||||
|
isModalOpen: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
|
@ -993,6 +993,8 @@ func registerControlHandlers() {
|
|||||||
http.HandleFunc("/control/dhcp/interfaces", postInstall(optionalAuth(ensureGET(handleDHCPInterfaces))))
|
http.HandleFunc("/control/dhcp/interfaces", postInstall(optionalAuth(ensureGET(handleDHCPInterfaces))))
|
||||||
http.HandleFunc("/control/dhcp/set_config", postInstall(optionalAuth(ensurePOST(handleDHCPSetConfig))))
|
http.HandleFunc("/control/dhcp/set_config", postInstall(optionalAuth(ensurePOST(handleDHCPSetConfig))))
|
||||||
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
|
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
|
||||||
|
http.HandleFunc("/control/dhcp/add_static_lease", postInstall(optionalAuth(ensurePOST(handleDHCPAddStaticLease))))
|
||||||
|
http.HandleFunc("/control/dhcp/remove_static_lease", postInstall(optionalAuth(ensurePOST(handleDHCPRemoveStaticLease))))
|
||||||
|
|
||||||
RegisterTLSHandlers()
|
RegisterTLSHandlers()
|
||||||
RegisterClientsHandlers()
|
RegisterClientsHandlers()
|
||||||
|
137
dhcp.go
137
dhcp.go
@ -20,23 +20,33 @@ import (
|
|||||||
|
|
||||||
var dhcpServer = dhcpd.Server{}
|
var dhcpServer = dhcpd.Server{}
|
||||||
|
|
||||||
|
// []dhcpd.Lease -> JSON
|
||||||
|
func convertLeases(inputLeases []dhcpd.Lease, includeExpires bool) []map[string]string {
|
||||||
|
leases := []map[string]string{}
|
||||||
|
for _, l := range inputLeases {
|
||||||
|
lease := map[string]string{
|
||||||
|
"mac": l.HWAddr.String(),
|
||||||
|
"ip": l.IP.String(),
|
||||||
|
"hostname": l.Hostname,
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeExpires {
|
||||||
|
lease["expires"] = l.Expiry.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
leases = append(leases, lease)
|
||||||
|
}
|
||||||
|
return leases
|
||||||
|
}
|
||||||
|
|
||||||
func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Tracef("%s %v", r.Method, r.URL)
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
rawLeases := dhcpServer.Leases()
|
leases := convertLeases(dhcpServer.Leases(), true)
|
||||||
leases := []map[string]string{}
|
staticLeases := convertLeases(dhcpServer.StaticLeases(), false)
|
||||||
for i := range rawLeases {
|
|
||||||
lease := map[string]string{
|
|
||||||
"mac": rawLeases[i].HWAddr.String(),
|
|
||||||
"ip": rawLeases[i].IP.String(),
|
|
||||||
"hostname": rawLeases[i].Hostname,
|
|
||||||
"expires": rawLeases[i].Expiry.Format(time.RFC3339),
|
|
||||||
}
|
|
||||||
leases = append(leases, lease)
|
|
||||||
|
|
||||||
}
|
|
||||||
status := map[string]interface{}{
|
status := map[string]interface{}{
|
||||||
"config": config.DHCP,
|
"config": config.DHCP,
|
||||||
"leases": leases,
|
"leases": leases,
|
||||||
|
"static_leases": staticLeases,
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@ -47,20 +57,43 @@ func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type leaseJSON struct {
|
||||||
|
HWAddr string `json:"mac"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dhcpServerConfigJSON struct {
|
||||||
|
dhcpd.ServerConfig `json:",inline"`
|
||||||
|
StaticLeases []leaseJSON `json:"static_leases"`
|
||||||
|
}
|
||||||
|
|
||||||
func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Tracef("%s %v", r.Method, r.URL)
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
newconfig := dhcpd.ServerConfig{}
|
newconfig := dhcpServerConfigJSON{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&newconfig)
|
err := json.NewDecoder(r.Body).Decode(&newconfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
|
httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = dhcpServer.CheckConfig(newconfig.ServerConfig)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = dhcpServer.Stop()
|
err = dhcpServer.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("failed to stop the DHCP server: %s", err)
|
log.Error("failed to stop the DHCP server: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = dhcpServer.Init(newconfig.ServerConfig)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if newconfig.Enabled {
|
if newconfig.Enabled {
|
||||||
|
|
||||||
staticIP, err := hasStaticIP(newconfig.InterfaceName)
|
staticIP, err := hasStaticIP(newconfig.InterfaceName)
|
||||||
@ -72,14 +105,14 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dhcpServer.Start(&newconfig)
|
err = dhcpServer.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
|
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.DHCP = newconfig
|
config.DHCP = newconfig.ServerConfig
|
||||||
httpUpdateConfigReloadDNSReturnOK(w, r)
|
httpUpdateConfigReloadDNSReturnOK(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,12 +366,80 @@ func setStaticIP(ifaceName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
|
lj := leaseJSON{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&lj)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := parseIPv4(lj.IP)
|
||||||
|
if ip == nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "invalid IP")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mac, _ := net.ParseMAC(lj.HWAddr)
|
||||||
|
|
||||||
|
lease := dhcpd.Lease{
|
||||||
|
IP: ip,
|
||||||
|
HWAddr: mac,
|
||||||
|
Hostname: lj.Hostname,
|
||||||
|
}
|
||||||
|
err = dhcpServer.AddStaticLease(lease)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Tracef("%s %v", r.Method, r.URL)
|
||||||
|
|
||||||
|
lj := leaseJSON{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&lj)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := parseIPv4(lj.IP)
|
||||||
|
if ip == nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "invalid IP")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mac, _ := net.ParseMAC(lj.HWAddr)
|
||||||
|
|
||||||
|
lease := dhcpd.Lease{
|
||||||
|
IP: ip,
|
||||||
|
HWAddr: mac,
|
||||||
|
Hostname: lj.Hostname,
|
||||||
|
}
|
||||||
|
err = dhcpServer.RemoveStaticLease(lease)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, http.StatusBadRequest, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
returnOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
func startDHCPServer() error {
|
func startDHCPServer() error {
|
||||||
if !config.DHCP.Enabled {
|
if !config.DHCP.Enabled {
|
||||||
// not enabled, don't do anything
|
// not enabled, don't do anything
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := dhcpServer.Start(&config.DHCP)
|
|
||||||
|
err := dhcpServer.Init(config.DHCP)
|
||||||
|
if err != nil {
|
||||||
|
return errorx.Decorate(err, "Couldn't init DHCP server")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dhcpServer.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.Decorate(err, "Couldn't start DHCP server")
|
return errorx.Decorate(err, "Couldn't start DHCP server")
|
||||||
}
|
}
|
||||||
@ -350,10 +451,6 @@ func stopDHCPServer() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !dhcpServer.Enabled {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := dhcpServer.Stop()
|
err := dhcpServer.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorx.Decorate(err, "Couldn't stop DHCP server")
|
return errorx.Decorate(err, "Couldn't stop DHCP server")
|
||||||
|
19
dhcpd/db.go
19
dhcpd/db.go
@ -23,8 +23,20 @@ type leaseJSON struct {
|
|||||||
Expiry int64 `json:"exp"`
|
Expiry int64 `json:"exp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safe version of dhcp4.IPInRange()
|
||||||
|
func ipInRange(start, stop, ip net.IP) bool {
|
||||||
|
if len(start) != len(stop) ||
|
||||||
|
len(start) != len(ip) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return dhcp4.IPInRange(start, stop, ip)
|
||||||
|
}
|
||||||
|
|
||||||
// Load lease table from DB
|
// Load lease table from DB
|
||||||
func (s *Server) dbLoad() {
|
func (s *Server) dbLoad() {
|
||||||
|
s.leases = nil
|
||||||
|
s.IPpool = make(map[[4]byte]net.HardwareAddr)
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(dbFilename)
|
data, err := ioutil.ReadFile(dbFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
@ -40,13 +52,12 @@ func (s *Server) dbLoad() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.leases = nil
|
|
||||||
s.IPpool = make(map[[4]byte]net.HardwareAddr)
|
|
||||||
|
|
||||||
numLeases := len(obj)
|
numLeases := len(obj)
|
||||||
for i := range obj {
|
for i := range obj {
|
||||||
|
|
||||||
if !dhcp4.IPInRange(s.leaseStart, s.leaseStop, obj[i].IP) {
|
if obj[i].Expiry != leaseExpireStatic &&
|
||||||
|
!ipInRange(s.leaseStart, s.leaseStop, obj[i].IP) {
|
||||||
|
|
||||||
log.Tracef("Skipping a lease with IP %s: not within current IP range", obj[i].IP)
|
log.Tracef("Skipping a lease with IP %s: not within current IP range", obj[i].IP)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
195
dhcpd/dhcpd.go
195
dhcpd/dhcpd.go
@ -14,6 +14,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const defaultDiscoverTime = time.Second * 3
|
const defaultDiscoverTime = time.Second * 3
|
||||||
|
const leaseExpireStatic = 1
|
||||||
|
|
||||||
// Lease contains the necessary information about a DHCP lease
|
// Lease contains the necessary information about a DHCP lease
|
||||||
// field ordering is important -- yaml fields will mirror ordering from here
|
// field ordering is important -- yaml fields will mirror ordering from here
|
||||||
@ -21,6 +22,9 @@ type Lease struct {
|
|||||||
HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"`
|
HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"`
|
||||||
IP net.IP `json:"ip"`
|
IP net.IP `json:"ip"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
|
|
||||||
|
// Lease expiration time
|
||||||
|
// 1: static lease
|
||||||
Expiry time.Time `json:"expires"`
|
Expiry time.Time `json:"expires"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +57,7 @@ type Server struct {
|
|||||||
|
|
||||||
// leases
|
// leases
|
||||||
leases []*Lease
|
leases []*Lease
|
||||||
|
leasesLock sync.RWMutex
|
||||||
leaseStart net.IP // parsed from config RangeStart
|
leaseStart net.IP // parsed from config RangeStart
|
||||||
leaseStop net.IP // parsed from config RangeEnd
|
leaseStop net.IP // parsed from config RangeEnd
|
||||||
leaseTime time.Duration // parsed from config LeaseDuration
|
leaseTime time.Duration // parsed from config LeaseDuration
|
||||||
@ -61,8 +66,7 @@ type Server struct {
|
|||||||
// IP address pool -- if entry is in the pool, then it's attached to a lease
|
// IP address pool -- if entry is in the pool, then it's attached to a lease
|
||||||
IPpool map[[4]byte]net.HardwareAddr
|
IPpool map[[4]byte]net.HardwareAddr
|
||||||
|
|
||||||
ServerConfig
|
conf ServerConfig
|
||||||
sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print information about the available network interfaces
|
// Print information about the available network interfaces
|
||||||
@ -75,62 +79,65 @@ func printInterfaces() {
|
|||||||
log.Info("Available network interfaces: %s", buf.String())
|
log.Info("Available network interfaces: %s", buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start will listen on port 67 and serve DHCP requests.
|
// CheckConfig checks the configuration
|
||||||
// Even though config can be nil, it is not optional (at least for now), since there are no default values (yet).
|
func (s *Server) CheckConfig(config ServerConfig) error {
|
||||||
func (s *Server) Start(config *ServerConfig) error {
|
tmpServer := Server{}
|
||||||
if config != nil {
|
return tmpServer.setConfig(config)
|
||||||
s.ServerConfig = *config
|
}
|
||||||
}
|
|
||||||
|
|
||||||
iface, err := net.InterfaceByName(s.InterfaceName)
|
// Init checks the configuration and initializes the server
|
||||||
|
func (s *Server) Init(config ServerConfig) error {
|
||||||
|
err := s.setConfig(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.dbLoad()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) setConfig(config ServerConfig) error {
|
||||||
|
s.conf = config
|
||||||
|
|
||||||
|
iface, err := net.InterfaceByName(config.InterfaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.closeConn() // in case it was already started
|
|
||||||
printInterfaces()
|
printInterfaces()
|
||||||
return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName)
|
return wrapErrPrint(err, "Couldn't find interface by name %s", config.InterfaceName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get ipv4 address of an interface
|
// get ipv4 address of an interface
|
||||||
s.ipnet = getIfaceIPv4(iface)
|
s.ipnet = getIfaceIPv4(iface)
|
||||||
if s.ipnet == nil {
|
if s.ipnet == nil {
|
||||||
s.closeConn() // in case it was already started
|
return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", config.InterfaceName, iface)
|
||||||
return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", s.InterfaceName, iface)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.LeaseDuration == 0 {
|
if config.LeaseDuration == 0 {
|
||||||
s.leaseTime = time.Hour * 2
|
s.leaseTime = time.Hour * 2
|
||||||
s.LeaseDuration = uint(s.leaseTime.Seconds())
|
|
||||||
} else {
|
} else {
|
||||||
s.leaseTime = time.Second * time.Duration(s.LeaseDuration)
|
s.leaseTime = time.Second * time.Duration(config.LeaseDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.leaseStart, err = parseIPv4(s.RangeStart)
|
s.leaseStart, err = parseIPv4(config.RangeStart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return wrapErrPrint(err, "Failed to parse range start address %s", config.RangeStart)
|
||||||
s.closeConn() // in case it was already started
|
|
||||||
return wrapErrPrint(err, "Failed to parse range start address %s", s.RangeStart)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.leaseStop, err = parseIPv4(s.RangeEnd)
|
s.leaseStop, err = parseIPv4(config.RangeEnd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.closeConn() // in case it was already started
|
return wrapErrPrint(err, "Failed to parse range end address %s", config.RangeEnd)
|
||||||
return wrapErrPrint(err, "Failed to parse range end address %s", s.RangeEnd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subnet, err := parseIPv4(s.SubnetMask)
|
subnet, err := parseIPv4(config.SubnetMask)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.closeConn() // in case it was already started
|
return wrapErrPrint(err, "Failed to parse subnet mask %s", config.SubnetMask)
|
||||||
return wrapErrPrint(err, "Failed to parse subnet mask %s", s.SubnetMask)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if !bytes.Equal(subnet, s.ipnet.Mask) {
|
// if !bytes.Equal(subnet, s.ipnet.Mask) {
|
||||||
// s.closeConn() // in case it was already started
|
|
||||||
// return wrapErrPrint(err, "specified subnet mask %s does not meatch interface %s subnet mask %s", s.SubnetMask, s.InterfaceName, s.ipnet.Mask)
|
// return wrapErrPrint(err, "specified subnet mask %s does not meatch interface %s subnet mask %s", s.SubnetMask, s.InterfaceName, s.ipnet.Mask)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
router, err := parseIPv4(s.GatewayIP)
|
router, err := parseIPv4(config.GatewayIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.closeConn() // in case it was already started
|
return wrapErrPrint(err, "Failed to parse gateway IP %s", config.GatewayIP)
|
||||||
return wrapErrPrint(err, "Failed to parse gateway IP %s", s.GatewayIP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.leaseOptions = dhcp4.Options{
|
s.leaseOptions = dhcp4.Options{
|
||||||
@ -139,12 +146,21 @@ func (s *Server) Start(config *ServerConfig) error {
|
|||||||
dhcp4.OptionDomainNameServer: s.ipnet.IP,
|
dhcp4.OptionDomainNameServer: s.ipnet.IP,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start will listen on port 67 and serve DHCP requests.
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
|
||||||
// TODO: don't close if interface and addresses are the same
|
// TODO: don't close if interface and addresses are the same
|
||||||
if s.conn != nil {
|
if s.conn != nil {
|
||||||
s.closeConn()
|
s.closeConn()
|
||||||
}
|
}
|
||||||
|
|
||||||
s.dbLoad()
|
iface, err := net.InterfaceByName(s.conf.InterfaceName)
|
||||||
|
if err != nil {
|
||||||
|
return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName)
|
||||||
|
}
|
||||||
|
|
||||||
c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets
|
c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -229,9 +245,9 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
|
|||||||
log.Tracef("Assigning IP address %s to %s (lease for %s expired at %s)",
|
log.Tracef("Assigning IP address %s to %s (lease for %s expired at %s)",
|
||||||
s.leases[i].IP, hwaddr, s.leases[i].HWAddr, s.leases[i].Expiry)
|
s.leases[i].IP, hwaddr, s.leases[i].HWAddr, s.leases[i].Expiry)
|
||||||
lease.IP = s.leases[i].IP
|
lease.IP = s.leases[i].IP
|
||||||
s.Lock()
|
s.leasesLock.Lock()
|
||||||
s.leases[i] = lease
|
s.leases[i] = lease
|
||||||
s.Unlock()
|
s.leasesLock.Unlock()
|
||||||
|
|
||||||
s.reserveIP(lease.IP, hwaddr)
|
s.reserveIP(lease.IP, hwaddr)
|
||||||
return lease, nil
|
return lease, nil
|
||||||
@ -239,9 +255,9 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
|
|||||||
|
|
||||||
log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String())
|
log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String())
|
||||||
lease.IP = ip
|
lease.IP = ip
|
||||||
s.Lock()
|
s.leasesLock.Lock()
|
||||||
s.leases = append(s.leases, lease)
|
s.leases = append(s.leases, lease)
|
||||||
s.Unlock()
|
s.leasesLock.Unlock()
|
||||||
return lease, nil
|
return lease, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,7 +277,7 @@ func (s *Server) findLease(p dhcp4.Packet) *Lease {
|
|||||||
func (s *Server) findExpiredLease() int {
|
func (s *Server) findExpiredLease() int {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
for i, lease := range s.leases {
|
for i, lease := range s.leases {
|
||||||
if lease.Expiry.Unix() <= now {
|
if lease.Expiry.Unix() <= now && lease.Expiry.Unix() != leaseExpireStatic {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -269,11 +285,6 @@ func (s *Server) findExpiredLease() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
|
func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
|
||||||
// if IP pool is nil, lazy initialize it
|
|
||||||
if s.IPpool == nil {
|
|
||||||
s.IPpool = make(map[[4]byte]net.HardwareAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// go from start to end, find unreserved IP
|
// go from start to end, find unreserved IP
|
||||||
var foundIP net.IP
|
var foundIP net.IP
|
||||||
for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ {
|
for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ {
|
||||||
@ -361,7 +372,7 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh
|
|||||||
// Return TRUE if it doesn't reply, which probably means that the IP is available
|
// Return TRUE if it doesn't reply, which probably means that the IP is available
|
||||||
func (s *Server) addrAvailable(target net.IP) bool {
|
func (s *Server) addrAvailable(target net.IP) bool {
|
||||||
|
|
||||||
if s.ICMPTimeout == 0 {
|
if s.conf.ICMPTimeout == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,7 +383,7 @@ func (s *Server) addrAvailable(target net.IP) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pinger.SetPrivileged(true)
|
pinger.SetPrivileged(true)
|
||||||
pinger.Timeout = time.Duration(s.ICMPTimeout) * time.Millisecond
|
pinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond
|
||||||
pinger.Count = 1
|
pinger.Count = 1
|
||||||
reply := false
|
reply := false
|
||||||
pinger.OnRecv = func(pkt *ping.Packet) {
|
pinger.OnRecv = func(pkt *ping.Packet) {
|
||||||
@ -395,11 +406,11 @@ func (s *Server) addrAvailable(target net.IP) bool {
|
|||||||
func (s *Server) blacklistLease(lease *Lease) {
|
func (s *Server) blacklistLease(lease *Lease) {
|
||||||
hw := make(net.HardwareAddr, 6)
|
hw := make(net.HardwareAddr, 6)
|
||||||
s.reserveIP(lease.IP, hw)
|
s.reserveIP(lease.IP, hw)
|
||||||
s.Lock()
|
s.leasesLock.Lock()
|
||||||
lease.HWAddr = hw
|
lease.HWAddr = hw
|
||||||
lease.Hostname = ""
|
lease.Hostname = ""
|
||||||
lease.Expiry = time.Now().Add(s.leaseTime)
|
lease.Expiry = time.Now().Add(s.leaseTime)
|
||||||
s.Unlock()
|
s.leasesLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return TRUE if DHCP packet is correct
|
// Return TRUE if DHCP packet is correct
|
||||||
@ -516,21 +527,103 @@ func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Pack
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddStaticLease adds a static lease (thread-safe)
|
||||||
|
func (s *Server) AddStaticLease(l Lease) error {
|
||||||
|
if s.IPpool == nil {
|
||||||
|
return fmt.Errorf("DHCP server isn't started")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(l.IP) != 4 {
|
||||||
|
return fmt.Errorf("Invalid IP")
|
||||||
|
}
|
||||||
|
if len(l.HWAddr) != 6 {
|
||||||
|
return fmt.Errorf("Invalid MAC")
|
||||||
|
}
|
||||||
|
l.Expiry = time.Unix(leaseExpireStatic, 0)
|
||||||
|
|
||||||
|
s.leasesLock.Lock()
|
||||||
|
defer s.leasesLock.Unlock()
|
||||||
|
|
||||||
|
if s.findReservedHWaddr(l.IP) != nil {
|
||||||
|
return fmt.Errorf("IP is already used")
|
||||||
|
}
|
||||||
|
s.leases = append(s.leases, &l)
|
||||||
|
s.reserveIP(l.IP, l.HWAddr)
|
||||||
|
s.dbStore()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveStaticLease removes a static lease (thread-safe)
|
||||||
|
func (s *Server) RemoveStaticLease(l Lease) error {
|
||||||
|
if s.IPpool == nil {
|
||||||
|
return fmt.Errorf("DHCP server isn't started")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(l.IP) != 4 {
|
||||||
|
return fmt.Errorf("Invalid IP")
|
||||||
|
}
|
||||||
|
if len(l.HWAddr) != 6 {
|
||||||
|
return fmt.Errorf("Invalid MAC")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.leasesLock.Lock()
|
||||||
|
defer s.leasesLock.Unlock()
|
||||||
|
|
||||||
|
if s.findReservedHWaddr(l.IP) == nil {
|
||||||
|
return fmt.Errorf("Lease not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var newLeases []*Lease
|
||||||
|
for _, lease := range s.leases {
|
||||||
|
if bytes.Equal(lease.IP.To4(), l.IP) {
|
||||||
|
if !bytes.Equal(lease.HWAddr, l.HWAddr) ||
|
||||||
|
lease.Hostname != l.Hostname {
|
||||||
|
return fmt.Errorf("Lease not found")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newLeases = append(newLeases, lease)
|
||||||
|
}
|
||||||
|
s.leases = newLeases
|
||||||
|
s.unreserveIP(l.IP)
|
||||||
|
s.dbStore()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Leases returns the list of current DHCP leases (thread-safe)
|
// Leases returns the list of current DHCP leases (thread-safe)
|
||||||
func (s *Server) Leases() []Lease {
|
func (s *Server) Leases() []Lease {
|
||||||
var result []Lease
|
var result []Lease
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
s.RLock()
|
s.leasesLock.RLock()
|
||||||
for _, lease := range s.leases {
|
for _, lease := range s.leases {
|
||||||
if lease.Expiry.Unix() > now {
|
if lease.Expiry.Unix() > now {
|
||||||
result = append(result, *lease)
|
result = append(result, *lease)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.RUnlock()
|
s.leasesLock.RUnlock()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StaticLeases returns the list of statically-configured DHCP leases (thread-safe)
|
||||||
|
func (s *Server) StaticLeases() []Lease {
|
||||||
|
s.leasesLock.Lock()
|
||||||
|
if s.IPpool == nil {
|
||||||
|
s.dbLoad()
|
||||||
|
}
|
||||||
|
s.leasesLock.Unlock()
|
||||||
|
|
||||||
|
var result []Lease
|
||||||
|
s.leasesLock.RLock()
|
||||||
|
for _, lease := range s.leases {
|
||||||
|
if lease.Expiry.Unix() == 1 {
|
||||||
|
result = append(result, *lease)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.leasesLock.RUnlock()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Print information about the current leases
|
// Print information about the current leases
|
||||||
func (s *Server) printLeases() {
|
func (s *Server) printLeases() {
|
||||||
log.Tracef("Leases:")
|
log.Tracef("Leases:")
|
||||||
@ -543,8 +636,8 @@ func (s *Server) printLeases() {
|
|||||||
// FindIPbyMAC finds an IP address by MAC address in the currently active DHCP leases
|
// FindIPbyMAC finds an IP address by MAC address in the currently active DHCP leases
|
||||||
func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
|
func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
s.RLock()
|
s.leasesLock.RLock()
|
||||||
defer s.RUnlock()
|
defer s.leasesLock.RUnlock()
|
||||||
for _, l := range s.leases {
|
for _, l := range s.leases {
|
||||||
if l.Expiry.Unix() > now && bytes.Equal(mac, l.HWAddr) {
|
if l.Expiry.Unix() > now && bytes.Equal(mac, l.HWAddr) {
|
||||||
return l.IP
|
return l.IP
|
||||||
@ -555,8 +648,8 @@ func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
|
|||||||
|
|
||||||
// Reset internal state
|
// Reset internal state
|
||||||
func (s *Server) reset() {
|
func (s *Server) reset() {
|
||||||
s.Lock()
|
s.leasesLock.Lock()
|
||||||
s.leases = nil
|
s.leases = nil
|
||||||
s.Unlock()
|
s.leasesLock.Unlock()
|
||||||
s.IPpool = make(map[[4]byte]net.HardwareAddr)
|
s.IPpool = make(map[[4]byte]net.HardwareAddr)
|
||||||
}
|
}
|
||||||
|
16
helpers.go
16
helpers.go
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -387,3 +388,18 @@ func _Func() string {
|
|||||||
f := runtime.FuncForPC(pc[0])
|
f := runtime.FuncForPC(pc[0])
|
||||||
return path.Base(f.Name())
|
return path.Base(f.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse input string and return IPv4 address
|
||||||
|
func parseIPv4(s string) net.IP {
|
||||||
|
ip := net.ParseIP(s)
|
||||||
|
if ip == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v4InV6Prefix := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}
|
||||||
|
if !bytes.Equal(ip[0:12], v4InV6Prefix) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip.To4()
|
||||||
|
}
|
||||||
|
@ -387,6 +387,42 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/DhcpSearchResult"
|
$ref: "#/definitions/DhcpSearchResult"
|
||||||
|
|
||||||
|
/dhcp/add_static_lease:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- dhcp
|
||||||
|
operationId: dhcpAddStaticLease
|
||||||
|
summary: "Adds a static lease"
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/DhcpStaticLease"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
|
||||||
|
/dhcp/remove_static_lease:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- dhcp
|
||||||
|
operationId: dhcpRemoveStaticLease
|
||||||
|
summary: "Removes a static lease"
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/DhcpStaticLease"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: OK
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Filtering status methods
|
# Filtering status methods
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
@ -1152,7 +1188,7 @@ definitions:
|
|||||||
properties:
|
properties:
|
||||||
mac:
|
mac:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "001109b3b3b8"
|
example: "00:11:09:b3:b3:b8"
|
||||||
ip:
|
ip:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "192.168.1.22"
|
example: "192.168.1.22"
|
||||||
@ -1163,6 +1199,24 @@ definitions:
|
|||||||
type: "string"
|
type: "string"
|
||||||
format: "date-time"
|
format: "date-time"
|
||||||
example: "2017-07-21T17:32:28Z"
|
example: "2017-07-21T17:32:28Z"
|
||||||
|
DhcpStaticLease:
|
||||||
|
type: "object"
|
||||||
|
description: "DHCP static lease information"
|
||||||
|
required:
|
||||||
|
- "mac"
|
||||||
|
- "ip"
|
||||||
|
- "hostname"
|
||||||
|
- "expires"
|
||||||
|
properties:
|
||||||
|
mac:
|
||||||
|
type: "string"
|
||||||
|
example: "00:11:09:b3:b3:b8"
|
||||||
|
ip:
|
||||||
|
type: "string"
|
||||||
|
example: "192.168.1.22"
|
||||||
|
hostname:
|
||||||
|
type: "string"
|
||||||
|
example: "dell"
|
||||||
DhcpStatus:
|
DhcpStatus:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "Built-in DHCP server configuration and status"
|
description: "Built-in DHCP server configuration and status"
|
||||||
@ -1176,6 +1230,10 @@ definitions:
|
|||||||
type: "array"
|
type: "array"
|
||||||
items:
|
items:
|
||||||
$ref: "#/definitions/DhcpLease"
|
$ref: "#/definitions/DhcpLease"
|
||||||
|
static_leases :
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/DhcpStaticLease"
|
||||||
DhcpSearchResult:
|
DhcpSearchResult:
|
||||||
type: "object"
|
type: "object"
|
||||||
description: "Information about a DHCP server discovered in the current network"
|
description: "Information about a DHCP server discovered in the current network"
|
||||||
|
Loading…
Reference in New Issue
Block a user