601 lines
18 KiB
Go
601 lines
18 KiB
Go
package main
|
||
|
||
import (
|
||
// "bufio"
|
||
// "fmt"
|
||
// "io/ioutil"
|
||
"bufio"
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"net"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"syscall"
|
||
)
|
||
|
||
// ОС серверов Платформа и Локации
|
||
// (источник ОС)
|
||
// - Серверная платформа
|
||
// dmidecode -t system
|
||
|
||
func dmidecodeTSystem() (biosInfo, error) {
|
||
var biosInfo biosInfo
|
||
// нужно получить информацию из bios
|
||
// cat /sys/firmware/dmi/entries/1-0/raw
|
||
bi, err := os.ReadFile("/sys/firmware/dmi/entries/1-0/raw")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка получения dmi данных: %s", err)
|
||
}
|
||
biosInfo.Name = string(bi)
|
||
return biosInfo, nil
|
||
|
||
}
|
||
|
||
// - Операционная система
|
||
// cat /etc/os-release
|
||
// cat /etc/astra_version
|
||
// cat /etc/astra_license
|
||
|
||
func getOs() (operatingSystem, []error) {
|
||
var opsys operatingSystem
|
||
var errors []error
|
||
|
||
fOsInfo, err := os.ReadFile("/etc/os-release")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка чтения os-release: %s", err)
|
||
errors = append(errors, err)
|
||
} else {
|
||
osInfoSpl := strings.Split(string(fOsInfo), "\n")
|
||
for _, r := range osInfoSpl {
|
||
capt, _ := regexp.MatchString("PRETTY_NAME*", r)
|
||
if capt {
|
||
opsys.OsRelease = strings.Split(r, "=")[1]
|
||
break
|
||
}
|
||
}
|
||
}
|
||
_, err = os.Stat("/etc/astra_version")
|
||
if err != nil {
|
||
err = fmt.Errorf("ошибка чтения asrta_version")
|
||
errors = append(errors, err)
|
||
} else {
|
||
|
||
}
|
||
|
||
return opsys, errors
|
||
}
|
||
|
||
// - Пакеты
|
||
// apt list --installed
|
||
|
||
func getDebPackages() ([]installedPackage, error) {
|
||
// читаем /var/lib/dpkg/status
|
||
var debPackages []installedPackage
|
||
|
||
b_dpkgStatus, err := os.ReadFile("/var/lib/dpkg/status")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка чтения файла /var/lib/dpkg/status: %s", err)
|
||
return nil, err
|
||
}
|
||
|
||
dpkgStatus := strings.Split(string(b_dpkgStatus), "\n\n")
|
||
for _, ds := range dpkgStatus {
|
||
var debPackage installedPackage
|
||
if len(ds) == 0 {
|
||
continue
|
||
}
|
||
ds_s := strings.Split(ds, "\n")
|
||
for _, d := range ds_s {
|
||
if strings.Contains(d, "Package: ") {
|
||
debPackage.Name = strings.Split(d, ": ")[1]
|
||
}
|
||
if strings.Contains(d, "Version: ") {
|
||
debPackage.Version = strings.Split(d, ": ")[1]
|
||
}
|
||
}
|
||
debPackages = append(debPackages, debPackage)
|
||
}
|
||
|
||
return debPackages, err
|
||
}
|
||
|
||
func getRpmPackages() ([]installedPackage, error) {
|
||
var rpmPackages []installedPackage
|
||
|
||
cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME};%{VERSION}\n")
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка выпонения команды rpm -qa: %s", err)
|
||
return nil, err
|
||
}
|
||
scanner := bufio.NewScanner(strings.NewReader(string(output)))
|
||
for scanner.Scan() {
|
||
var instpack installedPackage
|
||
line := scanner.Text()
|
||
parts := strings.Split(line, ";")
|
||
instpack.Name = parts[0]
|
||
instpack.Version = parts[1]
|
||
rpmPackages = append(rpmPackages, instpack)
|
||
}
|
||
return rpmPackages, err
|
||
}
|
||
|
||
// - Размер диска и объем свободного места на диске
|
||
// lsblk
|
||
// df
|
||
// df -i
|
||
|
||
func getFsInfo() ([]fsInfo, error) {
|
||
var fsInfoList []fsInfo
|
||
fProcMounts, err := os.ReadFile("/proc/mounts")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка чтения файла /proc/mounts : %s", err)
|
||
return fsInfoList, err
|
||
}
|
||
for _, mp := range strings.Split(string(fProcMounts), "\n") {
|
||
var fi fsInfo
|
||
var sysStatfs syscall.Statfs_t
|
||
if len(mp) == 0 {
|
||
continue
|
||
}
|
||
mp_s := strings.Split(mp, " ")
|
||
fi.MountPointDevice = mp_s[0]
|
||
fi.MountPointOsPath = mp_s[1]
|
||
err = syscall.Statfs(fi.MountPointOsPath, &sysStatfs)
|
||
if err != nil {
|
||
log.Println(err)
|
||
} else {
|
||
fi.MountPointSize = sysStatfs.Blocks * uint64(sysStatfs.Bsize)
|
||
fi.MountPointFree = sysStatfs.Blocks * uint64(sysStatfs.Bfree)
|
||
}
|
||
fsInfoList = append(fsInfoList, fi)
|
||
// fmt.Printf("Dev: %s Path: %s Size: %d Free: %d \n", fi.MountPointDevice, fi.MountPointOsPath, fi.MountPointSize, fi.MountPointFree)
|
||
}
|
||
return fsInfoList, err
|
||
}
|
||
|
||
// - LA
|
||
// top -bn1 | head -n 1
|
||
// cat /proc/loadavg
|
||
|
||
func getLa() (loadAverage, error) {
|
||
var la loadAverage
|
||
laFile, err := os.ReadFile("/proc/loadavg")
|
||
if err != nil {
|
||
err = fmt.Errorf("файл proc loadavg недоступен %s", err)
|
||
return la, err
|
||
}
|
||
laFileSplit := strings.Split(string(laFile), " ")
|
||
la.OneMin = laFileSplit[0]
|
||
la.FiveMin = laFileSplit[1]
|
||
la.FifteenMin = laFileSplit[2]
|
||
la.Processes = laFileSplit[3]
|
||
|
||
return la, nil
|
||
}
|
||
|
||
// - Процессор
|
||
// cat /proc/cpuinfo
|
||
// lscpu
|
||
|
||
// func getCpu() (cpu, error) {
|
||
// var cpu cpu
|
||
// cpuFile, err := os.ReadFile("/proc/cpuinfo")
|
||
// if err != nil {
|
||
// err = fmt.Errorf("Проблема с доступом к файлу proc cpuinfo %s", err)
|
||
// return cpu, err
|
||
// }
|
||
// prevCpu := " "
|
||
// for _, line := range strings.Split(string(cpuFile), "\n") {
|
||
// matched, err := regexp.Match("model name", []byte(line))
|
||
// if err != nil {
|
||
// err = fmt.Errorf("Ошибка при оценке регулярного выражения %s", err)
|
||
// return cpu, err
|
||
// }
|
||
// if matched {
|
||
// cpuFound := strings.Split(line, ":")[1]
|
||
// if cpuFound != prevCpu {
|
||
// cpu.Model = append(cpu.Model, cpuFound)
|
||
// prevCpu = cpuFound
|
||
// }
|
||
// }
|
||
// }
|
||
// return cpu, nil
|
||
// }
|
||
func getCpuinfo() ([]cpu, error) {
|
||
var cpus []cpu
|
||
var err error
|
||
rawCpuinfo, err := os.ReadFile("/proc/cpuinfo")
|
||
|
||
var c cpu
|
||
var physId []string
|
||
physId = append(physId, "a")
|
||
for _, cpuLine := range strings.Split(string(rawCpuinfo), "\n") {
|
||
if len(strings.TrimSpace(cpuLine)) == 0 {
|
||
controlPoint := 0
|
||
for _, arrayIndex := range physId {
|
||
if c.PhysicalId == "" {
|
||
controlPoint = 1
|
||
}
|
||
if arrayIndex == c.PhysicalId {
|
||
controlPoint = 1
|
||
}
|
||
}
|
||
if controlPoint == 0 {
|
||
cpus = append(cpus, c)
|
||
physId = append(physId, c.PhysicalId)
|
||
c = cpu{}
|
||
} else {
|
||
c = cpu{}
|
||
}
|
||
} else {
|
||
if strings.Contains(cpuLine, "vendor_id") {
|
||
c.VendorId = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "cpu family") {
|
||
c.CpuFamily = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "model") && !strings.Contains(cpuLine, "name") {
|
||
c.Model = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "model name") {
|
||
c.ModelName = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "stepping") {
|
||
c.Stepping = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "microcode") {
|
||
c.Microcode = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "cpu MHz") {
|
||
c.CpuMhz = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "cache size") {
|
||
c.CacheSize = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "physical id") {
|
||
c.PhysicalId = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "cpu cores") {
|
||
c.CpuCores = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "flags") {
|
||
c.Flags = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "bugs") {
|
||
c.Bugs = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "bogomips") {
|
||
c.Bogomips = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "TLB size") {
|
||
c.TlbSize = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "clflush size") {
|
||
c.ClflushSize = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "cache_alignment") {
|
||
c.CacheAlignment = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "address size") {
|
||
c.AddressSize = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
if strings.Contains(cpuLine, "power management") {
|
||
c.PowerManagement = strings.TrimSpace(strings.Split(cpuLine, ":")[1])
|
||
}
|
||
}
|
||
}
|
||
return cpus, err
|
||
}
|
||
|
||
// - Объём и тип ОЗУ, объём свободной памяти в моменте
|
||
// vmstat -s
|
||
// free
|
||
// /proc/meminfo
|
||
|
||
func getRam() (ram, error) {
|
||
var ram ram
|
||
ramFile, err := os.ReadFile("/proc/meminfo")
|
||
if err != nil {
|
||
err = fmt.Errorf("Проблема с доступом к файлу proc meminfo %s", err)
|
||
return ram, err
|
||
}
|
||
for _, line := range strings.Split(string(ramFile), "\n") {
|
||
matched, _ := regexp.Match("MemTotal", []byte(line))
|
||
if matched {
|
||
ram.MemTotal = strings.Trim(strings.Split(line, ":")[1], " ")
|
||
}
|
||
matched, _ = regexp.Match("MemFree", []byte(line))
|
||
if matched {
|
||
ram.MemFree = strings.Trim(strings.Split(line, ":")[1], " ")
|
||
}
|
||
matched, _ = regexp.Match("Available", []byte(line))
|
||
if matched {
|
||
ram.MemAvailable = strings.Trim(strings.Split(line, ":")[1], " ")
|
||
}
|
||
}
|
||
return ram, nil
|
||
}
|
||
|
||
// - Uptime сервера
|
||
// uptime
|
||
|
||
func getUptime() (uptime, error) {
|
||
var uptime uptime
|
||
uptimeFile, err := os.ReadFile("/proc/uptime")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка досутпа к файлу proc uptime: %s", err)
|
||
return uptime, err
|
||
}
|
||
uptime.WorkSeconds = strings.Split(string(uptimeFile), " ")[0]
|
||
return uptime, nil
|
||
}
|
||
|
||
// - NTP (nrp ntpsec chrony)
|
||
// Запущен ли сервис, выполнялась ли синхронизация
|
||
// date -R на всех серверах инфраструктуры
|
||
|
||
func getTimeService() (timeService, []error) {
|
||
var timeService timeService
|
||
var errors []error
|
||
cmd := exec.Command("timedatectl", "show", "--property=NTPSynchronized")
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка получения данных timedatectl: %s", err)
|
||
errors = append(errors, err)
|
||
err = nil
|
||
}
|
||
timeService.TimeSync = strings.Trim(strings.Split(string(output), "=")[1], "\n")
|
||
dir, err := os.ReadDir("/run/systemd/units")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка при получении содержимого run systemd units : %s", err)
|
||
errors = append(errors, err)
|
||
err = nil
|
||
} else {
|
||
for _, entry := range dir {
|
||
matchedChrony, _ := regexp.Match("chrony", []byte(entry.Name()))
|
||
if matchedChrony {
|
||
timeService.TimeService = strings.Split(entry.Name(), ":")[1]
|
||
break
|
||
}
|
||
matchedNtp, _ := regexp.Match("ntp", []byte(entry.Name()))
|
||
if matchedNtp {
|
||
timeService.TimeService = strings.Split(entry.Name(), ":")[1]
|
||
break
|
||
}
|
||
timeService.TimeService = "nothing"
|
||
}
|
||
}
|
||
return timeService, errors
|
||
}
|
||
|
||
// - Стандартный сбор ошибок
|
||
// journalctl -xe -p4 -o json-pretty
|
||
// dmesg -T --level=err,warn
|
||
|
||
// dmesg \| grep -i -E 'error\|failed\|critical\|bug\|panic' или dmesg --level=warn,err
|
||
// journalctl \| grep -i -E 'error\|failed\|critical\|bug\|panic' или journalctl -xe -p 4 можно since "1 day ago" -o json-pretty
|
||
// journalctl -xe -p 4 --since "1 day ago" -o json-pretty
|
||
|
||
func getJournalctl() ([]journalctlEntry, error) {
|
||
var jList []journalctlEntry
|
||
cmd := exec.Command("journalctl", "-xe", "-p 4", "--since", "1 day ago", "-o", "json-pretty")
|
||
output, err := cmd.Output()
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка выполнения команды journalctl -xe -p 4 --since '5 minutes ago' -o json-pretty : %s", err)
|
||
return jList, err
|
||
}
|
||
|
||
outputSplited := strings.Split(string(output), "}\n{")
|
||
for _, event := range outputSplited {
|
||
var je journalctlEntry
|
||
if len(event) == 0 {
|
||
continue
|
||
}
|
||
event = strings.Trim(event, "{}")
|
||
eventFull := "{" + event + "}"
|
||
// err = json.Unmarshal([]byte(eventFull), &je)
|
||
buf := bytes.NewBuffer([]byte(eventFull))
|
||
err = json.NewDecoder(buf).Decode(&je)
|
||
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка десериализации данных: %s в итерации %s", err, eventFull)
|
||
return jList, err
|
||
}
|
||
jList = append(jList, je)
|
||
}
|
||
|
||
return jList, nil
|
||
}
|
||
|
||
// - Порты
|
||
// nmap от Платформы к Локациям и наоборот
|
||
// оставим пока, двигаем в самый низ, как реализовать пока не представляю. Нужны проверки портов с учетом фаерволов
|
||
// возможно это будет выполнение в лоб команды docker exec -it dci_back dcissh -a "nmap"
|
||
// пока вместо открытых портов проверим запущен ли фаервол
|
||
|
||
func getFirewalls() (firewalls, error) {
|
||
var fwl firewalls
|
||
units, err := os.ReadDir("/run/systemd/units")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка при получении содержимого run systemd units : %s", err)
|
||
return fwl, err
|
||
} else {
|
||
for _, unit := range units {
|
||
ufw, _ := regexp.Match("ufw", []byte(unit.Name()))
|
||
if ufw {
|
||
fwl.Ufw = "running"
|
||
}
|
||
nftables, _ := regexp.Match("nftables", []byte(unit.Name()))
|
||
if nftables {
|
||
fwl.Nftables = "running"
|
||
}
|
||
iptables, _ := regexp.Match("iptables", []byte(unit.Name()))
|
||
if iptables {
|
||
fwl.Iptables = "running"
|
||
}
|
||
firewalld, _ := regexp.Match("firewalld", []byte(unit.Name()))
|
||
if firewalld {
|
||
fwl.Firewalld = "running"
|
||
}
|
||
}
|
||
}
|
||
return fwl, nil
|
||
}
|
||
|
||
// - Настройки сети
|
||
// NetworkManager
|
||
// настройки интерфейсов
|
||
// etc/hosts
|
||
// etc/resolv.conf
|
||
|
||
func getNetworkConf() (networkConfig, []error) {
|
||
var networkConfig networkConfig
|
||
var errors []error
|
||
|
||
interfaces, err := net.Interfaces()
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка при получении сетевых интерфейсов: %s", err)
|
||
errors = append(errors, err)
|
||
err = nil
|
||
} else {
|
||
for _, interf := range interfaces {
|
||
var netInterface netInterface
|
||
netInterface.Device = interf.Name
|
||
netInterface.Flags = interf.Flags.String()
|
||
netInterface.HwAddr = interf.HardwareAddr.String()
|
||
adrs, _ := interf.Addrs()
|
||
for _, adr := range adrs {
|
||
netInterface.IpAddr = append(netInterface.IpAddr, adr.String())
|
||
}
|
||
netInterface.Index = strconv.Itoa(interf.Index)
|
||
netInterface.MTU = strconv.Itoa(interf.MTU)
|
||
networkConfig.NetInterfaces = append(networkConfig.NetInterfaces, netInterface)
|
||
// fmt.Printf("interf: %v\n", interf)
|
||
}
|
||
}
|
||
|
||
units, err := os.ReadDir("/run/systemd/units")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка при получении содержимого run systemd units : %s", err)
|
||
errors = append(errors, err)
|
||
err = nil
|
||
} else {
|
||
for _, unit := range units {
|
||
// Полчить статус NetworkManager
|
||
matched, _ := regexp.Match("NetworkManager.services", []byte(unit.Name()))
|
||
if matched {
|
||
networkConfig.NetworkManager = "running"
|
||
}
|
||
// Полчить статус networking
|
||
matchedNetworking, _ := regexp.Match("networking", []byte(unit.Name()))
|
||
if matchedNetworking {
|
||
networkConfig.Networking = "running"
|
||
}
|
||
}
|
||
}
|
||
nmConns, err := os.ReadDir("/run/NetworkManager/system-connections")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка при получении содержимого run NetworkManager")
|
||
errors = append(errors, err)
|
||
err = nil
|
||
} else {
|
||
for _, nmConn := range nmConns {
|
||
networkConfig.NetworkManagerConn = append(networkConfig.NetworkManagerConn, nmConn.Name())
|
||
}
|
||
}
|
||
|
||
// Содержимое /etc/hosts
|
||
linesByte, err := os.ReadFile("/etc/hosts")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка при получении содержимого etc hosts : %s", err)
|
||
errors = append(errors, err)
|
||
err = nil
|
||
} else {
|
||
lines := strings.Split(string(linesByte), "\n")
|
||
for _, line := range lines {
|
||
if strings.HasPrefix(line, "#") {
|
||
continue
|
||
} else {
|
||
networkConfig.EtcHosts = append(networkConfig.EtcHosts, line)
|
||
}
|
||
}
|
||
}
|
||
return networkConfig, errors
|
||
}
|
||
|
||
// Возможность доступа к основным ресурсам в internet
|
||
// docker-registry.ispsystem.com, download.docker.com — для доступа к Docker;
|
||
// download.ispsystem.com — для обновления и установки платформы;
|
||
// license6.ispsystem.com — для проверки лицензий;
|
||
// metricreport.ispsystem.net — для работы сервера метрик.
|
||
|
||
func getInternetResource(ispUrl string) (internetResource, error) {
|
||
var ir internetResource
|
||
var outErr error
|
||
ipList, err := net.LookupIP(ispUrl)
|
||
if err != nil {
|
||
return ir, err
|
||
}
|
||
ir.Name = ispUrl
|
||
var i []string
|
||
for _, ip := range ipList {
|
||
i = append(i, ip.String())
|
||
}
|
||
ir.IpList = strings.Join(i, ",")
|
||
|
||
httpUrl := "http://" + ispUrl
|
||
httpsUrl := "https://" + ispUrl
|
||
|
||
resp, err := http.Get(httpUrl)
|
||
if err != nil {
|
||
outErr = fmt.Errorf("Ошибка HTTP %s :: %s. ", httpUrl, err)
|
||
ir.Http = err.Error()
|
||
} else {
|
||
ir.Http = strconv.Itoa(resp.StatusCode)
|
||
}
|
||
|
||
resp, err = http.Get(httpsUrl)
|
||
if err != nil {
|
||
outErr = fmt.Errorf("%s Ошибка HTTPS %s :: %s", outErr, httpUrl, err)
|
||
ir.Https = err.Error()
|
||
} else {
|
||
ir.Https = strconv.Itoa(resp.StatusCode)
|
||
}
|
||
return ir, outErr
|
||
}
|
||
|
||
// 2026-02.5 получить историю. просто прочитаем файл с историей bash у root
|
||
|
||
func getRootHistory() ([]rootHistoryCommand, error) {
|
||
var rootHistoryCommands []rootHistoryCommand
|
||
rawHistory, err := os.ReadFile("/root/.bash_history")
|
||
if err != nil {
|
||
err = fmt.Errorf("Ошибка при открытии файла /root/.bash_history : %s", err.Error())
|
||
return nil, err
|
||
}
|
||
i := 0
|
||
execTime := ""
|
||
for _, line := range strings.Split(string(rawHistory), "\n") {
|
||
var c rootHistoryCommand
|
||
if strings.HasPrefix(line, "#") {
|
||
execTime = strings.TrimLeft(line, "#")
|
||
} else {
|
||
c.Id = i
|
||
i += 1
|
||
c.ExecutionTime = execTime
|
||
c.Command = line
|
||
execTime = ""
|
||
rootHistoryCommands = append(rootHistoryCommands, c)
|
||
}
|
||
|
||
}
|
||
return rootHistoryCommands, err
|
||
}
|