github.com/caos/orbos@v1.5.14-0.20221103111702-e6cd0cea7ad4/internal/operator/orbiter/kinds/providers/cs/machinesservice.go (about) 1 package cs 2 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "sync" 8 9 "github.com/cloudscale-ch/cloudscale-go-sdk" 10 11 "github.com/caos/orbos/internal/helpers" 12 "github.com/caos/orbos/internal/operator/orbiter/kinds/clusters/core/infra" 13 "github.com/caos/orbos/internal/operator/orbiter/kinds/loadbalancers" 14 "github.com/caos/orbos/internal/operator/orbiter/kinds/providers/core" 15 "github.com/caos/orbos/internal/operator/orbiter/kinds/providers/ssh" 16 sshgen "github.com/caos/orbos/internal/ssh" 17 "github.com/caos/orbos/mntr" 18 "github.com/caos/orbos/pkg/secret" 19 "github.com/caos/orbos/pkg/tree" 20 ) 21 22 func ListMachines(monitor mntr.Monitor, desiredTree *tree.Tree, orbID, providerID string) (map[string]infra.Machine, error) { 23 desired, err := parseDesired(desiredTree) 24 if err != nil { 25 return nil, fmt.Errorf("parsing desired state failed: %w", err) 26 } 27 desiredTree.Parsed = desired 28 29 _, _, _, _, _, err = loadbalancers.GetQueryAndDestroyFunc(monitor, nil, desired.Loadbalancing, &tree.Tree{}, nil) 30 if err != nil { 31 return nil, err 32 } 33 34 ctx := buildContext(monitor, &desired.Spec, orbID, providerID, true) 35 36 if err := ctx.machinesService.use(desired.Spec.SSHKey); err != nil { 37 invalidKey := &secret.Secret{Value: "invalid"} 38 if err := ctx.machinesService.use(&SSHKey{ 39 Private: invalidKey, 40 Public: invalidKey, 41 }); err != nil { 42 panic(err) 43 } 44 } 45 46 return core.ListMachines(ctx.machinesService) 47 } 48 49 var _ core.MachinesService = (*machinesService)(nil) 50 51 type machinesService struct { 52 context *context 53 oneoff bool 54 key *SSHKey 55 cache struct { 56 instances map[string][]*machine 57 sync.Mutex 58 } 59 onCreate func(pool string, machine infra.Machine) error 60 } 61 62 func newMachinesService(context *context, oneoff bool) *machinesService { 63 return &machinesService{ 64 context: context, 65 oneoff: oneoff, 66 } 67 } 68 69 func (m *machinesService) DesiredMachines(poolName string, instances int) int { 70 _, ok := m.context.desired.Pools[poolName] 71 if !ok { 72 return 0 73 } 74 75 return instances 76 } 77 78 func (m *machinesService) use(key *SSHKey) error { 79 if key == nil || key.Private == nil || key.Public == nil || key.Private.Value == "" || key.Public.Value == "" { 80 return mntr.ToUserError(errors.New("machines are not connectable. have you configured the orb by running orbctl configure?")) 81 } 82 m.key = key 83 return nil 84 } 85 86 func (m *machinesService) Create(poolName string, _ int) (infra.Machines, error) { 87 88 desired, ok := m.context.desired.Pools[poolName] 89 if !ok { 90 return nil, fmt.Errorf("Pool %s is not configured", poolName) 91 } 92 93 name := newName() 94 monitor := machineMonitor(m.context.monitor, name, poolName) 95 96 monitor.Debug("Creating instance") 97 98 userData, err := NewCloudinit().AddGroupWithoutUsers( 99 "orbiter", 100 ).AddUser( 101 "orbiter", 102 true, 103 "", 104 []string{"orbiter", "wheel"}, 105 "orbiter", 106 []string{m.context.desired.SSHKey.Public.Value}, 107 "ALL=(ALL) NOPASSWD:ALL", 108 ).AddCmd( 109 "sudo echo \"\n\nPermitRootLogin no\n\" >> /etc/ssh/sshd_config", 110 ).AddCmd( 111 "sudo service sshd restart", 112 ).ToYamlString() 113 if err != nil { 114 return nil, err 115 } 116 117 // TODO: How do we connect to the VM if we ignore the private key??? 118 _, pub := sshgen.Generate() 119 if err != nil { 120 return nil, err 121 } 122 123 newServer, err := m.context.client.Servers.Create(m.context.ctx, &cloudscale.ServerRequest{ 124 ZonalResourceRequest: cloudscale.ZonalResourceRequest{}, 125 TaggedResourceRequest: cloudscale.TaggedResourceRequest{ 126 Tags: map[string]string{ 127 "orb": m.context.orbID, 128 "provider": m.context.providerID, 129 "pool": poolName, 130 }, 131 }, 132 Name: name, 133 Flavor: desired.Flavor, 134 Image: "centos-7", 135 Zone: desired.Zone, 136 VolumeSizeGB: desired.VolumeSizeGB, 137 Volumes: nil, 138 Interfaces: nil, 139 BulkVolumeSizeGB: 0, 140 SSHKeys: []string{pub}, 141 Password: "", 142 UsePublicNetwork: boolPtr(m.oneoff || true), // Always use public Network 143 UsePrivateNetwork: boolPtr(true), 144 UseIPV6: boolPtr(false), 145 AntiAffinityWith: "", 146 ServerGroups: nil, 147 UserData: userData, 148 }) 149 if err != nil { 150 return nil, err 151 } 152 153 monitor.Info("Instance created") 154 155 infraMachine, err := m.toMachine(newServer, monitor, desired, poolName) 156 if err != nil { 157 return nil, err 158 } 159 160 if m.cache.instances != nil { 161 if _, ok := m.cache.instances[poolName]; !ok { 162 m.cache.instances[poolName] = make([]*machine, 0) 163 } 164 m.cache.instances[poolName] = append(m.cache.instances[poolName], infraMachine) 165 } 166 167 if err := m.onCreate(poolName, infraMachine); err != nil { 168 return nil, err 169 } 170 171 monitor.Info("Machine created") 172 return []infra.Machine{infraMachine}, nil 173 } 174 175 func (m *machinesService) toMachine(server *cloudscale.Server, monitor mntr.Monitor, pool *Pool, poolName string) (*machine, error) { 176 internalIP, sshIP := createdIPs(server.Interfaces, m.oneoff || true /* always use public ip */) 177 178 sshMachine := ssh.NewMachine(monitor, "orbiter", sshIP) 179 if err := sshMachine.UseKey([]byte(m.key.Private.Value)); err != nil { 180 return nil, err 181 } 182 183 infraMachine := newMachine( 184 server, 185 internalIP, 186 sshIP, 187 sshMachine, 188 m.removeMachineFunc(server.Tags["pool"], server.UUID), 189 m.context, 190 pool, 191 poolName, 192 ) 193 return infraMachine, nil 194 } 195 196 func createdIPs(interfaces []cloudscale.Interface, oneoff bool) (string, string) { 197 var internalIP string 198 var sshIP string 199 for idx := range interfaces { 200 interf := interfaces[idx] 201 202 if internalIP != "" && sshIP != "" { 203 break 204 } 205 206 if interf.Type == "private" && len(interf.Addresses) > 0 { 207 internalIP = interf.Addresses[0].Address 208 if !oneoff { 209 sshIP = internalIP 210 break 211 } 212 } 213 if oneoff && interf.Type == "public" && len(interf.Addresses) > 0 { 214 sshIP = interf.Addresses[0].Address 215 continue 216 } 217 } 218 return internalIP, sshIP 219 } 220 221 func (m *machinesService) ListPools() ([]string, error) { 222 223 pools, err := m.machines() 224 if err != nil { 225 return nil, err 226 } 227 228 var poolNames []string 229 for poolName := range pools { 230 poolNames = append(poolNames, poolName) 231 } 232 return poolNames, nil 233 } 234 235 func (m *machinesService) List(poolName string) (infra.Machines, error) { 236 pools, err := m.machines() 237 if err != nil { 238 return nil, err 239 } 240 241 pool := pools[poolName] 242 machines := make([]infra.Machine, len(pool)) 243 for idx := range pool { 244 machine := pool[idx] 245 machines[idx] = machine 246 } 247 248 return machines, nil 249 } 250 251 func (m *machinesService) machines() (map[string][]*machine, error) { 252 if m.cache.instances != nil { 253 return m.cache.instances, nil 254 } 255 256 // TODO: Doesn't work, all machines get destroyed that belong to the token 257 servers, err := m.context.client.Servers.List(m.context.ctx /**/, func(r *http.Request) { 258 params := r.URL.Query() 259 params["tag:orb"] = []string{m.context.orbID} 260 params["tag:provider"] = []string{m.context.providerID} 261 }) 262 if err != nil { 263 return nil, err 264 } 265 266 if m.cache.instances == nil { 267 m.cache.instances = make(map[string][]*machine) 268 } else { 269 for k := range m.cache.instances { 270 m.cache.instances[k] = nil 271 delete(m.cache.instances, k) 272 } 273 } 274 275 for idx := range servers { 276 server := servers[idx] 277 pool := server.Tags["pool"] 278 machine, err := m.toMachine(&server, machineMonitor(m.context.monitor, server.Name, pool), m.context.desired.Pools[pool], pool) 279 if err != nil { 280 return nil, err 281 } 282 m.cache.instances[pool] = append(m.cache.instances[pool], machine) 283 } 284 285 return m.cache.instances, nil 286 } 287 288 func (m *machinesService) removeMachineFunc(pool, uuid string) func() error { 289 290 return func() error { 291 m.cache.Lock() 292 cleanMachines := make([]*machine, 0) 293 for idx := range m.cache.instances[pool] { 294 cachedMachine := m.cache.instances[pool][idx] 295 if cachedMachine.server.UUID != uuid { 296 cleanMachines = append(cleanMachines, cachedMachine) 297 } 298 } 299 m.cache.instances[pool] = cleanMachines 300 m.cache.Unlock() 301 302 return m.context.client.Servers.Delete(m.context.ctx, uuid) 303 } 304 } 305 306 func machineMonitor(monitor mntr.Monitor, name string, poolName string) mntr.Monitor { 307 return monitor.WithFields(map[string]interface{}{ 308 "machine": name, 309 "pool": poolName, 310 }) 311 } 312 313 func boolPtr(b bool) *bool { return &b } 314 315 func newName() string { 316 return "orbos-" + helpers.RandomStringRunes(6, []rune("abcdefghijklmnopqrstuvwxyz0123456789")) 317 }