Ejemplo n.º 1
0
        /// <summary>
        /// Constructor.
        /// </summary>
        public MainForm()
        {
            MainForm.Current = this;

            InitializeComponent();

            Load  += MainForm_Load;
            Shown += (s, a) => Visible = false; // The main form should always be hidden

            // Ensure that temporary files are written to the users temporary folder because
            // there's a decent chance that this folder will be encrypted at rest.

            TempFile.Root   = KubeHelper.TempFolder;
            TempFolder.Root = KubeHelper.TempFolder;

            // Preload the notification icons and animations for better performance.

            appIcon             = new Icon(@"Images\app.ico");
            connectedIcon       = new Icon(@"Images\connected.ico");
            disconnectedIcon    = new Icon(@"Images\disconnected.ico");
            errorIcon           = new Icon(@"Images\error.ico");
            connectingAnimation = AnimatedIcon.Load("Images", "connecting", animationFrameRate);
            workingAnimation    = AnimatedIcon.Load("Images", "working", animationFrameRate);
            errorAnimation      = AnimatedIcon.Load("Images", "error", animationFrameRate);
            notifyStack         = new Stack <NotifyState>();

            // Initialize the cluster hosting provider components.

            HostingLoader.Initialize();

            // Initialize the client state.

            proxies      = new List <ReverseProxy>();
            portForwards = new List <PortForward>();
            Headend      = new HeadendClient();
            KubeHelper.LoadClientConfig();
        }
Ejemplo n.º 2
0
        /// <summary>
        /// Program entry point.
        /// </summary>
        /// <param name="args">The command line arguments.</param>
        /// <returns>The exit code.</returns>
        public static int Main(params string[] args)
        {
            string usage = $@"
neonKUBE Management Tool: neon [v{Program.Version}]
{Build.Copyright}

USAGE:

    neon [OPTIONS] COMMAND [ARG...]

COMMAND SUMMARY:

    neon help               COMMAND
    neon cluster prepare    [CLUSTER-DEF]
    neon cluster setup      [CLUSTER-DEF]
    neon couchbase          COMMNAND
    neon generate models    [OPTIONS] ASSEMBLY-PATH [OUTPUT-PATH]
    neon login              COMMAND
    neon logout
    neon password           COMMAND
    neon prepare            COMMAND
    neon run                -- COMMAND
    neon scp                [NODE]
    neon ssh                [NODE]
    neon vault              COMMAND
    neon version            [-n] [--git] [--minimum=VERSION]

ARGUMENTS:

    CLUSTER-DEF         - Path to a cluster definition file.  This is
                          optional for some commands when logged in
    COMMAND             - Subcommand and arguments.
    NODE                - A node name.

OPTIONS:

    --help                              - Display help

    --insecure                          - Use insecure temporary folder 
                                          (see remarks)
 
    --log-folder=LOG-FOLDER             - Optional log folder path

    -m=COUNT, --max-parallel=COUNT      - Maximum number of nodes to be 
                                          configured in parallel [default=6]

    --machine-password=PASSWORD         - Overrides default initial machine
                                          password: sysadmin0000

    --machine-username=USERNAME         - Overrides default initial machine
                                          username: sysadmin

    -q, --quiet                         - Disables operation progress

    -w=SECONDS, --wait=SECONDS          - Seconds to delay for cluster stablization 
                                          (defaults to 60s).

REMARKS:

By default, any tempory files generated will be written to your 
[$HOME/.neonkube/temp] folder which is encrypted at rest for
Windows 10 PRO and Windows Server workstations.  This encryption
may cause some problems (e.g. a [neon run ...] command that 
needs to mount a decrypted file to a local Docker container.

You can disable the use of this encrypted folder by specifying
[--insecure] for any command.
";

            // Disable any logging that might be performed by library classes.

            LogManager.Default.LogLevel = LogLevel.None;

            // Use the version of Powershell Core installed with the application,
            // if present.

            PowerShell.PwshPath = KubeHelper.PwshPath;

            // We need to verify that we're running with elevated permissions if we're not
            // shimmed into a Docker container.

            // $todo(jeff.lill):
            //
            // We're currently requiring elevated permissions for all commands, even those
            // that don't actually require elevated permissions.  We may wish to relax this
            // in the future.

            if (!KubeHelper.InToolContainer)
            {
                if (NeonHelper.IsWindows)
                {
                    var identity  = WindowsIdentity.GetCurrent();
                    var principal = new WindowsPrincipal(identity);

                    if (!principal.IsInRole(WindowsBuiltInRole.Administrator))
                    {
                        Console.Error.WriteLine("*** ERROR: This command requires elevated administrator permissions.");
                        Program.Exit(1);
                    }
                }
                else if (NeonHelper.IsOSX)
                {
                    throw new NotImplementedException("$todo(jeff.lill): Implement OSX elevated permissions check.");
                }
            }

            // Ensure that all of the cluster hosting manager implementations are loaded.

            new HostingManagerFactory(() => HostingLoader.Initialize());

            // Process the command line.

            try
            {
                ICommand command;

                CommandLine     = new CommandLine(args);
                LeftCommandLine = CommandLine.Split("--").Left;

                foreach (var cmdLine in new CommandLine[] { CommandLine, LeftCommandLine })
                {
                    cmdLine.DefineOption("--machine-username");
                    cmdLine.DefineOption("--machine-password");
                    cmdLine.DefineOption("-os").Default = "Ubuntu-18.04";
                    cmdLine.DefineOption("-q", "--quiet");
                    cmdLine.DefineOption("-m", "--max-parallel").Default = "6";
                    cmdLine.DefineOption("-w", "--wait").Default         = "60";
                    cmdLine.DefineOption("--log-folder").Default         = string.Empty;
                }

                var validOptions = new HashSet <string>();

                validOptions.Add("--debug");
                validOptions.Add("--help");
                validOptions.Add("--insecure");
                validOptions.Add("--log-folder");
                validOptions.Add("--machine-username");
                validOptions.Add("--machine-password");
                validOptions.Add("-m");
                validOptions.Add("--max-parallel");
                validOptions.Add("-q");
                validOptions.Add("--quiet");
                validOptions.Add("-w");
                validOptions.Add("--wait");

                if (CommandLine.Arguments.Length == 0)
                {
                    Console.WriteLine(usage);
                    Program.Exit(0);
                }

                if (!CommandLine.HasOption("--insecure"))
                {
                    // Ensure that temporary files are written to the users temporary folder because
                    // there's a decent chance that this folder will be encrypted at rest.

                    if (KubeTestManager.Current == null)
                    {
                        TempFile.Root   = KubeHelper.TempFolder;
                        TempFolder.Root = KubeHelper.TempFolder;
                    }
                }

                var commands = new List <ICommand>()
                {
                    new ClusterCommand(),
                    new ClusterPrepareCommand(),
                    new ClusterSetupCommand(),
                    new ClusterVerifyCommand(),
                    new CouchbaseCommand(),
                    new CouchbaseQueryCommand(),
                    new CouchbaseUpsertCommand(),
                    new GenerateCommand(),
                    new GenerateModelsCommand(),
                    new LoginCommand(),
                    new LoginExportCommand(),
                    new LoginImportCommand(),
                    new LoginListCommand(),
                    new LoginRemoveCommand(),
                    new LogoutCommand(),
                    new PasswordCommand(),
                    new PasswordExportCommand(),
                    new PasswordGenerateCommand(),
                    new PasswordGetCommand(),
                    new PasswordImportCommand(),
                    new PasswordListCommand(),
                    new PasswordRemoveCommand(),
                    new PasswordSetCommand(),
                    new PrepareCommand(),
                    new PrepareNodeTemplateCommand(),
                    new RunCommand(),
                    new ScpCommand(),
                    new SshCommand(),
                    new VaultCommand(),
                    new VaultCreateCommand(),
                    new VaultDecryptCommand(),
                    new VaultEditCommand(),
                    new VaultEncryptCommand(),
                    new VaultPasswordNameCommand(),
                    new VersionCommand()
                };

                // Short-circuit the help command.

                if (CommandLine.Arguments[0] == "help")
                {
                    if (CommandLine.Arguments.Length == 1)
                    {
                        Console.WriteLine(usage);
                        Program.Exit(0);
                    }

                    CommandLine = CommandLine.Shift(1);

                    command = GetCommand(CommandLine, commands);

                    if (command == null)
                    {
                        Console.Error.WriteLine($"*** ERROR: Unexpected [{CommandLine.Arguments[0]}] command.");
                        Program.Exit(1);
                    }

                    command.Help();
                    Program.Exit(0);
                }

                // Lookup the command.

                command = GetCommand(CommandLine, commands);

                if (command == null)
                {
                    Console.Error.WriteLine($"*** ERROR: Unexpected [{CommandLine.Arguments[0]}] command.");
                    Program.Exit(1);
                }

                // Handle the logging options.

                LogPath = LeftCommandLine.GetOption("--log-folder");
                Quiet   = LeftCommandLine.GetFlag("--quiet");

                if (KubeHelper.InToolContainer)
                {
                    // We hardcode logging to [/log] inside [neon-cli] containers.

                    LogPath = "/log";
                }
                else if (!string.IsNullOrEmpty(LogPath))
                {
                    LogPath = Path.GetFullPath(LogPath);

                    Directory.CreateDirectory(LogPath);
                }
                else
                {
                    LogPath = KubeHelper.LogFolder;

                    // We can clear this folder because we know that there shouldn't be
                    // any other files in here.

                    NeonHelper.DeleteFolderContents(LogPath);
                }

                //-------------------------------------------------------------
                // Process the standard command line options.

                // Load the user name and password from the command line options, if present.

                MachineUsername = LeftCommandLine.GetOption("--machine-username", "sysadmin");
                MachinePassword = LeftCommandLine.GetOption("--machine-password", "sysadmin0000");

                // Handle the other options.

                var maxParallelOption = LeftCommandLine.GetOption("--max-parallel");
                int maxParallel;

                if (!int.TryParse(maxParallelOption, out maxParallel) || maxParallel < 1)
                {
                    Console.Error.WriteLine($"*** ERROR: [--max-parallel={maxParallelOption}] option is not valid.");
                    Program.Exit(1);
                }

                Program.MaxParallel = maxParallel;

                var    waitSecondsOption = LeftCommandLine.GetOption("--wait");
                double waitSeconds;

                if (!double.TryParse(waitSecondsOption, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out waitSeconds) || waitSeconds < 0)
                {
                    Console.Error.WriteLine($"*** ERROR: [--wait={waitSecondsOption}] option is not valid.");
                    Program.Exit(1);
                }

                Program.WaitSeconds = waitSeconds;

                Debug = LeftCommandLine.HasOption("--debug");

                if (command.CheckOptions)
                {
                    // Ensure that there are no unexpected command line options.

                    foreach (var optionName in command.ExtendedOptions)
                    {
                        validOptions.Add(optionName);
                    }

                    foreach (var option in LeftCommandLine.Options)
                    {
                        if (!validOptions.Contains(option.Key))
                        {
                            var commandWords = string.Empty;

                            foreach (var word in command.Words)
                            {
                                if (commandWords.Length > 0)
                                {
                                    commandWords += " ";
                                }

                                commandWords += word;
                            }

                            Console.Error.WriteLine($"*** ERROR: [{commandWords}] command does not support [{option.Key}].");
                            Program.Exit(1);
                        }
                    }
                }

                // Run the command.

                if (command.NeedsSshCredentials(CommandLine))
                {
                    if (string.IsNullOrWhiteSpace(MachineUsername) || string.IsNullOrEmpty(MachinePassword))
                    {
                        Console.WriteLine();
                        Console.WriteLine("    Enter cluster SSH credentials:");
                        Console.WriteLine("    -------------------------------");
                    }

                    while (string.IsNullOrWhiteSpace(MachineUsername))
                    {
                        Console.Write("    username: "******"    password: "******"*** ERROR: {NeonHelper.ExceptionError(e)}");
                Console.Error.WriteLine(string.Empty);
                return(1);
            }

            return(0);
        }
Ejemplo n.º 3
0
        //---------------------------------------------------------------------
        // Static members

        /// <summary>
        /// Static constructor.
        /// </summary>
        static ClusterFixture()
        {
            HostingLoader.Initialize();
        }
Ejemplo n.º 4
0
        /// <summary>
        /// Application entry point.
        /// </summary>
        /// <param name="args">Command line arguments.</param>
        public static async Task Main(string[] args)
        {
            LogManager.Default.SetLogLevel(Environment.GetEnvironmentVariable("LOG_LEVEL"));
            log = LogManager.Default.GetLogger(typeof(Program));
            log.LogInfo(() => $"Starting [{serviceName}]");
            log.LogInfo(() => $"LOG_LEVEL={LogManager.Default.LogLevel.ToString().ToUpper()}");

            // Create process terminator to handle process termination signals.

            terminator = new ProcessTerminator(log);

            terminator.AddHandler(
                () =>
            {
                // Cancel any operations in progress.

                exit = true;

                terminator.CancellationTokenSource.Cancel();

                // This gracefully closes the [proxyNotifyChannel] so HiveMQ will
                // promptly remove the associated queue.

                if (proxyNotifyChannel != null)
                {
                    proxyNotifyChannel.Dispose();
                    proxyNotifyChannel = null;
                }

                try
                {
                    NeonHelper.WaitFor(() => !processingConfigs, terminator.Timeout);
                    log.LogInfo(() => "Tasks stopped gracefully.");
                }
                catch (TimeoutException)
                {
                    log.LogWarn(() => $"Tasks did not stop within [{terminator.Timeout}].");
                }
            });

            // Establish the hive connections.

            if (NeonHelper.IsDevWorkstation)
            {
                var vaultCredentialsSecret = "neon-proxy-manager-credentials";

                Environment.SetEnvironmentVariable("VAULT_CREDENTIALS", vaultCredentialsSecret);

                hive = HiveHelper.OpenHiveRemote(new DebugSecrets().VaultAppRole(vaultCredentialsSecret, "neon-proxy-manager"));
            }
            else
            {
                hive = HiveHelper.OpenHive();
            }

            // [neon-proxy-manager] requires access to the [IHostingManager] implementation for the
            // current environment, so we'll need to initialize the hosting loader.

            HostingLoader.Initialize();

            try
            {
                // Log into Vault using a Docker secret.

                var vaultCredentialsSecret = Environment.GetEnvironmentVariable("VAULT_CREDENTIALS");

                if (string.IsNullOrEmpty(vaultCredentialsSecret))
                {
                    log.LogCritical("[VAULT_CREDENTIALS] environment variable does not exist.");
                    Program.Exit(1, immediate: true);
                }

                var vaultSecret = HiveHelper.GetSecret(vaultCredentialsSecret);

                if (string.IsNullOrEmpty(vaultSecret))
                {
                    log.LogCritical($"Cannot read Docker secret [{vaultCredentialsSecret}].");
                    Program.Exit(1, immediate: true);
                }

                var vaultCredentials = HiveCredentials.ParseJson(vaultSecret);

                if (vaultCredentials == null)
                {
                    log.LogCritical($"Cannot parse Docker secret [{vaultCredentialsSecret}].");
                    Program.Exit(1, immediate: true);
                }

                // Open the hive data services and then start the main service task.

                log.LogInfo(() => $"Connecting: Vault");

                using (vault = HiveHelper.OpenVault(vaultCredentials))
                {
                    log.LogInfo(() => $"Connecting: Consul");

                    using (consul = HiveHelper.OpenConsul())
                    {
                        log.LogInfo(() => $"Connecting: Docker");

                        using (docker = HiveHelper.OpenDocker())
                        {
                            log.LogInfo(() => $"Connecting: {HiveMQChannels.ProxyNotify} channel");

                            // NOTE:
                            //
                            // We're passing [useBootstrap=true] here so that the HiveMQ client will
                            // connect directly to the HiveMQ cluster nodes as opposed to routing
                            // traffic through the private traffic manager.  This is necessary because
                            // the load balancers rely on HiveMQ to broadcast update notifications.
                            //
                            // One consequence of this is that this service will need to be restarted
                            // whenever HiveMQ instances are relocated to different hive hosts.
                            // We're going to monitor for changes to the HiveMQ bootstrap settings
                            // and gracefully terminate the process when this happens.  We're then
                            // depending on Docker to restart the process so we'll be able to pick
                            // up the change.

                            hive.HiveMQ.Internal.HiveMQBootstrapChanged +=
                                (s, a) =>
                            {
                                log.LogInfo("HiveMQ bootstrap settings change detected.  Terminating service with [exitcode=-1] expecting that Docker will restart it.");

                                // Use ExitCode=-1 so that we'll restart even if the service/container
                                // was not configured with [restart=always].

                                terminator.Exit(-1);
                            };

                            using (proxyNotifyChannel = hive.HiveMQ.Internal.GetProxyNotifyChannel(useBootstrap: true).Open())
                            {
                                // Read the service settings, initializing their default values
                                // if they don't already exist.

                                if (!await consul.KV.Exists(certWarnDaysKey))
                                {
                                    log.LogInfo($"Persisting setting [{certWarnDaysKey}=30.0]");
                                    await consul.KV.PutDouble(certWarnDaysKey, 30.0);
                                }

                                if (!await consul.KV.Exists(cacheRemoveSecondsKey))
                                {
                                    log.LogInfo($"Persisting setting [{cacheRemoveSecondsKey}=300.0]");
                                    await consul.KV.PutDouble(cacheRemoveSecondsKey, 300.0);
                                }

                                if (!await consul.KV.Exists(failsafeSecondsKey))
                                {
                                    log.LogInfo($"Persisting setting [{failsafeSecondsKey}=120.0]");
                                    await consul.KV.PutDouble(failsafeSecondsKey, 120);
                                }

                                certWarnTime     = TimeSpan.FromDays(await consul.KV.GetDouble(certWarnDaysKey));
                                cacheRemoveDelay = TimeSpan.FromDays(await consul.KV.GetDouble(cacheRemoveSecondsKey));
                                failsafeInterval = TimeSpan.FromSeconds(await consul.KV.GetDouble(failsafeSecondsKey));

                                log.LogInfo(() => $"Using setting [{certWarnDaysKey}={certWarnTime.TotalSeconds}]");
                                log.LogInfo(() => $"Using setting [{cacheRemoveSecondsKey}={cacheRemoveDelay.TotalSeconds}]");
                                log.LogInfo(() => $"Using setting [{failsafeSecondsKey}={failsafeInterval.TotalSeconds}]");

                                // Run the service tasks.

                                var tasks = new List <Task>();

                                tasks.Add(ConfigGeneratorAsync());
                                tasks.Add(FailsafeBroadcasterAsync());
                                await NeonHelper.WaitAllAsync(tasks);
                            }
                        }
                    }
                }
            }
            catch (Exception e)
            {
                log.LogCritical(e);
                Program.Exit(1);
                return;
            }
            finally
            {
                HiveHelper.CloseHive();
                terminator.ReadyToExit();
            }

            Program.Exit(0);
            return;
        }
Ejemplo n.º 5
0
        /// <inheritdoc/>
        public override void Run(CommandLine commandLine)
        {
            if (commandLine.HasHelpOption)
            {
                Help();
                Program.Exit(0);
            }

            // Special-case handling of the [--remove-templates] option.

            if (commandLine.HasOption("--remove-templates"))
            {
                Console.WriteLine("Removing cached virtual machine templates.");

                foreach (var fileName in Directory.GetFiles(HiveHelper.GetVmTemplatesFolder(), "*.*", SearchOption.TopDirectoryOnly))
                {
                    File.Delete(fileName);
                }

                Program.Exit(0);
            }

            // Implement the command.

            packageCacheUri = commandLine.GetOption("--package-cache");     // This overrides the hive definition, if specified.

            if (Program.HiveLogin != null)
            {
                Console.Error.WriteLine("*** ERROR: You are logged into a hive.  You need to logout before preparing another.");
                Program.Exit(1);
            }

            if (commandLine.Arguments.Length == 0)
            {
                Console.Error.WriteLine($"*** ERROR: HIVE-DEF expected.");
                Program.Exit(1);
            }

            hiveDefPath = commandLine.Arguments[0];
            force       = commandLine.GetFlag("--force");

            HiveDefinition.ValidateFile(hiveDefPath, strict: true);

            var hiveDefinition = HiveDefinition.FromFile(hiveDefPath, strict: true);

            hiveDefinition.Provisioner = $"neon-cli:{Program.Version}";  // Identify this tool/version as the hive provisioner

            // NOTE:
            //
            // Azure has implemented a more restrictive password policy and our
            // default machine password does not meet the requirements:
            //
            // The supplied password must be between 6-72 characters long and must
            // satisfy at least 3 of password complexity requirements from the following:
            //
            //      1. Contains an uppercase character
            //      2. Contains a lowercase character
            //      3. Contains a numeric digit
            //      4. Contains a special character
            //      5. Control characters are not allowed
            //
            // It's also probably not a great idea to use a static password when
            // provisioning VMs in public clouds because it might be possible for
            // somebody to use this fact the SSH into nodes while the hive is being
            // setup and before we set the secure password at the end.
            //
            // This is less problematic for non-cloud environments because it's
            // likely that the hosts won't initially be able to receive inbound
            // Internet traffic and besides, we need to have a known password
            // embedded into the VM templates.
            //
            // We're going to handle this for cloud environments by looking
            // at [Program.MachinePassword].  If this is set to the default
            // machine password then we're going to replace it with a randomlly
            // generated password with a few extra characters to ensure that
            // it meets the target cloud's password requirements.  We'll use
            // a non-default password if the operator specified one.

            if (hiveDefinition.Hosting.IsCloudProvider && Program.MachinePassword == HiveConst.DefaulVmTemplatePassword)
            {
                Program.MachinePassword = NeonHelper.GetRandomPassword(20);

                // Append a string that guarantees that the generated password meets
                // cloud minimum requirements.

                Program.MachinePassword += ".Aa0";
            }

            // Note that hive prepare starts new log files.

            hive = new HiveProxy(hiveDefinition, Program.CreateNodeProxy <NodeDefinition>, appendLog: false, useBootstrap: true, defaultRunOptions: RunOptions.LogOutput | RunOptions.FaultOnError);

            if (File.Exists(Program.GetHiveLoginPath(HiveConst.RootUser, hive.Definition.Name)))
            {
                Console.Error.WriteLine($"*** ERROR: A hive login named [{HiveConst.RootUser}@{hive.Definition.Name}] already exists.");
                Program.Exit(1);
            }

            Program.OSProperties = OSProperties.For(hiveDefinition.HiveNode.OperatingSystem);

            // Configure global options.

            if (commandLine.HasOption("--unredacted"))
            {
                hive.SecureRunOptions = RunOptions.None;
            }

            //-----------------------------------------------------------------
            // $todo(jeff.lill):
            //
            // We're temporarily disabling redaction to make it easier to investigate
            // Vault setup issues.  Remove this line before final launch.
            //
            //      https://github.com/jefflill/NeonForge/issues/225

            hive.SecureRunOptions = RunOptions.None;

            //-----------------------------------------------------------------

            // Assign the VPN client return subnets to the manager nodes if VPN is enabled.

            if (hive.Definition.Vpn.Enabled)
            {
                var vpnSubnet            = NetworkCidr.Parse(hive.Definition.Network.VpnPoolSubnet);
                var prefixLength         = 25;
                var nextVpnSubnetAddress = vpnSubnet.Address;

                // Note that we're not going to assign the first block of addresses in the
                // VPN subnet to any managers to prevent conflicts with addresses reserved
                // by some cloud platforms at the beginning of a subnet.  Azure for example
                // reserves 4 IP addresses for DNS servers and platform provided VPNs.

                foreach (var manager in hive.Definition.SortedManagers)
                {
                    var managerVpnSubnet = new NetworkCidr(NetHelper.AddressIncrement(nextVpnSubnetAddress, VpnOptions.ServerAddressCount), prefixLength);

                    manager.VpnPoolSubnet = managerVpnSubnet.ToString();
                    nextVpnSubnetAddress  = managerVpnSubnet.NextAddress;
                }
            }

            //-----------------------------------------------------------------
            // Try to ensure that no servers are already deployed on the IP addresses defined
            // for hive nodes because provisoning over an existing hive will likely
            // corrupt the existing hive and also probably prevent the new hive from
            // provisioning correctly.
            //
            // Note that we're not going to perform this check for the [Machine] hosting
            // environment because we're expecting the bare machines to be already running
            // with the assigned addresses and we're also not going to do this for cloud
            // environments because we're assuming that the hive will run in its own private
            // network so there'll ne no possibility of conflicts.

            if (hive.Definition.Hosting.Environment != HostingEnvironments.Machine &&
                !hive.Definition.Hosting.IsCloudProvider)
            {
                Console.WriteLine();
                Console.WriteLine("Scanning for IP address conflicts...");
                Console.WriteLine();

                var pingOptions   = new PingOptions(ttl: 32, dontFragment: true);
                var pingTimeout   = TimeSpan.FromSeconds(2);
                var pingConflicts = new List <NodeDefinition>();
                var pingAttempts  = 2;

                // I'm going to use up to 20 threads at a time here for simplicity
                // rather then doing this as async operations.

                var parallelOptions = new ParallelOptions()
                {
                    MaxDegreeOfParallelism = 20
                };

                Parallel.ForEach(hive.Definition.NodeDefinitions.Values, parallelOptions,
                                 node =>
                {
                    using (var ping = new Ping())
                    {
                        // We're going to try pinging up to [pingAttempts] times for each node
                        // just in case the network it sketchy and we're losing reply packets.

                        for (int i = 0; i < pingAttempts; i++)
                        {
                            var reply = ping.Send(node.PrivateAddress, (int)pingTimeout.TotalMilliseconds);

                            if (reply.Status == IPStatus.Success)
                            {
                                lock (pingConflicts)
                                {
                                    pingConflicts.Add(node);
                                }

                                break;
                            }
                        }
                    }
                });

                if (pingConflicts.Count > 0)
                {
                    Console.Error.WriteLine($"*** ERROR: Cannot provision the hive because [{pingConflicts.Count}] other");
                    Console.Error.WriteLine($"***        machines conflict with the following hive nodes:");
                    Console.Error.WriteLine();

                    foreach (var node in pingConflicts.OrderBy(n => NetHelper.AddressToUint(IPAddress.Parse(n.PrivateAddress))))
                    {
                        Console.Error.WriteLine($"{node.PrivateAddress, 16}:    {node.Name}");
                    }

                    Program.Exit(1);
                }
            }

            //-----------------------------------------------------------------
            // Perform basic environment provisioning.  This creates basic hive components
            // such as virtual machines, networks, load balancers, public IP addresses, security
            // groups,... as required for the environment.

            hostingManager = new HostingManagerFactory(() => HostingLoader.Initialize()).GetManager(hive, Program.LogPath);

            if (hostingManager == null)
            {
                Console.Error.WriteLine($"*** ERROR: No hosting manager for the [{hive.Definition.Hosting.Environment}] hosting environment could be located.");
                Program.Exit(1);
            }

            hostingManager.HostUsername = Program.MachineUsername;
            hostingManager.HostPassword = Program.MachinePassword;
            hostingManager.ShowStatus   = !Program.Quiet;
            hostingManager.MaxParallel  = Program.MaxParallel;
            hostingManager.WaitSeconds  = Program.WaitSeconds;

            if (hostingManager.RequiresAdminPrivileges)
            {
                Program.VerifyAdminPrivileges($"Provisioning to [{hive.Definition.Hosting.Environment}] requires elevated administrator privileges.");
            }

            if (!hostingManager.Provision(force))
            {
                Program.Exit(1);
            }

            // Get the mounted drive prefix from the hosting manager.

            hive.Definition.DrivePrefix = hostingManager.DrivePrefix;

            // Ensure that the nodes have valid IP addresses.

            hive.Definition.ValidatePrivateNodeAddresses();

            var ipAddressToServer = new Dictionary <IPAddress, SshProxy <NodeDefinition> >();

            foreach (var node in hive.Nodes.OrderBy(n => n.Name))
            {
                SshProxy <NodeDefinition> duplicateServer;

                if (node.PrivateAddress == IPAddress.Any)
                {
                    throw new ArgumentException($"Node [{node.Name}] has not been assigned an IP address.");
                }

                if (ipAddressToServer.TryGetValue(node.PrivateAddress, out duplicateServer))
                {
                    throw new ArgumentException($"Nodes [{duplicateServer.Name}] and [{node.Name}] have the same IP address [{node.Metadata.PrivateAddress}].");
                }

                ipAddressToServer.Add(node.PrivateAddress, node);
            }

            //-----------------------------------------------------------------
            // Perform basic node provisioning including operating system updates & configuration,
            // and configure OpenVPN on the manager nodes so that hive setup will be
            // able to reach the nodes on all ports.

            // Write the operation begin marker to all hive node logs.

            hive.LogLine(logBeginMarker);

            var operation = $"Preparing [{hive.Definition.Name}] nodes";

            var controller =
                new SetupController <NodeDefinition>(operation, hive.Nodes)
            {
                ShowStatus  = !Program.Quiet,
                MaxParallel = Program.MaxParallel
            };

            if (!string.IsNullOrEmpty(packageCacheUri))
            {
                hive.Definition.PackageProxy = packageCacheUri;
            }

            // Prepare the nodes.

            controller.AddWaitUntilOnlineStep(timeout: TimeSpan.FromMinutes(15));
            hostingManager.AddPostProvisionSteps(controller);
            controller.AddStep("verify OS",
                               (node, stepDelay) =>
            {
                Thread.Sleep(stepDelay);
                CommonSteps.VerifyOS(node);
            });

            controller.AddStep("prepare",
                               (node, stepDelay) =>
            {
                Thread.Sleep(stepDelay);
                CommonSteps.PrepareNode(node, hive.Definition, shutdown: false);
            },
                               stepStaggerSeconds: hive.Definition.Setup.StepStaggerSeconds);

            // Add any VPN configuration steps.

            if (hive.Definition.Vpn.Enabled)
            {
                controller.AddGlobalStep("vpn credentials", () => CreateVpnCredentials());
                controller.AddStep("vpn server",
                                   (node, stepDelay) =>
                {
                    Thread.Sleep(stepDelay);
                    ConfigManagerVpn(node);
                },
                                   node => node.Metadata.IsManager);

                // Add a step to establish a VPN connection if we're provisioning to a cloud.
                // We specifically don't want to do this if we're provisioning to a on-premise
                // datacenter because we're assuming that we're already directly connected to
                // the LAN while preparing and setting up the hive.

                if (hive.Definition.Hosting.IsCloudProvider)
                {
                    controller.AddStep("vpn connect",
                                       (manager, stepDelay) =>
                    {
                        Thread.Sleep(stepDelay);

                        // Create a hive login with just enough credentials to connect the VPN.
                        // Note that this isn't really a node specific command but I wanted to
                        // be able to display the connection status somewhere.

                        var vpnLogin =
                            new HiveLogin()
                        {
                            Definition     = hive.Definition,
                            VpnCredentials = vpnCredentials
                        };

                        // Ensure that we don't have an old VPN client for the hive running.

                        HiveHelper.VpnClose(vpnLogin.Definition.Name);

                        // ...and then start a new one.

                        HiveHelper.VpnOpen(vpnLogin,
                                           onStatus: message => manager.Status = $"{message}",
                                           onError: message => manager.Status  = $"ERROR: {message}");
                    },
                                       n => n == hive.FirstManager);
                }

                // Perform any post-VPN setup provisioning required by the hosting provider.

                hostingManager.AddPostVpnSteps(controller);
            }

            if (!controller.Run())
            {
                // Write the operation end/failed marker to all hive node logs.

                hive.LogLine(logFailedMarker);

                Console.Error.WriteLine("*** ERROR: One or more configuration steps failed.");
                Program.Exit(1);
            }

            // Write the hive login file.

            var hiveLoginPath = Program.GetHiveLoginPath(HiveConst.RootUser, hive.Definition.Name);
            var hiveLogin     = new HiveLogin()
            {
                Path                 = hiveLoginPath,
                Username             = HiveConst.RootUser,
                Definition           = hive.Definition,
                SshUsername          = Program.MachineUsername,
                SshPassword          = Program.MachinePassword,
                SshProvisionPassword = Program.MachinePassword,
                SetupPending         = true
            };

            if (hive.Definition.Vpn.Enabled)
            {
                hiveLogin.VpnCredentials = vpnCredentials;
            }

            // Generate the hive certificates.

            const int bitCount  = 2048;
            const int validDays = 365000;    // About 1,000 years.

            if (hiveLogin.HiveCertificate == null)
            {
                var hostnames = new string[]
                {
                    $"{hive.Name}.nhive.io",
                    $"*.{hive.Name}.nhive.io",
                    $"*.neon-vault.{hive.Name}.nhive.io",
                    $"*.neon-registry-cache.{hive.Name}.nhive.io",
                    $"*.neon-hivemq.{hive.Name}.nhive.io"
                };

                hiveLogin.HiveCertificate = TlsCertificate.CreateSelfSigned(hostnames, bitCount, validDays,
                                                                            issuedBy: "neonHIVE",
                                                                            issuedTo: $"neonHIVE: {hiveDefinition.Name}");

                hiveLogin.HiveCertificate.FriendlyName = $"neonHIVE: {hiveLogin.Definition.Name}";
            }

            // Persist the certificates into the hive login.

            hiveLogin.Save();

            // Write the operation end marker to all hive node logs.

            hive.LogLine(logEndMarker);
        }
Ejemplo n.º 6
0
        /// <inheritdoc/>
        public override void Run(CommandLine commandLine)
        {
            if (commandLine.HasHelpOption)
            {
                Help();
                Program.Exit(0);
            }

            // Special-case handling of the [--remove-templates] option.

            if (commandLine.HasOption("--remove-templates"))
            {
                Console.WriteLine("Removing cached virtual machine templates.");

                foreach (var fileName in Directory.GetFiles(KubeHelper.VmTemplatesFolder, "*.*", SearchOption.TopDirectoryOnly))
                {
                    File.Delete(fileName);
                }

                Program.Exit(0);
            }

            // Implement the command.

            if (KubeHelper.CurrentContext != null)
            {
                Console.Error.WriteLine("*** ERROR: You are logged into a cluster.  You need to logout before preparing another.");
                Program.Exit(1);
            }

            if (commandLine.Arguments.Length == 0)
            {
                Console.Error.WriteLine($"*** ERROR: CLUSTER-DEF expected.");
                Program.Exit(1);
            }

            clusterDefPath = commandLine.Arguments[0];
            force          = commandLine.GetFlag("--force");

            ClusterDefinition.ValidateFile(clusterDefPath, strict: true);

            var clusterDefinition = ClusterDefinition.FromFile(clusterDefPath, strict: true);

            clusterDefinition.Provisioner = $"neon-cli:{Program.Version}";  // Identify this tool/version as the cluster provisioner

            // NOTE:
            //
            // Azure has a more restrictive password policy and our default
            // machine password does not meet the requirements:
            //
            // The supplied password must be between 6-72 characters long and must
            // satisfy at least 3 of password complexity requirements from the following:
            //
            //      1. Contains an uppercase character
            //      2. Contains a lowercase character
            //      3. Contains a numeric digit
            //      4. Contains a special character
            //      5. Control characters are not allowed
            //
            // It's also probably not a great idea to use a static password when
            // provisioning VMs in public clouds because it might be possible for
            // somebody to use this fact the SSH into nodes while the cluster is
            // being setup and before we set the secure password at the end.
            //
            // This is less problematic for non-cloud environments because it's
            // likely that the hosts won't initially be able to receive inbound
            // Internet traffic and besides, we need to have a known password
            // embedded into the VM templates.
            //
            // We're going to handle this for cloud environments by looking
            // at [Program.MachinePassword].  If this is set to the default
            // machine password then we're going to replace it with a randomlly
            // generated password with a few extra characters to ensure that
            // it meets the target cloud's password requirements.  We'll use
            // a non-default password if the operator specified one.

            if (clusterDefinition.Hosting.IsCloudProvider && Program.MachinePassword == KubeConst.DefaulVmTemplatePassword)
            {
                Program.MachinePassword = NeonHelper.GetCryptoRandomPassword(20);

                // Append a string that guarantees that the generated password meets
                // cloud minimum requirements.

                Program.MachinePassword += ".Aa0";
            }

            // NOTE: Cluster prepare starts new log files.

            cluster = new ClusterProxy(clusterDefinition, Program.CreateNodeProxy <NodeDefinition>, appendToLog: false, defaultRunOptions: RunOptions.LogOutput | RunOptions.FaultOnError);

            if (KubeHelper.Config.GetContext(cluster.Definition.Name) != null)
            {
                Console.Error.WriteLine($"*** ERROR: A context named [{cluster.Definition.Name}] already exists.");
                Program.Exit(1);
            }

            // Configure global options.

            if (commandLine.HasOption("--unredacted"))
            {
                cluster.SecureRunOptions = RunOptions.None;
            }

            var failed = false;

            try
            {
                KubeHelper.Desktop.StartOperationAsync($"Preparing [{cluster.Name}]").Wait();

                //-----------------------------------------------------------------
                // Try to ensure that no servers are already deployed on the IP addresses defined
                // for cluster nodes because provisoning over an existing cluster will likely
                // corrupt the existing cluster and also probably prevent the new cluster from
                // provisioning correctly.
                //
                // Note that we're not going to perform this check for the [Machine] hosting
                // environment because we're expecting the bare machines to be already running
                // with the assigned addresses and we're also not going to do this for cloud
                // environments because we're assuming that the cluster will run in its own
                // private network so there'll ne no possibility of conflicts.

                if (cluster.Definition.Hosting.Environment != HostingEnvironments.Machine &&
                    !cluster.Definition.Hosting.IsCloudProvider)
                {
                    Console.WriteLine();
                    Console.WriteLine(" Scanning for IP address conflicts...");
                    Console.WriteLine();

                    var pingOptions   = new PingOptions(ttl: 32, dontFragment: true);
                    var pingTimeout   = TimeSpan.FromSeconds(2);
                    var pingConflicts = new List <NodeDefinition>();
                    var pingAttempts  = 2;

                    // I'm going to use up to 20 threads at a time here for simplicity
                    // rather then doing this as async operations.

                    var parallelOptions = new ParallelOptions()
                    {
                        MaxDegreeOfParallelism = 20
                    };

                    Parallel.ForEach(cluster.Definition.NodeDefinitions.Values, parallelOptions,
                                     node =>
                    {
                        using (var pinger = new Pinger())
                        {
                            // We're going to try pinging up to [pingAttempts] times for each node
                            // just in case the network it sketchy and we're losing reply packets.

                            for (int i = 0; i < pingAttempts; i++)
                            {
                                var reply = pinger.SendPingAsync(node.PrivateAddress, (int)pingTimeout.TotalMilliseconds).Result;

                                if (reply.Status == IPStatus.Success)
                                {
                                    lock (pingConflicts)
                                    {
                                        pingConflicts.Add(node);
                                    }

                                    break;
                                }
                            }
                        }
                    });

                    if (pingConflicts.Count > 0)
                    {
                        Console.Error.WriteLine($"*** ERROR: Cannot provision the cluster because [{pingConflicts.Count}] other");
                        Console.Error.WriteLine($"***        machines conflict with the following cluster nodes:");
                        Console.Error.WriteLine();

                        foreach (var node in pingConflicts.OrderBy(n => NetHelper.AddressToUint(IPAddress.Parse(n.PrivateAddress))))
                        {
                            Console.Error.WriteLine($"{node.PrivateAddress, 16}:    {node.Name}");
                        }

                        Program.Exit(1);
                    }
                }

                //-----------------------------------------------------------------
                // Perform basic environment provisioning.  This creates basic cluster components
                // such as virtual machines, networks, load balancers, public IP addresses, security
                // groups,... as required for the environment.

                hostingManager = new HostingManagerFactory(() => HostingLoader.Initialize()).GetMaster(cluster, Program.LogPath);

                if (hostingManager == null)
                {
                    Console.Error.WriteLine($"*** ERROR: No hosting manager for the [{cluster.Definition.Hosting.Environment}] hosting environment could be located.");
                    Program.Exit(1);
                }

                hostingManager.HostUsername = Program.MachineUsername;
                hostingManager.HostPassword = Program.MachinePassword;
                hostingManager.ShowStatus   = !Program.Quiet;
                hostingManager.MaxParallel  = Program.MaxParallel;
                hostingManager.WaitSeconds  = Program.WaitSeconds;

                if (hostingManager.RequiresAdminPrivileges)
                {
                    Program.VerifyAdminPrivileges($"Provisioning to [{cluster.Definition.Hosting.Environment}] requires elevated administrator privileges.");
                }

                if (!hostingManager.Provision(force))
                {
                    Program.Exit(1);
                }

                // Get the mounted drive prefix from the hosting manager.

                cluster.Definition.DrivePrefix = hostingManager.DrivePrefix;

                // Ensure that the nodes have valid IP addresses.

                cluster.Definition.ValidatePrivateNodeAddresses();

                var ipAddressToServer = new Dictionary <IPAddress, SshProxy <NodeDefinition> >();

                foreach (var node in cluster.Nodes.OrderBy(n => n.Name))
                {
                    SshProxy <NodeDefinition> duplicateServer;

                    if (node.PrivateAddress == IPAddress.Any)
                    {
                        throw new ArgumentException($"Node [{node.Name}] has not been assigned an IP address.");
                    }

                    if (ipAddressToServer.TryGetValue(node.PrivateAddress, out duplicateServer))
                    {
                        throw new ArgumentException($"Nodes [{duplicateServer.Name}] and [{node.Name}] have the same IP address [{node.Metadata.PrivateAddress}].");
                    }

                    ipAddressToServer.Add(node.PrivateAddress, node);
                }

                // We're going to use the masters as package caches unless the user
                // specifies something else.

                packageCaches = commandLine.GetOption("--package-cache");     // This overrides the cluster definition, if specified.

                if (!string.IsNullOrEmpty(packageCaches))
                {
                    cluster.Definition.PackageProxy = packageCaches;
                }

                if (string.IsNullOrEmpty(cluster.Definition.PackageProxy))
                {
                    var sbProxies = new StringBuilder();

                    foreach (var master in cluster.Masters)
                    {
                        sbProxies.AppendWithSeparator($"{master.PrivateAddress}:{NetworkPorts.AppCacherNg}");
                    }

                    cluster.Definition.PackageProxy = sbProxies.ToString();
                }

                //-----------------------------------------------------------------
                // Prepare the cluster.

                // Write the operation begin marker to all cluster node logs.

                cluster.LogLine(logBeginMarker);

                var nodesText = cluster.Nodes.Count() == 1 ? "node" : "nodes";
                var operation = $"Preparing [{cluster.Definition.Name}] {nodesText}";

                var controller =
                    new SetupController <NodeDefinition>(operation, cluster.Nodes)
                {
                    ShowStatus  = !Program.Quiet,
                    MaxParallel = Program.MaxParallel
                };

                controller.AddGlobalStep("setup details",
                                         () =>
                {
                    using (var client = new HeadendClient())
                    {
                        kubeSetupInfo = client.GetSetupInfoAsync(cluster.Definition).Result;
                    }
                });

                // Prepare the nodes.

                controller.AddWaitUntilOnlineStep(timeout: TimeSpan.FromMinutes(15));
                hostingManager.AddPostProvisionSteps(controller);
                controller.AddStep("verify OS", CommonSteps.VerifyOS);

                controller.AddStep("prepare",
                                   (node, stepDelay) =>
                {
                    Thread.Sleep(stepDelay);
                    CommonSteps.PrepareNode(node, cluster.Definition, kubeSetupInfo, shutdown: false);
                },
                                   stepStaggerSeconds: cluster.Definition.Setup.StepStaggerSeconds);

                if (!controller.Run())
                {
                    // Write the operation end/failed marker to all cluster node logs.

                    cluster.LogLine(logFailedMarker);

                    Console.Error.WriteLine("*** ERROR: One or more configuration steps failed.");
                    Program.Exit(1);
                }

                // Persist the cluster context extension.

                var contextExtensionsPath = KubeHelper.GetContextExtensionPath((KubeContextName)$"{KubeConst.RootUser}@{clusterDefinition.Name}");
                var contextExtension      = new KubeContextExtension(contextExtensionsPath)
                {
                    ClusterDefinition = clusterDefinition,
                    SshUsername       = Program.MachineUsername,
                    SshPassword       = Program.MachinePassword,
                    SetupDetails      = new KubeSetupDetails()
                    {
                        SetupPending = true
                    }
                };

                contextExtension.Save();

                // Write the operation end marker to all cluster node logs.

                cluster.LogLine(logEndMarker);
            }
            catch
            {
                failed = true;
                throw;
            }
            finally
            {
                if (!failed)
                {
                    KubeHelper.Desktop.EndOperationAsync($"Cluster [{cluster.Name}] has been prepared and is ready for setup.").Wait();
                }
                else
                {
                    KubeHelper.Desktop.EndOperationAsync($"Cluster [{cluster.Name}] prepare has failed.", failed: true).Wait();
                }
            }
        }