// Package demo starts fake PVE clusters (pvetest: API and SSH) and fills a // database with them, to try the web UI without a Proxmox cluster: a primary, // two targets with differences (missing objects, different fields, accounts // and passwords, a missing mapping) and a PVE 8 cluster that is refused. package demo import ( "context" "crypto/sha256" "encoding/hex" "strings" "pve-confsync/internal/adapter" "pve-confsync/internal/model" "pve-confsync/internal/nodesync" "pve-confsync/internal/pve" "pve-confsync/internal/pve/pvetest" "pve-confsync/internal/store" ) // Password of the demo accounts admin, operateur and lecteur. const Password = "confsync-demo" // Clusters are the fake clusters; Close stops them. type Clusters struct{ servers []*pvetest.Server } // Close stops the fake clusters. func (c *Clusters) Close() { for _, s := range c.servers { s.Close() } } // Start starts the fake clusters and records them (and the demo accounts) in st. func Start(ctx context.Context, st *store.Store) (*Clusters, error) { ref := pvetest.New("9.1.4") ref.Options = map[string]string{ "registered-tags": "backup;dev;prod", "tag-style": "color-map=prod:c0392b:ffffff;dev:2980b9:ffffff,ordering=config,shape=full", "next-id": "lower=100,upper=999999", "user-tag-access": "user-allow=list,user-allow-list=team-a;team-b", "keyboard": "fr", } ref.Pending.Zones["vlanz"] = map[string]any{"type": "vlan", "bridge": "vmbr0", "mtu": 1500, "ipam": "pve"} ref.Pending.Zones["stor"] = map[string]any{"type": "vlan", "bridge": "vmbr2", "mtu": 9000, "ipam": "pve"} ref.Pending.Zones["vxz"] = map[string]any{"type": "vxlan", "peers": "10.0.0.1,10.0.0.2", "mtu": 1450, "ipam": "pve"} ref.Pending.Zones["evpnz"] = map[string]any{"type": "evpn", "controller": "evpnctl", "vrf-vxlan": 4000} ref.Pending.VNets["v100"] = map[string]any{"zone": "vlanz", "tag": 100, "alias": "prod"} ref.Pending.VNets["v200"] = map[string]any{"zone": "vlanz", "tag": 200, "alias": "dev"} ref.Pending.VNets["ceph"] = map[string]any{"zone": "stor", "tag": 50, "alias": "ceph public"} ref.Pending.VNets["vx1"] = map[string]any{"zone": "vxz", "tag": 10001, "alias": "overlay"} ref.Pending.Subnets["vlanz-10.1.0.0-24"] = map[string]any{"vnet": "v100", "gateway": "10.1.0.1", "dhcp-range": []any{"start-address=10.1.0.100,end-address=10.1.0.200"}} ref.Pending.Subnets["vlanz-10.2.0.0-24"] = map[string]any{"vnet": "v200", "gateway": "10.2.0.1"} ref.Commit() ref.Groups["admins"] = map[string]any{"comment": "Administrateurs"} ref.Groups["ops"] = map[string]any{"comment": "Exploitation"} ref.Roles["ConfSyncOps"] = "Sys.Audit,VM.Audit,VM.PowerMgmt" ref.Users["alice@pve"] = map[string]any{"enable": 1, "firstname": "Alice", "email": "alice@example.org", "groups": "admins"} ref.Users["bob@pve"] = map[string]any{"enable": 1, "firstname": "Bob", "email": "bob@example.org", "groups": "ops", "expire": 1893456000} ref.Users["carol@pam"] = map[string]any{"enable": 1, "comment": "Linux account, not replicated"} ref.Shadow["alice"] = "$5$Kd1Xa9$Q8yJdemoAliceHashOnlyForTheDemo0123456789AB" ref.Shadow["bob"] = "$5$Pq7Zr2$W3vLdemoBobHashOnlyForTheDemo0123456789ABCDE" // site-a: tags and an MTU behind, the VXLAN zone and the dev network missing a := pvetest.New("9.0.10") a.Options = map[string]string{ "registered-tags": "dev;prod", "tag-style": "color-map=prod:c0392b:ffffff;dev:2980b9:ffffff,ordering=config,shape=full", "next-id": "lower=2000,upper=2999", "keyboard": "en-us", } a.Pending.Zones["vlanz"] = map[string]any{"type": "vlan", "bridge": "vmbr1", "mtu": 9000, "ipam": "pve"} a.Pending.Zones["stor"] = map[string]any{"type": "vlan", "bridge": "vmbr2", "mtu": 9000, "ipam": "pve", "nodes": "pve-a1,pve-a2"} a.Pending.VNets["v100"] = map[string]any{"zone": "vlanz", "tag": 100, "alias": "production"} a.Pending.VNets["ceph"] = map[string]any{"zone": "stor", "tag": 50, "alias": "ceph public"} a.Pending.Subnets["vlanz-10.1.0.0-24"] = map[string]any{"vnet": "v100", "gateway": "10.1.0.254"} a.Commit() a.Groups["admins"] = map[string]any{"comment": "Admins"} a.Users["alice@pve"] = map[string]any{"enable": 1, "firstname": "Alice", "email": "alice@old.example.org", "groups": "admins"} a.Shadow["alice"] = "$5$OldOld$oldAliceHashOnSiteA0123456789ABCDEFGHIJKLMNOP" // site-b: nearly in line, but no mapping for the bridge of the "stor" zone // (blocking error) and a local zone absent from the reference (kept, no prune) b := pvetest.New("9.1.2") b.Options = map[string]string{ "registered-tags": "backup;dev;prod", "tag-style": "color-map=prod:c0392b:ffffff;dev:2980b9:ffffff,ordering=config,shape=full", "next-id": "lower=100,upper=999999", "user-tag-access": "user-allow=list,user-allow-list=team-a;team-b", } b.Pending.Zones["vlanz"] = map[string]any{"type": "vlan", "bridge": "vmbr0", "mtu": 1500, "ipam": "pve"} b.Pending.Zones["vxz"] = map[string]any{"type": "vxlan", "peers": "172.16.0.1,172.16.0.2", "mtu": 1450, "ipam": "pve"} b.Pending.Zones["labz"] = map[string]any{"type": "vlan", "bridge": "vmbr9", "ipam": "pve"} b.Pending.VNets["v100"] = map[string]any{"zone": "vlanz", "tag": 100, "alias": "prod"} b.Pending.VNets["v200"] = map[string]any{"zone": "vlanz", "tag": 200, "alias": "dev"} b.Pending.Subnets["vlanz-10.1.0.0-24"] = map[string]any{"vnet": "v100", "gateway": "10.1.0.1", "dhcp-range": []any{"start-address=10.1.0.100,end-address=10.1.0.200"}} b.Pending.Subnets["vlanz-10.2.0.0-24"] = map[string]any{"vnet": "v200", "gateway": "10.2.0.1"} b.Commit() b.Groups["admins"] = map[string]any{"comment": "Administrateurs"} b.Groups["ops"] = map[string]any{"comment": "Exploitation"} b.Roles["ConfSyncOps"] = "Sys.Audit,VM.Audit,VM.PowerMgmt" b.Users["alice@pve"] = map[string]any{"enable": 1, "firstname": "Alice", "email": "alice@example.org", "groups": "admins"} b.Users["bob@pve"] = map[string]any{"enable": 1, "firstname": "Bob", "email": "bob@example.org", "groups": "ops", "expire": 1893456000} b.Shadow["alice"] = ref.Shadow["alice"] b.Shadow["bob"] = ref.Shadow["bob"] // nodes of the node replication: pve1 (master) and pve2 in pve-rennes, // pve-a1 in site-a with other NIC names n1, n2, a1 := ref.AddNode("pve1"), ref.AddNode("pve2"), a.AddNode("pve-a1") n1.Net = map[string]map[string]any{ "eno1": {"type": "eth", "mtu": "9000"}, "eno2": {"type": "eth", "mtu": "9000"}, "bond0": {"type": "bond", "slaves": "eno1 eno2", "bond_mode": "802.3ad", "bond_xmit_hash_policy": "layer3+4", "mtu": "9000"}, "vmbr0": {"type": "bridge", "bridge_ports": "bond0", "bridge_vlan_aware": "1", "cidr": "10.0.0.11/24", "gateway": "10.0.0.1", "autostart": "1"}, "bond0.50": {"type": "vlan", "mtu": "9000"}, "vmbr1": {"type": "bridge", "bridge_ports": "bond0.50", "cidr": "10.5.0.11/24", "comments": "stockage iSCSI", "autostart": "1"}, } n2.Net = map[string]map[string]any{ "eno1": {"type": "eth", "mtu": "9000"}, "eno2": {"type": "eth", "mtu": "9000"}, "bond0": {"type": "bond", "slaves": "eno1 eno2", "bond_mode": "active-backup", "mtu": "9000"}, "vmbr0": {"type": "bridge", "bridge_ports": "bond0", "cidr": "10.0.0.12/24", "gateway": "10.0.0.1", "autostart": "1"}, } a1.Net = map[string]map[string]any{ "enp65s0f0": {"type": "eth"}, "enp65s0f1": {"type": "eth"}, "bond0": {"type": "bond", "slaves": "enp65s0f0 enp65s0f1", "bond_mode": "802.3ad", "bond_xmit_hash_policy": "layer3+4"}, "vmbr0": {"type": "bridge", "bridge_ports": "bond0", "bridge_vlan_aware": "1", "cidr": "192.168.10.21/24", "gateway": "192.168.10.1", "autostart": "1"}, } n1.DNS = map[string]string{"search": "rennes.lab", "dns1": "10.0.0.53", "dns2": "10.0.0.54"} n2.DNS = map[string]string{"search": "rennes.lab", "dns1": "10.0.0.53"} a1.DNS = map[string]string{"search": "rennes.lab", "dns1": "10.0.0.53", "dns2": "10.0.0.54"} mp := "defaults {\n user_friendly_names yes\n find_multipaths no\n}\nblacklist {\n devnode \"^sda\"\n}\ndevices {\n device {\n vendor \"NETAPP\"\n product \"LUN.*\"\n path_grouping_policy group_by_prio\n no_path_retry queue\n }\n}\n" n1.Files["/etc/multipath.conf"] = pvetest.FakeFile{Content: mp} n2.Files["/etc/multipath.conf"] = pvetest.FakeFile{Content: strings.Replace(mp, "no_path_retry queue", "no_path_retry 12", 1)} n1.Files["/etc/multipath/wwids"] = pvetest.FakeFile{Content: "# Multipath wwids, Version : 1.0\n/3600a098038303053453f463045747a41/\n/3600a098038303053453f463045747a42/\n"} n2.Files["/etc/multipath/wwids"] = pvetest.FakeFile{Content: "# Multipath wwids, Version : 1.0\n/3600a098038303053453f463045747a41/\n"} n1.Files["/etc/chrony/chrony.conf"] = pvetest.FakeFile{Content: "server ntp1.rennes.lab iburst\nserver ntp2.rennes.lab iburst\ndriftfile /var/lib/chrony/chrony.drift\nmakestep 1.0 3\n"} n2.Files["/etc/chrony/chrony.conf"] = pvetest.FakeFile{Content: "pool 2.debian.pool.ntp.org iburst\ndriftfile /var/lib/chrony/chrony.drift\nmakestep 1.0 3\n"} a1.Files["/etc/chrony/chrony.conf"] = n1.Files["/etc/chrony/chrony.conf"] n1.Files["/etc/sysctl.d/90-network.conf"] = pvetest.FakeFile{Content: "net.core.rmem_max = 16777216\nnet.core.wmem_max = 16777216\n"} a1.Files["/etc/sysctl.d/90-network.conf"] = n1.Files["/etc/sysctl.d/90-network.conf"] a1.Files["/etc/sysctl.d/99-local.conf"] = pvetest.FakeFile{Content: "vm.swappiness = 10\n"} a1.Files["/etc/multipath.conf"] = n1.Files["/etc/multipath.conf"] // legacy: PVE 8, refused by the version check old := pvetest.New("8.4.1") c := &Clusters{servers: []*pvetest.Server{ref, a, b, old}} save := func(s *pvetest.Server, name, role string, pos int, mapping map[model.Kind]map[string]map[string]any, pol map[model.Kind]map[string]adapter.Policy) error { sum := sha256.Sum256(s.Certificate().Raw) host, port := s.SSHAddr() err := st.SaveCluster(ctx, "", store.Cluster{Name: name, Role: role, Position: pos, URL: s.URL, User: "root@pam", Password: pvetest.Password, TLS: store.TLSPin, Fingerprint: pve.FormatFingerprint(hex.EncodeToString(sum[:])), SSHHost: host, SSHPort: port, Mapping: mapping, Policies: pol}) if err != nil { return err } return st.Pin(ctx, name, "", s.SSHHostKey(), nil) } steps := []error{ save(ref, "pve-rennes", store.ClusterPrimary, 0, nil, nil), save(a, "site-a", store.ClusterTarget, 1, map[model.Kind]map[string]map[string]any{ model.KindDatacenter: {"datacenter": {"next-id": "lower=2000,upper=2999"}}, model.KindZone: { "vlanz": {"bridge": "vmbr1"}, "stor": {"bridge": "vmbr2", "nodes": "pve-a1,pve-a2"}, "vxz": {"peers": "192.168.10.1,192.168.10.2"}, }, }, nil), save(b, "site-b", store.ClusterTarget, 2, map[model.Kind]map[string]map[string]any{ model.KindZone: { "vlanz": {"bridge": "vmbr0"}, "vxz": {"peers": "172.16.0.1,172.16.0.2"}, }, }, map[model.Kind]map[string]adapter.Policy{model.KindZone: {"mtu": adapter.Ignore}}), save(old, "legacy-pve8", store.ClusterTarget, 3, nil, nil), st.SaveSettings(ctx, store.Settings{Kinds: model.Kinds, Policies: map[model.Kind]map[string]adapter.Policy{model.KindDatacenter: {"keyboard": adapter.Sync}}}), } node := func(cluster string, n *pvetest.Node) error { host, port := n.SSHAddr() if err := st.SaveNode(ctx, cluster, n.Name, host, port, "", false); err != nil { return err } return st.PinNode(ctx, cluster, n.Name, n.SSHHostKey(), nil) } steps = append(steps, st.SetNodes(ctx, "pve-rennes", []store.Discovered{{Name: "pve1", IP: "127.0.0.1"}, {Name: "pve2", IP: "127.0.0.1"}}), st.SetNodes(ctx, "site-a", []store.Discovered{{Name: "pve-a1", IP: "127.0.0.1"}}), node("pve-rennes", n1), node("pve-rennes", n2), node("site-a", a1), st.SaveProfile(ctx, "", nodesync.Profile{Name: "réseau et stockage", Master: "pve-rennes/pve1", Targets: []string{"pve-rennes/pve2", "site-a/pve-a1"}, Items: []string{"network", "dns", "multipath", "wwids", "chrony", "sysctl"}, Mapping: map[string]nodesync.TargetMap{ "pve-rennes/pve2": {Values: map[string]map[string]string{"network/vmbr1": {"cidr": "10.5.0.12/24"}}}, "site-a/pve-a1": {NICs: map[string]string{"eno1": "enp65s0f0", "eno2": "enp65s0f1"}}, }})) for _, u := range []struct{ name, role string }{{"admin", store.RoleAdmin}, {"operateur", store.RoleOperator}, {"lecteur", store.RoleViewer}} { steps = append(steps, st.SaveUser(ctx, u.name, u.role, Password)) } for _, err := range steps { if err != nil { c.Close() return nil, err } } return c, nil }