| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | NetworkTasks.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | Implementation of network command related execution logic. |
| 12 | |
| 13 | --*/ |
| 14 | #include "Argument.h" |
| 15 | #include "ArgumentConvertedTypes.h" |
| 16 | #include "CLIExecutionContext.h" |
| 17 | #include "NetworkModel.h" |
| 18 | #include "NetworkService.h" |
| 19 | #include "NetworkTasks.h" |
| 20 | #include "TableOutput.h" |
| 21 | #include <wslc_schema.h> |
| 22 | |
| 23 | using namespace wsl::shared; |
| 24 | using namespace wsl::windows::common; |
| 25 | using namespace wsl::windows::common::string; |
| 26 | using namespace wsl::windows::common::timestamp; |
| 27 | using namespace wsl::windows::common::wslutil; |
| 28 | using namespace wsl::windows::wslc::execution; |
| 29 | using namespace wsl::windows::wslc::models; |
| 30 | using namespace wsl::windows::wslc::services; |
| 31 | |
| 32 | namespace wsl::windows::wslc::task { |
| 33 | |
| 34 | namespace { |
| 35 | |
| 36 | // Shared by the table and json output so the two cannot drift. The id is truncated unless --no-trunc is passed. |
| 37 | NetworkOutputInformation ToNetworkOutput(const wslc_schema::NetworkListEntry& network, bool truncate) |
| 38 | { |
| 39 | NetworkOutputInformation entry; |
| 40 | entry.CreatedAt = Rfc3339ToUtcDisplayTime(network.Created); |
| 41 | entry.Driver = network.Driver; |
| 42 | entry.ID = TruncateId(network.Id, truncate); |
| 43 | entry.IPv4 = network.EnableIPv4 ? "true" : "false"; |
| 44 | entry.IPv6 = network.EnableIPv6 ? "true" : "false"; |
| 45 | entry.Internal = network.Internal ? "true" : "false"; |
| 46 | entry.Name = network.Name; |
| 47 | entry.Scope = network.Scope; |
| 48 | |
| 49 | for (const auto& [key, value] : network.Labels) |
| 50 | { |
| 51 | if (!entry.Labels.empty()) |
| 52 | { |
| 53 | entry.Labels += ","; |
| 54 | } |
| 55 | |
| 56 | entry.Labels += std::format("{}={}", key, value); |
| 57 | } |
| 58 | |
| 59 | return entry; |
| 60 | } |
| 61 | |
| 62 | } // namespace |
| 63 | |
| 64 | static bool TryInspectNetwork(Terminal& terminal, Session& session, const std::string& networkName, std::optional<wslc_schema::Network>& inspectData) |
| 65 | { |
| 66 | try |
| 67 | { |
| 68 | inspectData = NetworkService::Inspect(session, networkName); |
| 69 | return true; |
| 70 | } |
| 71 | catch (const wil::ResultException& ex) |
| 72 | { |
| 73 | if (ex.GetErrorCode() == WSLC_E_NETWORK_NOT_FOUND) |
| 74 | { |
| 75 | terminal.Error(L"{}\n", Localization::MessageWslcNetworkNotFound(networkName.c_str())); |
| 76 | return false; |
| 77 | } |
| 78 | |
| 79 | throw; |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | static bool TryDeleteNetwork(Terminal& terminal, Session& session, const std::string& networkName, bool force) |
| 84 | { |
| 85 | try |
| 86 | { |
| 87 | NetworkService::Delete(session, networkName); |
| 88 | return true; |
| 89 | } |
| 90 | catch (const wil::ResultException& ex) |
| 91 | { |
| 92 | if (ex.GetErrorCode() == WSLC_E_NETWORK_NOT_FOUND) |
| 93 | { |
| 94 | if (!force) |
| 95 | { |
| 96 | terminal.Error(L"{}\n", Localization::MessageWslcNetworkNotFound(networkName.c_str())); |
| 97 | } |
| 98 | |
| 99 | return false; |
| 100 | } |
| 101 | |
| 102 | throw; |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | void CreateNetwork(CLIExecutionContext& context) |
| 107 | { |
| 108 | WI_ASSERT(context.Data.Contains(Data::Session)); |
| 109 | WI_ASSERT(context.Args.Contains(ArgType::NetworkName)); |
| 110 | |
| 111 | models::CreateNetworkOptions options{}; |
| 112 | options.Name = WideToMultiByte(context.Args.GetValue<ArgType::NetworkName>()); |
| 113 | |
| 114 | for (const auto& option : context.Args.GetAllValues<ArgType::Options>()) |
| 115 | { |
| 116 | options.DriverOpts.push_back(option); |
| 117 | } |
| 118 | |
| 119 | for (const auto& label : context.Args.GetAllValues<ArgType::Label>()) |
| 120 | { |
| 121 | options.Labels.push_back(label); |
| 122 | } |
| 123 | |
| 124 | if (context.Args.Contains(ArgType::Driver)) |
| 125 | { |
| 126 | options.Driver = WideToMultiByte(context.Args.GetValue<ArgType::Driver>()); |
| 127 | } |
| 128 | |
| 129 | options.Internal = context.Args.GetValue<ArgType::Internal>(); |
| 130 | |
| 131 | if (context.Args.Contains(ArgType::Subnet)) |
| 132 | { |
| 133 | options.Subnet = WideToMultiByte(context.Args.GetValue<ArgType::Subnet>()); |
| 134 | } |
| 135 | |
| 136 | if (context.Args.Contains(ArgType::Gateway)) |
| 137 | { |
| 138 | options.Gateway = WideToMultiByte(context.Args.GetValue<ArgType::Gateway>()); |
| 139 | } |
| 140 | |
| 141 | if (context.Args.Contains(ArgType::IpRange)) |
| 142 | { |
| 143 | options.IpRange = WideToMultiByte(context.Args.GetValue<ArgType::IpRange>()); |
| 144 | } |
| 145 | |
| 146 | NetworkService::Create(context.Terminal, context.Data.Get<Data::Session>(), options); |
| 147 | context.Terminal.Output(L"{}\n", MultiByteToWide(options.Name)); |
| 148 | } |
| 149 | |
| 150 | void DeleteNetworks(CLIExecutionContext& context) |
| 151 | { |
| 152 | WI_ASSERT(context.Data.Contains(Data::Session)); |
| 153 | auto& session = context.Data.Get<Data::Session>(); |
| 154 | auto networkNames = context.Args.GetAllValues<ArgType::NetworkName>(); |
| 155 | const bool force = context.Args.GetValue<ArgType::Force>(); |
| 156 | for (const auto& name : networkNames) |
| 157 | { |
| 158 | if (TryDeleteNetwork(context.Terminal, session, WideToMultiByte(name), force)) |
| 159 | { |
| 160 | context.Terminal.Output(L"{}\n", name); |
| 161 | } |
| 162 | else if (!force) |
| 163 | { |
| 164 | context.ExitCode = 1; |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | void GetNetworks(CLIExecutionContext& context) |
| 170 | { |
| 171 | WI_ASSERT(context.Data.Contains(Data::Session)); |
| 172 | auto& session = context.Data.Get<Data::Session>(); |
| 173 | |
| 174 | auto filters = context.Args.GetAllValues<ArgType::Filter>(); |
| 175 | context.Data.Add<Data::Networks>(NetworkService::List(session, filters)); |
| 176 | } |
| 177 | |
| 178 | void InspectNetworks(CLIExecutionContext& context) |
| 179 | { |
| 180 | WI_ASSERT(context.Data.Contains(Data::Session)); |
| 181 | auto& session = context.Data.Get<Data::Session>(); |
| 182 | auto networkNames = context.Args.GetAllValues<ArgType::NetworkName>(); |
| 183 | std::vector<wsl::windows::common::wslc_schema::Network> result; |
| 184 | for (const auto& name : networkNames) |
| 185 | { |
| 186 | std::optional<wslc_schema::Network> inspectData; |
| 187 | if (TryInspectNetwork(context.Terminal, session, WideToMultiByte(name), inspectData)) |
| 188 | { |
| 189 | result.push_back(*inspectData); |
| 190 | } |
| 191 | else |
| 192 | { |
| 193 | context.ExitCode = 1; |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | auto json = ToJson(result, context.Args.GetValue<ArgType::InspectFormat>(c_jsonPrettyPrintIndent)); |
| 198 | context.Terminal.Output(L"{}\n", MultiByteToWide(json)); |
| 199 | } |
| 200 | |
| 201 | void ListNetworks(CLIExecutionContext& context) |
| 202 | { |
| 203 | WI_ASSERT(context.Data.Contains(Data::Networks)); |
| 204 | auto& networks = context.Data.Get<Data::Networks>(); |
| 205 | |
| 206 | // Networks are reported in name order regardless of how the daemon returns them. |
| 207 | std::ranges::sort(networks, {}, &wslc_schema::NetworkListEntry::Name); |
| 208 | |
| 209 | const auto format = context.Args.GetValue<ArgType::Format>(FormatType::Table); |
| 210 | const bool quiet = context.Args.GetValue<ArgType::Quiet>(); |
| 211 | const bool trunc = !context.Args.GetValue<ArgType::NoTrunc>(); |
| 212 | if (format == FormatType::Table && quiet) |
| 213 | { |
| 214 | for (const auto& network : networks) |
| 215 | { |
| 216 | context.Terminal.Output(L"{}\n", MultiByteToWide(TruncateId(network.Id, trunc))); |
| 217 | } |
| 218 | |
| 219 | return; |
| 220 | } |
| 221 | |
| 222 | switch (format) |
| 223 | { |
| 224 | case FormatType::Json: |
| 225 | { |
| 226 | for (const auto& network : networks) |
| 227 | { |
| 228 | context.Terminal.Output(L"{}\n", ToJsonW(ToNetworkOutput(network, trunc), c_jsonCompactIndent)); |
| 229 | } |
| 230 | |
| 231 | break; |
| 232 | } |
| 233 | case FormatType::Table: |
| 234 | { |
| 235 | // Every column has a minimum total width of ten characters, including the padding that follows it. |
| 236 | constexpr size_t c_minimumColumnWidth = 7; |
| 237 | auto table = wsl::windows::wslc::TableOutput<4>( |
| 238 | context.Terminal, |
| 239 | {L"NETWORK ID", L"NAME", L"DRIVER", L"SCOPE"}, |
| 240 | {ColumnWidthConfig{.MinWidth = c_minimumColumnWidth}, |
| 241 | ColumnWidthConfig{.MinWidth = c_minimumColumnWidth}, |
| 242 | ColumnWidthConfig{.MinWidth = c_minimumColumnWidth}, |
| 243 | ColumnWidthConfig{.MinWidth = c_minimumColumnWidth}}); |
| 244 | for (const auto& network : networks) |
| 245 | { |
| 246 | const auto entry = ToNetworkOutput(network, trunc); |
| 247 | table.WriteRow({ |
| 248 | MultiByteToWide(entry.ID), |
| 249 | MultiByteToWide(entry.Name), |
| 250 | MultiByteToWide(entry.Driver), |
| 251 | MultiByteToWide(entry.Scope), |
| 252 | }); |
| 253 | } |
| 254 | |
| 255 | table.Complete(); |
| 256 | break; |
| 257 | } |
| 258 | default: |
| 259 | THROW_HR(E_UNEXPECTED); |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | void PruneNetworks(CLIExecutionContext& context) |
| 264 | { |
| 265 | WI_ASSERT(context.Data.Contains(Data::Session)); |
| 266 | auto& session = context.Data.Get<Data::Session>(); |
| 267 | |
| 268 | // Filter values are parsed and cached during argument validation. |
| 269 | auto filters = context.Args.GetAllValues<ArgType::Filter>(); |
| 270 | |
| 271 | auto result = NetworkService::Prune(session, filters); |
| 272 | |
| 273 | if (result.PrunedNetworks.empty()) |
| 274 | { |
| 275 | return; |
| 276 | } |
| 277 | |
| 278 | context.Terminal.Output(L"{}\n", Localization::WSLCCLI_NetworkPruneDeletedHeader()); |
| 279 | for (const auto& networkName : result.PrunedNetworks) |
| 280 | { |
| 281 | context.Terminal.Output(L"{}\n", MultiByteToWide(networkName)); |
| 282 | } |
| 283 | |
| 284 | context.Terminal.Output(L"\n"); |
| 285 | } |
| 286 | |
| 287 | void ConnectNetwork(CLIExecutionContext& context) |
| 288 | { |
| 289 | WI_ASSERT(context.Data.Contains(Data::Session)); |
| 290 | WI_ASSERT(context.Data.Contains(Data::NetworkEndpointOptions)); |
| 291 | WI_ASSERT(context.Args.Contains(ArgType::NetworkName)); |
| 292 | WI_ASSERT(context.Args.Contains(ArgType::ContainerId)); |
| 293 | |
| 294 | const auto& endpoint = context.Data.Get<Data::NetworkEndpointOptions>(); |
| 295 | models::ConnectNetworkOptions options{}; |
| 296 | options.NetworkName = WideToMultiByte(context.Args.GetValue<ArgType::NetworkName>()); |
| 297 | options.ContainerId = WideToMultiByte(context.Args.GetValue<ArgType::ContainerId>()); |
| 298 | options.Aliases = endpoint.Aliases; |
| 299 | options.IpAddress = endpoint.IpAddress; |
| 300 | options.Links = endpoint.Links; |
| 301 | options.LinkLocalIps = endpoint.LinkLocalIps; |
| 302 | options.DriverOpts = endpoint.DriverOpts; |
| 303 | |
| 304 | NetworkService::Connect(context.Data.Get<Data::Session>(), options); |
| 305 | } |
| 306 | |
| 307 | void DisconnectNetwork(CLIExecutionContext& context) |
| 308 | { |
| 309 | WI_ASSERT(context.Data.Contains(Data::Session)); |
| 310 | WI_ASSERT(context.Args.Contains(ArgType::NetworkName)); |
| 311 | WI_ASSERT(context.Args.Contains(ArgType::ContainerId)); |
| 312 | |
| 313 | const auto networkName = WideToMultiByte(context.Args.GetValue<ArgType::NetworkName>()); |
| 314 | const auto containerId = WideToMultiByte(context.Args.GetValue<ArgType::ContainerId>()); |
| 315 | NetworkService::Disconnect(context.Data.Get<Data::Session>(), networkName, containerId); |
| 316 | } |
| 317 | |
| 318 | void SetNetworkEndpointOptionsFromArgs(CLIExecutionContext& context) |
| 319 | { |
| 320 | models::NetworkEndpointOptions options{}; |
| 321 | |
| 322 | for (const auto& alias : context.Args.GetAllValues<ArgType::NetworkAlias>()) |
| 323 | { |
| 324 | options.Aliases.emplace_back(WideToMultiByte(alias)); |
| 325 | } |
| 326 | |
| 327 | if (context.Args.Contains(ArgType::IpAddress)) |
| 328 | { |
| 329 | options.IpAddress = WideToMultiByte(context.Args.GetValue<ArgType::IpAddress>()); |
| 330 | } |
| 331 | |
| 332 | for (const auto& link : context.Args.GetAllValues<ArgType::Link>()) |
| 333 | { |
| 334 | options.Links.emplace_back(WideToMultiByte(link)); |
| 335 | } |
| 336 | |
| 337 | for (const auto& linkLocalIp : context.Args.GetAllValues<ArgType::LinkLocalIp>()) |
| 338 | { |
| 339 | options.LinkLocalIps.emplace_back(WideToMultiByte(linkLocalIp)); |
| 340 | } |
| 341 | |
| 342 | for (const auto& driverOpt : context.Args.GetAllValues<ArgType::DriverOpt>()) |
| 343 | { |
| 344 | options.DriverOpts.emplace_back(WideToMultiByte(driverOpt)); |
| 345 | } |
| 346 | |
| 347 | context.Data.Add<Data::NetworkEndpointOptions>(std::move(options)); |
| 348 | } |
| 349 | } // namespace wsl::windows::wslc::task |