diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-09-28 16:25:48 +0300 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-09-28 16:25:48 +0300 |
| commit | 38ac4db03560200df2517ed7e06e555828ce0a15 (patch) | |
| tree | 7bd8cc7d08d352bea05490e81b230a9d418a8553 /static/systemd-network.js | |
| parent | 8489f1f83da5dcc5818401393b1f6a430eea677c (diff) | |
| download | network-38ac4db03560200df2517ed7e06e555828ce0a15.tar.zst | |
Update
Diffstat (limited to 'static/systemd-network.js')
| -rw-r--r-- | static/systemd-network.js | 1301 |
1 files changed, 870 insertions, 431 deletions
diff --git a/static/systemd-network.js b/static/systemd-network.js index 0c57ec1..69020f1 100644 --- a/static/systemd-network.js +++ b/static/systemd-network.js @@ -1,54 +1,132 @@ /* jshint esversion: 2024, module: true */ -/** - * Systemd Network Configuration Parser - * Based on systemd.network(5) documentation - * @module SystemdNetwork - */ +import { + BooleanYesNo, + ClientIdentifier, + DHCPMode, + DNSSECOptions, + DuplexMode, + FieldType, + IPForward, + IPv6PrivacyExtensions, + LLMNROptions, + MulticastDNS, + PortType, + RouteScope, + RouteType, + UseDomains, + WakeOnLAN, +} from "./network-types.js"; /** * Base field type with standardized interface * @class BaseField */ class BaseField { - /** - * @param {*} value - Field value - * @param {string} type - Field type - * @param {string} description - Field description - * @param {Object} options - Additional options (pattern, enum, etc.) - */ - constructor(value = null, type = 'string', description = '', options = {}) { - this.value = value; - this.type = type; - this.description = description; - this.options = options; - } - - /** - * Validate field value - * @returns {boolean} - */ - validate() { - if (this.value === null || this.value === undefined) return true; - - if (this.options.pattern && typeof this.value === 'string') { - return this.options.pattern.test(this.value); - } - - if (this.options.enum && this.options.enum.length > 0) { - return this.options.enum.includes(this.value); - } - - return true; - } - - /** - * Convert to string representation - * @returns {string} - */ - toString() { - return this.value !== null ? String(this.value) : ''; - } + #value; + #type; + #description; + #options; + + /** + * @param {*} value - Field value + * @param {Symbol} type - Field type from FieldType enum + * @param {string} description - Field description + * @param {Object} options - Additional options (pattern, enum, etc.) + */ + constructor( + value = null, + type = FieldType.STRING, + description = "", + options = {}, + ) { + this.#value = value; + this.#type = type; + this.#description = description; + this.#options = options; + } + + /** + * Get field value + * @returns {*} + */ + get value() { + return this.#value; + } + + /** + * Set field value + * @param {*} newValue + */ + set value(newValue) { + this.#value = newValue; + } + + /** + * Get field type + * @returns {Symbol} + */ + get type() { + return this.#type; + } + + /** + * Get field description + * @returns {string} + */ + get description() { + return this.#description; + } + + /** + * Get field options + * @returns {Object} + */ + get options() { + return { ...this.#options }; + } + + /** + * Validate field value + * @returns {boolean} + */ + validate() { + if ( + this.#value === null || + this.#value === undefined || + this.#value === "" + ) { + return true; + } + + if (this.#options.pattern && typeof this.#value === "string") { + return this.#options.pattern.test(this.#value); + } + + if (this.#options.enum && this.#options.enum.length > 0) { + return this.#options.enum.includes(this.#value); + } + + return true; + } + + /** + * Convert to string representation + * @returns {string} + */ + toString() { + return this.#value !== null ? String(this.#value) : ""; + } + + /** + * Check if field has a value + * @returns {boolean} + */ + hasValue() { + return ( + this.#value !== null && this.#value !== undefined && this.#value !== "" + ); + } } /** @@ -56,11 +134,11 @@ class BaseField { * @class MACAddress */ class MACAddress extends BaseField { - constructor(value = null) { - super(value, 'mac-address', 'Hardware address (MAC)', { - pattern: /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/ - }); - } + constructor(value = null) { + super(value, FieldType.MAC_ADDRESS, "Hardware address (MAC)", { + pattern: /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/, + }); + } } /** @@ -68,11 +146,11 @@ class MACAddress extends BaseField { * @class IPv4Address */ class IPv4Address extends BaseField { - constructor(value = null) { - super(value, 'ipv4-address', 'IPv4 address', { - pattern: /^(\d{1,3}\.){3}\d{1,3}$/ - }); - } + constructor(value = null) { + super(value, FieldType.IPV4_ADDRESS, "IPv4 address", { + pattern: /^(\d{1,3}\.){3}\d{1,3}$/, + }); + } } /** @@ -80,23 +158,11 @@ class IPv4Address extends BaseField { * @class IPv6Address */ class IPv6Address extends BaseField { - constructor(value = null) { - super(value, 'ipv6-address', 'IPv6 address', { - pattern: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/ - }); - } -} - -/** - * Boolean type with yes/no values - * @class BooleanYesNo - */ -class BooleanYesNo extends BaseField { - constructor(value = null) { - super(value, 'boolean', 'Boolean (yes/no)', { - enum: ['yes', 'no'] - }); - } + constructor(value = null) { + super(value, FieldType.IPV6_ADDRESS, "IPv6 address", { + pattern: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/, + }); + } } /** @@ -104,11 +170,12 @@ class BooleanYesNo extends BaseField { * @class Port */ class Port extends BaseField { - constructor(value = null) { - super(value, 'port', 'Network port (1-65535)', { - pattern: /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/ - }); - } + constructor(value = null) { + super(value, FieldType.PORT, "Network port (1-65535)", { + pattern: + /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/, + }); + } } /** @@ -116,11 +183,11 @@ class Port extends BaseField { * @class MTU */ class MTU extends BaseField { - constructor(value = null) { - super(value, 'mtu', 'Maximum Transmission Unit', { - pattern: /^[1-9][0-9]*$/ - }); - } + constructor(value = null) { + super(value, FieldType.MTU, "Maximum Transmission Unit", { + pattern: /^[1-9][0-9]*$/, + }); + } } /** @@ -128,19 +195,43 @@ class MTU extends BaseField { * @class MatchSection */ class MatchSection { - constructor() { - this.MACAddress = new BaseField(null, 'mac-addresses', 'Space-separated MAC addresses'); - this.OriginalName = new BaseField(null, 'strings', 'Original interface names'); - this.Path = new BaseField(null, 'strings', 'Device path patterns'); - this.Driver = new BaseField(null, 'strings', 'Driver names'); - this.Type = new BaseField(null, 'strings', 'Interface types (ether, wifi, etc.)'); - this.Name = new BaseField(null, 'strings', 'Interface names'); - this.Property = new BaseField(null, 'string', 'Device property'); - this.Host = new BaseField(null, 'string', 'Host name'); - this.Virtualization = new BaseField(null, 'string', 'Virtualization detection'); - this.KernelCommandLine = new BaseField(null, 'string', 'Kernel command line'); - this.Architecture = new BaseField(null, 'string', 'System architecture'); - } + constructor() { + this.MACAddress = new BaseField( + null, + FieldType.STRINGS, + "Space-separated MAC addresses", + ); + this.OriginalName = new BaseField( + null, + FieldType.STRINGS, + "Original interface names", + ); + this.Path = new BaseField(null, FieldType.STRINGS, "Device path patterns"); + this.Driver = new BaseField(null, FieldType.STRINGS, "Driver names"); + this.Type = new BaseField( + null, + FieldType.STRINGS, + "Interface types (ether, wifi, etc.)", + ); + this.Name = new BaseField(null, FieldType.STRINGS, "Interface names"); + this.Property = new BaseField(null, FieldType.STRING, "Device property"); + this.Host = new BaseField(null, FieldType.STRING, "Host name"); + this.Virtualization = new BaseField( + null, + FieldType.STRING, + "Virtualization detection", + ); + this.KernelCommandLine = new BaseField( + null, + FieldType.STRING, + "Kernel command line", + ); + this.Architecture = new BaseField( + null, + FieldType.STRING, + "System architecture", + ); + } } /** @@ -148,24 +239,32 @@ class MatchSection { * @class LinkSection */ class LinkSection { - constructor() { - this.MACAddress = new MACAddress(); - this.MTUBytes = new MTU(); - this.BitsPerSecond = new BaseField(null, 'number', 'Link speed in bits per second'); - this.Duplex = new BaseField(null, 'string', 'Duplex mode', { - enum: ['half', 'full'] - }); - this.AutoNegotiation = new BooleanYesNo(); - this.WakeOnLan = new BaseField(null, 'string', 'Wake-on-LAN', { - enum: ['phy', 'unicast', 'broadcast', 'arp', 'magic', ''] - }); - this.Port = new BaseField(null, 'string', 'Port type', { - enum: ['tp', 'aui', 'bnc', 'mii', 'fibre', ''] - }); - this.Advertise = new BaseField(null, 'strings', 'Advertised features'); - this.RxFlowControl = new BooleanYesNo(); - this.TxFlowControl = new BooleanYesNo(); - } + constructor() { + this.MACAddress = new MACAddress(); + this.MTUBytes = new MTU(); + this.BitsPerSecond = new BaseField( + null, + FieldType.NUMBER, + "Link speed in bits per second", + ); + this.Duplex = new BaseField(null, FieldType.STRING, "Duplex mode", { + enum: Object.values(DuplexMode), + }); + this.AutoNegotiation = new BooleanYesNo(); + this.WakeOnLan = new BaseField(null, FieldType.STRING, "Wake-on-LAN", { + enum: Object.values(WakeOnLAN), + }); + this.Port = new BaseField(null, FieldType.STRING, "Port type", { + enum: Object.values(PortType), + }); + this.Advertise = new BaseField( + null, + FieldType.STRINGS, + "Advertised features", + ); + this.RxFlowControl = new BooleanYesNo(); + this.TxFlowControl = new BooleanYesNo(); + } } /** @@ -173,35 +272,55 @@ class LinkSection { * @class NetworkSection */ class NetworkSection { - constructor() { - this.Description = new BaseField(null, 'string', 'Interface description'); - this.DHCP = new BaseField(null, 'dhcp-mode', 'DHCP client', { - enum: ['yes', 'no', 'ipv4', 'ipv6'] - }); - this.DHCPServer = new BooleanYesNo(); - this.DNS = new BaseField(null, 'ip-addresses', 'DNS servers'); - this.NTP = new BaseField(null, 'ip-addresses', 'NTP servers'); - this.IPForward = new BaseField(null, 'ip-forward', 'IP forwarding', { - enum: ['yes', 'no', 'ipv4', 'ipv6'] - }); - this.IPv6PrivacyExtensions = new BaseField(null, 'privacy-extensions', 'IPv6 privacy extensions', { - enum: ['yes', 'no', 'prefer-public'] - }); - this.IPv6AcceptRA = new BooleanYesNo(); - this.LLMNR = new BaseField(null, 'llmnr', 'LLMNR support', { - enum: ['yes', 'no', 'resolve'] - }); - this.MulticastDNS = new BaseField(null, 'mdns', 'Multicast DNS', { - enum: ['yes', 'no', 'resolve'] - }); - this.DNSSEC = new BaseField(null, 'dnssec', 'DNSSEC support', { - enum: ['yes', 'no', 'allow-downgrade'] - }); - this.Domains = new BaseField(null, 'strings', 'DNS search domains'); - this.ConfigureWithoutCarrier = new BooleanYesNo(); - this.IgnoreCarrierLoss = new BooleanYesNo(); - this.KeepConfiguration = new BaseField(null, 'number', 'Keep configuration time in seconds'); - } + constructor() { + this.Description = new BaseField( + null, + FieldType.STRING, + "Interface description", + ); + this.DHCP = new BaseField(null, FieldType.DHCP_MODE, "DHCP client", { + enum: Object.values(DHCPMode), + }); + this.DHCPServer = new BooleanYesNo(); + this.DNS = new BaseField(null, FieldType.IP_ADDRESSES, "DNS servers"); + this.NTP = new BaseField(null, FieldType.IP_ADDRESSES, "NTP servers"); + this.IPForward = new BaseField( + null, + FieldType.IP_FORWARD, + "IP forwarding", + { + enum: Object.values(IPForward), + }, + ); + this.IPv6PrivacyExtensions = new BaseField( + null, + FieldType.PRIVACY_EXTENSIONS, + "IPv6 privacy extensions", + { + enum: Object.values(IPv6PrivacyExtensions), + }, + ); + this.IPv6AcceptRA = new BooleanYesNo(); + this.LLMNR = new BaseField(null, FieldType.LLMNR, "LLMNR support", { + enum: Object.values(LLMNROptions), + }); + this.MulticastDNS = new BaseField(null, FieldType.MDNS, "Multicast DNS", { + enum: Object.values(MulticastDNS), + }); + this.DNSSEC = new BaseField(null, FieldType.DNSSEC, "DNSSEC support", { + enum: Object.values(DNSSECOptions), + }); + this.Domains = new BaseField(null, FieldType.STRINGS, "DNS search domains"); + this.ConfigureWithoutCarrier = new BooleanYesNo(); + this.IgnoreCarrierLoss = new BooleanYesNo(); + this.KeepConfiguration = new BaseField( + null, + FieldType.NUMBER, + "Keep configuration time in seconds", + ); + this.LLDP = new BooleanYesNo(); + this.EmitLLDP = new BooleanYesNo(); + } } /** @@ -209,19 +328,142 @@ class NetworkSection { * @class DHCPSection */ class DHCPSection { - constructor() { - this.UseDNS = new BooleanYesNo(); - this.UseNTP = new BooleanYesNo(); - this.UseMTU = new BooleanYesNo(); - this.UseHostname = new BooleanYesNo(); - this.UseDomains = new BaseField(null, 'use-domains', 'Use domains from DHCP', { - enum: ['yes', 'no', 'route'] - }); - this.ClientIdentifier = new BaseField(null, 'client-identifier', 'DHCP client identifier', { - enum: ['mac', 'duid'] - }); - this.RouteMetric = new BaseField(null, 'number', 'Route metric for DHCP routes'); - } + constructor() { + this.UseDNS = new BooleanYesNo(); + this.UseNTP = new BooleanYesNo(); + this.UseMTU = new BooleanYesNo(); + this.UseHostname = new BooleanYesNo(); + this.UseDomains = new BaseField( + null, + FieldType.USE_DOMAINS, + "Use domains from DHCP", + { + enum: Object.values(UseDomains), + }, + ); + this.ClientIdentifier = new BaseField( + null, + FieldType.CLIENT_IDENTIFIER, + "DHCP client identifier", + { + enum: Object.values(ClientIdentifier), + }, + ); + this.RouteMetric = new BaseField( + null, + FieldType.NUMBER, + "Route metric for DHCP routes", + ); + this.UseRoutes = new BooleanYesNo(); + this.SendRelease = new BooleanYesNo(); + } +} + +/** + * [DHCPv4] section configuration + * @class DHCPv4Section + */ +class DHCPv4Section { + constructor() { + this.ClientIdentifier = new BaseField( + null, + FieldType.CLIENT_IDENTIFIER, + "DHCPv4 client identifier", + { + enum: Object.values(ClientIdentifier), + }, + ); + this.UseDNS = new BooleanYesNo(); + this.UseNTP = new BooleanYesNo(); + this.UseMTU = new BooleanYesNo(); + this.UseHostname = new BooleanYesNo(); + this.UseDomains = new BaseField( + null, + FieldType.USE_DOMAINS, + "Use domains from DHCPv4", + { + enum: Object.values(UseDomains), + }, + ); + this.SendRelease = new BooleanYesNo(); + } +} + +/** + * [DHCPv6] section configuration + * @class DHCPv6Section + */ +class DHCPv6Section { + constructor() { + this.UseDNS = new BooleanYesNo(); + this.UseNTP = new BooleanYesNo(); + this.UseHostname = new BooleanYesNo(); + this.UseDomains = new BaseField( + null, + FieldType.USE_DOMAINS, + "Use domains from DHCPv6", + { + enum: Object.values(UseDomains), + }, + ); + this.WithoutRA = new BooleanYesNo(); + this.UseAddress = new BooleanYesNo(); + } +} + +/** + * [IPv6AcceptRA] section configuration + * @class IPv6AcceptRASection + */ +class IPv6AcceptRASection { + constructor() { + this.UseDNS = new BooleanYesNo(); + this.UseDomains = new BaseField( + null, + FieldType.USE_DOMAINS, + "Use domains from RA", + { + enum: Object.values(UseDomains), + }, + ); + this.UseAutonomousPrefix = new BooleanYesNo(); + this.UseOnLinkPrefix = new BooleanYesNo(); + this.UseRoutePrefix = new BooleanYesNo(); + this.RouteMetric = new BaseField( + null, + FieldType.NUMBER, + "Route metric for RA routes", + ); + } +} + +/** + * [SLAAC] section configuration + * @class SLAACSection + */ +class SLAACSection { + constructor() { + this.UseDNS = new BooleanYesNo(); + this.UseDomains = new BaseField( + null, + FieldType.USE_DOMAINS, + "Use domains from SLAAC", + { + enum: Object.values(UseDomains), + }, + ); + this.UseAddress = new BooleanYesNo(); + this.RouteMetric = new BaseField( + null, + FieldType.NUMBER, + "Route metric for SLAAC routes", + ); + this.Critical = new BooleanYesNo(); + this.PreferTemporaryAddress = new BooleanYesNo(); + this.UseAutonomousPrefix = new BooleanYesNo(); + this.UseOnLinkPrefix = new BooleanYesNo(); + this.UseRoutePrefix = new BooleanYesNo(); + } } /** @@ -229,14 +471,23 @@ class DHCPSection { * @class AddressSection */ class AddressSection { - constructor() { - this.Address = new BaseField(null, 'ip-prefix', 'IP address with prefix'); - this.Peer = new BaseField(null, 'ip-address', 'Peer address'); - this.Broadcast = new BaseField(null, 'ip-address', 'Broadcast address'); - this.Label = new BaseField(null, 'string', 'Address label'); - this.Scope = new BaseField(null, 'number', 'Address scope'); - this.Flags = new BaseField(null, 'strings', 'Address flags'); - } + constructor() { + this.Address = new BaseField( + null, + FieldType.IP_PREFIX, + "IP address with prefix", + ); + this.Peer = new BaseField(null, FieldType.IP_ADDRESS, "Peer address"); + this.Broadcast = new BaseField( + null, + FieldType.IP_ADDRESS, + "Broadcast address", + ); + this.Label = new BaseField(null, FieldType.STRING, "Address label"); + this.Scope = new BaseField(null, FieldType.NUMBER, "Address scope"); + this.Flags = new BaseField(null, FieldType.STRINGS, "Address flags"); + this.Lifetime = new BaseField(null, FieldType.STRING, "Address lifetime"); + } } /** @@ -244,20 +495,40 @@ class AddressSection { * @class RouteSection */ class RouteSection { - constructor() { - this.Gateway = new BaseField(null, 'ip-address', 'Gateway address'); - this.GatewayOnLink = new BooleanYesNo(); - this.Destination = new BaseField(null, 'ip-prefix', 'Destination prefix'); - this.Source = new BaseField(null, 'ip-address', 'Source address'); - this.PreferredSource = new BaseField(null, 'ip-address', 'Preferred source address'); - this.Metric = new BaseField(null, 'number', 'Route metric'); - this.Scope = new BaseField(null, 'route-scope', 'Route scope', { - enum: ['global', 'link', 'host'] - }); - this.Type = new BaseField(null, 'route-type', 'Route type', { - enum: ['unicast', 'local', 'broadcast', 'anycast', 'multicast', 'blackhole', 'unreachable', 'prohibit'] - }); - } + constructor() { + this.Gateway = new BaseField(null, FieldType.IP_ADDRESS, "Gateway address"); + this.GatewayOnLink = new BooleanYesNo(); + this.Destination = new BaseField( + null, + FieldType.IP_PREFIX, + "Destination prefix", + ); + this.Source = new BaseField(null, FieldType.IP_ADDRESS, "Source address"); + this.PreferredSource = new BaseField( + null, + FieldType.IP_ADDRESS, + "Preferred source address", + ); + this.Metric = new BaseField(null, FieldType.NUMBER, "Route metric"); + this.Scope = new BaseField(null, FieldType.ROUTE_SCOPE, "Route scope", { + enum: Object.values(RouteScope), + }); + this.Type = new BaseField(null, FieldType.ROUTE_TYPE, "Route type", { + enum: Object.values(RouteType), + }); + this.InitialCongestionWindow = new BaseField( + null, + FieldType.NUMBER, + "Initial congestion window", + ); + this.InitialAdvertisedReceiveWindow = new BaseField( + null, + FieldType.NUMBER, + "Initial advertised receive window", + ); + this.Table = new BaseField(null, FieldType.NUMBER, "Routing table"); + this.Protocol = new BaseField(null, FieldType.NUMBER, "Routing protocol"); + } } /** @@ -265,264 +536,432 @@ class RouteSection { * @class NetworkConfiguration */ class NetworkConfiguration { - constructor() { - this.Match = new MatchSection(); - this.Link = new LinkSection(); - this.Network = new NetworkSection(); - this.DHCP = new DHCPSection(); - this.Address = []; - this.Route = []; - } - - /** - * Get schema for structured editor - * @returns {Object} Schema definition - */ - getSchema() { - return { - Match: this._getSectionSchema(this.Match), - Link: this._getSectionSchema(this.Link), - Network: this._getSectionSchema(this.Network), - DHCP: this._getSectionSchema(this.DHCP), - Address: this._getArraySectionSchema(AddressSection, 'Address'), - Route: this._getArraySectionSchema(RouteSection, 'Route') - }; - } - - /** - * Get schema for a single section - * @private - * @param {Object} section - Section instance - * @returns {Object} Section schema - */ - _getSectionSchema(section) { - const schema = {}; - for (const [key, field] of Object.entries(section)) { - schema[key] = { - value: field.value, - type: field.type, - description: field.description, - options: field.options - }; - } - return schema; - } - - /** - * Get schema for array sections (Address, Route) - * @private - * @param {Class} SectionClass - Section class - * @param {string} sectionName - Section name - * @returns {Object} Array section schema - */ - _getArraySectionSchema(SectionClass, sectionName) { - const template = new SectionClass(); - return { - itemSchema: this._getSectionSchema(template), - items: this[sectionName].map(item => this._getSectionSchema(item)) - }; - } - - /** - * Parse systemd network configuration from text - * @param {string} configText - Configuration file content - * @returns {NetworkConfiguration} - */ - static fromSystemdConfiguration(configText) { - const config = new NetworkConfiguration(); - const lines = configText.split('\n'); - let currentSection = null; - let currentAddress = null; - let currentRoute = null; - - for (const line of lines) { - const trimmed = line.trim(); - - // Skip empty lines and comments - if (!trimmed || trimmed.startsWith('#')) continue; - - // Section header - const sectionMatch = trimmed.match(/^\[(\w+)\]$/); - if (sectionMatch) { - currentSection = sectionMatch[1].toLowerCase(); - - // Start new array sections - if (currentSection === 'address') { - currentAddress = new AddressSection(); - config.Address.push(currentAddress); - } else if (currentSection === 'route') { - currentRoute = new RouteSection(); - config.Route.push(currentRoute); - } - continue; - } - - // Key-value pair - const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/); - if (kvMatch && currentSection) { - const key = kvMatch[1]; - const value = kvMatch[2]; - - config._setValue(currentSection, key, value, currentAddress, currentRoute); - } - } - - return config; - } - - /** - * Set configuration value - * @private - * @param {string} section - Section name - * @param {string} key - Key name - * @param {string} value - Value - * @param {AddressSection} currentAddress - Current address section - * @param {RouteSection} currentRoute - Current route section - */ - _setValue(section, key, value, currentAddress, currentRoute) { - switch (section) { - case 'match': - this._setMatchValue(key, value); - break; - case 'link': - this._setLinkValue(key, value); - break; - case 'network': - this._setNetworkValue(key, value); - break; - case 'dhcp': - this._setDHCPValue(key, value); - break; - case 'address': - if (currentAddress) { - this._setAddressValue(currentAddress, key, value); - } - break; - case 'route': - if (currentRoute) { - this._setRouteValue(currentRoute, key, value); - } - break; - } - } - - _setMatchValue(key, value) { - if (this.Match[key] !== undefined) { - this.Match[key].value = value; - } - } - - _setLinkValue(key, value) { - if (this.Link[key] !== undefined) { - this.Link[key].value = value; - } - } - - _setNetworkValue(key, value) { - if (this.Network[key] !== undefined) { - this.Network[key].value = value; - } - } - - _setDHCPValue(key, value) { - if (this.DHCP[key] !== undefined) { - this.DHCP[key].value = value; - } - } - - _setAddressValue(address, key, value) { - if (address[key] !== undefined) { - address[key].value = value; - } - } - - _setRouteValue(route, key, value) { - if (route[key] !== undefined) { - route[key].value = value; - } - } - - /** - * Convert to systemd network configuration format - * @returns {string} - */ - toSystemdConfiguration() { - const sections = []; - - // [Match] section - if (this._hasSectionValues(this.Match)) { - sections.push('[Match]'); - sections.push(...this._formatSection(this.Match)); - } - - // [Link] section - if (this._hasSectionValues(this.Link)) { - sections.push('[Link]'); - sections.push(...this._formatSection(this.Link)); - } - - // [Network] section - if (this._hasSectionValues(this.Network)) { - sections.push('[Network]'); - sections.push(...this._formatSection(this.Network)); - } - - // [DHCP] section - if (this._hasSectionValues(this.DHCP)) { - sections.push('[DHCP]'); - sections.push(...this._formatSection(this.DHCP)); - } - - // [Address] sections - this.Address.forEach(addr => { - if (this._hasSectionValues(addr)) { - sections.push('[Address]'); - sections.push(...this._formatSection(addr)); - } - }); - - // [Route] sections - this.Route.forEach(route => { - if (this._hasSectionValues(route)) { - sections.push('[Route]'); - sections.push(...this._formatSection(route)); - } - }); - - return `${sections.join('\n')}\n`; - } - - _hasSectionValues(section) { - return Object.values(section).some(field => - field.value !== null && field.value !== undefined && field.value !== '' - ); - } - - _formatSection(section) { - const lines = []; - for (const [key, field] of Object.entries(section)) { - if (field.value !== null && field.value !== undefined && field.value !== '') { - lines.push(`${key}=${field.toString()}`); - } - } - return lines; - } + #match; + #link; + #network; + #dhcp; + #dhcpv4; + #dhcpv6; + #ipv6AcceptRA; + #slaac; + #address; + #route; + + constructor() { + this.#match = new MatchSection(); + this.#link = new LinkSection(); + this.#network = new NetworkSection(); + this.#dhcp = new DHCPSection(); + this.#dhcpv4 = new DHCPv4Section(); + this.#dhcpv6 = new DHCPv6Section(); + this.#ipv6AcceptRA = new IPv6AcceptRASection(); + this.#slaac = new SLAACSection(); + this.#address = []; + this.#route = []; + } + + /** + * Get Match section + * @returns {MatchSection} + */ + get Match() { + return this.#match; + } + + /** + * Get Link section + * @returns {LinkSection} + */ + get Link() { + return this.#link; + } + + /** + * Get Network section + * @returns {NetworkSection} + */ + get Network() { + return this.#network; + } + + /** + * Get DHCP section + * @returns {DHCPSection} + */ + get DHCP() { + return this.#dhcp; + } + + /** + * Get DHCPv4 section + * @returns {DHCPv4Section} + */ + get DHCPv4() { + return this.#dhcpv4; + } + + /** + * Get DHCPv6 section + * @returns {DHCPv6Section} + */ + get DHCPv6() { + return this.#dhcpv6; + } + + /** + * Get IPv6AcceptRA section + * @returns {IPv6AcceptRASection} + */ + get IPv6AcceptRA() { + return this.#ipv6AcceptRA; + } + + /** + * Get SLAAC section + * @returns {SLAACSection} + */ + get SLAAC() { + return this.#slaac; + } + + /** + * Get Address sections + * @returns {Array<AddressSection>} + */ + get Address() { + return [...this.#address]; + } + + /** + * Get Route sections + * @returns {Array<RouteSection>} + */ + get Route() { + return [...this.#route]; + } + + /** + * Get schema for structured editor + * @returns {Object} Schema definition + */ + getSchema() { + return { + Match: this.#getSectionSchema(this.#match), + Link: this.#getSectionSchema(this.#link), + Network: this.#getSectionSchema(this.#network), + DHCP: this.#getSectionSchema(this.#dhcp), + DHCPv4: this.#getSectionSchema(this.#dhcpv4), + DHCPv6: this.#getSectionSchema(this.#dhcpv6), + IPv6AcceptRA: this.#getSectionSchema(this.#ipv6AcceptRA), + SLAAC: this.#getSectionSchema(this.#slaac), + Address: this.#getArraySectionSchema(AddressSection, "Address"), + Route: this.#getArraySectionSchema(RouteSection, "Route"), + }; + } + + /** + * Get schema for a single section + * @private + * @param {Object} section - Section instance + * @returns {Object} Section schema + */ + #getSectionSchema(section) { + const schema = {}; + for (const [key, field] of Object.entries(section)) { + schema[key] = { + value: field.value, + type: field.type, + description: field.description, + options: field.options, + }; + } + return schema; + } + + /** + * Get schema for array sections (Address, Route) + * @private + * @param {Class} SectionClass - Section class + * @param {string} sectionName - Section name + * @returns {Object} Array section schema + */ + #getArraySectionSchema(SectionClass, sectionName) { + const template = new SectionClass(); + return { + itemSchema: this.#getSectionSchema(template), + items: this[sectionName].map((item) => this.#getSectionSchema(item)), + }; + } + + /** + * Parse systemd network configuration from text + * @static + * @param {string} configText - Configuration file content + * @returns {NetworkConfiguration} + */ + static fromSystemdConfiguration(configText) { + const config = new NetworkConfiguration(); + const lines = configText.split("\n"); + let currentSection = null; + let currentAddress = null; + let currentRoute = null; + + for (const line of lines) { + const trimmed = line.trim(); + + // Skip empty lines and comments + if (!trimmed || trimmed.startsWith("#")) continue; + + // Section header + const sectionMatch = trimmed.match(/^\[(\w+)\]$/); + if (sectionMatch) { + currentSection = sectionMatch[1].toLowerCase(); + + // Start new array sections + if (currentSection === "address") { + currentAddress = new AddressSection(); + config.#address.push(currentAddress); + } else if (currentSection === "route") { + currentRoute = new RouteSection(); + config.#route.push(currentRoute); + } + continue; + } + + // Key-value pair + const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/); + if (kvMatch && currentSection) { + const key = kvMatch[1]; + const value = kvMatch[2]; + + config.#setValue( + currentSection, + key, + value, + currentAddress, + currentRoute, + ); + } + } + + return config; + } + + /** + * Set configuration value + * @private + * @param {string} section - Section name + * @param {string} key - Key name + * @param {string} value - Value + * @param {AddressSection} currentAddress - Current address section + * @param {RouteSection} currentRoute - Current route section + */ + #setValue(section, key, value, currentAddress, currentRoute) { + switch (section) { + case "match": + this.#setMatchValue(key, value); + break; + case "link": + this.#setLinkValue(key, value); + break; + case "network": + this.#setNetworkValue(key, value); + break; + case "dhcp": + this.#setDHCPValue(key, value); + break; + case "dhcpv4": + this.#setDHCPv4Value(key, value); + break; + case "dhcpv6": + this.#setDHCPv6Value(key, value); + break; + case "ipv6acceptra": + this.#setIPv6AcceptRAValue(key, value); + break; + case "slaac": + this.#setSLAACValue(key, value); + break; + case "address": + if (currentAddress) { + this.#setAddressValue(currentAddress, key, value); + } + break; + case "route": + if (currentRoute) { + this.#setRouteValue(currentRoute, key, value); + } + break; + } + } + + #setMatchValue(key, value) { + if (this.#match[key] !== undefined) { + this.#match[key].value = value; + } + } + + #setLinkValue(key, value) { + if (this.#link[key] !== undefined) { + this.#link[key].value = value; + } + } + + #setNetworkValue(key, value) { + if (this.#network[key] !== undefined) { + this.#network[key].value = value; + } + } + + #setDHCPValue(key, value) { + if (this.#dhcp[key] !== undefined) { + this.#dhcp[key].value = value; + } + } + + #setDHCPv4Value(key, value) { + if (this.#dhcpv4[key] !== undefined) { + this.#dhcpv4[key].value = value; + } + } + + #setDHCPv6Value(key, value) { + if (this.#dhcpv6[key] !== undefined) { + this.#dhcpv6[key].value = value; + } + } + + #setIPv6AcceptRAValue(key, value) { + if (this.#ipv6AcceptRA[key] !== undefined) { + this.#ipv6AcceptRA[key].value = value; + } + } + + #setSLAACValue(key, value) { + if (this.#slaac[key] !== undefined) { + this.#slaac[key].value = value; + } + } + + #setAddressValue(address, key, value) { + if (address[key] !== undefined) { + address[key].value = value; + } + } + + #setRouteValue(route, key, value) { + if (route[key] !== undefined) { + route[key].value = value; + } + } + + /** + * Convert to systemd network configuration format + * @returns {string} + */ + toSystemdConfiguration() { + const sections = []; + + // [Match] section + if (this.#hasSectionValues(this.#match)) { + sections.push("[Match]"); + sections.push(...this.#formatSection(this.#match)); + } + + // [Link] section + if (this.#hasSectionValues(this.#link)) { + sections.push("[Link]"); + sections.push(...this.#formatSection(this.#link)); + } + + // [Network] section + if (this.#hasSectionValues(this.#network)) { + sections.push("[Network]"); + sections.push(...this.#formatSection(this.#network)); + } + + // [DHCP] section + if (this.#hasSectionValues(this.#dhcp)) { + sections.push("[DHCP]"); + sections.push(...this.#formatSection(this.#dhcp)); + } + + // [DHCPv4] section + if (this.#hasSectionValues(this.#dhcpv4)) { + sections.push("[DHCPv4]"); + sections.push(...this.#formatSection(this.#dhcpv4)); + } + + // [DHCPv6] section + if (this.#hasSectionValues(this.#dhcpv6)) { + sections.push("[DHCPv6]"); + sections.push(...this.#formatSection(this.#dhcpv6)); + } + + // [IPv6AcceptRA] section + if (this.#hasSectionValues(this.#ipv6AcceptRA)) { + sections.push("[IPv6AcceptRA]"); + sections.push(...this.#formatSection(this.#ipv6AcceptRA)); + } + + // [SLAAC] section + if (this.#hasSectionValues(this.#slaac)) { + sections.push("[SLAAC]"); + sections.push(...this.#formatSection(this.#slaac)); + } + + // [Address] sections + this.#address.forEach((addr) => { + if (this.#hasSectionValues(addr)) { + sections.push("[Address]"); + sections.push(...this.#formatSection(addr)); + } + }); + + // [Route] sections + this.#route.forEach((route) => { + if (this.#hasSectionValues(route)) { + sections.push("[Route]"); + sections.push(...this.#formatSection(route)); + } + }); + + return sections.length > 0 ? `${sections.join("\n")}\n` : ""; + } + + #hasSectionValues(section) { + return Object.values(section).some((field) => field.hasValue()); + } + + #formatSection(section) { + const lines = []; + for (const [key, field] of Object.entries(section)) { + if (field.hasValue()) { + lines.push(`${key}=${field.toString()}`); + } + } + return lines; + } } // Export classes export { - BaseField, - MACAddress, - IPv4Address, - IPv6Address, - BooleanYesNo, - Port, - MTU, - MatchSection, - LinkSection, - NetworkSection, - DHCPSection, - AddressSection, - RouteSection, - NetworkConfiguration + BaseField, + MACAddress, + IPv4Address, + IPv6Address, + BooleanYesNo, + Port, + MTU, + MatchSection, + LinkSection, + NetworkSection, + DHCPSection, + DHCPv4Section, + DHCPv6Section, + IPv6AcceptRASection, + SLAACSection, + AddressSection, + RouteSection, + NetworkConfiguration, }; |
