Example #1
0
        /// <inheritdoc/>
        public void Run(ModuleContext context)
        {
            TrafficManager trafficManager = null;
            bool           isPublic       = false;
            string         name           = null;
            string         ruleName       = null;
            bool           deferUpdate    = false;

            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;
            }

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

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

            switch (name)
            {
            case "private":

                trafficManager = HiveHelper.Hive.PrivateTraffic;
                isPublic       = false;
                break;

            case "public":

                trafficManager = HiveHelper.Hive.PublicTraffic;
                isPublic       = true;
                break;

            default:

                throw new ArgumentException($"[name={name}] is not a one of the valid traffic manager names: [private] or [public].");
            }

            if (state == "present" || state == "absent")
            {
                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [rule_name]");

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

                if (!HiveDefinition.IsValidName(ruleName))
                {
                    throw new ArgumentException($"[rule_name={ruleName}] is not a valid traffic manager rule name.");
                }

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

                if (!context.Arguments.TryGetValue <bool>("defer_update", out deferUpdate))
                {
                    deferUpdate = false;
                }
            }

            // We have the required arguments, so perform the operation.

            switch (state)
            {
            case "absent":

                context.WriteLine(AnsibleVerbosity.Trace, $"Check if rule [{ruleName}] exists.");

                if (trafficManager.GetRule(ruleName) != null)
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"Rule [{ruleName}] does exist.");
                    context.WriteLine(AnsibleVerbosity.Info, $"Deleting rule [{ruleName}].");

                    if (context.CheckMode)
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"Rule [{ruleName}] will be deleted when CHECK-MODE is disabled.");
                    }
                    else
                    {
                        trafficManager.RemoveRule(ruleName, deferUpdate: deferUpdate);
                        context.WriteLine(AnsibleVerbosity.Trace, $"Rule [{ruleName}] deleted.");
                        context.Changed = true;
                    }
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"Rule [{ruleName}] does not exist.");
                }
                break;

            case "present":

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

                if (!context.Arguments.TryGetValue <JObject>("rule", out var routeObject))
                {
                    throw new ArgumentException($"[rule] module argument is required when [state={state}].");
                }

                var ruleText = routeObject.ToString();

                context.WriteLine(AnsibleVerbosity.Trace, "Parsing rule");

                var newRule = TrafficRule.Parse(ruleText, strict: true);

                context.WriteLine(AnsibleVerbosity.Trace, "Rule parsed successfully");

                // Use the name argument if the deserialized rule doesn't
                // have a name.  This will make it easier on operators because
                // they won't need to specify the name twice.

                if (string.IsNullOrWhiteSpace(newRule.Name))
                {
                    newRule.Name = ruleName;
                }

                // Ensure that the name passed as an argument and the
                // name within the rule definition match.

                if (!string.Equals(ruleName, newRule.Name, StringComparison.InvariantCultureIgnoreCase))
                {
                    throw new ArgumentException($"The [rule_name={ruleName}] argument and the rule's [{nameof(TrafficRule.Name)}={newRule.Name}] property are not the same.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, "Rule name matched.");

                // Validate the rule.

                context.WriteLine(AnsibleVerbosity.Trace, "Validating rule.");

                var proxySettings     = trafficManager.GetSettings();
                var validationContext = new TrafficValidationContext(name, proxySettings);

                // $hack(jeff.lill):
                //
                // This ensures that [proxySettings.Resolvers] is initialized with
                // the built-in Docker DNS resolver.

                proxySettings.Validate(validationContext);

                // Load the TLS certificates into the validation context so we'll
                // be able to verify that any referenced certificates mactually exist.

                // $todo(jeff.lill):
                //
                // This code assumes that the operator is currently logged in with
                // root Vault privileges.  We'll have to do something else for
                // non-root logins.
                //
                // One idea might be to save two versions of the certificates.
                // The primary certificate with private key in Vault and then
                // just the public certificate in Consul and then load just
                // the public ones here.
                //
                // A good time to make this change might be when we convert to
                // use the .NET X.509 certificate implementation.

                if (!context.Login.HasVaultRootCredentials)
                {
                    throw new ArgumentException("Access Denied: Root Vault credentials are required.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, "Reading hive certificates.");

                using (var vault = HiveHelper.OpenVault(Program.HiveLogin.VaultCredentials.RootToken))
                {
                    // List the certificate key/names and then fetch each one
                    // to capture details like the expiration date and covered
                    // hostnames.

                    foreach (var certName in vault.ListAsync("neon-secret/cert").Result)
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Reading: {certName}");

                        var certificate = vault.ReadJsonAsync <TlsCertificate>(HiveHelper.GetVaultCertificateKey(certName)).Result;

                        validationContext.Certificates.Add(certName, certificate);
                    }
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"[{validationContext.Certificates.Count}] hive certificates downloaded.");

                // Actually perform the rule validation.

                newRule.Validate(validationContext);

                if (validationContext.HasErrors)
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"[{validationContext.Errors.Count}] Route validation errors.");

                    foreach (var error in validationContext.Errors)
                    {
                        context.WriteLine(AnsibleVerbosity.Important, error);
                        context.WriteErrorLine(error);
                    }

                    context.Failed = true;
                    return;
                }

                context.WriteLine(AnsibleVerbosity.Trace, "Rule is valid.");

                // Try reading any existing rule with this name and then determine
                // whether the two versions of the rule are actually different.

                context.WriteLine(AnsibleVerbosity.Trace, $"Looking for existing rule [{ruleName}]");

                var existingRule = trafficManager.GetRule(ruleName);
                var changed      = false;

                if (existingRule != null)
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"Rule exists: checking for differences.");

                    // Normalize the new and existing rules so the JSON text comparision
                    // will work properly.

                    newRule.Normalize(isPublic);
                    existingRule.Normalize(isPublic);

                    changed = !NeonHelper.JsonEquals(newRule, existingRule);

                    if (changed)
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Rules are different.");
                    }
                    else
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"Rules are the same.  No need to update.");
                    }
                }
                else
                {
                    changed = true;
                    context.WriteLine(AnsibleVerbosity.Trace, $"Rule [name={ruleName}] does not exist.");
                }

                if (changed)
                {
                    if (context.CheckMode)
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"Rule [{ruleName}] will be updated when CHECK-MODE is disabled.");
                    }
                    else
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Writing rule [{ruleName}].");
                        trafficManager.SetRule(newRule);
                        context.WriteLine(AnsibleVerbosity.Info, $"Rule updated.");
                        context.Changed = !context.CheckMode;
                    }
                }
                break;

            case "update":

                trafficManager.Update();
                context.Changed = true;
                context.WriteLine(AnsibleVerbosity.Info, $"Update signalled.");
                break;

            case "purge":

                var purgeItems         = context.ParseStringArray("purge_list");
                var purgeCaseSensitive = context.ParseBool("purge_case_sensitive");

                if (!purgeCaseSensitive.HasValue)
                {
                    purgeCaseSensitive = false;
                }

                if (purgeItems.Count == 0)
                {
                    context.WriteLine(AnsibleVerbosity.Important, $"[purge_list] is missing or empty.");
                    break;
                }

                trafficManager.Purge(purgeItems.ToArray(), caseSensitive: purgeCaseSensitive.Value);

                context.Changed = true;
                context.WriteLine(AnsibleVerbosity.Info, $"Purge request submitted.");
                break;

            default:

                throw new ArgumentException($"[state={state}] is not one of the valid choices: [present], [absent], or [update].");
            }
        }
Example #2
0
        /// <inheritdoc/>
        public override void Run(CommandLine commandLine)
        {
            if (commandLine.HasHelpOption || commandLine.Arguments.Length == 0)
            {
                Console.WriteLine(usage);
                Program.Exit(0);
            }

            Program.ConnectHive();

            // Process the command arguments.

            var trafficManager = (TrafficManager)null;
            var yaml           = commandLine.HasOption("--yaml");
            var directorName   = commandLine.Arguments.FirstOrDefault();
            var isPublic       = false;

            switch (directorName)
            {
            case "help":

                // $hack: This isn't really a traffic manager name.

                Console.WriteLine(ruleHelp);
                Program.Exit(0);
                break;

            case "public":

                trafficManager = HiveHelper.Hive.PublicTraffic;
                isPublic       = true;
                break;

            case "private":

                trafficManager = HiveHelper.Hive.PrivateTraffic;
                isPublic       = false;
                break;

            default:

                Console.Error.WriteLine($"*** ERROR: Load balancer name must be one of [public] or [private] ([{directorName}] is not valid).");
                Program.Exit(1);
                break;
            }

            commandLine = commandLine.Shift(1);

            var command = commandLine.Arguments.FirstOrDefault();

            if (command == null)
            {
                Console.WriteLine(usage);
                Program.Exit(1);
            }

            commandLine = commandLine.Shift(1);

            string ruleName;

            switch (command)
            {
            case "get":

                ruleName = commandLine.Arguments.FirstOrDefault();

                if (string.IsNullOrEmpty(ruleName))
                {
                    Console.Error.WriteLine("*** ERROR: [RULE] argument expected.");
                    Program.Exit(1);
                }

                if (!HiveDefinition.IsValidName(ruleName))
                {
                    Console.Error.WriteLine($"*** ERROR: [{ruleName}] is not a valid rule name.");
                    Program.Exit(1);
                }

                // Fetch a specific traffic manager rule and output it.

                var rule = trafficManager.GetRule(ruleName);

                if (rule == null)
                {
                    Console.Error.WriteLine($"*** ERROR: Load balancer [{directorName}] rule [{ruleName}] does not exist.");
                    Program.Exit(1);
                }

                Console.WriteLine(yaml ? rule.ToYaml() : rule.ToJson());
                break;

            case "haproxy":
            case "haproxy-bridge":
            case "varnish":

                // We're going to download the traffic manager's ZIP archive containing the
                // [haproxy.cfg] or [varnish.vcl] file, extract and write it to the console.

                using (var consul = HiveHelper.OpenConsul())
                {
                    var proxy        = command.Equals("haproxy-bridge", StringComparison.InvariantCultureIgnoreCase) ? directorName + "-bridge" : directorName;
                    var confKey      = $"neon/service/neon-proxy-manager/proxies/{proxy}/proxy-conf";
                    var confZipBytes = consul.KV.GetBytesOrDefault(confKey).Result;

                    if (confZipBytes == null)
                    {
                        Console.Error.WriteLine($"*** ERROR: Proxy ZIP configuration was not found in Consul at [{confKey}].");
                        Program.Exit(1);
                    }

                    using (var msZipData = new MemoryStream(confZipBytes))
                    {
                        using (var zip = new ZipFile(msZipData))
                        {
                            var file  = command.Equals("varnish", StringComparison.InvariantCultureIgnoreCase) ? "varnish.vcl" : "haproxy.cfg";
                            var entry = zip.GetEntry(file);

                            if (entry == null || !entry.IsFile)
                            {
                                Console.Error.WriteLine($"*** ERROR: Proxy ZIP configuration in Consul at [{confKey}] appears to be corrupt.  Cannot locate the [{file}] entry.");
                                Program.Exit(1);
                            }

                            using (var entryStream = zip.GetInputStream(entry))
                            {
                                using (var reader = new StreamReader(entryStream))
                                {
                                    foreach (var line in reader.Lines())
                                    {
                                        Console.WriteLine(line);
                                    }
                                }
                            }
                        }
                    }
                }
                break;

            case "inspect":

                Console.WriteLine(NeonHelper.JsonSerialize(trafficManager.GetDefinition(), Formatting.Indented));
                break;

            case "list":
            case "ls":

                var showAll = commandLine.HasOption("--all");
                var showSys = commandLine.HasOption("--sys");
                var rules   = trafficManager.ListRules(
                    r =>
                {
                    if (showAll)
                    {
                        return(true);
                    }
                    else if (showSys)
                    {
                        return(r.System);
                    }
                    else
                    {
                        return(!r.System);
                    }
                });

                Console.WriteLine();
                Console.WriteLine($"[{rules.Count()}] {trafficManager.Name} rules");
                Console.WriteLine();

                foreach (var item in rules)
                {
                    Console.WriteLine(item.Name);
                }

                Console.WriteLine();
                break;

            case "purge":

                var purgeUri = commandLine.Arguments.FirstOrDefault();

                if (string.IsNullOrEmpty(purgeUri))
                {
                    Console.Error.WriteLine("*** ERROR: [URI-PATTERN] or [ALL] argument expected.");
                }

                if (purgeUri.Equals("all", StringComparison.InvariantCultureIgnoreCase))
                {
                    if (!commandLine.HasOption("--force") && !Program.PromptYesNo($"*** Are you sure you want to purge all cached items for [{directorName.ToUpperInvariant()}]?"))
                    {
                        return;
                    }

                    trafficManager.PurgeAll();
                }
                else
                {
                    trafficManager.Purge(new string[] { purgeUri });
                }

                Console.WriteLine();
                Console.WriteLine("Purge request submitted.");
                Console.WriteLine();
                break;

            case "update":

                trafficManager.Update();
                break;

            case "remove":
            case "rm":

                ruleName = commandLine.Arguments.FirstOrDefault();

                if (string.IsNullOrEmpty(ruleName))
                {
                    Console.Error.WriteLine("*** ERROR: [RULE] argument expected.");
                    Program.Exit(1);
                }

                if (!HiveDefinition.IsValidName(ruleName))
                {
                    Console.Error.WriteLine($"*** ERROR: [{ruleName}] is not a valid rule name.");
                    Program.Exit(1);
                }

                if (trafficManager.RemoveRule(ruleName))
                {
                    Console.Error.WriteLine($"Deleted load balancer [{directorName}] rule [{ruleName}].");
                }
                else
                {
                    Console.Error.WriteLine($"*** ERROR: Load balancer [{directorName}] rule [{ruleName}] does not exist.");
                    Program.Exit(1);
                }
                break;

            case "set":

                // $todo(jeff.lill):
                //
                // It would be really nice to download the existing rules and verify that
                // adding the new rule won't cause conflicts.  Currently errors will be
                // detected only by the [neon-proxy-manager] which will log them and cease
                // updating the hive until the errors are corrected.
                //
                // An alternative would be to have some kind of service available in the
                // hive to do this for us or perhaps having [neon-proxy-manager] generate
                // a summary of all of the certificates (names, covered hostnames, and
                // expiration dates) and save this to Consul so it would be easy to
                // download.  Perhaps do the same for the rules?

                if (commandLine.Arguments.Length != 2)
                {
                    Console.Error.WriteLine("*** ERROR: FILE or [-] argument expected.");
                    Program.Exit(1);
                }

                // Load the rule.  Note that we support reading rules as JSON or
                // YAML, automatcially detecting the format.  We always persist
                // rules as JSON though.

                var ruleFile = commandLine.Arguments[1];

                string ruleText;

                if (ruleFile == "-")
                {
                    using (var input = Console.OpenStandardInput())
                    {
                        using (var reader = new StreamReader(input, detectEncodingFromByteOrderMarks: true))
                        {
                            ruleText = reader.ReadToEnd();
                        }
                    }
                }
                else
                {
                    ruleText = File.ReadAllText(ruleFile);
                }

                var trafficManagerRule = TrafficRule.Parse(ruleText, strict: true);

                ruleName = trafficManagerRule.Name;

                if (!HiveDefinition.IsValidName(ruleName))
                {
                    Console.Error.WriteLine($"*** ERROR: [{ruleName}] is not a valid rule name.");
                    Program.Exit(1);
                }

                // Validate a clone of the rule with any implicit frontends.

                var clonedRule = NeonHelper.JsonClone(trafficManagerRule);
                var context    = new TrafficValidationContext(directorName, null)
                {
                    ValidateCertificates = false        // Disable this because we didn't download the certs (see note above)
                };

                clonedRule.Validate(context);
                clonedRule.Normalize(isPublic);

                if (context.HasErrors)
                {
                    Console.Error.WriteLine("*** ERROR: One or more rule errors:");
                    Console.Error.WriteLine();

                    foreach (var error in context.Errors)
                    {
                        Console.Error.WriteLine(error);
                    }

                    Program.Exit(1);
                }

                if (trafficManager.SetRule(trafficManagerRule))
                {
                    Console.WriteLine($"Load balancer [{directorName}] rule [{ruleName}] has been updated.");
                }
                else
                {
                    Console.WriteLine($"Load balancer [{directorName}] rule [{ruleName}] has been added.");
                }
                break;

            case "settings":

                var settingsFile = commandLine.Arguments.FirstOrDefault();

                if (string.IsNullOrEmpty(settingsFile))
                {
                    Console.Error.WriteLine("*** ERROR: [-] or FILE argument expected.");
                    Program.Exit(1);
                }

                string settingsText;

                if (settingsFile == "-")
                {
                    settingsText = NeonHelper.ReadStandardInputText();
                }
                else
                {
                    settingsText = File.ReadAllText(settingsFile);
                }

                var trafficManagerSettings = TrafficSettings.Parse(settingsText, strict: true);

                trafficManager.UpdateSettings(trafficManagerSettings);
                Console.WriteLine($"Traffic manager [{directorName}] settings have been updated.");
                break;

            case "status":

                using (var consul = HiveHelper.OpenConsul())
                {
                    var statusJson = consul.KV.GetStringOrDefault($"neon/service/neon-proxy-manager/status/{directorName}").Result;

                    if (statusJson == null)
                    {
                        Console.Error.WriteLine($"*** ERROR: Status for traffic manager [{directorName}] is not currently available.");
                        Program.Exit(1);
                    }

                    var trafficManagerStatus = NeonHelper.JsonDeserialize <TrafficStatus>(statusJson);

                    Console.WriteLine();
                    Console.WriteLine($"Snapshot Time: {trafficManagerStatus.TimestampUtc} (UTC)");
                    Console.WriteLine();

                    using (var reader = new StringReader(trafficManagerStatus.Status))
                    {
                        foreach (var line in reader.Lines())
                        {
                            Console.WriteLine(line);
                        }
                    }
                }
                break;

            default:

                Console.Error.WriteLine($"*** ERROR: Unknown command: [{command}]");
                Program.Exit(1);
                break;
            }
        }