Beispiel #1
0
        public void ToBash_ArgWhitespace()
        {
            var bundle = new CommandBundle("test", "arg 1");

            var bash = bundle.ToBash();

            Assert.Equal(
                @"test \
    ""arg 1""
", bash);
        }
Beispiel #2
0
        /// <summary>
        /// Executes a <b>docker config create</b> command.
        /// </summary>
        /// <param name="node">The target node.</param>
        /// <param name="rightCommandLine">The right split of the command line.</param>
        private void ConfigCreate(SshProxy <NodeDefinition> node, CommandLine rightCommandLine)
        {
            // We're expecting a command like:
            //
            //      docker config create [OPTIONS] CONFIG file|-
            //
            // where CONFIG is the name of the configuration and and [file]
            // is the path to the config file or [-] indicates that
            // the config is streaming in on stdin.
            //
            // We're going to run this as a command bundle that includes
            // the config file.

            if (rightCommandLine.Arguments.Length != 4)
            {
                Console.Error.WriteLine("*** ERROR: Expected: docker config create [OPTIONS] CONFIG file|-");
                Program.Exit(0);
            }

            string fileArg = rightCommandLine.Arguments[3];

            byte[] configData;

            if (fileArg == "-")
            {
                configData = NeonHelper.ReadStandardInputBytes();
            }
            else
            {
                configData = File.ReadAllBytes(fileArg);
            }

            // Create and execute a command bundle.  Note that we're going to hardcode
            // the config data path to [config.data].

            rightCommandLine.Items[rightCommandLine.Items.Length - 1] = "config.data";

            var bundle = new CommandBundle("docker", rightCommandLine.Items);

            bundle.AddFile("config.data", configData);

            var response = node.SudoCommand(bundle, RunOptions.None);

            Console.Write(response.AllText);
            Program.Exit(response.ExitCode);
        }
        /// <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);
                }
            }));
        }
Beispiel #4
0
        /// <summary>
        /// Updates Consul on a specific node.
        /// </summary>
        /// <param name="node">The target node.</param>
        /// <param name="stepDelay">The step delay.</param>
        private void UpdateConsul(SshProxy <NodeDefinition> node, TimeSpan stepDelay)
        {
            if (node.GetConsulVersion() >= (SemanticVersion)version)
            {
                return;     // Already updated
            }

            node.Status = $"stop: consul";
            node.SudoCommand("systemctl stop consul");

            node.Status = $"update: consul";

            var bundle = new CommandBundle("./install.sh", version);

            bundle.AddFile("install.sh",
                           $@"#!/bin/bash

set -euo pipefail

curl {Program.CurlOptions} https://releases.hashicorp.com/consul/$1/consul_$1_linux_amd64.zip -o /tmp/consul.zip 1>&2
unzip -u /tmp/consul.zip -d /tmp
cp /tmp/consul /usr/local/bin
chmod 770 /usr/local/bin/consul

rm /tmp/consul.zip
rm /tmp/consul 
",
                           isExecutable: true);

            node.SudoCommand(bundle);

            node.Status = $"restart: consul";
            node.SudoCommand("systemctl restart consul");

            if (node.Metadata.IsManager)
            {
                node.Status = $"stabilizing ({Program.WaitSeconds}s)";
                Thread.Sleep(TimeSpan.FromSeconds(Program.WaitSeconds));
            }
        }
Beispiel #5
0
        /// <summary>
        /// Updates Vault on a specific node.
        /// </summary>
        /// <param name="node">The target node.</param>
        /// <param name="stepDelay">The step delay.</param>
        private void UpdateVault(SshProxy <NodeDefinition> node, TimeSpan stepDelay)
        {
            if (node.GetVaultVersion() >= (SemanticVersion)version)
            {
                return;     // Already updated
            }

            node.Status = $"update: vault";

            var bundle = new CommandBundle("./install.sh", version);

            bundle.AddFile("install.sh",
                           $@"#!/bin/bash

set -euo pipefail

curl {Program.CurlOptions} https://releases.hashicorp.com/vault/$1/vault_$1_linux_amd64.zip -o /tmp/vault.zip 1>&2
unzip -o /tmp/vault.zip -d /tmp
rm /tmp/vault.zip

mv /tmp/vault /usr/local/bin/vault
chmod 700 /usr/local/bin/vault
",
                           isExecutable: true);

            node.SudoCommand(bundle);

            if (node.Metadata.IsManager)
            {
                node.Status = $"restart: vault";
                node.SudoCommand("systemctl restart vault");

                node.Status = $"unseal: vault";
                hive.Vault.Unseal();

                node.Status = $"stabilizing ({Program.WaitSeconds}s)";
                Thread.Sleep(TimeSpan.FromSeconds(Program.WaitSeconds));
            }
        }
        //---------------------------------------------------------------------
        // 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;
                });
            }
        }
Beispiel #9
0
        /// <summary>
        /// Executes a <b>docker deploy</b> or <b>docker stack deploy</b> command.
        /// </summary>
        /// <param name="node">The target node.</param>
        /// <param name="rightCommandLine">The right split of the command line.</param>
        private void Deploy(SshProxy <NodeDefinition> node, CommandLine rightCommandLine)
        {
            string path = null;

            // We're going to upload the file specified by the first
            // [--bundle-file], [--compose-file], or [-c] option.

            for (int i = 0; i < rightCommandLine.Items.Length; i++)
            {
                switch (rightCommandLine.Items[i])
                {
                case "--bundle-file":
                case "--compose-file":
                case "-c":

                    path = rightCommandLine.Items.Skip(i + 1).FirstOrDefault();
                    break;
                }

                if (path != null)
                {
                    // Convert the command line argument to a bundle relative path.

                    rightCommandLine.Items[i + 1] = Path.GetFileName(rightCommandLine.Items[i + 1]);
                    break;
                }
            }

            if (path == null)
            {
                // If that didn't work, try looking for arguments like:
                //
                //      --bundle-file=PATH

                var patterns =
                    new string[]
                {
                    "--bundle-file=",
                    "--compose-file=",
                    "-c="
                };

                for (int i = 0; i < rightCommandLine.Items.Length; i++)
                {
                    var item = rightCommandLine.Items[i];

                    foreach (var pattern in patterns)
                    {
                        if (item.StartsWith(pattern))
                        {
                            path = item.Substring(pattern.Length);

                            // Convert the command line argument to a bundle relative path.

                            rightCommandLine.Items[i] = pattern + Path.GetFileName(path);
                            break;
                        }
                    }

                    if (path != null)
                    {
                        break;
                    }
                }
            }

            if (path == null)
            {
                Console.Error.WriteLine("*** ERROR: No DAB or compose file specified.");
                Program.Exit(0);
            }

            var bundle = new CommandBundle("docker", rightCommandLine.Items);

            bundle.AddFile(Path.GetFileName(path), File.ReadAllText(path));

            var response = node.SudoCommand(bundle);

            Console.Write(response.AllText);
            Program.Exit(response.ExitCode);
        }
Beispiel #10
0
        /// <inheritdoc/>
        public override void Run(CommandLine commandLine)
        {
            var split            = commandLine.Split(SplitItem);
            var leftCommandLine  = split.Left;
            var rightCommandLine = split.Right;

            // Basic initialization.

            if (leftCommandLine.HasHelpOption)
            {
                Console.WriteLine(usage);
                Program.Exit(0);
            }

            // Initialize the hive.

            var hiveLogin = Program.ConnectHive();

            hive = new HiveProxy(hiveLogin);

            // Determine which node we're going to target.

            var node     = (SshProxy <NodeDefinition>)null;
            var nodeName = leftCommandLine.GetOption("--node", null);

            if (!string.IsNullOrEmpty(nodeName))
            {
                node = hive.GetNode(nodeName);
            }
            else
            {
                node = hive.GetReachableManager();
            }

            if (rightCommandLine == null)
            {
                Console.Error.WriteLine("*** ERROR: The [consul] command requires the \"--\" argument.");
                Program.Exit(1);
            }

            var command = rightCommandLine.Arguments.FirstOrDefault();

            switch (command)
            {
            case "watch":

                Console.Error.WriteLine("*** ERROR: [neon consul watch] is not supported.");
                Program.Exit(1);
                break;

            case "monitor":

                // We'll just relay the output we receive from the remote command
                // until the user kills this process.

                using (var shell = node.CreateSudoShell())
                {
                    shell.WriteLine($"sudo {remoteConsulPath} {rightCommandLine}");

                    while (true)
                    {
                        var line = shell.ReadLine();

                        if (line == null)
                        {
                            break;     // Just being defensive
                        }

                        Console.WriteLine(line);
                    }
                }
                break;

            default:

                if (rightCommandLine.Items.LastOrDefault() == "-")
                {
                    // This is the special case where we need to pipe the standard input sent
                    // to this command on to Consul on the remote machine.  We're going to use
                    // a CommandBundle by uploading the standard input data as a file.

                    var bundle = new CommandBundle($"cat stdin.dat | {remoteConsulPath} {rightCommandLine}");

                    bundle.AddFile("stdin.dat", NeonHelper.ReadStandardInputBytes());

                    var response = node.SudoCommand(bundle, RunOptions.IgnoreRemotePath);

                    Console.WriteLine(response.AllText);
                    Program.Exit(response.ExitCode);
                }
                else if (rightCommandLine.StartsWithArgs("kv", "put") && rightCommandLine.Arguments.Length == 4 && rightCommandLine.Arguments[3].StartsWith("@"))
                {
                    // We're going to special case PUT when saving a file
                    // whose name is prefixed with "@".

                    var filePath = rightCommandLine.Arguments[3].Substring(1);
                    var bundle   = new CommandBundle($"{remoteConsulPath} {rightCommandLine}");

                    bundle.AddFile(filePath, File.ReadAllBytes(filePath));

                    var response = node.SudoCommand(bundle, RunOptions.IgnoreRemotePath);

                    Console.Write(response.AllText);
                    Program.Exit(response.ExitCode);
                }
                else
                {
                    // All we need to do is to execute the command remotely.  We're going to special case
                    // the [consul kv get ...] command to process the result as binary.

                    CommandResponse response;

                    if (rightCommandLine.ToString().StartsWith("kv get"))
                    {
                        response = node.SudoCommand($"{remoteConsulPath} {rightCommandLine}", RunOptions.IgnoreRemotePath | RunOptions.BinaryOutput);

                        using (var remoteStandardOutput = response.OpenOutputBinaryStream())
                        {
                            if (response.ExitCode != 0)
                            {
                                // Looks like Consul writes its errors to standard output, so
                                // I'm going to open a text reader and write those lines
                                // to standard error.

                                using (var reader = new StreamReader(remoteStandardOutput))
                                {
                                    foreach (var line in reader.Lines())
                                    {
                                        Console.Error.WriteLine(line);
                                    }
                                }
                            }
                            else
                            {
                                // Write the remote binary output to standard output.

                                using (var output = Console.OpenStandardOutput())
                                {
                                    var buffer = new byte[8192];
                                    int cb;

                                    while (true)
                                    {
                                        cb = remoteStandardOutput.Read(buffer, 0, buffer.Length);

                                        if (cb == 0)
                                        {
                                            break;
                                        }

                                        output.Write(buffer, 0, cb);
                                    }
                                }
                            }
                        }
                    }
                    else
                    {
                        response = node.SudoCommand($"{remoteConsulPath} {rightCommandLine}", RunOptions.IgnoreRemotePath);

                        Console.WriteLine(response.AllText);
                    }

                    Program.Exit(response.ExitCode);
                }
                break;
            }
        }
Beispiel #11
0
        /// <inheritdoc/>
        public override void Run(CommandLine commandLine)
        {
            var split = commandLine.Split("--");

            var leftCommandLine  = split.Left;
            var rightCommandLine = split.Right;

            // Basic initialization.

            if (leftCommandLine.HasHelpOption)
            {
                Console.WriteLine(usage);
                Program.Exit(0);
            }

            Program.ConnectHive();

            var hive = HiveHelper.Hive;

            // Process the nodes.

            var nodeDefinitions = new List <NodeDefinition>();
            var nodeOption      = leftCommandLine.GetOption("--node", null);

            if (!string.IsNullOrWhiteSpace(nodeOption))
            {
                if (nodeOption == "+")
                {
                    foreach (var manager in hive.Definition.SortedManagers)
                    {
                        nodeDefinitions.Add(manager);
                    }

                    foreach (var worker in hive.Definition.SortedWorkers)
                    {
                        nodeDefinitions.Add(worker);
                    }

                    foreach (var pet in hive.Definition.SortedPets)
                    {
                        nodeDefinitions.Add(pet);
                    }
                }
                else
                {
                    foreach (var name in nodeOption.Split(',', StringSplitOptions.RemoveEmptyEntries))
                    {
                        var trimmedName = name.Trim();

                        NodeDefinition node;

                        if (!hive.Definition.NodeDefinitions.TryGetValue(trimmedName, out node))
                        {
                            Console.Error.WriteLine($"*** ERROR: Node [{trimmedName}] is not present in the hive.");
                            Program.Exit(1);
                        }

                        nodeDefinitions.Add(node);
                    }
                }
            }

            var groupName = leftCommandLine.GetOption("--group");

            if (!string.IsNullOrEmpty(groupName))
            {
                var nodeGroups = hive.Definition.GetHostGroups();

                if (!nodeGroups.TryGetValue(groupName, out var group))
                {
                    Console.Error.WriteLine($"*** ERROR: Node group [{groupName}] is not defined for the hive.");
                    Program.Exit(1);
                }

                // Add the group nodes to the node definitions if they aren't
                // already present.

                foreach (var node in group)
                {
                    if (nodeDefinitions.Count(n => n.Name.Equals(node.Name, StringComparison.InvariantCultureIgnoreCase)) == 0)
                    {
                        nodeDefinitions.Add(node);
                    }
                }
            }

            if (nodeDefinitions.Count == 0)
            {
                // Default to a healthy manager.

                nodeDefinitions.Add(hive.GetReachableManager().Metadata);
            }

            // Create the command bundle by appending the right command.

            if (rightCommandLine == null)
            {
                Console.Error.WriteLine($"*** ERROR: [exec] command expectes: [-- COMMAND...]");
                Program.Exit(1);
            }

            string command = rightCommandLine.Items.First();
            var    args    = rightCommandLine.Items.Skip(1).ToArray();

            var bundle = new CommandBundle(command, args.ToArray());

            // Append any script, text, or data files to the bundle.

            foreach (var scriptPath in leftCommandLine.GetOptionValues("--script"))
            {
                if (!File.Exists(scriptPath))
                {
                    Console.Error.WriteLine($"*** ERROR: Script [{scriptPath}] does not exist.");
                    Program.Exit(1);
                }

                bundle.AddFile(Path.GetFileName(scriptPath), File.ReadAllText(scriptPath), isExecutable: true);
            }

            foreach (var textPath in leftCommandLine.GetOptionValues("--text"))
            {
                if (!File.Exists(textPath))
                {
                    Console.Error.WriteLine($"*** ERROR: Text file [{textPath}] does not exist.");
                    Program.Exit(1);
                }

                bundle.AddFile(Path.GetFileName(textPath), File.ReadAllText(textPath));
            }

            foreach (var dataPath in leftCommandLine.GetOptionValues("--data"))
            {
                if (!File.Exists(dataPath))
                {
                    Console.Error.WriteLine($"*** ERROR: Data file [{dataPath}] does not exist.");
                    Program.Exit(1);
                }

                bundle.AddFile(Path.GetFileName(dataPath), File.ReadAllBytes(dataPath));
            }

            // Perform the operation.

            if (nodeDefinitions.Count == 1)
            {
                // Run the command on a single node and return the output and exit code.

                var node     = hive.GetNode(nodeDefinitions.First().Name);
                var response = node.SudoCommand(bundle);

                Console.WriteLine(response.OutputText);

                Program.Exit(response.ExitCode);
            }
            else
            {
                // Run the command on multiple nodes and return an overall exit code.

                var controller = new SetupController <NodeDefinition>(Program.SafeCommandLine, hive.Nodes.Where(n => nodeDefinitions.Exists(nd => nd.Name == n.Name)))
                {
                    ShowStatus  = !Program.Quiet,
                    MaxParallel = Program.MaxParallel
                };

                controller.SetDefaultRunOptions(RunOptions.FaultOnError);

                controller.AddWaitUntilOnlineStep();
                controller.AddStep($"run: {bundle.Command}",
                                   (node, stepDelay) =>
                {
                    Thread.Sleep(stepDelay);

                    node.Status = "running";
                    node.SudoCommand(bundle, RunOptions.FaultOnError | RunOptions.LogOutput);

                    if (Program.WaitSeconds > 0)
                    {
                        node.Status = $"stabilize ({Program.WaitSeconds}s)";
                        Thread.Sleep(TimeSpan.FromSeconds(Program.WaitSeconds));
                    }
                });

                if (!controller.Run())
                {
                    Console.Error.WriteLine("*** ERROR: [exec] on one or more nodes failed.");
                    Program.Exit(1);
                }
            }
        }
Beispiel #12
0
        /// <inheritdoc/>
        public override void Run(CommandLine commandLine)
        {
            // Split the command line on "--".

            var split = commandLine.Split(SplitItem);

            var leftCommandLine  = split.Left;
            var rightCommandLine = split.Right;

            // Basic initialization.

            if (leftCommandLine.HasHelpOption)
            {
                Console.WriteLine(usage);
                Program.Exit(0);
            }

            // Initialize the hive.

            var hiveLogin = Program.ConnectHive();

            hive             = new HiveProxy(hiveLogin);
            vaultCredentials = hiveLogin.VaultCredentials;

            if (rightCommandLine == null)
            {
                Console.WriteLine("*** ERROR: The [--] command separator is required.");
                Console.WriteLine();
                Console.WriteLine(usage);
                Program.Exit(1);
            }

            // Determine which node we're going to target.

            SshProxy <NodeDefinition> node;
            string          nodeName = leftCommandLine.GetOption("--node", null);
            CommandBundle   bundle;
            CommandResponse response;
            bool            failed = false;

            if (!string.IsNullOrEmpty(nodeName))
            {
                node = hive.GetNode(nodeName);
            }
            else
            {
                node = hive.GetReachableManager();
            }

            var command = rightCommandLine.Arguments.FirstOrDefault();;

            switch (command)
            {
            case "init":
            case "rekey":
            case "server":
            case "ssh":

                Console.Error.WriteLine($"*** ERROR: [neon vault {command}] is not supported.");
                Program.Exit(1);
                break;

            case "seal":

                // Just run the command on the target node if one was specified.

                if (nodeName != null)
                {
                    ExecuteOnNode(node, rightCommandLine);
                    return;
                }

                // We need to seal the Vault instance on every manager node unless a
                // specific node was requsted via [--node].
                //
                // Note also that it's not possible to seal a node that's in standby
                // mode so we'll restart the Vault container instead.

                Console.WriteLine();

                if (!string.IsNullOrEmpty(nodeName))
                {
                    response = node.SudoCommand($"vault-direct status");

                    if (response.ExitCode != 0)
                    {
                        Console.WriteLine($"[{node.Name}] is already sealed");
                    }
                    else
                    {
                        var vaultStatus = new VaultStatus(response.OutputText);

                        if (vaultStatus.HAMode == "standby")
                        {
                            Console.WriteLine($"[{node.Name}] restarting to seal standby node...");

                            response = node.SudoCommand($"systemctl restart vault");

                            if (response.ExitCode == 0)
                            {
                                Console.WriteLine($"[{node.Name}] sealed");
                            }
                            else
                            {
                                Console.WriteLine($"[{node.Name}] restart failed");
                            }
                        }
                        else
                        {
                            Console.WriteLine($"[{node.Name}] sealing...");

                            response = node.SudoCommand($"export VAULT_TOKEN={vaultCredentials.RootToken} && vault-direct operator seal", RunOptions.Redact);

                            if (response.ExitCode == 0)
                            {
                                Console.WriteLine($"[{node.Name}] sealed");
                            }
                            else
                            {
                                Console.WriteLine($"[{node.Name}] seal failed");
                            }
                        }
                    }

                    failed = response.ExitCode != 0;
                }
                else
                {
                    foreach (var manager in hive.Managers)
                    {
                        try
                        {
                            response = manager.SudoCommand($"vault-direct status");
                        }
                        catch (SshOperationTimeoutException)
                        {
                            Console.WriteLine($"[{manager.Name}] ** unavailable **");
                            continue;
                        }

                        var vaultStatus = new VaultStatus(response.OutputText);

                        if (response.ExitCode != 0)
                        {
                            Console.WriteLine($"[{manager.Name}] is already sealed");
                        }
                        else
                        {
                            response = manager.SudoCommand($"vault-direct operator seal");

                            if (vaultStatus.HAMode == "standby")
                            {
                                Console.WriteLine($"[{manager.Name}] restarting to seal standby node...");

                                response = manager.SudoCommand($"systemctl restart vault");

                                if (response.ExitCode == 0)
                                {
                                    Console.WriteLine($"[{manager.Name}] restart/seal [standby]");
                                }
                                else
                                {
                                    Console.WriteLine($"[{manager.Name}] restart/seal failed [standby]");
                                }
                            }
                            else
                            {
                                Console.WriteLine($"[{manager.Name}] sealing...");

                                response = manager.SudoCommand($"export VAULT_TOKEN={vaultCredentials.RootToken} && vault-direct operator seal", RunOptions.Redact);

                                if (response.ExitCode == 0)
                                {
                                    Console.WriteLine($"[{manager.Name}] sealed");
                                }
                                else
                                {
                                    failed = true;
                                    Console.WriteLine($"[{manager.Name}] seal failed");
                                }
                            }
                        }
                    }

                    if (!failed)
                    {
                        // Disable auto unseal until the operator explicitly unseals Vault again.

                        hive.Consul.Client.KV.PutBool($"{HiveConst.GlobalKey}/{HiveGlobals.UserDisableAutoUnseal}", true).Wait();
                    }

                    Program.Exit(failed ? 1 : 0);
                }
                break;

            case "status":

                // Just run the command on the target node if one was specified.

                if (nodeName != null)
                {
                    ExecuteOnNode(node, rightCommandLine);
                    return;
                }

                // We need to obtain the status from the Vault instance on every manager node unless a
                // specific node was requsted via [--node].

                Console.WriteLine();

                if (!string.IsNullOrEmpty(nodeName))
                {
                    response = node.SudoCommand("vault-direct status");

                    Console.WriteLine(response.AllText);
                    Program.Exit(response.ExitCode);
                }
                else
                {
                    var allSealed = true;

                    foreach (var manager in hive.Managers)
                    {
                        try
                        {
                            response = manager.SudoCommand("vault-direct status");
                        }
                        catch (SshOperationTimeoutException)
                        {
                            Console.WriteLine($"[{manager.Name}] ** unavailable **");
                            continue;
                        }

                        var vaultStatus = new VaultStatus(response.OutputText);

                        if (response.ExitCode == 0)
                        {
                            allSealed = false;

                            var status = vaultStatus.HAMode;

                            if (status == "active")
                            {
                                status += "  <-- LEADER";
                            }

                            Console.WriteLine($"[{manager.Name}] unsealed {status}");
                        }
                        else if (response.ExitCode == 2)
                        {
                            Console.WriteLine($"[{manager.Name}] sealed");
                        }
                        else
                        {
                            failed = true;
                            Console.WriteLine($"[{manager.Name}] error getting status");
                        }
                    }

                    if (allSealed)
                    {
                        Program.Exit(2);
                    }
                    else
                    {
                        Program.Exit(failed ? 1 : 0);
                    }
                }
                break;

            case "unseal":

                // Just run the command on the target node if one was specified.

                if (nodeName != null)
                {
                    ExecuteOnNode(node, rightCommandLine);
                    return;
                }

                // We need to unseal the Vault instance on every manager node unless a
                // specific node was requsted via [--node].

                Console.WriteLine();

                if (!string.IsNullOrEmpty(nodeName))
                {
                    // Verify that the instance isn't already unsealed.

                    response = node.SudoCommand($"vault-direct status");

                    if (response.ExitCode == 2)
                    {
                        Console.WriteLine($"[{node.Name}] unsealing...");
                    }
                    else if (response.ExitCode == 0)
                    {
                        Console.WriteLine($"[{node.Name}] is already unsealed");
                        break;
                    }
                    else
                    {
                        Console.WriteLine($"[{node.Name}] unseal failed");
                        Program.Exit(response.ExitCode);
                    }

                    // Note that we're passing the [-reset] option to ensure that
                    // any keys from previous attempts have been cleared.

                    node.SudoCommand($"vault-direct operator unseal -reset");

                    foreach (var key in vaultCredentials.UnsealKeys)
                    {
                        response = node.SudoCommand($"vault-direct operator unseal {key}", RunOptions.None);

                        if (response.ExitCode != 0)
                        {
                            Console.WriteLine($"[{node.Name}] unseal failed");
                            Program.Exit(1);
                        }
                    }

                    Console.WriteLine($"[{node.Name}] unsealed");
                }
                else
                {
                    var commandFailed = false;

                    foreach (var manager in hive.Managers)
                    {
                        // Verify that the instance isn't already unsealed.

                        try
                        {
                            response = manager.SudoCommand($"vault-direct status");
                        }
                        catch (SshOperationTimeoutException)
                        {
                            Console.WriteLine($"[{manager.Name}] ** unavailable **");
                            continue;
                        }

                        if (response.ExitCode == 2)
                        {
                            Console.WriteLine($"[{manager.Name}] unsealing...");
                        }
                        else if (response.ExitCode == 0)
                        {
                            Console.WriteLine($"[{manager.Name}] is already unsealed");
                            continue;
                        }
                        else
                        {
                            Console.WriteLine($"[{manager.Name}] unseal failed");
                            continue;
                        }

                        // Note that we're passing the [-reset] option to ensure that
                        // any keys from previous attempts have been cleared.

                        manager.SudoCommand($"vault-direct operator unseal -reset");

                        foreach (var key in vaultCredentials.UnsealKeys)
                        {
                            response = manager.SudoCommand($"vault-direct operator unseal {key}", RunOptions.None);

                            if (response.ExitCode != 0)
                            {
                                failed        = true;
                                commandFailed = true;

                                Console.WriteLine($"[{manager.Name}] unseal failed");
                            }
                        }

                        if (!failed)
                        {
                            Console.WriteLine($"[{manager.Name}] unsealed");
                        }
                    }

                    if (!commandFailed)
                    {
                        // Reenable auto unseal.

                        hive.Consul.Client.KV.PutBool($"{HiveConst.GlobalKey}/{HiveGlobals.UserDisableAutoUnseal}", false).Wait();
                    }

                    Program.Exit(commandFailed ? 1 : 0);
                }
                break;

            case "write":

            {
                // We need handle any [key=@file] arguments specially by including them
                // in a command bundle as data files.

                var files         = new List <CommandFile>();
                var commandString = rightCommandLine.ToString();

                foreach (var dataArg in rightCommandLine.Arguments.Skip(2))
                {
                    var fields = dataArg.Split(new char[] { '=' }, 2);

                    if (fields.Length == 2 && fields[1].StartsWith("@"))
                    {
                        var fileName      = fields[1].Substring(1);
                        var localFileName = $"{files.Count}.data";

                        files.Add(
                            new CommandFile()
                            {
                                Path = localFileName,
                                Data = File.ReadAllBytes(fileName)
                            });

                        commandString = commandString.Replace($"@{fileName}", $"@{localFileName}");
                    }
                }

                bundle = new CommandBundle($"export VAULT_TOKEN={vaultCredentials.RootToken} && {remoteVaultPath} {commandString}");

                foreach (var file in files)
                {
                    bundle.Add(file);
                }

                response = node.SudoCommand(bundle, RunOptions.IgnoreRemotePath | RunOptions.Redact);

                Console.WriteLine(response.AllText);
                Program.Exit(response.ExitCode);
            }
            break;

            case "policy-write":

                // The last command line item is either:
                //
                //      * A "-", indicating that the content should come from standard input.
                //      * A file name prefixed by "@"
                //      * A string holding JSON or HCL

                if (rightCommandLine.Items.Length < 2)
                {
                    response = node.SudoCommand($"export VAULT_TOKEN={vaultCredentials.RootToken} && {remoteVaultPath} {rightCommandLine}", RunOptions.IgnoreRemotePath | RunOptions.Redact);

                    Console.WriteLine(response.AllText);
                    Program.Exit(response.ExitCode);
                }

                var lastItem   = rightCommandLine.Items.Last();
                var policyText = string.Empty;

                if (lastItem == "-")
                {
                    policyText = NeonHelper.ReadStandardInputText();
                }
                else if (lastItem.StartsWith("@"))
                {
                    policyText = File.ReadAllText(lastItem.Substring(1), Encoding.UTF8);
                }
                else
                {
                    policyText = lastItem;
                }

                // We're going to upload a text file holding the policy text and
                // then run a script piping the policy file into the Vault command passed,
                // with the last item replaced by a "-".

                bundle = new CommandBundle("./set-vault-policy.sh.sh");

                var sbScript = new StringBuilder();

                sbScript.AppendLine($"export VAULT_TOKEN={vaultCredentials.RootToken}");
                sbScript.Append($"cat policy | {remoteVaultPath}");

                for (int i = 0; i < rightCommandLine.Items.Length - 1; i++)
                {
                    sbScript.Append(' ');
                    sbScript.Append(rightCommandLine.Items[i]);
                }

                sbScript.AppendLine(" -");

                bundle.AddFile("set-vault-policy.sh", sbScript.ToString(), isExecutable: true);
                bundle.AddFile("policy", policyText);

                response = node.SudoCommand(bundle, RunOptions.IgnoreRemotePath | RunOptions.Redact);

                Console.WriteLine(response.AllText);
                Program.Exit(response.ExitCode);
                break;

            default:

                ExecuteOnNode(node, rightCommandLine);
                break;
            }
        }
        /// <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}");
                    }
                }
            }
        }
Beispiel #14
0
        /// <inheritdoc/>
        public void Run(ModuleContext context)
        {
            var    hive = HiveHelper.Hive;
            string hostname;

            if (!context.ValidateArguments(context.Arguments, validModuleArgs))
            {
                context.Failed = true;
                return;
            }

            // Obtain common arguments.

            context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [state]");

            if (!context.Arguments.TryGetValue <string>("state", out var state))
            {
                state = "present";
            }

            state = state.ToLowerInvariant();

            if (context.HasErrors)
            {
                return;
            }

            var manager      = hive.GetReachableManager();
            var sbErrorNodes = new StringBuilder();

            // Determine whether the registry service is already deployed and
            // also retrieve the registry credentials from Vault if present.
            // Note that the current registry hostname will be persisted to
            // Consul at [neon/service/neon-registry/hostname] when a registry
            // is deployed.

            context.WriteLine(AnsibleVerbosity.Trace, $"Inspecting the [neon-registry] service.");

            var currentService = hive.Docker.InspectService("neon-registry");

            context.WriteLine(AnsibleVerbosity.Trace, $"Getting current registry hostname from Consul.");

            var currentHostname = hive.Registry.GetLocalHostname();
            var currentSecret   = hive.Registry.GetLocalSecret();
            var currentImage    = currentService?.Spec.TaskTemplate.ContainerSpec.ImageWithoutSHA;

            var currentCredentials =        // Set blank properties for the change detection below.
                                     new RegistryCredentials()
            {
                Registry = string.Empty,
                Username = string.Empty,
                Password = string.Empty
            };

            if (!string.IsNullOrEmpty(currentHostname))
            {
                context.WriteLine(AnsibleVerbosity.Trace, $"Reading existing registry credentials for [{currentHostname}].");

                currentCredentials = hive.Registry.GetCredentials(currentHostname);

                if (currentCredentials != null)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"Registry credentials for [{currentHostname}] exist.");
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"Registry credentials for [{currentHostname}] do not exist.");
                }
            }

            // Obtain the current registry TLS certificate (if any).

            var currentCertificate = hive.Certificate.Get("neon-registry");

            // Perform the operation.

            switch (state)
            {
            case "absent":

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [hostname]");

                if (!context.Arguments.TryGetValue <string>("hostname", out hostname))
                {
                    throw new ArgumentException($"[hostname] module argument is required.");
                }

                if (currentService == null)
                {
                    context.WriteLine(AnsibleVerbosity.Important, "[neon-registry] is not currently deployed.");
                }

                if (context.CheckMode)
                {
                    context.WriteLine(AnsibleVerbosity.Important, $"Local registry will be removed when CHECK-MODE is disabled.");
                    return;
                }

                if (currentService == null)
                {
                    return;     // Nothing to do
                }

                context.Changed = true;

                // Logout of the registry.

                if (currentCredentials != null)
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive out of the [{currentHostname}] registry.");
                    hive.Registry.Logout(currentHostname);
                }

                // Delete the [neon-registry] service and volume.  Note that
                // the volume should exist on all of the manager nodes.

                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] service.");
                manager.DockerCommand(RunOptions.None, "docker", "service", "rm", "neon-registry");

                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] volumes.");

                var volumeRemoveActions = new List <Action>();
                var volumeRetryPolicy   = new LinearRetryPolicy(typeof(TransientException), maxAttempts: 10, retryInterval: TimeSpan.FromSeconds(2));

                foreach (var node in hive.Managers)
                {
                    volumeRemoveActions.Add(
                        () =>
                    {
                        // $hack(jeff.lill):
                        //
                        // Docker service removal appears to be synchronous but the removal of the
                        // actual service task containers is not.  We're going to detect this and
                        // throw a [TransientException] and then retry.

                        using (var clonedNode = node.Clone())
                        {
                            lock (context)
                            {
                                context.WriteLine(AnsibleVerbosity.Trace, $"Removing [neon-registry] volume on [{clonedNode.Name}].");
                            }

                            volumeRetryPolicy.InvokeAsync(
                                async() =>
                            {
                                var response = clonedNode.DockerCommand(RunOptions.None, "docker", "volume", "rm", "neon-registry");

                                if (response.ExitCode != 0)
                                {
                                    var message = $"Error removing [neon-registry] volume from [{clonedNode.Name}: {response.ErrorText}";

                                    lock (syncLock)
                                    {
                                        context.WriteLine(AnsibleVerbosity.Info, message);
                                    }

                                    if (response.AllText.Contains("volume is in use"))
                                    {
                                        throw new TransientException(message);
                                    }
                                }
                                else
                                {
                                    lock (context)
                                    {
                                        context.WriteLine(AnsibleVerbosity.Trace, $"Removed [neon-registry] volume on [{clonedNode.Name}].");
                                    }
                                }

                                await Task.Delay(0);
                            }).Wait();
                        }
                    });
                }

                NeonHelper.WaitForParallel(volumeRemoveActions);

                // Remove the traffic manager rule and certificate.

                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] traffic manager rule.");
                hive.PublicTraffic.RemoveRule("neon-registry");
                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] traffic manager certificate.");
                hive.Certificate.Remove("neon-registry");

                // Remove any related Consul state.

                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] Consul [hostname] and [secret].");
                hive.Registry.SetLocalHostname(null);
                hive.Registry.SetLocalSecret(null);

                // Logout the hive from the registry.

                context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive out of the [{currentHostname}] registry.");
                hive.Registry.Logout(currentHostname);

                // Remove the hive DNS host entry.

                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [{currentHostname}] registry DNS hosts entry.");
                hive.Dns.Remove(hostname);
                break;

            case "present":

                if (!hive.Definition.HiveFS.Enabled)
                {
                    context.WriteErrorLine("The local registry service requires hive CephFS.");
                    return;
                }

                // Parse the [hostname], [certificate], [username] and [password] arguments.

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [hostname]");

                if (!context.Arguments.TryGetValue <string>("hostname", out hostname))
                {
                    throw new ArgumentException($"[hostname] module argument is required.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [certificate]");

                if (!context.Arguments.TryGetValue <string>("certificate", out var certificatePem))
                {
                    throw new ArgumentException($"[certificate] module argument is required.");
                }

                if (!TlsCertificate.TryParse(certificatePem, out var certificate))
                {
                    throw new ArgumentException($"[certificate] is not a valid certificate.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [username]");

                if (!context.Arguments.TryGetValue <string>("username", out var username))
                {
                    throw new ArgumentException($"[username] module argument is required.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [password]");

                if (!context.Arguments.TryGetValue <string>("password", out var password))
                {
                    throw new ArgumentException($"[password] module argument is required.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [secret]");

                if (!context.Arguments.TryGetValue <string>("secret", out var secret) || string.IsNullOrEmpty(secret))
                {
                    throw new ArgumentException($"[secret] module argument is required.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [image]");

                if (!context.Arguments.TryGetValue <string>("image", out var image))
                {
                    image = HiveConst.NeonProdRegistry + "/neon-registry:latest";
                }

                // Detect service changes.

                var hostnameChanged    = hostname != currentCredentials?.Registry;
                var usernameChanged    = username != currentCredentials?.Username;
                var passwordChanged    = password != currentCredentials?.Password;
                var secretChanged      = secret != currentSecret;
                var imageChanged       = image != currentImage;
                var certificateChanged = certificate?.CombinedPemNormalized != currentCertificate?.CombinedPemNormalized;
                var updateRequired     = hostnameChanged ||
                                         usernameChanged ||
                                         passwordChanged ||
                                         secretChanged ||
                                         imageChanged ||
                                         certificateChanged;

                if (hostnameChanged)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[hostname] changed from [{currentCredentials?.Registry}] --> [{hostname}]");
                }

                if (usernameChanged)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[username] changed from [{currentCredentials?.Username}] --> [{username}]");
                }

                if (usernameChanged)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[password] changed from [{currentCredentials?.Password}] --> [**REDACTED**]");
                }

                if (secretChanged)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[secret] changed from [{currentSecret}] --> [**REDACTED**]");
                }

                if (imageChanged)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[image] changed from [{currentImage}] --> [{image}]");
                }

                if (certificateChanged)
                {
                    var currentCertRedacted = currentCertificate != null ? "**REDACTED**" : "**NONE**";

                    context.WriteLine(AnsibleVerbosity.Info, $"[certificate] changed from [{currentCertRedacted}] --> [**REDACTED**]");
                }

                // Handle CHECK-MODE.

                if (context.CheckMode)
                {
                    if (currentService == null)
                    {
                        context.WriteLine(AnsibleVerbosity.Important, $"Local registry will be deployed when CHECK-MODE is disabled.");
                        return;
                    }

                    if (updateRequired)
                    {
                        context.WriteLine(AnsibleVerbosity.Important, $"One or more of the arguments have changed so the registry will be updated when CHECK-MODE is disabled.");
                        return;
                    }

                    return;
                }

                // Create the hive DNS host entry we'll use to redirect traffic targeting the registry
                // hostname to the hive managers.  We need to do this because registry IP addresses
                // are typically public, typically targeting the external firewall or load balancer
                // interface.
                //
                // The problem is that hive nodes will generally be unable to connect to the
                // local managers through the firewall/load balancer because most network routers
                // block network traffic that originates from inside the hive, then leaves
                // to hit the external router interface with the expectation of being routed
                // back inside.  I believe this is an anti-spoofing security measure.

                var dnsRedirect = GetRegistryDnsEntry(hostname);

                // Perform the operation.

                if (currentService == null)
                {
                    context.WriteLine(AnsibleVerbosity.Important, $"[neon-registry] service needs to be created.");
                    context.Changed = true;

                    // The registry service isn't running, so we'll do a full deployment.

                    context.WriteLine(AnsibleVerbosity.Trace, $"Setting certificate.");
                    hive.Certificate.Set("neon-registry", certificate);

                    context.WriteLine(AnsibleVerbosity.Trace, $"Updating Consul settings.");
                    hive.Registry.SetLocalHostname(hostname);
                    hive.Registry.SetLocalSecret(secret);

                    context.WriteLine(AnsibleVerbosity.Trace, $"Adding hive DNS host entry for [{hostname}].");
                    hive.Dns.Set(dnsRedirect, waitUntilPropagated: true);

                    context.WriteLine(AnsibleVerbosity.Trace, $"Writing traffic manager rule.");
                    hive.PublicTraffic.SetRule(GetRegistryTrafficManagerRule(hostname));

                    context.WriteLine(AnsibleVerbosity.Trace, $"Creating the [neon-registry] service.");

                    var createResponse = manager.DockerCommand(RunOptions.None,
                                                               "docker service create",
                                                               "--name", "neon-registry",
                                                               "--mode", "global",
                                                               "--constraint", "node.role==manager",
                                                               "--env", $"USERNAME={username}",
                                                               "--env", $"PASSWORD={password}",
                                                               "--env", $"SECRET={secret}",
                                                               "--env", $"LOG_LEVEL=info",
                                                               "--env", $"READ_ONLY=false",
                                                               "--mount", "type=volume,src=neon-registry,volume-driver=neon,dst=/var/lib/neon-registry",
                                                               "--network", "neon-public",
                                                               "--restart-delay", "10s",
                                                               image);

                    if (createResponse.ExitCode != 0)
                    {
                        context.WriteErrorLine($"[neon-registry] service create failed: {createResponse.ErrorText}");
                        return;
                    }

                    context.WriteLine(AnsibleVerbosity.Trace, $"Service created.");
                    context.WriteLine(AnsibleVerbosity.Trace, $"Wait for [neon-registry] service to stabilize (30s).");
                    Thread.Sleep(TimeSpan.FromSeconds(30));
                    context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive into the [{hostname}] registry.");
                    hive.Registry.Login(hostname, username, password);
                }
                else if (updateRequired)
                {
                    context.WriteLine(AnsibleVerbosity.Important, $"[neon-registry] service update is required.");
                    context.Changed = true;

                    // Update the service and related settings as required.

                    if (certificateChanged)
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating certificate.");
                        hive.Certificate.Set("neon-registry", certificate);
                    }

                    if (hostnameChanged)
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating traffic manager rule.");
                        hive.PublicTraffic.SetRule(GetRegistryTrafficManagerRule(hostname));

                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating hive DNS host entry for [{hostname}] (60 seconds).");
                        hive.Dns.Set(dnsRedirect, waitUntilPropagated: true);

                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating local hive hostname [{hostname}].");
                        hive.Registry.SetLocalHostname(hostname);

                        if (!string.IsNullOrEmpty(currentHostname))
                        {
                            context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive out of the [{currentHostname}] registry.");
                            hive.Registry.Logout(currentHostname);
                        }
                    }

                    if (secretChanged)
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating local hive secret.");
                        hive.Registry.SetLocalSecret(secret);
                    }

                    context.WriteLine(AnsibleVerbosity.Trace, $"Updating service.");

                    var updateResponse = manager.DockerCommand(RunOptions.None,
                                                               "docker service update",
                                                               "--env-add", $"USERNAME={username}",
                                                               "--env-add", $"PASSWORD={password}",
                                                               "--env-add", $"SECRET={secret}",
                                                               "--env-add", $"LOG_LEVEL=info",
                                                               "--env-add", $"READ_ONLY=false",
                                                               "--image", image,
                                                               "neon-registry");

                    if (updateResponse.ExitCode != 0)
                    {
                        context.WriteErrorLine($"[neon-registry] service update failed: {updateResponse.ErrorText}");
                        return;
                    }

                    context.WriteLine(AnsibleVerbosity.Trace, $"Service updated.");

                    context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive into the [{hostname}] registry.");
                    hive.Registry.Login(hostname, username, password);
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Important, $"[neon-registry] service update is not required but we're logging all nodes into [{hostname}] to ensure hive consistency.");
                    hive.Registry.Login(hostname, username, password);

                    context.Changed = false;
                }
                break;

            case "prune":

                if (currentService == null)
                {
                    context.WriteLine(AnsibleVerbosity.Important, "Registry service is not running.");
                    return;
                }

                if (context.CheckMode)
                {
                    context.WriteLine(AnsibleVerbosity.Important, "Registry will be pruned when CHECK-MODE is disabled.");
                    return;
                }

                context.Changed = true;     // Always set this to TRUE for prune.

                // We're going to upload a script to one of the managers that handles
                // putting the [neon-registry] service into READ-ONLY mode, running
                // the garbage collection container and then restoring [neon-registry]
                // to READ/WRITE mode.
                //
                // The nice thing about this is that the operation will continue to
                // completion on the manager node even if we lose the SSH connection.

                var updateScript =
                    $@"#!/bin/bash
# Update [neon-registry] to READ-ONLY mode:

docker service update --env-rm READ_ONLY --env-add READ_ONLY=true neon-registry

# Prune the registry:

docker run \
   --name neon-registry-prune \
   --restart-condition=none \
   --mount type=volume,src=neon-registry,volume-driver=neon,dst=/var/lib/neon-registry \
   {HiveConst.NeonProdRegistry}/neon-registry garbage-collect

# Restore [neon-registry] to READ/WRITE mode:

docker service update --env-rm READ_ONLY --env-add READ_ONLY=false neon-registry
";
                var bundle = new CommandBundle("./collect.sh");

                bundle.AddFile("collect.sh", updateScript, isExecutable: true);

                context.WriteLine(AnsibleVerbosity.Info, "Registry prune started.");

                var pruneResponse = manager.SudoCommand(bundle, RunOptions.None);

                if (pruneResponse.ExitCode != 0)
                {
                    context.WriteErrorLine($"The prune operation failed.  The registry may be running in READ-ONLY mode: {pruneResponse.ErrorText}");
                    return;
                }

                context.WriteLine(AnsibleVerbosity.Info, "Registry prune completed.");
                break;

            default:

                throw new ArgumentException($"[state={state}] is not one of the valid choices: [present], [absent], or [prune].");
            }
        }
Beispiel #15
0
        /// <inheritdoc/>
        public void Run(ModuleContext context)
        {
            var hive = HiveHelper.Hive;

            if (!context.ValidateArguments(context.Arguments, validModuleArgs))
            {
                context.Failed = true;
                return;
            }

            // Obtain common arguments.

            if (!context.Arguments.TryGetValue <string>("name", out var name))
            {
                throw new ArgumentException($"[name] module argument is required.");
            }

            if (!context.Arguments.TryGetValue <string>("state", out var state))
            {
                state = "deploy";
            }

            state = state.ToLowerInvariant();

            var manager  = hive.GetReachableManager();
            var response = (CommandResponse)null;

            switch (state)
            {
            case "deploy":

                if (!context.Arguments.TryGetValue("stack", out var stackObject))
                {
                    throw new ArgumentException($"[stack] module argument is required when [state=deploy].");
                }

                var stackJson = NeonHelper.JsonSerialize(stackObject);
                var stackYaml = NeonHelper.JsonToYaml(stackJson);
                var bundle    = new CommandBundle($"docker stack deploy --compose-file ./compose.yaml {name}");

                bundle.AddFile("compose.yaml", stackYaml);

                response = manager.SudoCommand(bundle, RunOptions.None);

                if (response.ExitCode != 0)
                {
                    context.WriteErrorLine(response.ErrorText);
                }

                context.Changed = true;
                break;

            case "remove":

                response = manager.SudoCommand("docker stack rm", RunOptions.None, name);

                if (response.ExitCode != 0)
                {
                    context.WriteErrorLine(response.ErrorText);
                }

                context.Changed = true;
                break;

            default:

                throw new ArgumentException($"[state={state}] is not one of the valid choices: [present], [absent], or [rollback].");
            }
        }
Beispiel #16
0
        /// <summary>
        /// Performs the Docker registry cache related configuration of the node.
        /// </summary>
        public void Configure(SshProxy <NodeDefinition> node)
        {
            // NOTE:
            //
            // We're going to configure the certificates even if the registry cache
            // isn't enabled so it'll be easier to upgrade the hive later.

            // For managers, upload the individual cache certificate and
            // private key files for managers [cache.crt] and [cache.key] at
            // [/etc/neon-registry-cache/].  This directory will be
            // mapped into the cache container.
            //
            // Then create the cache's data volume and start the manager's
            // Registry cache container.

            if (node.Metadata.IsManager)
            {
                node.InvokeIdempotentAction("setup/registrycache",
                                            () =>
                {
                    // Copy the registry cache certificate and private key to
                    //
                    //      /etc/neon-registry-cache

                    node.Status = "run: registry-cache-server-certs.sh";

                    var copyCommand  = new CommandBundle("./registry-cache-server-certs.sh");
                    var sbCopyScript = new StringBuilder();

                    sbCopyScript.AppendLine("mkdir -p /etc/neon-registry-cache");
                    sbCopyScript.AppendLine("chmod 750 /etc/neon-registry-cache");

                    copyCommand.AddFile($"cache.crt", hive.HiveLogin.HiveCertificate.CertPem);
                    copyCommand.AddFile($"cache.key", hive.HiveLogin.HiveCertificate.KeyPem);

                    sbCopyScript.AppendLine($"cp cache.crt /etc/neon-registry-cache/cache.crt");
                    sbCopyScript.AppendLine($"cp cache.key /etc/neon-registry-cache/cache.key");
                    sbCopyScript.AppendLine($"chmod 640 /etc/neon-registry-cache/*");

                    copyCommand.AddFile("registry-cache-server-certs.sh", sbCopyScript.ToString(), isExecutable: true);
                    node.SudoCommand(copyCommand);

                    // Upload the cache certificates to every hive node at:
                    //
                    //      /etc/docker/certs.d/<hostname>:{HiveHostPorts.RegistryCache}/ca.crt
                    //
                    // and then have Linux reload the trusted certificates.

                    node.InvokeIdempotentAction("setup/registrycache-cert",
                                                () =>
                    {
                        node.Status = "upload: registry cache certs";

                        var uploadCommand  = new CommandBundle("./registry-cache-client-certs.sh");
                        var sbUploadScript = new StringBuilder();

                        uploadCommand.AddFile($"hive-neon-registry-cache.crt", hive.HiveLogin.HiveCertificate.CertPem);

                        foreach (var manager in hive.Definition.SortedManagers)
                        {
                            var cacheHostName = hive.Definition.GetRegistryCacheHost(manager);

                            sbUploadScript.AppendLine($"mkdir -p /etc/docker/certs.d/{cacheHostName}:{HiveHostPorts.DockerRegistryCache}");
                            sbUploadScript.AppendLine($"cp hive-neon-registry-cache.crt /etc/docker/certs.d/{cacheHostName}:{HiveHostPorts.DockerRegistryCache}/ca.crt");
                        }

                        uploadCommand.AddFile("registry-cache-client-certs.sh", sbUploadScript.ToString(), isExecutable: true);
                        node.SudoCommand(uploadCommand);
                    });

                    // Start the registry cache containers if enabled for the hive.

                    if (hive.Definition.Docker.RegistryCache)
                    {
                        // Create the registry data volume.

                        node.Status = "create: registry cache volume";
                        node.SudoCommand(new CommandBundle("docker-volume-create \"neon-registry-cache\""));

                        // Start the registry cache using the required Docker public registry
                        // credentials, if any.

                        var publicRegistryCredentials = hive.Definition.Docker.Registries.SingleOrDefault(r => HiveHelper.IsDockerPublicRegistry(r.Registry));

                        publicRegistryCredentials = publicRegistryCredentials ?? new RegistryCredentials()
                        {
                            Registry = HiveConst.DockerPublicRegistry
                        };
                        publicRegistryCredentials.Username = publicRegistryCredentials.Username ?? string.Empty;
                        publicRegistryCredentials.Password = publicRegistryCredentials.Password ?? string.Empty;

                        node.Status = "start: neon-registry-cache";

                        var registry = publicRegistryCredentials.Registry;

                        if (string.IsNullOrEmpty(registry) || registry.Equals("docker.io", StringComparison.InvariantCultureIgnoreCase))
                        {
                            registry = "registry-1.docker.io";
                        }

                        ServiceHelper.StartContainer(node, "neon-registry-cache", hive.Definition.Image.RegistryCache, RunOptions.FaultOnError | hive.SecureRunOptions,
                                                     new CommandBundle(
                                                         "docker run",
                                                         "--name", "neon-registry-cache",
                                                         "--detach",
                                                         "--restart", "always",
                                                         "--publish", $"{HiveHostPorts.DockerRegistryCache}:5000",
                                                         "--volume", "/etc/neon-registry-cache:/etc/neon-registry-cache:ro", // Registry cache certificates folder
                                                         "--volume", "neon-registry-cache:/var/lib/neon-registry-cache",
                                                         "--env", $"HOSTNAME={node.Name}.{hive.Definition.Hostnames.RegistryCache}",
                                                         "--env", $"REGISTRY=https://{registry}",
                                                         "--env", $"USERNAME={publicRegistryCredentials.Username}",
                                                         "--env", $"PASSWORD={publicRegistryCredentials.Password}",
                                                         "--env", "LOG_LEVEL=info",
                                                         ServiceHelper.ImagePlaceholderArg));
                    }
                });

                node.Status = string.Empty;
            }
        }
        /// <summary>
        /// Uploads the files for the program being debugged to the Raspberry, replacing
        /// any existing files.
        /// </summary>
        /// <param name="programName">The program name</param>
        /// <param name="assemblyName">The addembly name.</param>
        /// <param name="publishedBinaryFolder">Path to the workstation folder holding the program files.</param>
        /// <returns><c>true</c> on success.</returns>
        public async Task <bool> UploadProgramAsync(string programName, string assemblyName, string publishedBinaryFolder)
        {
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(programName), nameof(programName));
            Covenant.Requires <ArgumentException>(!programName.Contains(' '), nameof(programName));
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(assemblyName), nameof(assemblyName));
            Covenant.Requires <ArgumentException>(!assemblyName.Contains(' '), nameof(assemblyName));
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(publishedBinaryFolder), nameof(publishedBinaryFolder));
            Covenant.Requires <ArgumentNullException>(Directory.Exists(publishedBinaryFolder), nameof(publishedBinaryFolder));

            // We're going to ZIP the program files locally and then transfer the zipped
            // files to the Raspberry to be expanded there.

            var debugFolder = LinuxPath.Combine(PackageHelper.RemoteDebugBinaryRoot(Username), programName);
            var groupScript = string.Empty;

            if (!string.IsNullOrEmpty(projectSettings.TargetGroup))
            {
                groupScript =
                    $@"
# Add the program assembly to the user specified target group (if any).  This
# defaults to [gpio] so users will be able to access the GPIO pins.

if ! chgrp {projectSettings.TargetGroup} {debugFolder}/{assemblyName} ; then
    exit 1
fi
";
            }

            var uploadScript =
                $@"

# Ensure that the debug folder exists.

if ! mkdir -p {debugFolder} ; then
    exit 1
fi

# Clear all existing program files.

if ! rm -rf {debugFolder}/* ; then
    exit 1
fi

# Unzip the binary and other files to the debug folder.

if ! unzip program.zip -d {debugFolder} ; then
    exit 1
fi

# The program assembly needs execute permissions.

if ! chmod 770 {debugFolder}/{assemblyName} ; then
    exit 1
fi
{groupScript}
exit 0
";

            // I'm not going to do a progress dialog because this should be fast.

            try
            {
                LogInfo($"Uploading program to: [{debugFolder}]");

                var bundle = new CommandBundle(uploadScript);

                bundle.AddZip("program.zip", publishedBinaryFolder);

                var response = RunCommand(bundle);

                if (response.ExitCode == 0)
                {
                    LogInfo($"Program uploaded");
                    return(await Task.FromResult(true));
                }
                else
                {
                    LogError(response.AllText);
                    return(await Task.FromResult(false));
                }
            }
            catch (Exception e)
            {
                LogException(e);
                return(await Task.FromResult(false));
            }
        }
Beispiel #18
0
        /// <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);
        }