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  }