Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reuse existing MicroCeph and MicroOVN clusters #259

Merged
merged 11 commits into from
May 3, 2024
Merged
29 changes: 29 additions & 0 deletions doc/how-to/initialise.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,35 @@ Complete the following steps to initialise MicroCloud:

See an example of the full initialisation process in the :ref:`Get started with MicroCloud <initialisation-process>` tutorial.

Excluding MicroCeph or MicroOVN from MicroCloud
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If the MicroOVN or MicroCeph snap is not installed on the system that runs :command:`microcloud init`, you will be prompted with the following question::

MicroCeph not found. Continue anyway? (yes/no) [default=yes]:

MicroOVN not found. Continue anyway? (yes/no) [default=yes]:

If you choose ``yes``, only existing services will be configured on all systems.
If you choose ``no``, the setup will be cancelled.

All other systems must have at least the same set of snaps installed as the system that runs :command:`microcloud init`, otherwise they will not be available to select from the list of systems.
Any questions associated to these systems will be skipped. For example, if MicroCeph is not installed, you will not be prompted for distributed storage configuration.

Reusing an existing MicroCeph or MicroOVN with MicroCloud
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If some of the systems are already part of a MicroCeph or MicroOVN cluster, you can choose to reuse this cluster when initialising MicroCloud when prompted with the following question::

"micro01" is already part of a MicroCeph cluster. Do you want to add this cluster to MicroCloud? (add/skip) [default=add]:

"micro01" is already part of a MicroOVN cluster. Do you want to add this cluster to MicroCloud? (add/skip) [default=add]:

If you choose ``add``, MicroCloud will add the remaining systems selected for initialisation to the pre-existing cluster.
If you choose ``skip``, the respective service will not be set up at all.

If more than one MicroCeph or MicroOVN cluster exists among the systems, the MicroCloud initialisation will be cancelled.

.. _howto-initialise-preseed:

Non-interactive configuration
Expand Down
43 changes: 43 additions & 0 deletions microcloud/api/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"

"github.com/canonical/lxd/lxd/response"
"github.com/canonical/lxd/lxd/util"
"github.com/canonical/lxd/shared/logger"
"github.com/canonical/microcluster/rest"
"github.com/canonical/microcluster/state"
"github.com/gorilla/mux"

"github.com/canonical/microcloud/microcloud/api/types"
"github.com/canonical/microcloud/microcloud/service"
Expand Down Expand Up @@ -55,6 +57,47 @@ var ServicesCmd = func(sh *service.Handler) rest.Endpoint {
}
}

// ServiceTokensCmd represents the /1.0/services/serviceType/tokens API on MicroCloud.
var ServiceTokensCmd = func(sh *service.Handler) rest.Endpoint {
return rest.Endpoint{
AllowedBeforeInit: true,
Name: "services/{serviceType}/tokens",
Path: "services/{serviceType}/tokens",

Post: rest.EndpointAction{Handler: authHandler(sh, serviceTokensPost), AllowUntrusted: true, ProxyTarget: true},
}
}

// serviceTokensPost issues a token for service using the MicroCloud proxy.
// Normally a token request to a service would be restricted to trusted systems,
// so this endpoint validates the mDNS auth token and then proxies the request to the local unix socket of the remote system.
func serviceTokensPost(s *state.State, r *http.Request) response.Response {
serviceType, err := url.PathUnescape(mux.Vars(r)["serviceType"])
if err != nil {
return response.SmartError(err)
}

// Parse the request.
req := types.ServiceTokensPost{}

err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
return response.BadRequest(err)
}

sh, err := service.NewHandler(s.Name(), req.ClusterAddress, s.OS.StateDir, false, false, types.ServiceType(serviceType))
if err != nil {
return response.SmartError(err)
}

token, err := sh.Services[types.ServiceType(serviceType)].IssueToken(s.Context, req.JoinerName)
if err != nil {
return response.SmartError(fmt.Errorf("Failed to issue %s token for peer %q: %w", serviceType, req.JoinerName, err))
}

return response.SyncResponse(true, token)
}

// servicesPut updates the cluster status of the MicroCloud peer.
func servicesPut(state *state.State, r *http.Request) response.Response {
// Parse the request.
Expand Down
6 changes: 6 additions & 0 deletions microcloud/api/types/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ type ServiceToken struct {
Service ServiceType `json:"service" yaml:"service"`
JoinToken string `json:"join_token" yaml:"join_token"`
}

// ServiceTokensPost represents a request to issue a join token for a MicroCloud service.
type ServiceTokensPost struct {
ClusterAddress string `json:"cluster_address" yaml:"cluster_address"`
JoinerName string `json:"joiner_name" yaml:"joiner_name"`
roosterfish marked this conversation as resolved.
Show resolved Hide resolved
}
14 changes: 14 additions & 0 deletions microcloud/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,17 @@ func JoinServices(ctx context.Context, c *client.Client, data types.ServicesPut)

return nil
}

// RemoteIssueToken issues a token on the remote MicroCloud, trusted by the mDNS auth secret.
func RemoteIssueToken(ctx context.Context, c *client.Client, serviceType types.ServiceType, data types.ServiceTokensPost) (string, error) {
queryCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()

var token string
err := c.Query(queryCtx, "POST", api.NewURL().Path("services", string(serviceType), "tokens"), data, &token)
if err != nil {
return "", fmt.Errorf("Failed to issue remote token: %w", err)
}

return token, nil
}
42 changes: 42 additions & 0 deletions microcloud/cmd/microcloud/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"sort"
"strings"

"github.com/canonical/lxd/shared"
"github.com/canonical/lxd/shared/api"
cli "github.com/canonical/lxd/shared/cmd"
"github.com/canonical/lxd/shared/logger"
Expand Down Expand Up @@ -822,3 +823,44 @@ func (c *CmdControl) askNetwork(sh *service.Handler, systems map[string]InitSyst

return nil
}

// askClustered checks whether any of the selected systems have already initialized any expected services.
// If a service is already initialized on some systems, we will offer to add the remaining systems, or skip that service.
// If multiple systems have separately initialized the same service, we will abort initialization.
// Preseed yamls will have a flag that sets whether to reuse the cluster.
// In auto setup, we will expect no initialized services so that we can be opinionated about how we configure the cluster without user input.
func (c *CmdControl) askClustered(s *service.Handler, autoSetup bool, systems map[string]InitSystem) error {
expectedServices := make(map[types.ServiceType]service.Service, len(s.Services))
for k, v := range s.Services {
expectedServices[k] = v
}

for serviceType := range expectedServices {
initializedSystem, _, err := checkClustered(s, autoSetup, serviceType, systems)
if err != nil {
return err
}

if initializedSystem != "" {
question := fmt.Sprintf("%q is already part of a %s cluster. Do you want to add this cluster to Microcloud? (add/skip) [default=add]", initializedSystem, serviceType)
validator := func(s string) error {
if !shared.ValueInSlice[string](s, []string{"add", "skip"}) {
return fmt.Errorf("Invalid input, expected one of (add,skip) but got %q", s)
}

return nil
}

addOrSkip, err := c.asker.AskString(question, "add", validator)
if err != nil {
return err
}

if addOrSkip != "add" {
delete(s.Services, serviceType)
}
}
}

return nil
}
Loading
Loading