mirror of
https://github.com/speatzle/nfsense.git
synced 2025-05-11 02:48:21 +00:00
Use Config Manager
This commit is contained in:
parent
081aeec142
commit
70d69f04cf
11 changed files with 55 additions and 104 deletions
|
@ -12,6 +12,6 @@ type GetDestinationNATRulesResult struct {
|
||||||
|
|
||||||
func (f *Firewall) GetDestinationNATRules(ctx context.Context, params struct{}) (GetDestinationNATRulesResult, error) {
|
func (f *Firewall) GetDestinationNATRules(ctx context.Context, params struct{}) (GetDestinationNATRulesResult, error) {
|
||||||
return GetDestinationNATRulesResult{
|
return GetDestinationNATRulesResult{
|
||||||
DestinationNATRules: f.Conf.Firewall.DestinationNATRules,
|
DestinationNATRules: f.ConfigManager.GetPendingConfig().Firewall.DestinationNATRules,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package firewall
|
package firewall
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"nfsense.net/nfsense/internal/definitions"
|
"nfsense.net/nfsense/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Firewall struct {
|
type Firewall struct {
|
||||||
Conf *definitions.Config
|
ConfigManager *config.ConfigManager
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,6 @@ type GetForwardRulesResult struct {
|
||||||
|
|
||||||
func (f *Firewall) GetForwardRules(ctx context.Context, params struct{}) (GetForwardRulesResult, error) {
|
func (f *Firewall) GetForwardRules(ctx context.Context, params struct{}) (GetForwardRulesResult, error) {
|
||||||
return GetForwardRulesResult{
|
return GetForwardRulesResult{
|
||||||
ForwardRules: f.Conf.Firewall.ForwardRules,
|
ForwardRules: f.ConfigManager.GetPendingConfig().Firewall.ForwardRules,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,6 @@ type GetSourceNATRulesResult struct {
|
||||||
|
|
||||||
func (f *Firewall) GetSourceNATRules(ctx context.Context, params struct{}) (GetSourceNATRulesResult, error) {
|
func (f *Firewall) GetSourceNATRules(ctx context.Context, params struct{}) (GetSourceNATRulesResult, error) {
|
||||||
return GetSourceNATRulesResult{
|
return GetSourceNATRulesResult{
|
||||||
SourceNATRules: f.Conf.Firewall.SourceNATRules,
|
SourceNATRules: f.ConfigManager.GetPendingConfig().Firewall.SourceNATRules,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"nfsense.net/nfsense/internal/definitions"
|
"nfsense.net/nfsense/internal/definitions"
|
||||||
"nfsense.net/nfsense/internal/interfaces"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetInterfacesResult struct {
|
type GetInterfacesResult struct {
|
||||||
|
@ -14,7 +13,7 @@ type GetInterfacesResult struct {
|
||||||
|
|
||||||
func (f *Network) GetInterfaces(ctx context.Context, params struct{}) (GetInterfacesResult, error) {
|
func (f *Network) GetInterfaces(ctx context.Context, params struct{}) (GetInterfacesResult, error) {
|
||||||
return GetInterfacesResult{
|
return GetInterfacesResult{
|
||||||
Interfaces: f.Conf.Network.Interfaces,
|
Interfaces: f.ConfigManager.GetPendingConfig().Network.Interfaces,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,12 +23,12 @@ type CreateInterfaceParameters struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Network) CreateInterface(ctx context.Context, params CreateInterfaceParameters) (struct{}, error) {
|
func (f *Network) CreateInterface(ctx context.Context, params CreateInterfaceParameters) (struct{}, error) {
|
||||||
_, ok := f.Conf.Network.Interfaces[params.Name]
|
_, ok := f.ConfigManager.GetPendingConfig().Network.Interfaces[params.Name]
|
||||||
if ok {
|
if ok {
|
||||||
return struct{}{}, fmt.Errorf("Interface already Exists")
|
return struct{}{}, fmt.Errorf("Interface already Exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Conf.Network.Interfaces[params.Name] = params.Interface
|
f.ConfigManager.GetPendingConfig().Network.Interfaces[params.Name] = params.Interface
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,12 +38,12 @@ type UpdateInterfaceParameters struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Network) UpdateInterface(ctx context.Context, params CreateInterfaceParameters) (struct{}, error) {
|
func (f *Network) UpdateInterface(ctx context.Context, params CreateInterfaceParameters) (struct{}, error) {
|
||||||
_, ok := f.Conf.Network.Interfaces[params.Name]
|
_, ok := f.ConfigManager.GetPendingConfig().Network.Interfaces[params.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Conf.Network.Interfaces[params.Name] = params.Interface
|
f.ConfigManager.GetPendingConfig().Network.Interfaces[params.Name] = params.Interface
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,29 +52,11 @@ type DeleteInterfaceParameters struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Network) DeleteInterface(ctx context.Context, params DeleteInterfaceParameters) (struct{}, error) {
|
func (f *Network) DeleteInterface(ctx context.Context, params DeleteInterfaceParameters) (struct{}, error) {
|
||||||
_, ok := f.Conf.Network.Interfaces[params.Name]
|
_, ok := f.ConfigManager.GetPendingConfig().Network.Interfaces[params.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(f.Conf.Network.Interfaces, params.Name)
|
delete(f.ConfigManager.GetPendingConfig().Network.Interfaces, params.Name)
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApplyInterfacesResult struct {
|
|
||||||
Log string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Network) ApplyInterfaces(ctx context.Context, params struct{}) (ApplyInterfacesResult, error) {
|
|
||||||
data, err := interfaces.GenerateInterfacesFile(*f.Conf)
|
|
||||||
if err != nil {
|
|
||||||
return ApplyInterfacesResult{}, fmt.Errorf("Generating Interfaces File: %w", err)
|
|
||||||
}
|
|
||||||
log, err := interfaces.ApplyInterfacesFile(data)
|
|
||||||
if err != nil {
|
|
||||||
return ApplyInterfacesResult{}, fmt.Errorf("Applying Interfaces File: %w", err)
|
|
||||||
}
|
|
||||||
return ApplyInterfacesResult{
|
|
||||||
Log: log,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import "nfsense.net/nfsense/internal/config"
|
||||||
"nfsense.net/nfsense/internal/definitions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Network struct {
|
type Network struct {
|
||||||
Conf *definitions.Config
|
ConfigManager *config.ConfigManager
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ type GetAddressesResult struct {
|
||||||
|
|
||||||
func (f *Object) GetAddresses(ctx context.Context, params struct{}) (GetAddressesResult, error) {
|
func (f *Object) GetAddresses(ctx context.Context, params struct{}) (GetAddressesResult, error) {
|
||||||
return GetAddressesResult{
|
return GetAddressesResult{
|
||||||
Addresses: f.Conf.Object.Addresses,
|
Addresses: f.ConfigManager.GetPendingConfig().Object.Addresses,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,12 +23,12 @@ type CreateAddressParameters struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Object) CreateAddress(ctx context.Context, params CreateAddressParameters) (struct{}, error) {
|
func (f *Object) CreateAddress(ctx context.Context, params CreateAddressParameters) (struct{}, error) {
|
||||||
_, ok := f.Conf.Object.Addresses[params.Name]
|
_, ok := f.ConfigManager.GetPendingConfig().Object.Addresses[params.Name]
|
||||||
if ok {
|
if ok {
|
||||||
return struct{}{}, fmt.Errorf("Address already Exists")
|
return struct{}{}, fmt.Errorf("Address already Exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Conf.Object.Addresses[params.Name] = params.Address
|
f.ConfigManager.GetPendingConfig().Object.Addresses[params.Name] = params.Address
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,12 +38,12 @@ type UpdateAddressParameters struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Object) UpdateAddress(ctx context.Context, params CreateAddressParameters) (struct{}, error) {
|
func (f *Object) UpdateAddress(ctx context.Context, params CreateAddressParameters) (struct{}, error) {
|
||||||
_, ok := f.Conf.Object.Addresses[params.Name]
|
_, ok := f.ConfigManager.GetPendingConfig().Object.Addresses[params.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return struct{}{}, fmt.Errorf("Address does not Exist")
|
return struct{}{}, fmt.Errorf("Address does not Exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Conf.Object.Addresses[params.Name] = params.Address
|
f.ConfigManager.GetPendingConfig().Object.Addresses[params.Name] = params.Address
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,11 +52,11 @@ type DeleteAddressParameters struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Object) DeleteAddress(ctx context.Context, params DeleteAddressParameters) (struct{}, error) {
|
func (f *Object) DeleteAddress(ctx context.Context, params DeleteAddressParameters) (struct{}, error) {
|
||||||
_, ok := f.Conf.Object.Addresses[params.Name]
|
_, ok := f.ConfigManager.GetPendingConfig().Object.Addresses[params.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(f.Conf.Object.Addresses, params.Name)
|
delete(f.ConfigManager.GetPendingConfig().Object.Addresses, params.Name)
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package object
|
package object
|
||||||
|
|
||||||
import "nfsense.net/nfsense/internal/definitions"
|
import "nfsense.net/nfsense/internal/config"
|
||||||
|
|
||||||
type Object struct {
|
type Object struct {
|
||||||
Conf *definitions.Config
|
ConfigManager *config.ConfigManager
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ type GetServicesResult struct {
|
||||||
|
|
||||||
func (f *Object) GetServices(ctx context.Context, params struct{}) (GetServicesResult, error) {
|
func (f *Object) GetServices(ctx context.Context, params struct{}) (GetServicesResult, error) {
|
||||||
return GetServicesResult{
|
return GetServicesResult{
|
||||||
Services: f.Conf.Object.Services,
|
Services: f.ConfigManager.GetPendingConfig().Object.Services,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,12 +23,12 @@ type CreateServiceParameters struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Object) CreateService(ctx context.Context, params CreateServiceParameters) (struct{}, error) {
|
func (f *Object) CreateService(ctx context.Context, params CreateServiceParameters) (struct{}, error) {
|
||||||
_, ok := f.Conf.Object.Services[params.Name]
|
_, ok := f.ConfigManager.GetPendingConfig().Object.Services[params.Name]
|
||||||
if ok {
|
if ok {
|
||||||
return struct{}{}, fmt.Errorf("Service already Exists")
|
return struct{}{}, fmt.Errorf("Service already Exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Conf.Object.Services[params.Name] = params.Service
|
f.ConfigManager.GetPendingConfig().Object.Services[params.Name] = params.Service
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,12 +38,12 @@ type UpdateServiceParameters struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Object) UpdateService(ctx context.Context, params CreateServiceParameters) (struct{}, error) {
|
func (f *Object) UpdateService(ctx context.Context, params CreateServiceParameters) (struct{}, error) {
|
||||||
_, ok := f.Conf.Object.Services[params.Name]
|
_, ok := f.ConfigManager.GetPendingConfig().Object.Services[params.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return struct{}{}, fmt.Errorf("Service does not Exist")
|
return struct{}{}, fmt.Errorf("Service does not Exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Conf.Object.Services[params.Name] = params.Service
|
f.ConfigManager.GetPendingConfig().Object.Services[params.Name] = params.Service
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,11 +52,11 @@ type DeleteServiceParameters struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Object) DeleteService(ctx context.Context, params DeleteServiceParameters) (struct{}, error) {
|
func (f *Object) DeleteService(ctx context.Context, params DeleteServiceParameters) (struct{}, error) {
|
||||||
_, ok := f.Conf.Object.Services[params.Name]
|
_, ok := f.ConfigManager.GetPendingConfig().Object.Services[params.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
return struct{}{}, fmt.Errorf("Interface does not Exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(f.Conf.Object.Services, params.Name)
|
delete(f.ConfigManager.GetPendingConfig().Object.Services, params.Name)
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
"golang.org/x/exp/slog"
|
||||||
|
|
||||||
"nfsense.net/nfsense/internal/definitions"
|
"nfsense.net/nfsense/internal/config"
|
||||||
"nfsense.net/nfsense/internal/jsonrpc"
|
"nfsense.net/nfsense/internal/jsonrpc"
|
||||||
"nfsense.net/nfsense/internal/session"
|
"nfsense.net/nfsense/internal/session"
|
||||||
)
|
)
|
||||||
|
@ -18,7 +18,7 @@ var mux = http.NewServeMux()
|
||||||
var apiHandler *jsonrpc.Handler
|
var apiHandler *jsonrpc.Handler
|
||||||
var stopCleanup chan struct{}
|
var stopCleanup chan struct{}
|
||||||
|
|
||||||
func StartWebserver(conf *definitions.Config, _apiHandler *jsonrpc.Handler) {
|
func StartWebserver(configManager *config.ConfigManager, _apiHandler *jsonrpc.Handler) {
|
||||||
server.Addr = ":8080"
|
server.Addr = ":8080"
|
||||||
server.Handler = mux
|
server.Handler = mux
|
||||||
apiHandler = _apiHandler
|
apiHandler = _apiHandler
|
||||||
|
|
74
main.go
74
main.go
|
@ -2,21 +2,19 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/exp/slog"
|
"golang.org/x/exp/slog"
|
||||||
|
configAPI "nfsense.net/nfsense/internal/api/config"
|
||||||
"nfsense.net/nfsense/internal/api/firewall"
|
"nfsense.net/nfsense/internal/api/firewall"
|
||||||
"nfsense.net/nfsense/internal/api/network"
|
"nfsense.net/nfsense/internal/api/network"
|
||||||
"nfsense.net/nfsense/internal/api/object"
|
"nfsense.net/nfsense/internal/api/object"
|
||||||
"nfsense.net/nfsense/internal/definitions"
|
"nfsense.net/nfsense/internal/config"
|
||||||
"nfsense.net/nfsense/internal/jsonrpc"
|
"nfsense.net/nfsense/internal/jsonrpc"
|
||||||
"nfsense.net/nfsense/internal/nftables"
|
|
||||||
"nfsense.net/nfsense/internal/server"
|
"nfsense.net/nfsense/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,27 +24,31 @@ func main() {
|
||||||
|
|
||||||
slog.Info("Starting...")
|
slog.Info("Starting...")
|
||||||
|
|
||||||
conf, err := LoadConfiguration("config.json")
|
configManager := config.CreateConfigManager()
|
||||||
|
|
||||||
|
err := configManager.LoadCurrentConfigFromDisk()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Loading Config", err)
|
slog.Error("Loading Current Config", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Config Loaded", "config", conf)
|
slog.Info("Config Loaded")
|
||||||
|
|
||||||
err = definitions.ValidateConfig(conf)
|
err = configManager.LoadPendingConfigFromDisk()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Validating Config", err)
|
slog.Error("Loading Pending Config", err)
|
||||||
os.Exit(1)
|
err = configManager.DiscardPendingConfig()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Discarding Pending Config", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Validating Config...")
|
|
||||||
|
|
||||||
if *applyPtr {
|
if *applyPtr {
|
||||||
slog.Info("Applying Config...")
|
slog.Info("Applying Config...")
|
||||||
err := apply(conf)
|
err := configManager.ApplyPendingChanges()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Applying Config", err)
|
slog.Error("Applying Pending Config", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
slog.Info("Config Applied, Exiting...")
|
slog.Info("Config Applied, Exiting...")
|
||||||
|
@ -55,10 +57,10 @@ func main() {
|
||||||
|
|
||||||
slog.Info("Setup API...")
|
slog.Info("Setup API...")
|
||||||
apiHandler := jsonrpc.NewHandler(100 << 20)
|
apiHandler := jsonrpc.NewHandler(100 << 20)
|
||||||
RegisterAPIMethods(apiHandler, conf)
|
RegisterAPIMethods(apiHandler, configManager)
|
||||||
|
|
||||||
slog.Info("Starting Webserver...")
|
slog.Info("Starting Webserver...")
|
||||||
server.StartWebserver(conf, apiHandler)
|
server.StartWebserver(configManager, apiHandler)
|
||||||
|
|
||||||
slog.Info("Ready.")
|
slog.Info("Ready.")
|
||||||
|
|
||||||
|
@ -76,39 +78,9 @@ func main() {
|
||||||
slog.Info("Done")
|
slog.Info("Done")
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfiguration(file string) (*definitions.Config, error) {
|
func RegisterAPIMethods(apiHandler *jsonrpc.Handler, configManager *config.ConfigManager) {
|
||||||
var config definitions.Config
|
apiHandler.Register("Config", &configAPI.Config{ConfigManager: configManager})
|
||||||
configFile, err := os.Open(file)
|
apiHandler.Register("Firewall", &firewall.Firewall{ConfigManager: configManager})
|
||||||
if err != nil {
|
apiHandler.Register("Network", &network.Network{ConfigManager: configManager})
|
||||||
return nil, fmt.Errorf("opening Config File %w", err)
|
apiHandler.Register("Object", &object.Object{ConfigManager: configManager})
|
||||||
}
|
|
||||||
defer configFile.Close()
|
|
||||||
|
|
||||||
jsonParser := json.NewDecoder(configFile)
|
|
||||||
jsonParser.DisallowUnknownFields()
|
|
||||||
err = jsonParser.Decode(&config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("decoding Config File %w", err)
|
|
||||||
}
|
|
||||||
return &config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterAPIMethods(apiHandler *jsonrpc.Handler, conf *definitions.Config) {
|
|
||||||
apiHandler.Register("Firewall", &firewall.Firewall{Conf: conf})
|
|
||||||
apiHandler.Register("Network", &network.Network{Conf: conf})
|
|
||||||
apiHandler.Register("Object", &object.Object{Conf: conf})
|
|
||||||
}
|
|
||||||
|
|
||||||
func apply(conf *definitions.Config) error {
|
|
||||||
fileContent, err := nftables.GenerateNfTablesFile(*conf)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Generating nftables file %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = nftables.ApplyNfTablesFile(fileContent)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Applying nftables %w", err)
|
|
||||||
}
|
|
||||||
slog.Info("Wrote nftables File!")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue