/// <summary> /// Installs the <b>vsdbg</b> debugger on the Raspberry if it's not already installed. /// </summary> /// <returns><c>true</c> on success.</returns> public async Task <bool> InstallDebuggerAsync() { if (PiStatus.HasDebugger) { return(await Task.FromResult(true)); } LogInfo($"Installing VSDBG to: [{PackageHelper.RemoteDebuggerFolder}]"); return(await PackageHelper.ExecuteWithProgressAsync <bool>($"Installing [vsdbg] debugger...", async() => { var installScript = $@" if ! curl -sSL https://aka.ms/getvsdbgsh | /bin/sh /dev/stdin -v latest -l {PackageHelper.RemoteDebuggerFolder} ; then exit 1 fi exit 0 "; try { var response = SudoCommand(CommandBundle.FromScript(installScript)); if (response.ExitCode == 0) { // Indicate that debugger is now installed. PiStatus.HasDebugger = true; return await Task.FromResult(true); } else { LogError(response.AllText); return await Task.FromResult(false); } } catch (Exception e) { LogException(e); return await Task.FromResult(false); } })); }
//--------------------------------------------------------------------- // Static members /// <summary> /// Establishes a SSH connection to the remote Raspberry Pi whose /// connection information is passed. /// </summary> /// <param name="connectionInfo">The connection information.</param> /// <param name="usePassword">Optionally forces use of the password instead of the public key.</param> /// <param name="projectSettings"> /// Optionally specifies the project settings. This must be specified for connections that /// will be used for remote debugging but may be omitted for connections just used for setting /// things up like SSH keys, etc. /// </param> /// <returns>The connection.</returns> /// <exception cref="Exception">Thrown when the connection could not be established.</exception> public static async Task <Connection> ConnectAsync(ConnectionInfo connectionInfo, bool usePassword = false, ProjectSettings projectSettings = null) { Covenant.Requires <ArgumentNullException>(connectionInfo != null, nameof(connectionInfo)); try { if (!NetHelper.TryParseIPv4Address(connectionInfo.Host, out var address)) { Log($"DNS lookup for: {connectionInfo.Host}"); address = (await Dns.GetHostAddressesAsync(connectionInfo.Host)).FirstOrDefault(); } if (address == null) { throw new ConnectionException(connectionInfo.Host, "DNS lookup failed."); } SshCredentials credentials; if (string.IsNullOrEmpty(connectionInfo.PrivateKeyPath) || usePassword) { Log($"[{connectionInfo.Host}]: Auth via username/password"); credentials = SshCredentials.FromUserPassword(connectionInfo.User, connectionInfo.Password); } else { Log($"[{connectionInfo.Host}]: Auth via SSH keys"); credentials = SshCredentials.FromPrivateKey(connectionInfo.User, File.ReadAllText(connectionInfo.PrivateKeyPath)); } var connection = new Connection(connectionInfo.Host, address, connectionInfo, credentials, projectSettings); connection.Connect(TimeSpan.Zero); await connection.InitializeAsync(); return(connection); } catch (SshProxyException e) { if (usePassword || e.InnerException == null || e.InnerException.GetType() != typeof(SshAuthenticationException)) { RaspberryDebugger.Log.Exception(e, $"[{connectionInfo.Host}]"); throw; } if (string.IsNullOrEmpty(connectionInfo.PrivateKeyPath) || string.IsNullOrEmpty(connectionInfo.Password)) { RaspberryDebugger.Log.Exception(e, $"[{connectionInfo.Host}]: The connection must have a password or SSH private key."); throw; } RaspberryDebugger.Log.Warning($"[{connectionInfo.Host}]: SSH auth failed: Try using the password and reauthorizing the public key"); // SSH private key authentication didn't work. This commonly happens // after the user has reimaged the Raspberry. It's likely that the // user has setup the same username/password, so we'll try logging in // with those and just configure the current public key to be accepted // on the Raspberry. try { var connection = await ConnectAsync(connectionInfo, usePassword : true); // Append the public key to the user's [authorized_keys] file if it's // not already present. RaspberryDebugger.Log.Info($"[{connectionInfo.Host}]: Reauthorizing the public key"); var homeFolder = LinuxPath.Combine("/", "home", connectionInfo.User); var publicKey = File.ReadAllText(connectionInfo.PublicKeyPath).Trim(); var keyScript = $@" mkdir -p {homeFolder}/.ssh touch {homeFolder}/.ssh/authorized_keys if ! grep --quiet '{publicKey}' {homeFolder}/.ssh/authorized_keys ; then echo '{publicKey}' >> {homeFolder}/.ssh/authorized_keys exit $? fi exit 0 "; connection.ThrowOnError(connection.RunCommand(CommandBundle.FromScript(keyScript))); return(connection); } catch (Exception e2) { // We've done all we can. RaspberryDebugger.Log.Exception(e2, $"[{connectionInfo.Host}]"); throw; } } catch (Exception e) { RaspberryDebugger.Log.Exception(e, $"[{connectionInfo.Host}]"); throw; } }
/// <summary> /// Installs the specified .NET Core SDK on the Raspberry if it's not already installed. /// </summary> /// <param name="sdkVersion">The SDK version.</param> /// <returns><c>true</c> on success.</returns> public async Task <bool> InstallSdkAsync(string sdkVersion) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(sdkVersion), nameof(sdkVersion)); // $todo(jefflill): // // Note that we're going to install that standalone SDK for the SDK // version rather than the SDK that shipped with Visual Studio. I'm // assuming that the Visual Studio SDKs might have extra stuff we don't // need and it's also possible that the Visual Studio SDK for the SDK // version may not have shipped yet. // // We may want to re-evaluate this in the future. if (PiStatus.InstalledSdks.Any(sdk => sdk.Version == sdkVersion)) { return(await Task.FromResult(true)); // Already installed } LogInfo($".NET Core SDK [v{sdkVersion}] is not installed."); // Locate the standalone SDK for the request .NET version. var targetSdk = PackageHelper.SdkCatalog.Items.SingleOrDefault(item => item.IsStandalone && item.Version == sdkVersion && item.Architecture == SdkArchitecture.ARM32); if (targetSdk == null) { // Fall back to the Visual Studio SDK, if there is one. targetSdk = PackageHelper.SdkCatalog.Items.SingleOrDefault(item => item.Version == sdkVersion); LogInfo($"Cannot find standalone SDK for [{sdkVersion}] for falling back to [{targetSdk.Name}], version [v{targetSdk.Version}]."); } if (targetSdk == null) { LogError($"RasberryDebug is unaware of .NET Core SDK [v{sdkVersion}]."); LogError($"Try updating the RasberryDebug extension or report this issue at:"); LogError($"https://github.com/nforgeio/RaspberryDebugger/issues"); return(await Task.FromResult(false)); } // Install the SDK. LogInfo($"Installing SDK v{targetSdk.Version}"); return(await PackageHelper.ExecuteWithProgressAsync <bool>($"Download and install SDK v{targetSdk.Version} on Raspberry...", async() => { var installScript = $@" export DOTNET_ROOT={PackageHelper.RemoteDotnetFolder} # Ensure that the packages required by .NET Core are installed: # # https://docs.microsoft.com/en-us/dotnet/core/install/linux-debian#dependencies if ! apt-get update ; then exit 1 fi if ! apt-get install -yq libc6 libgcc1 libgssapi-krb5-2 libicu-dev libssl1.1 libstdc++6 zlib1g libgdiplus ; then exit 1 fi # Remove any existing SDK download. This might be present if a # previous installation attempt failed. if ! rm -f /tmp/dotnet-sdk.tar.gz ; then exit 1 fi # Download the SDK installation file to a temporary file. if ! wget --quiet -O /tmp/dotnet-sdk.tar.gz {targetSdk.Link} ; then exit 1 fi # Verify the SHA512. orgDir=$cwd cd /tmp if ! echo '{targetSdk.SHA512} dotnet-sdk.tar.gz' | sha512sum --check - ; then cd $orgDir exit 1 fi cd $orgDir # Make sure the installation directory exists. if ! mkdir -p $DOTNET_ROOT ; then exit 1 fi # Unpack the SDK to the installation directory. if ! tar -zxf /tmp/dotnet-sdk.tar.gz -C $DOTNET_ROOT --no-same-owner ; then exit 1 fi # Remove the temporary installation file. if ! rm /tmp/dotnet-sdk.tar.gz ; then exit 1 fi exit 0 "; try { var response = SudoCommand(CommandBundle.FromScript(installScript)); if (response.ExitCode == 0) { // Add the newly installed SDK to the list of installed SDKs. PiStatus.InstalledSdks.Add(new Sdk(targetSdk.Name, targetSdk.Version)); return await Task.FromResult(true); } else { LogError(response.AllText); return await Task.FromResult(false); } } catch (Exception e) { LogException(e); return await Task.FromResult(false); } })); }
/// <summary> /// Initializes the connection by retrieving status from the remote Raspberry and ensuring /// that any packages required for executing remote commands are installed. This will also /// create and configure a SSH key pair on both the workstation and remote Raspberry if one /// doesn't already exist so that subsequent connections can use key based authentication. /// </summary> /// <returns>The tracking <see cref="Task"/>.</returns> private async Task InitializeAsync() { await PackageHelper.ExecuteWithProgressAsync( $"Connecting to [{Name}]...", async() => { // Disabling this because it looks like SUDO passwork prompting is disabled // by default for Raspberry Pi OS. #if DISABLED // This call ensures that SUDO password prompting is disabled and the // the required hidden folders exist in the user's home directory. DisableSudoPrompt(password); #endif // We need to ensure that [unzip] is installed so that [LinuxSshProxy] command // bundles will work. Log($"[{Name}]: Checking for: [unzip]"); var response = SudoCommand("which unzip"); if (response.ExitCode != 0) { Log($"[{Name}]: Installing: [unzip]"); ThrowOnError(SudoCommand("sudo apt-get update")); ThrowOnError(SudoCommand("sudo apt-get install -yq unzip")); } // We're going to execute a script the gathers everything in a single operation for speed. Log($"[{Name}]: Retrieving status"); var statusScript = $@" # This script will return the status information via STDOUT line-by-line # in this order: # # Chip Architecture # PATH environment variable # Unzip Installed (""unzip"" or ""unzip-missing"") # Debugger Installed (""debugger-installed"" or ""debugger-missing"") # List of installed SDKs names (e.g. 3.1.108) separated by commas # Raspberry Model like: Raspberry Pi 4 Model B Rev 1.2 # Raspberry Revision like: c03112 # # This script also ensures that the [/lib/dotnet] directory exists, that # it has reasonable permissions, and that the folder exists on the system # PATH and that DOTNET_ROOT points to the folder. # Set the SDK and debugger installation paths. DOTNET_ROOT={PackageHelper.RemoteDotnetFolder} DEBUGFOLDER={PackageHelper.RemoteDebuggerFolder} # Get the chip architecture uname -m # Get the current PATH echo $PATH # Detect whether [unzip] is installed. if which unzip &> /dev/nul ; then echo 'unzip' else echo 'unzip-missing' fi # Detect whether the [vsdbg] debugger is installed. if [ -d $DEBUGFOLDER ] ; then echo 'debugger-installed' else echo 'debugger-missing' fi # List the SDK folders. These folder names are the same as the # corresponding SDK name. We'll list the files on one line # with the SDK names separated by commas. We'll return a blank # line if the SDK directory doesn't exist. if [ -d $DOTNET_ROOT/sdk ] ; then ls -m $DOTNET_ROOT/sdk else echo '' fi # Output the Raspberry board model. cat /proc/cpuinfo | grep '^Model\s' | grep -o 'Raspberry.*$' # Output the Raspberry board revision. cat /proc/cpuinfo | grep 'Revision\s' | grep -o '[0-9a-fA-F]*$' # Ensure that the [/lib/dotnet] folder exists, that it's on the # PATH and that DOTNET_ROOT are defined. mkdir -p /lib/dotnet chown root:root /lib/dotnet chmod 755 /lib/dotnet # Set these for the current session: export DOTNET_ROOT={PackageHelper.RemoteDotnetFolder} export PATH=$PATH:$DOTNET_ROOT # and for future sessions too: if ! grep --quiet DOTNET_ROOT /etc/profile ; then echo """" >> /etc/profile echo ""#------------------------------"" >> /etc/profile echo ""# Raspberry Debugger:"" >> /etc/profile echo ""export DOTNET_ROOT=$DOTNET_ROOT"" >> /etc/profile echo ""export PATH=$PATH"" >> /etc/profile echo ""#------------------------------"" >> /etc/profile fi "; Log($"[{Name}]: Fetching status"); response = ThrowOnError(SudoCommand(CommandBundle.FromScript(statusScript))); using (var reader = new StringReader(response.OutputText)) { var architecture = await reader.ReadLineAsync(); var path = await reader.ReadLineAsync(); var hasUnzip = await reader.ReadLineAsync() == "unzip"; var hasDebugger = await reader.ReadLineAsync() == "debugger-installed"; var sdkLine = await reader.ReadLineAsync(); var model = await reader.ReadLineAsync(); var revision = await reader.ReadToEndAsync(); revision = revision.Trim(); // Remove any whitespace at the end. Log($"[{Name}]: architecture: {architecture}"); Log($"[{Name}]: path: {path}"); Log($"[{Name}]: unzip: {hasUnzip}"); Log($"[{Name}]: debugger: {hasDebugger}"); Log($"[{Name}]: sdks: {sdkLine}"); Log($"[{Name}]: model: {model}"); Log($"[{Name}]: revision: {revision}"); // Convert the comma separated SDK names into a [PiSdk] list. var sdks = new List <Sdk>(); foreach (var sdkName in sdkLine.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(sdk => sdk.Trim())) { // $todo(jefflill): We're only supporting 32-bit SDKs at this time. var sdkCatalogItem = PackageHelper.SdkCatalog.Items.SingleOrDefault(item => item.Name == sdkName && item.Architecture == SdkArchitecture.ARM32); if (sdkCatalogItem != null) { sdks.Add(new Sdk(sdkName, sdkCatalogItem.Version)); } else { LogWarning($".NET SDK [{sdkName}] is present on [{Name}] but is not known to the RaspberryDebugger extension. Consider updating the extension."); } } PiStatus = new Status( architecture: architecture, path: path, hasUnzip: hasUnzip, hasDebugger: hasDebugger, installedSdks: sdks, model: model, revision: revision ); } }); // Create and configure an SSH key for this connection if one doesn't already exist. if (string.IsNullOrEmpty(connectionInfo.PrivateKeyPath) || !File.Exists(connectionInfo.PrivateKeyPath)) { await PackageHelper.ExecuteWithProgressAsync("Creating SSH keys...", async() => { // Create a 2048-bit private key with no passphrase on the Raspberry // and then download it to our keys folder. The key file name will // be the host name of the Raspberry. LogInfo("Creating SSH keys"); var workstationUser = Environment.GetEnvironmentVariable("USERNAME"); var workstationName = Environment.GetEnvironmentVariable("COMPUTERNAME"); var keyName = Guid.NewGuid().ToString("d"); var homeFolder = LinuxPath.Combine("/", "home", connectionInfo.User); var tempPrivateKeyPath = LinuxPath.Combine(homeFolder, keyName); var tempPublicKeyPath = LinuxPath.Combine(homeFolder, $"{keyName}.pub"); try { var createKeyScript = $@" # Create the key pair if ! ssh-keygen -t rsa -b 2048 -P '' -C '{workstationUser}@{workstationName}' -f {tempPrivateKeyPath} -m pem ; then exit 1 fi # Append the public key to the user's [authorized_keys] file to enable it. mkdir -p {homeFolder}/.ssh touch {homeFolder}/.ssh/authorized_keys cat {tempPublicKeyPath} >> {homeFolder}/.ssh/authorized_keys exit 0 "; ThrowOnError(RunCommand(CommandBundle.FromScript(createKeyScript))); // Download the public and private keys, persist them to the workstation // and then update the connection info. var connections = PackageHelper.ReadConnections(); var existingConnectionInfo = connections.SingleOrDefault(c => c.Name == connectionInfo.Name); var publicKeyPath = Path.Combine(PackageHelper.KeysFolder, $"{connectionInfo.Name}.pub"); var privateKeyPath = Path.Combine(PackageHelper.KeysFolder, connectionInfo.Name); File.WriteAllBytes(publicKeyPath, DownloadBytes(tempPublicKeyPath)); File.WriteAllBytes(privateKeyPath, DownloadBytes(tempPrivateKeyPath)); connectionInfo.PrivateKeyPath = privateKeyPath; connectionInfo.PublicKeyPath = publicKeyPath; if (existingConnectionInfo != null) { existingConnectionInfo.PrivateKeyPath = privateKeyPath; existingConnectionInfo.PublicKeyPath = publicKeyPath; PackageHelper.WriteConnections(connections, disableLogging: true); } } finally { // Delete the temporary key files on the Raspberry. var removeKeyScript = $@" rm -f {tempPrivateKeyPath} rm -f {tempPublicKeyPath} "; ThrowOnError(SudoCommand(CommandBundle.FromScript(removeKeyScript))); } await Task.CompletedTask; }); } }
/// <summary> /// This function is the callback used to execute the command when the menu item is clicked. /// See the constructor to see how the menu item is associated with this function using /// OleMenuCommandService service and MenuCommand class. /// </summary> /// <param name="sender">Event sender.</param> /// <param name="e">Event args.</param> #pragma warning disable VSTHRD100 private async void Execute(object sender, EventArgs e) #pragma warning restore VSTHRD100 { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); if (!await DebugHelper.EnsureOpenSshAsync()) { return; } var project = DebugHelper.GetTargetProject(dte); if (project == null) { return; } var projectProperties = ProjectProperties.CopyFrom(dte.Solution, project); if (!await DebugHelper.PublishProjectWithUIAsync(dte, dte.Solution, project, projectProperties)) { return; } var connectionInfo = DebugHelper.GetDebugConnectionInfo(projectProperties); if (connectionInfo == null) { return; } // Identify the most recent SDK installed on the workstation that has the same // major and minor version numbers as the project. We'll ensure that the same // SDK is installed on the Raspberry (further below). var targetSdk = DebugHelper.GetTargetSdk(projectProperties); if (targetSdk == null) { return; } // Establish a Raspberry connection to handle some things before we start the debugger. var connection = await DebugHelper.InitializeConnectionAsync(connectionInfo, targetSdk, projectProperties, PackageHelper.GetProjectSettings(dte.Solution, project)); if (connection == null) { return; } using (connection) { // Generate a temporary [launch.json] file and launch the debugger. using (var tempFile = await CreateLaunchSettingsAsync(connectionInfo, projectProperties)) { dte.ExecuteCommand("DebugAdapterHost.Launch", $"/LaunchJson:\"{tempFile.Path}\""); } // Launch the browser for ASPNET apps if requested. Note that we're going to do this // on a background task to poll the Raspberry, waiting for the app to create the create // the LISTENING socket. if (projectProperties.IsAspNet && projectProperties.AspLaunchBrowser) { var baseUri = $"http://{connectionInfo.Host}:{projectProperties.AspPort}"; var launchReady = false; await NeonHelper.WaitForAsync( async() => { if (dte.Mode != vsIDEMode.vsIDEModeDebug) { // The developer must have stopped debugging before the ASPNET // application was able to begin servicing requests. return(true); } try { var appListeningScript = $@" if lsof -i -P -n | grep --quiet 'TCP \*:{projectProperties.AspPort} (LISTEN)' ; then exit 0 else exit 1 fi "; var response = connection.SudoCommand(CommandBundle.FromScript(appListeningScript)); if (response.ExitCode != 0) { return(false); } // Wait just a bit longer to give the application a chance to // perform any additional initialization. await Task.Delay(TimeSpan.FromSeconds(1)); launchReady = true; return(true); } catch { return(false); } }, timeout : TimeSpan.FromSeconds(30), pollInterval : TimeSpan.FromSeconds(0.5)); if (launchReady) { NeonHelper.OpenBrowser($"{baseUri}{projectProperties.AspRelativeBrowserUri}"); } } } }
/// <inheritdoc/> public override void Run(CommandLine commandLine) { if (commandLine.Arguments.Count() == 0) { Help(); Program.Exit(0); } var hyperv = commandLine.HasOption("--hyperv"); var xenserver = commandLine.HasOption("--xenserver"); if (!hyperv && !xenserver) { Console.Error.WriteLine("**** ERROR: One of [--hyperv] or [--xenserver] must be specified."); Program.Exit(1); } else if (hyperv && xenserver) { Console.Error.WriteLine("**** ERROR: Only one of [--hyperv] or [--xenserver] can be specified."); Program.Exit(1); } var address = commandLine.Arguments.ElementAtOrDefault(0); if (string.IsNullOrEmpty(address)) { Console.Error.WriteLine("**** ERROR: ADDRESS argument is required."); Program.Exit(1); } if (!IPAddress.TryParse(address, out var ipAddress)) { Console.Error.WriteLine($"**** ERROR: [{address}] is not a valid IP address."); Program.Exit(1); } Program.MachineUsername = Program.CommandLine.GetOption("--machine-username", "sysadmin"); Program.MachinePassword = Program.CommandLine.GetOption("--machine-password", "sysadmin0000"); Covenant.Assert(Program.MachineUsername == KubeConst.SysAdminUser); Console.WriteLine(); Console.WriteLine("** Prepare VM Template ***"); Console.WriteLine(); using (var server = Program.CreateNodeProxy <string>("vm-template", address, ipAddress, appendToLog: false)) { // Disable sudo password prompts. Console.WriteLine("Disable [sudo] password"); server.DisableSudoPrompt(Program.MachinePassword); } using (var server = Program.CreateNodeProxy <string>("vm-template", address, ipAddress, appendToLog: false)) { Console.WriteLine($"Connect as [{KubeConst.SysAdminUser}]"); server.WaitForBoot(); // Install required packages: Console.WriteLine("Install packages"); server.SudoCommand("apt-get update", RunOptions.FaultOnError); server.SudoCommand("apt-get install -yq --allow-downgrades zip secure-delete", RunOptions.FaultOnError); // Disable SWAP by editing [/etc/fstab] to remove the [/swap.img] line: var sbFsTab = new StringBuilder(); using (var reader = new StringReader(server.DownloadText("/etc/fstab"))) { foreach (var line in reader.Lines()) { if (!line.Contains("/swap.img")) { sbFsTab.AppendLine(line); } } } Console.WriteLine("Disable SWAP"); server.UploadText("/etc/fstab", sbFsTab, permissions: "644", owner: "root:root"); // We need to relocate the [sysadmin] UID/GID to 1234 so we // can create the [container] user and group at 1000. We'll // need to create a temporary user with root permissions to // delete and then recreate the [sysadmin] account. Console.WriteLine("Create [temp] user"); var tempUserScript = $@"#!/bin/bash # Create the [temp] user. useradd --uid 5000 --create-home --groups root temp echo 'temp:{Program.MachinePassword}' | chpasswd adduser temp sudo chown temp:temp /home/temp "; server.SudoCommand(CommandBundle.FromScript(tempUserScript), RunOptions.FaultOnError); } // We need to reconnect with the new temporary account so // we can relocate the [sysadmin] user to its new UID. Program.MachineUsername = "******"; using (var server = Program.CreateNodeProxy <string>("vm-template", address, ipAddress, appendToLog: false)) { Console.WriteLine($"Connecting as [temp]"); server.WaitForBoot(createHomeFolders: true); var sysadminUserScript = $@"#!/bin/bash # Update all file references from the old to new [sysadmin] # user and group IDs: find / -group 1000 -exec chgrp -h {KubeConst.SysAdminGroup} {{}} \; find / -user 1000 -exec chown -h {KubeConst.SysAdminUser} {{}} \; # Relocate the user ID: groupmod --gid {KubeConst.SysAdminGID} {KubeConst.SysAdminGroup} usermod --uid {KubeConst.SysAdminUID} --gid {KubeConst.SysAdminGID} --groups root,sysadmin,sudo {KubeConst.SysAdminUser} "; Console.WriteLine("Relocate [sysadmin] user"); server.SudoCommand(CommandBundle.FromScript(sysadminUserScript), RunOptions.FaultOnError); } // We need to reconnect again with [sysadmin] so we can remove // the [temp] user and create the [container] user and then // wrap things up. Program.MachineUsername = KubeConst.SysAdminUser; using (var server = Program.CreateNodeProxy <string>("vm-template", address, ipAddress, appendToLog: false)) { Console.WriteLine($"Connect as [{KubeConst.SysAdminUser}]"); server.WaitForBoot(); // Ensure that the owner and group for files in the [sysadmin] // home folder are correct. Console.WriteLine("Set [sysadmin] home folder owner"); server.SudoCommand($"chown -R {KubeConst.SysAdminUser}:{KubeConst.SysAdminGroup} .*", RunOptions.FaultOnError); // Remove the [temp] user. Console.WriteLine("Remove [temp] user"); server.SudoCommand($"userdel temp", RunOptions.FaultOnError); server.SudoCommand($"rm -rf /home/temp", RunOptions.FaultOnError); // Create the [container] user with no home directory. This // means that the [container] user will have no chance of // logging into the machine. Console.WriteLine("Create [container] user", RunOptions.FaultOnError); server.SudoCommand($"useradd --uid {KubeConst.ContainerUID} --no-create-home {KubeConst.ContainerUser}", RunOptions.FaultOnError); // Configure the Linux guest integration services. var guestServicesScript = @"#!/bin/bash cat <<EOF >> /etc/initramfs-tools/modules hv_vmbus hv_storvsc hv_blkvsc hv_netvsc EOF apt-get install -yq --allow-downgrades linux-virtual linux-cloud-tools-virtual linux-tools-virtual update-initramfs -u "; Console.WriteLine("Install guest integration services"); server.SudoCommand(CommandBundle.FromScript(guestServicesScript), RunOptions.FaultOnError); if (hyperv) { // Clean cached packages, DHCP leases, and then zero the disk so // the image will compress better. var cleanScript = @"#!/bin/bash apt-get clean rm -rf /var/lib/dhcp/* sfill -fllz / "; Console.WriteLine("Clean up"); server.SudoCommand(CommandBundle.FromScript(cleanScript), RunOptions.FaultOnError); // Shut the the VM down so the user can compress and upload // the disk image. Console.WriteLine("Shut down"); server.Shutdown(); Console.WriteLine(); Console.WriteLine("*** Node template is ready ***"); } else if (xenserver) { // NOTE: We need to to install the XenCenter tools manually. Console.WriteLine(); Console.WriteLine("*** IMPORTANT: You need to manually complete the remaining steps ***"); } } Program.Exit(0); }