Skip to content

Commit 3b9acd1

Browse files
bors[bot]luis4a0
andauthored
Merge #1886
1886: Show multiple IP's in `list` and `info` commands r=ricab a=luis4a0 This PR adds the ability to gather all the IP's used by each instance. The private bits of this PR are in https://github.com/canonical/multipass-private/pull/338. Co-authored-by: Luis Peñaranda <[email protected]>
2 parents 8fd7871 + d281627 commit 3b9acd1

19 files changed

+239
-110
lines changed

include/multipass/virtual_machine.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class VirtualMachine
6363
};
6464
virtual std::string ssh_hostname(std::chrono::milliseconds timeout) = 0;
6565
virtual std::string ssh_username() = 0;
66-
virtual std::string ipv4() = 0;
66+
virtual std::string management_ipv4() = 0;
6767
virtual std::string ipv6() = 0;
6868
virtual void wait_until_ssh_up(std::chrono::milliseconds timeout) = 0;
6969
virtual void ensure_vm_is_running() = 0;
@@ -73,7 +73,7 @@ class VirtualMachine
7373
const std::string vm_name;
7474
std::condition_variable state_wait;
7575
std::mutex state_mutex;
76-
optional<IPAddress> ip;
76+
optional<IPAddress> management_ip;
7777
bool shutdown_while_starting{false};
7878

7979
protected:

src/client/cli/formatter/csv_formatter.cpp

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,22 @@ std::string mp::CSVFormatter::format(const InfoReply& reply) const
2828
fmt::memory_buffer buf;
2929
fmt::format_to(
3030
buf, "Name,State,Ipv4,Ipv6,Release,Image hash,Image release,Load,Disk usage,Disk total,Memory usage,Memory "
31-
"total,Mounts\n");
31+
"total,Mounts,AllIPv4\n");
3232

3333
for (const auto& info : format::sorted(reply.info()))
3434
{
3535
fmt::format_to(buf, "{},{},{},{},{},{},{},{},{},{},{},{},", info.name(),
36-
mp::format::status_string_for(info.instance_status()), info.ipv4(), info.ipv6(),
37-
info.current_release(), info.id(), info.image_release(), info.load(), info.disk_usage(),
38-
info.disk_total(), info.memory_usage(), info.memory_total());
36+
mp::format::status_string_for(info.instance_status()), info.ipv4_size() ? info.ipv4(0) : "",
37+
info.ipv6_size() ? info.ipv6(0) : "", info.current_release(), info.id(), info.image_release(),
38+
info.load(), info.disk_usage(), info.disk_total(), info.memory_usage(), info.memory_total());
3939

4040
auto mount_paths = info.mount_info().mount_paths();
4141
for (auto mount = mount_paths.cbegin(); mount != mount_paths.cend(); ++mount)
4242
{
4343
fmt::format_to(buf, "{} => {};", mount->source_path(), mount->target_path());
4444
}
4545

46-
fmt::format_to(buf, "\n");
46+
fmt::format_to(buf, ",\"{}\"\n", fmt::join(info.ipv4(), ","));
4747
}
4848
return fmt::to_string(buf);
4949
}
@@ -52,13 +52,14 @@ std::string mp::CSVFormatter::format(const ListReply& reply) const
5252
{
5353
fmt::memory_buffer buf;
5454

55-
fmt::format_to(buf, "Name,State,IPv4,IPv6,Release\n");
55+
fmt::format_to(buf, "Name,State,IPv4,IPv6,Release,AllIPv4\n");
5656

5757
for (const auto& instance : format::sorted(reply.instances()))
5858
{
59-
fmt::format_to(buf, "{},{},{},{},{}\n", instance.name(),
60-
mp::format::status_string_for(instance.instance_status()), instance.ipv4(), instance.ipv6(),
61-
instance.current_release());
59+
fmt::format_to(buf, "{},{},{},{},{},\"{}\"\n", instance.name(),
60+
mp::format::status_string_for(instance.instance_status()),
61+
instance.ipv4_size() ? instance.ipv4(0) : "", instance.ipv6_size() ? instance.ipv6(0) : "",
62+
instance.current_release(), fmt::join(instance.ipv4(), ","));
6263
}
6364

6465
return fmt::to_string(buf);

src/client/cli/formatter/json_formatter.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ std::string mp::JsonFormatter::format(const InfoReply& reply) const
6969
instance_info.insert("memory", memory);
7070

7171
QJsonArray ipv4_addrs;
72-
if (!info.ipv4().empty())
73-
ipv4_addrs.append(QString::fromStdString(info.ipv4()));
72+
for (const auto& ip : info.ipv4())
73+
ipv4_addrs.append(QString::fromStdString(ip));
7474
instance_info.insert("ipv4", ipv4_addrs);
7575

7676
QJsonObject mounts;
@@ -121,8 +121,8 @@ std::string mp::JsonFormatter::format(const ListReply& reply) const
121121
instance_obj.insert("state", QString::fromStdString(mp::format::status_string_for(instance.instance_status())));
122122

123123
QJsonArray ipv4_addrs;
124-
if (!instance.ipv4().empty())
125-
ipv4_addrs.append(QString::fromStdString(instance.ipv4()));
124+
for (const auto& ip : instance.ipv4())
125+
ipv4_addrs.append(QString::fromStdString(ip));
126126
instance_obj.insert("ipv4", ipv4_addrs);
127127

128128
instance_obj.insert("release", QString::fromStdString(instance.current_release()));

src/client/cli/formatter/table_formatter.cpp

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,19 @@ std::string mp::TableFormatter::format(const InfoReply& reply) const
6666
{
6767
fmt::format_to(buf, "{:<16}{}\n", "Name:", info.name());
6868
fmt::format_to(buf, "{:<16}{}\n", "State:", mp::format::status_string_for(info.instance_status()));
69-
fmt::format_to(buf, "{:<16}{}\n", "IPv4:", info.ipv4().empty() ? "--" : info.ipv4());
7069

71-
if (!info.ipv6().empty())
70+
int ipv4_size = info.ipv4_size();
71+
fmt::format_to(buf, "{:<16}{}\n", "IPv4:", ipv4_size ? info.ipv4(0) : "--");
72+
73+
for (int i = 1; i < ipv4_size; ++i)
74+
fmt::format_to(buf, "{:<16}{}\n", "", info.ipv4(i));
75+
76+
if (int ipv6_size = info.ipv6_size())
7277
{
73-
fmt::format_to(buf, "{:<16}{}\n", "IPv6:", info.ipv6());
78+
fmt::format_to(buf, "{:<16}{}\n", "IPv6:", info.ipv6(0));
79+
80+
for (int i = 1; i < ipv6_size; ++i)
81+
fmt::format_to(buf, "{:<16}{}\n", "", info.ipv6(i));
7482
}
7583

7684
fmt::format_to(buf, "{:<16}{}\n", "Release:", info.current_release().empty() ? "--" : info.current_release());
@@ -144,11 +152,19 @@ std::string mp::TableFormatter::format(const ListReply& reply) const
144152

145153
for (const auto& instance : format::sorted(reply.instances()))
146154
{
155+
int ipv4_size = instance.ipv4_size();
156+
147157
fmt::format_to(buf, row_format, instance.name(), name_column_width,
148158
mp::format::status_string_for(instance.instance_status()), state_column_width,
149-
instance.ipv4().empty() ? "--" : instance.ipv4(), ip_column_width,
159+
ipv4_size ? instance.ipv4(0) : "--", ip_column_width,
150160
instance.current_release().empty() ? "Not Available"
151161
: fmt::format("Ubuntu {}", instance.current_release()));
162+
163+
for (int i = 1; i < ipv4_size; ++i)
164+
{
165+
fmt::format_to(buf, row_format, "", name_column_width, "", state_column_width, instance.ipv4(i),
166+
instance.ipv4(i).size(), "");
167+
}
152168
}
153169

154170
return fmt::to_string(buf);

src/client/cli/formatter/yaml_formatter.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@ std::string mp::YamlFormatter::format(const InfoReply& reply) const
8080

8181
instance_node["memory"] = memory;
8282

83-
if (!info.ipv4().empty())
84-
instance_node["ipv4"].push_back(info.ipv4());
83+
instance_node["ipv4"] = YAML::Node(YAML::NodeType::Sequence);
84+
for (const auto& ip : info.ipv4())
85+
instance_node["ipv4"].push_back(ip);
8586

8687
YAML::Node mounts;
8788
for (const auto& mount : info.mount_info().mount_paths())
@@ -120,7 +121,10 @@ std::string mp::YamlFormatter::format(const ListReply& reply) const
120121
YAML::Node instance_node;
121122
instance_node["state"] = mp::format::status_string_for(instance.instance_status());
122123

123-
instance_node["ipv4"].push_back(instance.ipv4());
124+
instance_node["ipv4"] = YAML::Node(YAML::NodeType::Sequence);
125+
for (const auto& ip : instance.ipv4())
126+
instance_node["ipv4"].push_back(ip);
127+
124128
instance_node["release"] = instance.current_release();
125129

126130
list[instance.name()].push_back(instance_node);

src/daemon/daemon.cpp

Lines changed: 91 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <multipass/exceptions/not_implemented_on_this_backend_exception.h>
2828
#include <multipass/exceptions/sshfs_missing_error.h>
2929
#include <multipass/exceptions/start_exception.h>
30+
#include <multipass/ip_address.h>
3031
#include <multipass/logging/client_logger.h>
3132
#include <multipass/logging/log.h>
3233
#include <multipass/name_generator.h>
@@ -53,6 +54,7 @@
5354
#include <QJsonDocument>
5455
#include <QJsonObject>
5556
#include <QJsonParseError>
57+
#include <QRegularExpression>
5658
#include <QString>
5759
#include <QSysInfo>
5860
#include <QtConcurrent/QtConcurrent>
@@ -824,6 +826,65 @@ std::string generate_unused_mac_address(std::unordered_set<std::string>& s)
824826
max_tries, s.size())};
825827
}
826828

829+
// Executes a given command on the given session. Returns the output of the command, with spaces and feeds trimmed.
830+
// Caveat emptor: if the command fails, an empty string is returned.
831+
std::string run_in_vm(mp::SSHSession& session, const std::string& cmd)
832+
{
833+
auto proc = exec_and_log(session, cmd);
834+
835+
if (proc.exit_code() != 0)
836+
{
837+
auto error_msg = proc.read_std_error();
838+
mpl::log(mpl::Level::warning, category,
839+
fmt::format("failed to run '{}', error message: '{}'", cmd, mp::utils::trim_end(error_msg)));
840+
return std::string{};
841+
}
842+
843+
auto output = proc.read_std_output();
844+
if (output.empty())
845+
{
846+
mpl::log(mpl::Level::warning, category, fmt::format("no output after running '{}'", cmd));
847+
return std::string{};
848+
}
849+
850+
return mp::utils::trim_end(output);
851+
}
852+
853+
bool is_ipv4_valid(const std::string& ipv4)
854+
{
855+
try
856+
{
857+
(mp::IPAddress(ipv4));
858+
}
859+
catch (std::invalid_argument&)
860+
{
861+
return false;
862+
}
863+
864+
return true;
865+
}
866+
867+
std::vector<std::string> get_all_ipv4(mp::SSHSession& session)
868+
{
869+
std::vector<std::string> all_ipv4;
870+
871+
auto ip_a_output = QString::fromStdString(run_in_vm(session, "ip -brief -family inet address show scope global"));
872+
873+
QRegularExpression ipv4_re{QStringLiteral("([\\d\\.]+)\\/\\d+\\s*$"), QRegularExpression::MultilineOption};
874+
875+
QRegularExpressionMatchIterator ip_it = ipv4_re.globalMatch(ip_a_output);
876+
877+
while (ip_it.hasNext())
878+
{
879+
auto ip_match = ip_it.next();
880+
auto ip = ip_match.captured(1).toStdString();
881+
882+
all_ipv4.push_back(ip);
883+
}
884+
885+
return all_ipv4;
886+
}
887+
827888
} // namespace
828889

829890
mp::Daemon::Daemon(std::unique_ptr<const DaemonConfig> the_config)
@@ -1324,37 +1385,25 @@ try // clang-format on
13241385
mp::SSHSession session{vm->ssh_hostname(), vm->ssh_port(), vm_specs.ssh_username,
13251386
*config->ssh_key_provider};
13261387

1327-
auto run_in_vm = [&session](const std::string& cmd) {
1328-
auto proc = session.exec(cmd);
1329-
if (proc.exit_code() != 0)
1330-
{
1331-
auto error_msg = proc.read_std_error();
1332-
mpl::log(
1333-
mpl::Level::warning, category,
1334-
fmt::format("failed to run '{}', error message: '{}'", cmd, mp::utils::trim_end(error_msg)));
1335-
return std::string{};
1336-
}
1388+
info->set_load(run_in_vm(session, "cat /proc/loadavg | cut -d ' ' -f1-3"));
1389+
info->set_memory_usage(run_in_vm(session, "free -b | sed '1d;3d' | awk '{printf $3}'"));
1390+
info->set_memory_total(run_in_vm(session, "free -b | sed '1d;3d' | awk '{printf $2}'"));
1391+
info->set_disk_usage(
1392+
run_in_vm(session, "df --output=used `awk '$2 == \"/\" { print $1 }' /proc/mounts` -B1 | sed 1d"));
1393+
info->set_disk_total(
1394+
run_in_vm(session, "df --output=size `awk '$2 == \"/\" { print $1 }' /proc/mounts` -B1 | sed 1d"));
13371395

1338-
auto output = proc.read_std_output();
1339-
if (output.empty())
1340-
{
1341-
mpl::log(mpl::Level::warning, category, fmt::format("no output after running '{}'", cmd));
1342-
return std::string{};
1343-
}
1396+
std::string management_ip = vm->management_ipv4();
1397+
auto all_ipv4 = get_all_ipv4(session);
13441398

1345-
return mp::utils::trim_end(output);
1346-
};
1399+
if (is_ipv4_valid(management_ip))
1400+
info->add_ipv4(management_ip);
13471401

1348-
info->set_load(run_in_vm("cat /proc/loadavg | cut -d ' ' -f1-3"));
1349-
info->set_memory_usage(run_in_vm("free -b | sed '1d;3d' | awk '{printf $3}'"));
1350-
info->set_memory_total(run_in_vm("free -b | sed '1d;3d' | awk '{printf $2}'"));
1351-
info->set_disk_usage(
1352-
run_in_vm("df --output=used `awk '$2 == \"/\" { print $1 }' /proc/mounts` -B1 | sed 1d"));
1353-
info->set_disk_total(
1354-
run_in_vm("df --output=size `awk '$2 == \"/\" { print $1 }' /proc/mounts` -B1 | sed 1d"));
1355-
info->set_ipv4(vm->ipv4());
1402+
for (const auto& extra_ipv4 : all_ipv4)
1403+
if (extra_ipv4 != management_ip)
1404+
info->add_ipv4(extra_ipv4);
13561405

1357-
auto current_release = run_in_vm("lsb_release -ds");
1406+
auto current_release = run_in_vm(session, "lsb_release -ds");
13581407
info->set_current_release(!current_release.empty() ? current_release : original_release);
13591408
}
13601409
}
@@ -1407,7 +1456,21 @@ try // clang-format on
14071456
entry->set_current_release(current_release);
14081457

14091458
if (mp::utils::is_running(present_state))
1410-
entry->set_ipv4(vm->ipv4());
1459+
{
1460+
auto vm_specs = vm_instance_specs[name];
1461+
mp::SSHSession session{vm->ssh_hostname(), vm->ssh_port(), vm_specs.ssh_username,
1462+
*config->ssh_key_provider};
1463+
1464+
std::string management_ip = vm->management_ipv4();
1465+
auto all_ipv4 = get_all_ipv4(session);
1466+
1467+
if (is_ipv4_valid(management_ip))
1468+
entry->add_ipv4(management_ip);
1469+
1470+
for (const auto& extra_ipv4 : all_ipv4)
1471+
if (extra_ipv4 != management_ip)
1472+
entry->add_ipv4(extra_ipv4);
1473+
}
14111474
}
14121475

14131476
for (const auto& instance : deleted_instances)

src/platform/backends/libvirt/libvirt_virtual_machine.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -424,18 +424,18 @@ std::string mp::LibVirtVirtualMachine::ssh_username()
424424
return username;
425425
}
426426

427-
std::string mp::LibVirtVirtualMachine::ipv4()
427+
std::string mp::LibVirtVirtualMachine::management_ipv4()
428428
{
429-
if (!ip)
429+
if (!management_ip)
430430
{
431431
auto result = instance_ip_for(mac_addr, libvirt_wrapper);
432432
if (result)
433-
ip.emplace(result.value());
433+
management_ip.emplace(result.value());
434434
else
435435
return "UNKNOWN";
436436
}
437437

438-
return ip.value().as_string();
438+
return management_ip.value().as_string();
439439
}
440440

441441
std::string mp::LibVirtVirtualMachine::ipv6()
@@ -465,7 +465,7 @@ mp::LibVirtVirtualMachine::DomainUPtr mp::LibVirtVirtualMachine::initialize_doma
465465
if (mac_addr.empty())
466466
mac_addr = instance_mac_addr_for(domain.get(), libvirt_wrapper);
467467

468-
ipv4(); // To set ip
468+
management_ipv4(); // To set ip
469469
state = refresh_instance_state_for_domain(domain.get(), state, libvirt_wrapper);
470470

471471
return domain;

src/platform/backends/libvirt/libvirt_virtual_machine.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class LibVirtVirtualMachine final : public VirtualMachine
4646
int ssh_port() override;
4747
std::string ssh_hostname(std::chrono::milliseconds timeout) override;
4848
std::string ssh_username() override;
49-
std::string ipv4() override;
49+
std::string management_ipv4() override;
5050
std::string ipv6() override;
5151
void wait_until_ssh_up(std::chrono::milliseconds timeout) override;
5252
void ensure_vm_is_running() override;

src/platform/backends/lxd/lxd_virtual_machine.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,19 +304,19 @@ std::string mp::LXDVirtualMachine::ssh_username()
304304
return username;
305305
}
306306

307-
std::string mp::LXDVirtualMachine::ipv4()
307+
std::string mp::LXDVirtualMachine::management_ipv4()
308308
{
309-
if (!ip)
309+
if (!management_ip)
310310
{
311-
ip = get_ip_for(mac_addr, manager, network_leases_url());
312-
if (!ip)
311+
management_ip = get_ip_for(mac_addr, manager, network_leases_url());
312+
if (!management_ip)
313313
{
314314
mpl::log(mpl::Level::trace, name.toStdString(), "IP address not found.");
315315
return "UNKNOWN";
316316
}
317317
}
318318

319-
return ip.value().as_string();
319+
return management_ip.value().as_string();
320320
}
321321

322322
std::string mp::LXDVirtualMachine::ipv6()

src/platform/backends/lxd/lxd_virtual_machine.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class LXDVirtualMachine final : public VirtualMachine
4444
int ssh_port() override;
4545
std::string ssh_hostname(std::chrono::milliseconds timeout) override;
4646
std::string ssh_username() override;
47-
std::string ipv4() override;
47+
std::string management_ipv4() override;
4848
std::string ipv6() override;
4949
void ensure_vm_is_running() override;
5050
void ensure_vm_is_running(const std::chrono::milliseconds& timeout);

0 commit comments

Comments
 (0)