Skip to content

Commit 331c3fa

Browse files
bors[bot]ricab
authored andcommitted
Merge #1227
1227: WIP: [primary] Auto-mount home in primary on launch r=Saviq a=ricab Fixes #1224. Co-authored-by: Ricardo Abreu <[email protected]>
1 parent a10120e commit 331c3fa

File tree

6 files changed

+138
-8
lines changed

6 files changed

+138
-8
lines changed

include/multipass/constants.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ constexpr auto min_cpu_cores = "1";
2828
constexpr auto default_memory_size = "1G";
2929
constexpr auto default_disk_size = "5G";
3030
constexpr auto default_cpu_cores = min_cpu_cores;
31+
constexpr auto home_automount_dir = "Home";
3132
constexpr auto driver_env_var = "MULTIPASS_VM_DRIVER";
3233
constexpr auto petenv_key = "client.primary-name"; // This will eventually be moved to some dynamic settings schema
3334
constexpr auto driver_key = "local.driver"; // idem

src/client/cli/cmd/launch.cpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
#include <multipass/cli/argparser.h>
2323
#include <multipass/cli/client_platform.h>
2424
#include <multipass/constants.h>
25+
#include <multipass/settings.h>
2526

2627
#include <multipass/format.h>
2728

2829
#include <yaml-cpp/yaml.h>
2930

31+
#include <QDir>
3032
#include <QTimeZone>
3133

3234
#include <regex>
@@ -47,15 +49,26 @@ const std::regex show{"s|show", std::regex::icase | std::regex::optimize};
4749

4850
mp::ReturnCode cmd::Launch::run(mp::ArgParser* parser)
4951
{
50-
auto ret = parse_args(parser);
51-
if (ret != ParseCode::Ok)
52+
petenv_name = Settings::instance().get(petenv_key);
53+
if (auto ret = parse_args(parser); ret != ParseCode::Ok)
5254
{
5355
return parser->returnCodeFrom(ret);
5456
}
5557

5658
request.set_time_zone(QTimeZone::systemTimeZoneId().toStdString());
5759

58-
return request_launch();
60+
auto ret = request_launch();
61+
if (ret == ReturnCode::Ok && request.instance_name() == petenv_name.toStdString())
62+
{
63+
const auto mount_source = QDir::toNativeSeparators(QDir::homePath());
64+
const auto mount_target = QString{"%1:%2"}.arg(petenv_name, mp::home_automount_dir);
65+
66+
ret = run_cmd({"multipass", "mount", mount_source, mount_target}, parser, cout, cerr);
67+
if (ret == ReturnCode::Ok)
68+
cout << fmt::format("Mounted '{}' into '{}'\n", mount_source, mount_target);
69+
}
70+
71+
return ret;
5972
}
6073

6174
std::string cmd::Launch::name() const
@@ -102,7 +115,12 @@ mp::ParseCode cmd::Launch::parse_args(mp::ArgParser* parser)
102115
"in bytes, or with K, M, G suffix.\nMinimum: {}, default: {}.",
103116
min_memory_size, default_memory_size)),
104117
"mem", QString::fromUtf8(default_memory_size)); // In MB's
105-
QCommandLineOption nameOption({"n", "name"}, "Name for the instance", "name");
118+
QCommandLineOption nameOption(
119+
{"n", "name"},
120+
QString{"Name for the instance. If it is '%1' (the configured primary instance name), the user's home "
121+
"directory is mounted inside the newly launched instance, in '%2'."}
122+
.arg(petenv_name, mp::home_automount_dir),
123+
"name");
106124
QCommandLineOption cloudInitOption("cloud-init", "Path to a user-data cloud-init configuration, or '-' for stdin",
107125
"file");
108126
parser->addOptions({cpusOption, diskOption, memOption, nameOption, cloudInitOption});

src/client/cli/cmd/launch.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
#include <multipass/cli/command.h>
2222

23+
#include <QString>
24+
2325
namespace multipass
2426
{
2527
namespace cmd
@@ -39,6 +41,7 @@ class Launch final : public Command
3941
ReturnCode request_launch();
4042

4143
LaunchRequest request;
44+
QString petenv_name;
4245
};
4346
} // namespace cmd
4447
} // namespace multipass

src/client/cli/cmd/shell.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,9 @@ mp::ParseCode cmd::Shell::parse_args(mp::ArgParser* parser)
106106
{
107107
parser->addPositionalArgument(
108108
"name",
109-
QString{"Name of the instance to open a shell on. If omitted, '%1' will be assumed. If "
110-
"the instance is not running, an attempt is made to start it."}
109+
QString{
110+
"Name of the instance to open a shell on. If omitted, '%1' (the configured primary instance name) will be "
111+
"assumed. If the instance is not running, an attempt is made to start it (see `start` for more info)."}
111112
.arg(petenv_name),
112113
"[<name>]");
113114

src/client/cli/cmd/start.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,10 @@ mp::ParseCode cmd::Start::parse_args(mp::ArgParser* parser)
134134
{
135135
parser->addPositionalArgument(
136136
"name",
137-
QString{"Names of instances to start. If omitted, and without the --all option, '%1' will be assumed."}.arg(
138-
petenv_name),
137+
QString{"Names of instances to start. If omitted, and without the --all option, '%1' (the configured primary "
138+
"instance name) will be assumed. If '%1' does not exist but is included in a successful start command "
139+
"(either implicitly or explicitly), it is launched automatically (see `launch` for more info)."}
140+
.arg(petenv_name),
139141
"[<name> ...]");
140142

141143
QCommandLineOption all_option(all_option_name, "Start all instances");

tests/test_cli_client.cpp

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*
1616
*/
1717

18+
#include "mock_environment_helpers.h"
1819
#include "mock_settings.h"
1920
#include "mock_stdcin.h"
2021
#include "path.h"
@@ -35,6 +36,7 @@
3536
#include <gmock/gmock.h>
3637
#include <gtest/gtest.h>
3738

39+
#include <QtCore/QTemporaryDir>
3840
#include <sstream>
3941

4042
namespace mp = multipass;
@@ -136,6 +138,20 @@ struct Client : public Test
136138
return ret;
137139
}
138140

141+
auto make_automount_matcher(const QTemporaryDir& fake_home) const
142+
{
143+
const auto automount_source_matcher =
144+
Property(&mp::MountRequest::source_path, StrEq(fake_home.path().toStdString()));
145+
146+
const auto target_instance_matcher = Property(&mp::TargetPathInfo::instance_name, StrEq(petenv_name()));
147+
const auto target_path_matcher = Property(&mp::TargetPathInfo::target_path, StrEq(mp::home_automount_dir));
148+
const auto target_info_matcher = AllOf(target_instance_matcher, target_path_matcher);
149+
const auto automount_target_matcher =
150+
Property(&mp::MountRequest::target_paths, AllOf(Contains(target_info_matcher), SizeIs(1)));
151+
152+
return AllOf(automount_source_matcher, automount_target_matcher);
153+
}
154+
139155
auto make_launch_instance_matcher(const std::string& instance_name)
140156
{
141157
return Property(&mp::LaunchRequest::instance_name, StrEq(instance_name));
@@ -349,6 +365,8 @@ TEST_F(Client, shell_cmd_launches_petenv_if_absent)
349365
const auto petenv_launch_matcher = Property(&mp::LaunchRequest::instance_name, StrEq(petenv_name()));
350366
const grpc::Status ok{}, notfound{grpc::StatusCode::NOT_FOUND, "msg"};
351367

368+
EXPECT_CALL(mock_daemon, mount).WillRepeatedly(Return(ok)); // 0 or more times
369+
352370
InSequence seq;
353371
EXPECT_CALL(mock_daemon, ssh_info(_, petenv_ssh_info_matcher, _)).WillOnce(Return(notfound));
354372
EXPECT_CALL(mock_daemon, launch(_, petenv_launch_matcher, _)).WillOnce(Return(ok));
@@ -357,6 +375,31 @@ TEST_F(Client, shell_cmd_launches_petenv_if_absent)
357375
EXPECT_THAT(send_command({"shell", petenv_name()}), Eq(mp::ReturnCode::Ok));
358376
}
359377

378+
TEST_F(Client, shell_cmd_automounts_when_launching_petenv)
379+
{
380+
const grpc::Status ok{}, notfound{grpc::StatusCode::NOT_FOUND, "msg"};
381+
382+
InSequence seq;
383+
EXPECT_CALL(mock_daemon, ssh_info(_, _, _)).WillOnce(Return(notfound));
384+
EXPECT_CALL(mock_daemon, launch(_, _, _)).WillOnce(Return(ok));
385+
EXPECT_CALL(mock_daemon, mount(_, _, _)).WillOnce(Return(ok));
386+
EXPECT_CALL(mock_daemon, ssh_info(_, _, _)).WillOnce(Return(ok));
387+
EXPECT_THAT(send_command({"shell", petenv_name()}), Eq(mp::ReturnCode::Ok));
388+
}
389+
390+
TEST_F(Client, shell_cmd_fails_when_automounting_in_petenv_fails)
391+
{
392+
const auto ok = grpc::Status{};
393+
const auto notfound = grpc::Status{grpc::StatusCode::NOT_FOUND, "msg"};
394+
const auto mount_failure = grpc::Status{grpc::StatusCode::INVALID_ARGUMENT, "msg"};
395+
396+
InSequence seq;
397+
EXPECT_CALL(mock_daemon, ssh_info(_, _, _)).WillOnce(Return(notfound));
398+
EXPECT_CALL(mock_daemon, launch(_, _, _)).WillOnce(Return(ok));
399+
EXPECT_CALL(mock_daemon, mount(_, _, _)).WillOnce(Return(mount_failure));
400+
EXPECT_THAT(send_command({"shell", petenv_name()}), Eq(mp::ReturnCode::CommandFail));
401+
}
402+
360403
TEST_F(Client, shell_cmd_starts_instance_if_stopped_or_suspended)
361404
{
362405
const auto instance = "ordinary";
@@ -512,6 +555,39 @@ TEST_F(Client, launch_cmd_cloudinit_option_reads_stdin_ok)
512555
EXPECT_THAT(send_command({"launch", "--cloud-init", "-"}, trash_stream, trash_stream, ss), Eq(mp::ReturnCode::Ok));
513556
}
514557

558+
#ifndef WIN32 // TODO make home mocking work for windows
559+
TEST_F(Client, launch_cmd_automounts_home_in_petenv)
560+
{
561+
const auto fake_home = QTemporaryDir{}; // the client checks the mount source exists
562+
const auto env_scope = mpt::SetEnvScope{"HOME", fake_home.path().toUtf8()};
563+
const auto home_automount_matcher = make_automount_matcher(fake_home);
564+
const auto petenv_launch_matcher = make_launch_instance_matcher(petenv_name());
565+
const auto ok = grpc::Status{};
566+
567+
InSequence seq;
568+
EXPECT_CALL(mock_daemon, launch(_, petenv_launch_matcher, _)).WillOnce(Return(ok));
569+
EXPECT_CALL(mock_daemon, mount(_, home_automount_matcher, _)).WillOnce(Return(ok));
570+
EXPECT_THAT(send_command({"launch", "--name", petenv_name()}), Eq(mp::ReturnCode::Ok));
571+
}
572+
#endif
573+
574+
TEST_F(Client, launch_cmd_fails_when_automounting_in_petenv_fails)
575+
{
576+
const grpc::Status ok{}, mount_failure{grpc::StatusCode::INVALID_ARGUMENT, "msg"};
577+
578+
InSequence seq;
579+
EXPECT_CALL(mock_daemon, launch(_, _, _)).WillOnce(Return(ok));
580+
EXPECT_CALL(mock_daemon, mount(_, _, _)).WillOnce(Return(mount_failure));
581+
EXPECT_THAT(send_command({"launch", "--name", petenv_name()}), Eq(mp::ReturnCode::CommandFail));
582+
}
583+
584+
TEST_F(Client, launch_cmd_does_not_automount_in_normal_instances)
585+
{
586+
EXPECT_CALL(mock_daemon, launch(_, _, _));
587+
EXPECT_CALL(mock_daemon, mount(_, _, _)).Times(0); // because we may want to move from a Strict mock in the future
588+
EXPECT_THAT(send_command({"launch"}), Eq(mp::ReturnCode::Ok));
589+
}
590+
515591
// purge cli tests
516592
TEST_F(Client, purge_cmd_ok_no_args)
517593
{
@@ -845,13 +921,40 @@ TEST_F(Client, start_cmd_launches_petenv_if_absent)
845921
const auto petenv_launch_matcher = make_launch_instance_matcher(petenv_name());
846922
const grpc::Status ok{}, aborted = aborted_start_status({petenv_name()});
847923

924+
EXPECT_CALL(mock_daemon, mount).WillRepeatedly(Return(ok)); // 0 or more times
925+
848926
InSequence seq;
849927
EXPECT_CALL(mock_daemon, start(_, petenv_start_matcher, _)).WillOnce(Return(aborted));
850928
EXPECT_CALL(mock_daemon, launch(_, petenv_launch_matcher, _)).WillOnce(Return(ok));
851929
EXPECT_CALL(mock_daemon, start(_, petenv_start_matcher, _)).WillOnce(Return(ok));
852930
EXPECT_THAT(send_command({"start", petenv_name()}), Eq(mp::ReturnCode::Ok));
853931
}
854932

933+
TEST_F(Client, start_cmd_automounts_when_launching_petenv)
934+
{
935+
const grpc::Status ok{}, aborted = aborted_start_status({petenv_name()});
936+
937+
InSequence seq;
938+
EXPECT_CALL(mock_daemon, start(_, _, _)).WillOnce(Return(aborted));
939+
EXPECT_CALL(mock_daemon, launch(_, _, _)).WillOnce(Return(ok));
940+
EXPECT_CALL(mock_daemon, mount(_, _, _)).WillOnce(Return(ok));
941+
EXPECT_CALL(mock_daemon, start(_, _, _)).WillOnce(Return(ok));
942+
EXPECT_THAT(send_command({"start", petenv_name()}), Eq(mp::ReturnCode::Ok));
943+
}
944+
945+
TEST_F(Client, start_cmd_fails_when_automounting_in_petenv_fails)
946+
{
947+
const auto ok = grpc::Status{};
948+
const auto aborted = aborted_start_status({petenv_name()});
949+
const auto mount_failure = grpc::Status{grpc::StatusCode::INVALID_ARGUMENT, "msg"};
950+
951+
InSequence seq;
952+
EXPECT_CALL(mock_daemon, start(_, _, _)).WillOnce(Return(aborted));
953+
EXPECT_CALL(mock_daemon, launch(_, _, _)).WillOnce(Return(ok));
954+
EXPECT_CALL(mock_daemon, mount(_, _, _)).WillOnce(Return(mount_failure));
955+
EXPECT_THAT(send_command({"start", petenv_name()}), Eq(mp::ReturnCode::CommandFail));
956+
}
957+
855958
TEST_F(Client, start_cmd_launches_petenv_if_absent_among_others_present)
856959
{
857960
std::vector<std::string> instances{"a", "b", petenv_name(), "c"}, cmd = concat({"start"}, instances);
@@ -860,6 +963,8 @@ TEST_F(Client, start_cmd_launches_petenv_if_absent_among_others_present)
860963
const auto petenv_launch_matcher = make_launch_instance_matcher(petenv_name());
861964
const grpc::Status ok{}, aborted = aborted_start_status({petenv_name()});
862965

966+
EXPECT_CALL(mock_daemon, mount).WillRepeatedly(Return(ok)); // 0 or more times
967+
863968
InSequence seq;
864969
EXPECT_CALL(mock_daemon, start(_, instance_start_matcher, _)).WillOnce(Return(aborted));
865970
EXPECT_CALL(mock_daemon, launch(_, petenv_launch_matcher, _)).WillOnce(Return(ok));

0 commit comments

Comments
 (0)