Skip to content

Instantly share code, notes, and snippets.

@ricab
Created January 16, 2019 10:08
Show Gist options
  • Save ricab/0628d54397711e627637f25fc5bdc276 to your computer and use it in GitHub Desktop.
Save ricab/0628d54397711e627637f25fc5bdc276 to your computer and use it in GitHub Desktop.
Pet Environment Feature for Multipass

Pet Environment Feature for Multipass

Dev Impacts, Plans, and Decisions

Notice

This draft is based on my current understanding, which may well be limited/incomplete/wrong. It presents subjective recommendations and opinions which may be flawed and/or subject to change.

Contents

General Goal

Provide a special VM instance that is readily available to experiment/test/demonstrate.

Scope

  • Overall: daemon, clients (CLI & GUI), and possibly installer/snap packaging.
  • For now: daemon, CLI (not GUI), and possibly installer/snap packaging.

Baseline

Based on the references below, a summary of current intentions for this feature could be:

  1. Current name: primary
  2. primary created automatically (upon installation, current default image)
  3. Users may delete the primary instance
  4. Users may set which instance is the primary one
  5. Commands operate on primary by default (i.e. if no target instance is specified)

Daemon impacts

Primary instance lifetime

In the simplest scenario, the lifetime of the primary instance closely matches multipass’s own installation-lifetime. The daemon creates the pet instance ASAP after it first starts and keeps it indefinitely (this accomplishes proposition 2 above, as the daemon is launched when multipass is installed). Uninstalling multipass finally removes primary along with any other existing instances (stopping the daemon stops any running VMs and uninstalling removes persistified data).

However, a primary instance may also be deleted. Moreover, a different instance may be set as primary (although a maximum of one primary instance is supported at any one time). Creation/destruction and set-primary/unset-primary times are discussed below (not to be confused with start/stop times).

In any case, multipass needs to function properly with and without a primary instance.

Creation

Preconditions may be unfulfilled when the daemon first starts and tries to create the primary instance (e.g. no network to download the image, not enough disk space).

Decision: What should we do in this case? Options:

  1. Ignore and keep going without primary
  2. Keep going without primary, retry periodically until success

Note: 1 is easier than 2 and pretty much contained in it. So, if we want 2, perhaps it can be done in a separate issue/PR. This is the approach I would favor.

Warning: IIUC, multipass is already supposed to handle multiple client requests simultaneously (it did respond well to quick smoke tests with concurrent but orthogonal requests). However, there could be other concurrency issues when a client is used right away, while primary is still launching (see below).

Feedback to the user

Should the setup of primary be synchronous with installation, or asynchronous? That is, should the installation wait until primary is setup before completing? Current design discussions indicate not, IIUC, but the feedback to the user may be less than ideal in that case.

Ideally the user would be notified that an instance was being created/downloaded/launched. They probably want to know that a substantial download is under way and that they have a VM setup. This is especially important if the instance is automatically started (see below).

Decision: do we rely only on the tray indicator for this? Is the tray/GUI client meant to be shipped within the same multipass package? What happens in text-only mode? Do we leave the user uninformed until he tries to attach to the instance? Options:

  1. info message in installation -> would be easily missed; I think each snap installation message is erased by the one that follows? Can we print a "sticky" message (one that moves output to a new line)?
  2. creation process synchronous with installation; e.g. installer outputs creation progress, waiting for it to finish -> would certainly make it noticeable, but delay installation.
  3. option during installation ("Do you want to start a primary instance? [Y/n]") -> I guess those should be kept to a minimum(?) Some deb packages have such interactive installation, but I haven’t seen it in a snap...

Options 2 and 3 above would effectively make the installer a client of the multipass daemon. Perhaps an indirect one, if it called the CLI client and echoed its output. This has implications on how much new functionality needs to be exposed as interface.

In any case, I suggest starting with 1, but together with option 1 in Create vs Launch (that is, creating but not starting primary. The other options here could be separately implemented, later on, if so decided.

Note: any of the options above would expand the scope to the installer level (snap), even if the impact of option 1 would be limited to a single output message.

Create vs Launch

Decision: the instance should be created upon installation, but do we also want to start it? Options:

  1. No -> A separate create operation would have to be added. Currently, multipassd provides no operation to simply create an instance without starting it. This could be extracted from the current launch, which would internally become a combination of create and start (that is, conceptually, what launching is). A separate decision would be whether to expose create to clients (so they could offer a create command).
  2. Yes -> Feedback to the user becomes especially important; extra step needed to stop VM if not needed;

I would favor option 1.

Destruction

The user may delete a primary instance with a regular delete request. I suggest it retains the primary status after deleted and until it is purged or the status removed/changed explicitly. All instances are removed at uninstallation at the latest.

Set primary

The daemon needs to extend its interface, to provide a way to set an instance as primary. This can be achieved with a new request, say set-primary, with a single existing instance as argument. The operation would move the primary status from any previous instance holding it, in an atomized step, to the new intended primary instance (including persistification).

By atomized step I mean that other threads should not see intermediate states. In other words, concurrent threads serving multiple set-primary client requests should be synchronized and execute those operations sequentially. A consistent state, with a single primary instance, needs to prevail in the end. In practice this can be done with a lock around this critical code.

Warning: I am uneasy about other operations’ current thread safety. For instance, what happens if two clients try to delete and recover an instance concurrently? Looking at the code, this does not seem to be covered. It was probably never noticed because typical usage currently employs a single client. But that will change once we have a tray icon/GUI. Also, concurrent requests will become more likely if primary is created asynchronously: the user may start using multipass right after installation, while primary is still being created.

launch as primary

Decision: should the launch request (and create, if we decide to add it) provide a knob to set the new instance as the primary one? Options:

  1. Yes -> that would provide the interface to create primary with a single request, which would be useful if we opted for the synchronous alternatives in Feedback to the user. The commands would have to consider thread-safety
  2. No, only allow setting existing instances as primary -> two steps required to achieve the same thing

I think I would favor 1, but perhaps as a later addition.

Unset primary

Decision: do we provide a new "knob" to remove the primary status of an instance? Or can this state only be overwritten? Options:

  1. Yes: new request, say unset-primary -> one more interface element to support, more functionality to synchronize; one more command to list in help may contribute to obscure essentials
  2. No -> the user may feel "uncared for" (he can still achieve the same thing by setting a new instance as primary before deleting it)

I think I would favor 2 here.

Primary name vs attribute

The term primary is currently used in both the instance name and to distinguish the status of an instance that receives special treatment. But the possibility to set/unset arbitrary instances as primary introduces a simetry break: the status is moved, but the name isn’t. This can lead to confusion, especially when primary is no longer primary. A simple solution is to use different terms for instance name and status.

Decision: what terms do we use? Options:

  1. Name the primary instance primary but start calling something else to the status (e.g. "default") -> but a primary instance that was no longer the default could still be confusing
  2. Give the instance some other fixed name (e.g. default) and continue using "primary" for the status -> analogous problem, although perhaps slightly better
  3. Name the instance variably, with a pet name, just like any other instance, still reserving the term "primary" for the status -> makes it clear that primary status cannot be determined from name
  4. Rename instances dynamically when setting primary status, so that the current primary instance would always be called primary -> would introduce a lot of complication in the way instance records are kept in memory and disk and it would still be confusing to have the name primary designate different instances at different times.

I favor option 3.

Note: this document still uses primary to refer to the primary instance in many places

Default target instance

Many multipass commands receive the instances they target as arguments. Commands that support multiple instances also provide the flag --all to target all instances. An instance argument is required unless the flag --all is present (e.g. multipass start --all).

Baseline proposition 5 means that commands with neither instance arguments nor the --all flag would target the primary instance (if available). So something like multipass stop would mean the same as multipass stop primary.

However, client requests with an empty instance field are currently interpreted by the daemon to mean that all instances are to be targeted. So, to accomplish this default-instance feature, one of two things would need to happen:

  1. the logic of checking for a default target and selecting it would have to be placed in the client
  2. the client/server communication protocol would have to be changed in all such requests

From these options, I favor the second, but I wonder...

Decision: is this default instance behavior really what we want? I am not sure the pros compensate the cons, especially when considering that:

  • if only primary exists, auto-complete already selects it with a simple tab key press
  • if other instances exist, muscle memory could cause people to target primary by mistake
  • primary will often not be an appropriate default. For instance, it would not be appropriate to say multipass start if primary was trashed; or multipass purge unless primary was deleted; or multipass info if no primary was set -> A default that does not account for that makes it easy to try wrong things. And only the daemon is in the proper place to discern that. Unless the client made preliminary requests... Again, autocomplete already deals with this sort of thing
  • if we removed this default targeting functionality, the only thing distinguishing a primary instance would be that it was created automatically! Primary status would otherwise be irrelevant, so it would make no sense to support moving it. That would greatly simplify this feature: much of the complexity discussed here would disappear
  • I think the "default instance" functionality has no counterpart in the GUI client, so the effect of setting an instance as primary may be unclear to someone using only the GUI

In my opinion, we should strive for simplicity here. Are the additional complications worth the occasional typing economy? I am not trying to argue for something like Apple’s single button approach, which I don’t really like (everyone has edge cases once in a while). But a balance should still be struck. I find that removing the "default instance" feature could actually contribute more to a "slick" multipass usage experience than the other way around.

Detail: if the user wants to target primary in a command that also targets other instances, they have to mention it explicitly. It is perhaps worth highlighting that a command may have 2 more arguments than another and yet only target one additional instance. This is a potential pitfall for both developers and users: it is easy to mistake the number of arguments with the number of targets (e.g. imagine a script’s maintainer trying to add an additional target to an exiting command-line variable).

Note: this document still deals with the current state of affairs: a Pet Environment feature that includes the default instance.

Persistifying

The instance with primary status has to be persistified. Since only one is allowed at any point, this could be achieved with a separate field, which could either be empty or specify a single instance. The field should be saved whenever the corresponding status changes in memory (i.e. set/unset primary, launch, etc.).

Canceling a start-up

Canceling an instance creation procedure is mentioned a few times in multipass-design. While this could be supported by multipassd, this is not currently the case. SIGINT on CLI only aborts the client, but the daemon keeps handling the request. Implementing this would require actions to be transactional, so they could be rolled back, lest we get into inconsistent intermediate states. It would be a big feature on its own.

CLI client impacts

Existing commands

--help

The command itself is unaffected. The documentation it relies upon needs to change in the following ways:

  • If we create new commands, they need to be documented
  • If we want to target primary by default, change the documentation of all affected commands to explain this, mentioning that it only works if the instance exists (and that other requirements still apply)
--verbose

No direct impact, just accompany new functionality with appropriate logging.

copy-files

If we decide to target primary by default in other commands, we may choose to abbreviate primary:<path> as :<path> here. Omitting the instance still requires a leading colon, to distinguish which end of the copy is inside the instance (multipass copy-files /a/path /another/path would be ambiguous).

The command would fail if we defaulted to primary and its state was not RUNNING.

delete

A default instance would be applicable here if so decided.

Decision: would we allow a default also with --purge? Options:

  1. Yes -> dangerous, too punishing on stray-enter mistake
  2. No -> another special case, another asymmetry

The command would be a no-op if we defaulted to primary and its state was DELETED.

exec

A default instance would be applicable here if so decided. If so, then the argument -- becomes mandatory when no instance argument is provided, to distinguish where the command begins. In other words, the first free argument would still be an instance name when before --.

The command would fail if we defaulted to primary and its state was not RUNNING.

find

No impact I can think of.

help

See --help

info

The output should clearly indicate when an instance was tagged as primary. The primary instance should also be listed first, when part of the output.

A default instance here could confuse expectations. In particular, a distracted user may expect multipass info to encompass all instances.

Note: today, output order simply follows argument order.

launch

If we decide to support launching an instance as primary (see launch as primary) this command would accept an additional flag (e.g. --set-primary).

Note: this command receives a free argument specifying an image. That argument already has a default, but here it designates the image rather than an instance. This means that there is a small asymmetry relatively to the other commands, which may present confusing affordance clues. The asymmetry is not introduced by the default-instance feature on this occasion – it is already there. But its impact may be aggravated. In particular, the unfamiliar user may confuse launch with start and expect multipass launch to mean multipass start [primary], although they are quite different.

list

The primary instance should be listed in the first place.

Decision should the output somehow highlight the primary instance? Options:

  1. No -> ...but it would be nice :)
  2. Yes, with a dedicated column -> but the output table is already quite wide...
  3. Yes, with a single character mark (perhaps '*'?) -> would have to be documented; where to put it? beginning or end of name?

I would favor option 3, end of name.

mount

As in copy-files, if we decide to target primary by default in other commands, we may choose to abbreviate primary:<path> as :<path> here. Unlike in the former command, mount endpoints are distinguished by position, so the leading colon would not be strictly required. But I suggest maintaining it, for consistency and clarity.

The command would fail if we defaulted to primary and its state was not RUNNING.

purge

A default instance would be applicable here if so decided.

Decision: do we allow a default here in particular? Options:

  1. Yes -> dangerous, too punishing on stray-enter mistake
  2. No -> another special case, another asymmetry

The command would fail if we defaulted to primary and its state was not DELETED.

recover

A default instance would be applicable here if so decided.

The command would be a no-op if we defaulted to primary and its state was not DELETED.

restart

A default instance would be applicable here if so decided.

The command would fail if we defaulted to primary and its state was not RUNNING (actually depends on the outcome of this issue.

shell

A default instance would be applicable here if so decided.

The command would fail if we defaulted to primary and its state was not RUNNING.

start

A default instance would be applicable here if so decided.

The command would be a no-op if we defaulted to primary and its state was RUNNING. The command would fail if we defaulted to primary and its state was DELETED.

stop

A default instance would be applicable here if so decided.

The command would be a no-op if we defaulted to primary and its state was either STOPPED or SUSPENDED. The command would fail if we defaulted to primary and its state was DELETED.

suspend

A default instance would be applicable here if so decided.

The command would be a no-op if we defaulted to primary and its state was SUSPENDED or STOPPED. The command would fail if we defaulted to primary and its state was DELETED.

umount

As in copy-files, if we decide to target primary by default in other commands, we may choose to abbreviate primary:<path> as :<path> here. In this case, and since we may already omit paths to unmount all mounts in an instance, we should also support multipass umount to unmount all mount points in the primary instance. Omitting the instance still requires the leading colon, for clarity and consistency with copy-files, and to provide a quick way to distinguish instance names from paths.

The command would be a no-op if we defaulted to primary and there were no mount points in that instance. The command would fail if we defaulted to primary and its state was DELETED (unless this issue turns out to be valid).

version

No impact.

Possible new commands

create

If it was added, this would have the same interface as launch, except it would not start the created instance. See the Create vs Launch discussion above.

set-primary

This receives a single free argument, identifying an existing instance to set s primary. Whether we require that instance not to be deleted should match what we do in umount.

See the section set primary above.

unset-primary

If this was added, it would not receive any arguments and it would remove the primary status from the instance holding it, if any (no-op otherwise). Whether we require that instance not to be deleted should match what we do in umount.

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment