dynamically determine network backend
This commit is contained in:
parent
dac1b10c5d
commit
054b7a6d9d
6
Makefile
6
Makefile
|
@ -4,10 +4,14 @@ LIBGUESTFS_PATH = guestfs/appliance
|
||||||
LIBGUESTFS_HV = scripts/qemu.sh
|
LIBGUESTFS_HV = scripts/qemu.sh
|
||||||
APPLIANCE_VERSION = 1.46.0
|
APPLIANCE_VERSION = 1.46.0
|
||||||
|
|
||||||
|
ifeq ($(shell test -e $(DEPS_DIR)/usr/lib/x86_64-linux-gnu/libvirt.so.0 && echo -n yes),yes)
|
||||||
|
CGO_LDFLAGS = -lvirt -lyajl
|
||||||
|
endif
|
||||||
|
|
||||||
# Export LIBGUESTFS_DEBUG=1 to debug
|
# Export LIBGUESTFS_DEBUG=1 to debug
|
||||||
|
|
||||||
all:
|
all:
|
||||||
LIBRARY_PATH=$(LIBRARY_PATH) go build
|
LIBRARY_PATH=$(LIBRARY_PATH) CGO_LDFLAGS="$(CGO_LDFLAGS)" go build
|
||||||
|
|
||||||
run:
|
run:
|
||||||
LD_LIBRARY_PATH=$(LIBRARY_PATH) LIBGUESTFS_PATH=$(LIBGUESTFS_PATH) LIBGUESTFS_HV=$(LIBGUESTFS_HV) LIBGUESTFS_BACKEND_SETTINGS=force_tcg ./cloudbuild
|
LD_LIBRARY_PATH=$(LIBRARY_PATH) LIBGUESTFS_PATH=$(LIBGUESTFS_PATH) LIBGUESTFS_HV=$(LIBGUESTFS_HV) LIBGUESTFS_BACKEND_SETTINGS=force_tcg ./cloudbuild
|
||||||
|
|
|
@ -68,8 +68,8 @@ func (mgr *AlmaLinuxTemplateManager) DownloadTemplate(version, codename string)
|
||||||
return mgr.DownloadTemplateGeneric(filename, url)
|
return mgr.DownloadTemplateGeneric(filename, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *AlmaLinuxTemplateManager) addCloudInitSnippet(handle *guestfs.Guestfs) error {
|
func (mgr *AlmaLinuxTemplateManager) addAlmaLinuxCloudInitSnippet(handle *guestfs.Guestfs) error {
|
||||||
path := "/etc/cloud/cloud.cfg.d/99_csclub.cfg"
|
path := "/etc/cloud/cloud.cfg.d/99_csclub_misc.cfg"
|
||||||
mgr.logger.Debug().Msg("Writing to " + path)
|
mgr.logger.Debug().Msg("Writing to " + path)
|
||||||
return handle.Write(path, getResource("fedora-cloud-init"))
|
return handle.Write(path, getResource("fedora-cloud-init"))
|
||||||
}
|
}
|
||||||
|
@ -92,13 +92,7 @@ func (mgr *AlmaLinuxTemplateManager) transformAlmaLinuxYumRepoBaseUrl(url string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *AlmaLinuxTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
|
func (mgr *AlmaLinuxTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
|
||||||
if err = mgr.setChronyOptions(handle); err != nil {
|
if err = mgr.addAlmaLinuxCloudInitSnippet(handle); err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = mgr.setNetworkManagerOptions(handle); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = mgr.addCloudInitSnippet(handle); err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = mgr.replaceYumMirrorUrls(handle, mgr.transformAlmaLinuxYumRepoBaseUrl); err != nil {
|
if err = mgr.replaceYumMirrorUrls(handle, mgr.transformAlmaLinuxYumRepoBaseUrl); err != nil {
|
||||||
|
|
|
@ -75,15 +75,6 @@ func (mgr *DebianTemplateManager) CommandToUpdatePackageCache() []string {
|
||||||
return debianCommandToUpdatePackageCache()
|
return debianCommandToUpdatePackageCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *DebianTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
|
func (mgr *DebianTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) error {
|
||||||
if err = mgr.setChronyOptions(handle); err != nil {
|
return mgr.replaceDebianMirrorUrls(handle)
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = mgr.setDhclientOptions(handle); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = mgr.replaceDebianMirrorUrls(handle); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,8 +113,8 @@ func (mgr *FedoraTemplateManager) DownloadTemplate(version, codename string) (pa
|
||||||
return mgr.DownloadTemplateGeneric(filename, url)
|
return mgr.DownloadTemplateGeneric(filename, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *FedoraTemplateManager) addCloudInitSnippet(handle *guestfs.Guestfs) error {
|
func (mgr *FedoraTemplateManager) addFedoraCloudInitSnippet(handle *guestfs.Guestfs) error {
|
||||||
path := "/etc/cloud/cloud.cfg.d/99_csclub.cfg"
|
path := "/etc/cloud/cloud.cfg.d/99_csclub_misc.cfg"
|
||||||
mgr.logger.Debug().Msg("Writing to " + path)
|
mgr.logger.Debug().Msg("Writing to " + path)
|
||||||
return handle.Write(path, getResource("fedora-cloud-init"))
|
return handle.Write(path, getResource("fedora-cloud-init"))
|
||||||
}
|
}
|
||||||
|
@ -137,13 +137,7 @@ func (mgr *FedoraTemplateManager) transformFedoraYumRepoBaseUrl(url string) stri
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *FedoraTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
|
func (mgr *FedoraTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
|
||||||
if err = mgr.setChronyOptions(handle); err != nil {
|
if err = mgr.addFedoraCloudInitSnippet(handle); err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = mgr.setNetworkManagerOptions(handle); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = mgr.addCloudInitSnippet(handle); err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = mgr.replaceYumMirrorUrls(handle, mgr.transformFedoraYumRepoBaseUrl); err != nil {
|
if err = mgr.replaceYumMirrorUrls(handle, mgr.transformFedoraYumRepoBaseUrl); err != nil {
|
||||||
|
|
|
@ -97,9 +97,6 @@ func (mgr *OpensuseTumbleweedTemplateManager) CommandToUpdatePackageCache() []st
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *OpensuseTumbleweedTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
|
func (mgr *OpensuseTumbleweedTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
|
||||||
if err = mgr.setChronyOptions(handle); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = mgr.maskSystemdUnit(handle, "wickedd-dhcp6.service"); err != nil {
|
if err = mgr.maskSystemdUnit(handle, "wickedd-dhcp6.service"); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
network:
|
||||||
|
version: 2
|
||||||
|
ethernets:
|
||||||
|
id0:
|
||||||
|
match:
|
||||||
|
name: e*
|
||||||
|
dhcp4: true
|
||||||
|
accept-ra: false
|
|
@ -1,11 +1,2 @@
|
||||||
network:
|
|
||||||
version: 2
|
|
||||||
ethernets:
|
|
||||||
id0:
|
|
||||||
match:
|
|
||||||
name: e*
|
|
||||||
dhcp4: true
|
|
||||||
accept-ra: false
|
|
||||||
|
|
||||||
apt_preserve_sources_list: true
|
apt_preserve_sources_list: true
|
||||||
manage_etc_hosts: true
|
manage_etc_hosts: true
|
||||||
|
|
|
@ -106,12 +106,18 @@ func fedoraCommandToUpdatePackageCache() []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *TemplateManager) performDistroAgnosticModifications(handle *guestfs.Guestfs) (err error) {
|
func (mgr *TemplateManager) performDistroAgnosticModifications(handle *guestfs.Guestfs) (err error) {
|
||||||
|
if err = mgr.setupCommonNetworkConfigs(handle); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err = mgr.setupIpv6Scripts(handle); err != nil {
|
if err = mgr.setupIpv6Scripts(handle); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = mgr.setResolvConf(handle); err != nil {
|
if err = mgr.setResolvConf(handle); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = mgr.setupCommonNTPConfigs(handle); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err = mgr.setMotd(handle); err != nil {
|
if err = mgr.setMotd(handle); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -242,6 +248,35 @@ func (mgr *TemplateManager) maskSystemdUnit(handle *guestfs.Guestfs, unit string
|
||||||
return handle.Ln_sf("/dev/null", "/etc/systemd/system/"+unit)
|
return handle.Ln_sf("/dev/null", "/etc/systemd/system/"+unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func systemdServiceIsEnabled(handle *guestfs.Guestfs, unit string) (bool, error) {
|
||||||
|
linkName := ""
|
||||||
|
isSymlink := false
|
||||||
|
for _, dirPrefix := range []string{"/etc", "/lib"} {
|
||||||
|
linkName = dirPrefix + "/systemd/system/multi-user.target.wants/" + unit
|
||||||
|
var err error
|
||||||
|
isSymlink, err = handle.Is_symlink(linkName)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("could not determine if %s is a symlink: %w", linkName, err)
|
||||||
|
}
|
||||||
|
if isSymlink {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isSymlink {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
target, err := handle.Readlink(linkName)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("could not read link at %s: %w", linkName, err)
|
||||||
|
}
|
||||||
|
for _, dirPrefix := range []string{"/etc", "/lib"} {
|
||||||
|
if target == dirPrefix+"/systemd/system/"+unit {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// setChronyOptions sets custom NTP server URLs in a chrony config file.
|
// setChronyOptions sets custom NTP server URLs in a chrony config file.
|
||||||
func (mgr *TemplateManager) setChronyOptions(handle *guestfs.Guestfs) (err error) {
|
func (mgr *TemplateManager) setChronyOptions(handle *guestfs.Guestfs) (err error) {
|
||||||
possiblePaths := []string{"/etc/chrony.conf", "/etc/chrony/chrony.conf"}
|
possiblePaths := []string{"/etc/chrony.conf", "/etc/chrony/chrony.conf"}
|
||||||
|
@ -315,8 +350,7 @@ func (mgr *TemplateManager) setChronyOptions(handle *guestfs.Guestfs) (err error
|
||||||
// Otherwise, assume we need to modify chrony.conf directly
|
// Otherwise, assume we need to modify chrony.conf directly
|
||||||
// e.g. Fedora
|
// e.g. Fedora
|
||||||
if newLines == nil {
|
if newLines == nil {
|
||||||
mgr.logger.Warn().Msg("could not find chrony.conf, skipping")
|
return errors.New("could not find chrony.conf")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
serverDirectiveExists := false
|
serverDirectiveExists := false
|
||||||
for _, line := range newLines {
|
for _, line := range newLines {
|
||||||
|
@ -334,6 +368,45 @@ func (mgr *TemplateManager) setChronyOptions(handle *guestfs.Guestfs) (err error
|
||||||
return handle.Write(path, []byte(newContent))
|
return handle.Write(path, []byte(newContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mgr *TemplateManager) setTimesyncdConf(handle *guestfs.Guestfs) (err error) {
|
||||||
|
mgr.logger.Debug().Msg("Writing custom timesyncd.conf")
|
||||||
|
if err = handle.Mkdir_p("/etc/systemd/timesyncd.conf.d"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return handle.Write("/etc/systemd/timesyncd.conf.d/csclub.conf", getResource("timesyncd.conf"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func usesChrony(handle *guestfs.Guestfs) (bool, error) {
|
||||||
|
// The systemd service for chrony is called on chrony.service on e.g. Debian,
|
||||||
|
// but is called chronyd.service on e.g. Fedora
|
||||||
|
chronyIsEnabled, err := systemdServiceIsEnabled(handle, "chrony.service")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if chronyIsEnabled {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return systemdServiceIsEnabled(handle, "chronyd.service")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mgr *TemplateManager) setupCommonNTPConfigs(handle *guestfs.Guestfs) error {
|
||||||
|
usesChrony, err := usesChrony(handle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usesChrony {
|
||||||
|
return mgr.setChronyOptions(handle)
|
||||||
|
}
|
||||||
|
usesSystemdTimesyncd, err := systemdServiceIsEnabled(handle, "systemd-timesyncd.service")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usesSystemdTimesyncd {
|
||||||
|
return mgr.setTimesyncdConf(handle)
|
||||||
|
}
|
||||||
|
return errors.New("could not determine NTP backend")
|
||||||
|
}
|
||||||
|
|
||||||
func (mgr *TemplateManager) setNetworkManagerOptions(handle *guestfs.Guestfs) (err error) {
|
func (mgr *TemplateManager) setNetworkManagerOptions(handle *guestfs.Guestfs) (err error) {
|
||||||
if err = handle.Mkdir_p("/etc/NetworkManager/conf.d"); err != nil {
|
if err = handle.Mkdir_p("/etc/NetworkManager/conf.d"); err != nil {
|
||||||
return
|
return
|
||||||
|
@ -344,7 +417,54 @@ func (mgr *TemplateManager) setNetworkManagerOptions(handle *guestfs.Guestfs) (e
|
||||||
}
|
}
|
||||||
|
|
||||||
func usesNetworkManager(handle *guestfs.Guestfs) (bool, error) {
|
func usesNetworkManager(handle *guestfs.Guestfs) (bool, error) {
|
||||||
return handle.Is_file("/usr/sbin/NetworkManager", nil)
|
return systemdServiceIsEnabled(handle, "NetworkManager.service")
|
||||||
|
}
|
||||||
|
|
||||||
|
func usesEtcNetworkInterfaces(handle *guestfs.Guestfs) (bool, error) {
|
||||||
|
configPath := "/etc/network/interfaces"
|
||||||
|
configExists, err := handle.Is_file(configPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("Could not determine if "+configPath+" is a file: %w", err)
|
||||||
|
}
|
||||||
|
if !configExists {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return systemdServiceIsEnabled(handle, "networking.service")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mgr *TemplateManager) addSystemdNetworkdCloudInitSnippet(handle *guestfs.Guestfs) error {
|
||||||
|
// Use 98 so that each distro can use 99 for extra customization
|
||||||
|
path := "/etc/cloud/cloud.cfg.d/98_csclub_network.cfg"
|
||||||
|
mgr.logger.Debug().Msg("Writing to " + path)
|
||||||
|
return handle.Write(path, getResource("systemd-networkd-cloud-init"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mgr *TemplateManager) setupCommonNetworkConfigs(handle *guestfs.Guestfs) error {
|
||||||
|
usesNetworkManager, err := usesNetworkManager(handle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usesNetworkManager {
|
||||||
|
return mgr.setNetworkManagerOptions(handle)
|
||||||
|
}
|
||||||
|
usesSystemdNetworkd, err := systemdServiceIsEnabled(handle, "systemd-networkd.service")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usesSystemdNetworkd {
|
||||||
|
return mgr.addSystemdNetworkdCloudInitSnippet(handle)
|
||||||
|
}
|
||||||
|
usesEtcNetworkInterfaces, err := usesEtcNetworkInterfaces(handle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if usesEtcNetworkInterfaces {
|
||||||
|
return mgr.setDhclientOptions(handle)
|
||||||
|
}
|
||||||
|
// The only other network backend used by one of our supported distros is
|
||||||
|
// Sysconfig, which is currently only used by OpenSUSE Tumbleweed.
|
||||||
|
// That case is handled separately in opensuse_tumbleweed.go.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *TemplateManager) setupIpv6Scripts(handle *guestfs.Guestfs) (err error) {
|
func (mgr *TemplateManager) setupIpv6Scripts(handle *guestfs.Guestfs) (err error) {
|
||||||
|
@ -703,14 +823,6 @@ func (mgr *TemplateManager) updateSshdConfig(handle *guestfs.Guestfs) error {
|
||||||
return handle.Aug_set("/files/etc/ssh/sshd_config/PrintLastLog", "no")
|
return handle.Aug_set("/files/etc/ssh/sshd_config/PrintLastLog", "no")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *TemplateManager) setTimesyncdConf(handle *guestfs.Guestfs) (err error) {
|
|
||||||
mgr.logger.Debug().Msg("Writing custom timesyncd.conf")
|
|
||||||
if err = handle.Mkdir_p("/etc/systemd/timesyncd.conf.d"); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return handle.Write("/etc/systemd/timesyncd.conf.d/csclub.conf", getResource("timesyncd.conf"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mgr *TemplateManager) setJournaldConf(handle *guestfs.Guestfs) (err error) {
|
func (mgr *TemplateManager) setJournaldConf(handle *guestfs.Guestfs) (err error) {
|
||||||
mgr.logger.Debug().Msg("Writing custom journald.conf")
|
mgr.logger.Debug().Msg("Writing custom journald.conf")
|
||||||
if err = handle.Mkdir_p("/etc/systemd/journald.conf.d"); err != nil {
|
if err = handle.Mkdir_p("/etc/systemd/journald.conf.d"); err != nil {
|
||||||
|
|
|
@ -79,8 +79,8 @@ func (mgr *UbuntuTemplateManager) DownloadTemplate(version, codename string) (pa
|
||||||
return mgr.DownloadTemplateGeneric(filename, url)
|
return mgr.DownloadTemplateGeneric(filename, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *UbuntuTemplateManager) addCloudInitSnippet(handle *guestfs.Guestfs) error {
|
func (mgr *UbuntuTemplateManager) addUbuntuCloudInitSnippet(handle *guestfs.Guestfs) error {
|
||||||
path := "/etc/cloud/cloud.cfg.d/99_csclub.cfg"
|
path := "/etc/cloud/cloud.cfg.d/99_csclub_misc.cfg"
|
||||||
mgr.logger.Debug().Msg("Writing to " + path)
|
mgr.logger.Debug().Msg("Writing to " + path)
|
||||||
return handle.Write(path, getResource("ubuntu-cloud-init"))
|
return handle.Write(path, getResource("ubuntu-cloud-init"))
|
||||||
}
|
}
|
||||||
|
@ -109,16 +109,10 @@ func (mgr *UbuntuTemplateManager) CommandToUpdatePackageCache() []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mgr *UbuntuTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
|
func (mgr *UbuntuTemplateManager) PerformDistroSpecificModifications(handle *guestfs.Guestfs) (err error) {
|
||||||
if err = mgr.setTimesyncdConf(handle); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = mgr.setDhclientOptions(handle); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = mgr.replaceDebianMirrorUrls(handle); err != nil {
|
if err = mgr.replaceDebianMirrorUrls(handle); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = mgr.addCloudInitSnippet(handle); err != nil {
|
if err = mgr.addUbuntuCloudInitSnippet(handle); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mgr.disableNoisyMotdMessages(handle)
|
mgr.disableNoisyMotdMessages(handle)
|
||||||
|
|
|
@ -27,6 +27,7 @@ libvirt_dependencies=(
|
||||||
libvdeplug2
|
libvdeplug2
|
||||||
libvirglrenderer1
|
libvirglrenderer1
|
||||||
libvirt0
|
libvirt0
|
||||||
|
libvirt-dev
|
||||||
libxencall1
|
libxencall1
|
||||||
libxendevicemodel1
|
libxendevicemodel1
|
||||||
libxenevtchn1
|
libxenevtchn1
|
||||||
|
@ -37,6 +38,7 @@ libvirt_dependencies=(
|
||||||
libxentoolcore1
|
libxentoolcore1
|
||||||
libxentoollog1
|
libxentoollog1
|
||||||
libyajl2
|
libyajl2
|
||||||
|
libyajl-dev
|
||||||
qemu-system-x86
|
qemu-system-x86
|
||||||
seabios
|
seabios
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue