Esempio n. 1
0
        /// <inheritdoc/>
        public void Run(ModuleContext context)
        {
            var hive   = HiveHelper.Hive;
            var consul = HiveHelper.Consul;

            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 = "get";
            }

            state = state.ToLowerInvariant();

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

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

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

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

            var validate = context.ParseBool("validate");

            validate = validate ?? true;

            if (context.HasErrors)
            {
                return;
            }

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

            switch (state)
            {
            case "get":

                if (hive.Globals.TryGetString(name, out var output))
                {
                    context.WriteLine(AnsibleVerbosity.Important, output);
                }
                else
                {
                    context.WriteErrorLine($"Hive global [{name}] does not exist.");
                }
                break;

            case "set":

                if (validate.Value)
                {
                    hive.Globals.SetUser(name, value);
                }
                else
                {
                    hive.Globals.Set(name, value);
                }
                break;

            default:

                throw new ArgumentException($"[state={state}] is not one of the valid choices: [get] or [set].");
            }
        }
Esempio n. 2
0
        /// <inheritdoc/>
        public void Run(ModuleContext context)
        {
            var hive = HiveHelper.Hive;

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

            // Obtain common arguments.

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

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

            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, $"Reading existing credentials for [{registry}].");

            var existingCredentials = hive.Registry.GetCredentials(registry);

            if (existingCredentials != null)
            {
                context.WriteLine(AnsibleVerbosity.Info, $"Credentials for [{registry}] exist.");
            }
            else
            {
                context.WriteLine(AnsibleVerbosity.Info, $"Credentials for [{registry}] do not exist.");
            }

            var sbErrorNodes = new StringBuilder();

            switch (state)
            {
            case "absent":

                if (context.CheckMode)
                {
                    if (existingCredentials != null)
                    {
                        context.WriteLine(AnsibleVerbosity.Important, $"Credentials for [{registry}] will be deleted when CHECK-MODE is disabled.");
                    }

                    return;
                }

                // Log the hive out of the registry.

                if (existingCredentials != null)
                {
                    context.Changed = true;
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive out of the [{registry}] registry.");
                hive.Registry.Logout(registry);
                context.WriteLine(AnsibleVerbosity.Trace, $"All hive nodes are logged out.");
                break;

            case "present":

                if (context.CheckMode)
                {
                    if (existingCredentials == null)
                    {
                        context.WriteLine(AnsibleVerbosity.Important, $"Credentials for [{registry}] will be added when CHECK-MODE is disabled.");
                    }

                    return;
                }

                // Parse the [username] and [password] credentials.

                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, $"Logging the hive into the [{registry}] registry.");
                hive.Registry.Login(registry, username, password);

                // Log all of the nodes in with the new registry credentials.
                //
                // Note that we won't do this if the registry cache is enabled and we're
                // updating credentials for the Docker public registry because for this
                // configuration, only the registry cache needs the upstream credentials.
                // The nodes don't authenticate against the local registry cache.

                if (!hive.Definition.Docker.RegistryCache || !HiveHelper.IsDockerPublicRegistry(registry))
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive into the [{registry}] registry.");
                    hive.Registry.Login(registry, username, password);
                }
                else
                {
                    // Restart the hive registry cache containers with the new credentials.

                    context.WriteLine(AnsibleVerbosity.Trace, $"Restarting the hive registry caches.");

                    if (!hive.Registry.RestartCache(registry, username, password))
                    {
                        context.WriteErrorLine("Unable to restart one or more of the hive registry caches.");
                        return;
                    }

                    context.WriteLine(AnsibleVerbosity.Trace, $"Hive registry caches restarted.");
                }

                context.Changed = existingCredentials == null;
                break;

            default:

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

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

            // Obtain common arguments.

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

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

            if (!HiveDefinition.DnsHostRegex.IsMatch(hostname))
            {
                throw new ArgumentException($"[hostname={hostname}] is not a valid DNS hostname.");
            }

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

            var system = context.ParseBool("system") ?? false;

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

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

            state = state.ToLowerInvariant();

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

            if (!context.Arguments.TryGetValue <JToken>("endpoints", out var endpointsToken) && state == "present")
            {
                throw new ArgumentException($"[endpoints] module argument is required when [state={state}].");
            }

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

            var wait = context.ParseBool("wait");

            wait = wait ?? false;

            if (context.HasErrors)
            {
                return;
            }

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

            switch (state)
            {
            case "absent":

                context.WriteLine(AnsibleVerbosity.Trace, $"Check if DNS entry [{hostname}] exists.");

                if (hive.Dns.Get(hostname) != null)
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"DNS entry [{hostname}] does exist.");
                    context.WriteLine(AnsibleVerbosity.Info, $"Deleting DNS entry [{hostname}].");

                    if (context.CheckMode)
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"DNS entry [{hostname}] will be deleted when CHECK-MODE is disabled.");
                    }
                    else
                    {
                        hive.Dns.Remove(hostname, waitUntilPropagated: wait.Value);
                        context.WriteLine(AnsibleVerbosity.Trace, $"DNS entry [{hostname}] deleted.");
                    }

                    context.Changed = !context.CheckMode;
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"DNS entry [{hostname}] does not exist.");
                }
                break;

            case "present":

                var endpointsArray = endpointsToken as JArray;

                if (endpointsArray == null)
                {
                    throw new ArgumentException($"[endpoints] module argument must be an array.");
                }

                var endpoints = new List <DnsEndpoint>();

                foreach (var item in endpointsArray)
                {
                    var endpointJObject = item as JObject;

                    if (endpointJObject == null)
                    {
                        context.WriteErrorLine("One or more [endpoints] are invalid.");
                        context.Failed = true;
                        return;
                    }

                    if (!context.ValidateArguments(endpointJObject, endpointArgs, "endpoints"))
                    {
                        context.Failed = true;
                        return;
                    }

                    endpoints.Add(item.ToObject <DnsEndpoint>());
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"[{endpoints.Count}] endpoints parsed.");

                // Construct the new entry.

                var newEntry = new DnsEntry()
                {
                    Hostname  = hostname,
                    IsSystem  = system,
                    Endpoints = endpoints
                };

                // Validate the new DNS entry.

                context.WriteLine(AnsibleVerbosity.Trace, "Validating DNS entry.");

                var errors = newEntry.Validate(hive.Definition, hive.Definition.GetHostGroups(excludeAllGroup: true));

                if (errors.Count > 0)
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"[{errors.Count}] DNS entry validation errors.");

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

                    context.Failed = true;
                    return;
                }

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

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

                context.WriteLine(AnsibleVerbosity.Trace, $"Look up existing DNS entry for [{hostname}].");

                var existingEntry = hive.Dns.Get(hostname);
                var changed       = false;

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

                    changed = !NeonHelper.JsonEquals(newEntry, existingEntry);

                    if (changed)
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"DNS entries are different.");
                    }
                    else
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"DNS entries are the same.  No need to update.");
                    }
                }
                else
                {
                    changed = true;
                    context.WriteLine(AnsibleVerbosity.Trace, $"DNS entry for [hostname={hostname}] does not exist.");
                }

                if (changed)
                {
                    if (context.CheckMode)
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"DNS entry [{hostname}] will be updated when CHECK-MODE is disabled.");
                    }
                    else
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating DNS entry.");
                        hive.Dns.Set(newEntry, waitUntilPropagated: wait.Value);
                        context.WriteLine(AnsibleVerbosity.Info, $"DNS entry updated.");
                    }

                    context.Changed = !context.CheckMode;
                }

                break;

            default:

                throw new ArgumentException($"[state={state}] is not one of the valid choices: [present] or [absent].");
            }
        }
Esempio n. 4
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 (!HiveDefinition.IsValidName(name))
            {
                throw new ArgumentException($"[name={name}] is not a valid certificate name.");
            }

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

            state = state.ToLowerInvariant();

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

            if (context.HasErrors)
            {
                return;
            }

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

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

            switch (state)
            {
            case "absent":

                context.WriteLine(AnsibleVerbosity.Trace, $"Vault: checking for [{name}] certificate");

                if (hive.Certificate.Get(name) != null)
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"Vault: [{name}] certificate exists");

                    if (context.CheckMode)
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"Certificate [{name}] will be removed when CHECK-MODE is disabled.");
                    }
                    else
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Removing [{name}] certyificate.");
                        hive.Certificate.Remove(name);
                        context.WriteLine(AnsibleVerbosity.Info, $"[{name}] certificate removed");
                    }

                    context.Changed = !context.CheckMode;
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[{name}] certificate does not exist");
                }
                break;

            case "present":

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

                var certificate = TlsCertificate.Parse(value);        // This validates the certificate/private key

                context.WriteLine(AnsibleVerbosity.Trace, $"Reading [{name}] certificate");

                var existingCert = hive.Certificate.Get(name);
                var changed      = false;

                if (existingCert == null)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[{name}] certificate does not exist");
                    context.Changed = !context.CheckMode;

                    changed = true;
                }
                else if (!NeonHelper.JsonEquals(existingCert, certificate) || force)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[{name}] certificate does exists but is different");
                    context.Changed = !context.CheckMode;

                    changed = true;
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[{name}] certificate is unchanged");
                }

                if (changed)
                {
                    if (context.CheckMode)
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"Certificate [{name}] will be updated when CHECK-MODE is disabled.");
                    }
                    else
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Saving [{name}] certificate");
                        hive.Certificate.Set(name, certificate);
                        context.WriteLine(AnsibleVerbosity.Info, $"[{name}] certificate saved");
                    }
                }

                break;

            default:

                throw new ArgumentException($"[state={state}] is not one of the valid choices: [present] or [absent].");
            }
        }
Esempio n. 5
0
        /// <inheritdoc/>
        public void Run(ModuleContext context)
        {
            var hive       = HiveHelper.Hive;
            var nodeGroups = hive.Definition.GetHostGroups(excludeAllGroup: true);

            //-----------------------------------------------------------------
            // Parse the module arguments.

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

            var couchbaseArgs = CouchbaseArgs.Parse(context);

            if (couchbaseArgs == null)
            {
                return;
            }

            var query = context.ParseString("query", q => !string.IsNullOrWhiteSpace(q));

            if (context.HasErrors)
            {
                return;
            }

            var format = context.ParseEnum <CouchbaseFileFormat>("format");

            if (!format.HasValue)
            {
                format = CouchbaseFileFormat.JsonLines;
            }

            var limit = context.ParseLong("limit", v => v >= 0);

            if (!limit.HasValue || limit.Value == 0)
            {
                limit = long.MaxValue;
            }

            var output = context.ParseString("output");

            //-----------------------------------------------------------------
            // Execute the query.

            using (var bucket = couchbaseArgs.Settings.OpenBucket(couchbaseArgs.Credentials))
            {
                try
                {
                    var results = bucket.QuerySafeAsync <JObject>(query).Result;
                    var count   = Math.Min(results.Count, limit.Value);

                    using (var writer = new CouchbaseQueryResultWriter(context, format.Value, output))
                    {
                        for (int i = 0; i < count; i++)
                        {
                            var document = results[i];
                            var isLast   = i == count - 1;

                            writer.WriteDocument(document, isLast);
                        }
                    }
                }
                catch (AggregateException e)
                {
                    var queryException = e.Find <CouchbaseQueryResponseException>();

                    if (queryException == null)
                    {
                        throw;
                    }

                    foreach (var error in queryException.Errors)
                    {
                        context.WriteErrorLine($"Couchbase [{error.Code}]: {error.Message}");
                    }
                }
            }
        }
Esempio n. 6
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].");
            }
        }
Esempio n. 7
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].");
            }
        }
Esempio n. 8
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>("state", out var state))
            {
                state = "command";
            }

            state = state.ToLowerInvariant();

            var node = (SshProxy <NodeDefinition>)null;

            if (context.Arguments.TryGetValue <string>("node", out var nodeName) && !string.IsNullOrEmpty(nodeName))
            {
                try
                {
                    node = hive.GetNode(nodeName);
                }
                catch (KeyNotFoundException)
                {
                    context.WriteErrorLine($"*** ERROR: Node [{nodeName}] does not exist.");
                    return;
                }

                if (!node.Metadata.Labels.HiveMQ && !node.Metadata.Labels.HiveMQManager)
                {
                    context.WriteErrorLine($"*** ERROR: Node [{nodeName}] does not host HiveMQ.");
                    return;
                }
            }
            else
            {
                // Find a reachable hive node hosting a RabbitMQ management node.

                node = hive.GetReachableNode(n => n.Metadata.Labels.HiveMQManager, ReachableHostMode.ReturnNull);

                if (node == null)
                {
                    context.WriteErrorLine($"*** ERROR: None of the hive nodes hosting HiveMQ appear to be online.");
                    return;
                }
            }

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

            switch (state)
            {
            case "command":

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

                var commandArray = command as JArray;

                if (commandArray == null)
                {
                    throw new ArgumentException("[command] module argument must be an array specifying the command and arguments.");
                }

                if (commandArray.Count == 0)
                {
                    throw new ArgumentException("[command] module argument must be specify either the [rabbitmqctl] or [rabbitmqadmin] command.");
                }

                var commandAndArgs = new List <string>();

                foreach (var item in commandArray)
                {
                    commandAndArgs.Add(item.ToString());
                }

                var rabbitCommand = commandAndArgs.First();

                switch (rabbitCommand)
                {
                case "rabbitmqctl":
                case "rabbitmqadmin":

                    break;

                default:

                    throw new ArgumentException($"[{commandAndArgs.First()}] is not a valid command.  Only [rabbitmqctl] or [rabbitmqadmin] are supported.");
                }

                response = node.SudoCommand($"docker exec neon-hivemq {rabbitCommand}", RunOptions.None, commandAndArgs.Skip(1));

                if (response.ExitCode != 0)
                {
                    context.WriteErrorLine(response.AllText);
                }
                else
                {
                    using (var reader = new StringReader(response.OutputText))
                    {
                        foreach (var line in reader.Lines())
                        {
                            context.WriteLine(AnsibleVerbosity.Important, line);
                        }
                    }
                }

                context.Changed = true;
                break;

            default:

                throw new ArgumentException($"[state={state}] is not one of the valid choices: [present], [absent], or [rollback].");
            }
        }
Esempio n. 9
0
        /// <inheritdoc/>
        public void Run(ModuleContext context)
        {
            var hive       = HiveHelper.Hive;
            var nodeGroups = hive.Definition.GetHostGroups(excludeAllGroup: true);

            //-----------------------------------------------------------------
            // Parse the module arguments.

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

            var couchbaseArgs = CouchbaseArgs.Parse(context);

            if (couchbaseArgs == null)
            {
                return;
            }

            var format = context.ParseEnum <CouchbaseFileFormat>("format");

            if (!format.HasValue)
            {
                format = default(CouchbaseFileFormat);
            }

            var source = context.ParseString("source");

            if (string.IsNullOrEmpty(source))
            {
                context.WriteErrorLine("[source] module parameter is required.");
                return;
            }

            if (!File.Exists(source))
            {
                context.WriteErrorLine($"File [{source}] does not exist.");
                return;
            }

            var keyPattern = context.ParseString("key");
            var firstKey   = context.ParseLong("first_key") ?? 1;

            if (context.HasErrors)
            {
                return;
            }

            //-----------------------------------------------------------------
            // Import the data.

            using (var bucket = couchbaseArgs.Settings.OpenBucket(couchbaseArgs.Credentials))
            {
                var importer = new CouchbaseImporter(message => context.WriteErrorLine(message), bucket, keyPattern, firstKey, context.CheckMode);

                switch (format.Value)
                {
                case CouchbaseFileFormat.JsonArray:

                    // $todo(jeff.lill):
                    //
                    // Would be nice not to read this whole thing in memory and then
                    // effectibely duplicating it in memory again when parsing.

                    var jToken = JToken.Parse(File.ReadAllText(source));

                    if (jToken.Type != JTokenType.Array)
                    {
                        context.WriteErrorLine($"[{source}] is not a JSON array of documents.");
                        return;
                    }

                    var jArray = (JArray)jToken;

                    foreach (var item in jArray)
                    {
                        if (item.Type != JTokenType.Object)
                        {
                            context.WriteErrorLine($"[{source}] includes one or more non-document objects in the array.");
                            return;
                        }

                        importer.WriteDocument((JObject)item);
                    }
                    break;

                case CouchbaseFileFormat.JsonLines:

                    using (var reader = new StreamReader(source, Encoding.UTF8))
                    {
                        foreach (var line in reader.Lines())
                        {
                            if (line.Trim() == string.Empty)
                            {
                                continue;       // Ignore blank lines
                            }

                            var item = JToken.Parse(line);

                            if (item.Type != JTokenType.Object)
                            {
                                context.WriteErrorLine($"[{source}] includes one or more lines with non-document objects.");
                                return;
                            }

                            importer.WriteDocument((JObject)item);
                        }
                    }
                    break;

                default:

                    throw new NotImplementedException($"Format [{format}] is not implemented.");
                }

                context.Changed = importer.DocumentCount > 0;

                if (context.CheckMode)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[{importer.DocumentCount}] documents will be added when CHECK-MODE is disabled.");
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[{importer.DocumentCount}] documents were imported.");
                }
            }
        }
Esempio n. 10
0
        /// <inheritdoc/>
        public void Run(ModuleContext context)
        {
            var hive       = HiveHelper.Hive;
            var nodeGroups = hive.Definition.GetHostGroups(excludeAllGroup: true);

            //-----------------------------------------------------------------
            // Parse the module arguments.

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

            var couchbaseArgs = CouchbaseArgs.Parse(context);

            if (couchbaseArgs == null)
            {
                return;
            }

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

            state = state.ToLowerInvariant();

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

            var buildNow = context.ParseBool("build_all");

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

            var primary = context.ParseBool("primary") ?? false;

            string name;

            if (primary)
            {
                name = "#primary";
            }
            else
            {
                name = context.ParseString("name", v => new Regex(@"[a-z][a-z0-0#_]*", RegexOptions.IgnoreCase).IsMatch(v));
            }

            if (string.IsNullOrEmpty(name) && state != "build")
            {
                context.WriteErrorLine("[name] argument is required.");
            }

            var type        = (context.ParseEnum <IndexType>("using") ?? default(IndexType)).ToString().ToUpperInvariant();
            var namespaceId = context.ParseString("namespace");

            if (string.IsNullOrEmpty(namespaceId))
            {
                namespaceId = "default";
            }

            var keys = context.ParseStringArray("keys");

            var where = context.ParseString("where");
            var nodes = context.ParseStringArray("nodes");
            var defer = context.ParseBool("build_defer") ?? false;
            var wait  = context.ParseBool("build_wait") ?? true;

            var replicas = context.ParseInt("replicas");

            if (state == "present")
            {
                if (primary)
                {
                    if (keys.Count > 0)
                    {
                        context.WriteErrorLine("PRIMARY indexes do not allow any [keys].");
                        return;
                    }

                    if (!string.IsNullOrEmpty(where))
                    {
                        context.WriteErrorLine("PRIMARY indexes do not support the [where] clause.");
                        return;
                    }
                }
                else
                {
                    if (keys.Count == 0)
                    {
                        context.WriteErrorLine("Non-PRIMARY indexes must specify at least one [key].");
                        return;
                    }
                }

                if (type == "GSI" && replicas.HasValue && nodes.Count > 0)
                {
                    context.WriteErrorLine("Only one of [nodes] or [replicas] may be specified for GSI indexes.");
                    return;
                }
            }

            string keyspace;

            if (!string.IsNullOrEmpty(namespaceId) && namespaceId != "default")
            {
                keyspace = $"{namespaceId}:{couchbaseArgs.Settings.Bucket}";
            }
            else
            {
                keyspace = couchbaseArgs.Settings.Bucket;
            }

            if (context.HasErrors)
            {
                return;
            }

            //-----------------------------------------------------------------
            // Perform the operation.

            Task.Run(
                async() =>
            {
                using (var bucket = couchbaseArgs.Settings.OpenBucket(couchbaseArgs.Credentials))
                {
                    var indexId = $"{bucket.Name}.{name}";

                    // Fetch the index if it already exists.

                    var existingIndex = await bucket.GetIndexAsync(name);

                    if (existingIndex == null)
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Index [{name}] does not exist.");
                    }
                    else
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Index [{name}] exists.");
                    }

                    var existingIsPrimary = existingIndex != null && existingIndex.IsPrimary;

                    switch (state.ToLowerInvariant())
                    {
                    case "present":

                        // Generate the index creation query.

                        var sbCreateIndexCommand = new StringBuilder();

                        if (primary)
                        {
                            sbCreateIndexCommand.Append($"create primary index {CbHelper.LiteralName(name)} on {CbHelper.LiteralName(bucket.Name)}");
                        }
                        else
                        {
                            var sbKeys = new StringBuilder();

                            foreach (var key in keys)
                            {
                                sbKeys.AppendWithSeparator(key, ", ");
                            }

                            sbCreateIndexCommand.Append($"create index {CbHelper.LiteralName(name)} on {CbHelper.LiteralName(bucket.Name)} ( {sbKeys} )");
                        }

                        // Append the WHERE clause for non-PRIMARY indexes.

                        if (!primary && !string.IsNullOrEmpty(where))
                        {
                            // Ensure that the WHERE clause is surrounded by "( ... )".

                            if (!where.StartsWith("(") && !where.EndsWith(")"))
                            {
                                where = $"({where})";
                            }

                            // Now strip the parens off the where clause to be added
                            // to the query.

                            var queryWhere = where.Substring(1, where.Length - 2);

                            // Append the clause.

                            sbCreateIndexCommand.AppendWithSeparator($"where {queryWhere}");
                        }

                        // Append the USING clause.

                        sbCreateIndexCommand.AppendWithSeparator($"using {type}");

                        // Append the WITH clause for GSI indexes.

                        if (type == "GSI")
                        {
                            var sbWithSettings = new StringBuilder();

                            if (defer)
                            {
                                sbWithSettings.AppendWithSeparator("\"defer_build\":true", ", ");
                            }

                            context.WriteLine(AnsibleVerbosity.Trace, "Query for the hive nodes.");

                            var clusterNodes = await bucket.QuerySafeAsync <dynamic>("select nodes.name from system:nodes");

                            context.WriteLine(AnsibleVerbosity.Trace, $"Hive has [{clusterNodes.Count}] nodes.");

                            if ((!replicas.HasValue || replicas.Value == 0) && nodes.Count == 0)
                            {
                                // We're going to default to hosting GSI indexes explicitly
                                // on all nodes unless directed otherwise.  We'll need query
                                // the database for the current nodes.

                                foreach (JObject node in clusterNodes)
                                {
                                    nodes.Add((string)node.GetValue("name"));
                                }
                            }
                            else if (replicas.HasValue && replicas.Value > 0)
                            {
                                if (clusterNodes.Count <= replicas.Value)
                                {
                                    context.WriteErrorLine($"[replicas={replicas.Value}] cannot equal or exceed the number of Couchbase nodes.  [replicas={clusterNodes.Count - 1}] is the maximum allowed value for this hive.");
                                    return;
                                }
                            }

                            if (nodes.Count > 0)
                            {
                                sbWithSettings.AppendWithSeparator("\"nodes\": [", ", ");

                                var first = true;

                                foreach (var server in nodes)
                                {
                                    if (first)
                                    {
                                        first = false;
                                    }
                                    else
                                    {
                                        sbCreateIndexCommand.Append(",");
                                    }

                                    sbWithSettings.Append(CbHelper.Literal(server));
                                }

                                sbWithSettings.Append("]");
                            }

                            if (replicas.HasValue && type == "GSI")
                            {
                                sbWithSettings.AppendWithSeparator($"\"num_replica\":{CbHelper.Literal(replicas.Value)}", ", ");
                            }

                            if (sbWithSettings.Length > 0)
                            {
                                sbCreateIndexCommand.AppendWithSeparator($"with {{ {sbWithSettings} }}");
                            }
                        }

                        // Add or update the index.

                        if (existingIndex != null)
                        {
                            context.WriteLine(AnsibleVerbosity.Info, $"Index [{indexId}] already exists.");

                            // An index with this name already exists, so we'll compare its
                            // properties with the module parameters to determine whether we
                            // need to remove and recreate it.

                            var changed = false;

                            if (force)
                            {
                                changed = true;
                                context.WriteLine(AnsibleVerbosity.Info, "Rebuilding index because [force=yes].");
                            }
                            // Compare the old/new index types.

                            var orgType = existingIndex.Type.ToUpperInvariant();

                            if (!string.Equals(orgType, type, StringComparison.InvariantCultureIgnoreCase))
                            {
                                changed = true;
                                context.WriteLine(AnsibleVerbosity.Info, $"Rebuilding index because using changed from [{orgType}] to [{type}].");
                            }

                            // Compare the old/new index keys.

                            var keysChanged = false;

                            if (!primary)
                            {
                                var orgKeys = existingIndex.Keys;

                                if (orgKeys.Length != keys.Count)
                                {
                                    keysChanged = true;
                                }
                                else
                                {
                                    // This assumes that the order of the indexed keys doesn't
                                    // matter.

                                    var keysSet = new Dictionary <string, bool>(StringComparer.InvariantCultureIgnoreCase);

                                    for (int i = 0; i < orgKeys.Length; i++)
                                    {
                                        keysSet[(string)orgKeys[i]] = false;
                                    }

                                    for (int i = 0; i < orgKeys.Length; i++)
                                    {
                                        keysSet[CbHelper.LiteralName(keys[i])] = true;
                                    }

                                    keysChanged = keysSet.Values.Count(k => !k) > 0;
                                }

                                if (keysChanged)
                                {
                                    changed = true;

                                    var sbOrgKeys = new StringBuilder();
                                    var sbNewKeys = new StringBuilder();

                                    foreach (string key in orgKeys)
                                    {
                                        sbOrgKeys.AppendWithSeparator(key, ", ");
                                    }

                                    foreach (string key in keys)
                                    {
                                        sbNewKeys.AppendWithSeparator(key, ", ");
                                    }

                                    context.WriteLine(AnsibleVerbosity.Info, $"Rebuilding index because keys changed from [{sbOrgKeys}] to [{sbNewKeys}].");
                                }
                            }

                            // Compare the filter condition.

                            var orgWhere = existingIndex.Where;

                            if (orgWhere != where)
                            {
                                changed = true;
                                context.WriteLine(AnsibleVerbosity.Info, $"Rebuilding index because where clause changed from [{orgWhere ?? string.Empty}] to [{where ?? string.Empty}].");
                            }

                            // We need to remove and recreate the index if it differs
                            // from what was requested.

                            if (changed)
                            {
                                if (context.CheckMode)
                                {
                                    context.WriteLine(AnsibleVerbosity.Important, $"Index [{indexId}] will be rebuilt when CHECK-MODE is disabled.");
                                }
                                else
                                {
                                    context.Changed = !context.CheckMode;

                                    context.WriteLine(AnsibleVerbosity.Trace, $"Removing existing index [{indexId}].");
                                    string dropCommand;

                                    if (existingIsPrimary)
                                    {
                                        dropCommand = $"drop primary index on {CbHelper.LiteralName(bucket.Name)} using {orgType.ToUpperInvariant()}";
                                    }
                                    else
                                    {
                                        dropCommand = $"drop index {CbHelper.LiteralName(bucket.Name)}.{CbHelper.LiteralName(name)} using {orgType.ToUpperInvariant()}";
                                    }

                                    context.WriteLine(AnsibleVerbosity.Trace, $"DROP COMMAND: {dropCommand}");
                                    await bucket.QuerySafeAsync <dynamic>(dropCommand);
                                    context.WriteLine(AnsibleVerbosity.Trace, $"Dropped index [{indexId}].");

                                    context.WriteLine(AnsibleVerbosity.Trace, $"CREATE COMMAND: {sbCreateIndexCommand}");
                                    await bucket.QuerySafeAsync <dynamic>(sbCreateIndexCommand.ToString());
                                    context.WriteLine(AnsibleVerbosity.Info, $"Created index [{indexId}].");

                                    if (!defer && wait)
                                    {
                                        // Wait for the index to come online.

                                        context.WriteLine(AnsibleVerbosity.Info, $"Waiting for index [{name}] to be built.");
                                        await bucket.WaitForIndexAsync(name, "online");
                                        context.WriteLine(AnsibleVerbosity.Info, $"Completed building index [{name}].");
                                    }
                                }
                            }
                            else
                            {
                                context.WriteLine(AnsibleVerbosity.Trace, $"No changes detected for index [{indexId}].");
                            }
                        }
                        else
                        {
                            if (context.CheckMode)
                            {
                                context.WriteLine(AnsibleVerbosity.Important, $"Index [{indexId}] will be created when CHECK-MODE is disabled.");
                                context.WriteLine(AnsibleVerbosity.Trace, $"{sbCreateIndexCommand}");
                            }
                            else
                            {
                                context.Changed = true;

                                context.WriteLine(AnsibleVerbosity.Trace, $"Creating index.");
                                context.WriteLine(AnsibleVerbosity.Trace, $"CREATE COMMAND: {sbCreateIndexCommand}");
                                await bucket.QuerySafeAsync <dynamic>(sbCreateIndexCommand.ToString());
                                context.WriteLine(AnsibleVerbosity.Info, $"Created index [{indexId}].");

                                if (!defer && wait)
                                {
                                    // Wait for the index to come online.

                                    context.WriteLine(AnsibleVerbosity.Info, $"Waiting for index [{name}] to be built.");
                                    await bucket.WaitForIndexAsync(name, "online");
                                    context.WriteLine(AnsibleVerbosity.Info, $"Completed building index [{name}].");
                                }
                            }
                        }
                        break;

                    case "absent":

                        if (existingIndex != null)
                        {
                            if (context.CheckMode)
                            {
                                context.WriteLine(AnsibleVerbosity.Important, $"Index [{indexId}] will be dropped when CHECK-MODE is disabled.");
                            }
                            else
                            {
                                context.Changed = true;
                                context.WriteLine(AnsibleVerbosity.Info, $"Dropping index [{indexId}].");

                                string orgType = existingIndex.Type;
                                string dropCommand;

                                if (existingIsPrimary)
                                {
                                    dropCommand = $"drop primary index on {CbHelper.LiteralName(bucket.Name)} using {orgType.ToUpperInvariant()}";
                                }
                                else
                                {
                                    dropCommand = $"drop index {CbHelper.LiteralName(bucket.Name)}.{CbHelper.LiteralName(name)} using {orgType.ToUpperInvariant()}";
                                }

                                context.WriteLine(AnsibleVerbosity.Trace, $"COMMAND: {dropCommand}");
                                await bucket.QuerySafeAsync <dynamic>(dropCommand);
                                context.WriteLine(AnsibleVerbosity.Trace, $"Index [{indexId}] was dropped.");
                            }
                        }
                        else
                        {
                            context.WriteLine(AnsibleVerbosity.Important, $"Index [{indexId}] does not exist so there's no need to drop it.");
                        }
                        break;

                    case "build":

                        // List the names of the deferred GSI indexes.

                        var deferredIndexes = ((await bucket.ListIndexesAsync()).Where(index => index.State == "deferred" && index.Type == "gsi")).ToList();

                        context.WriteLine(AnsibleVerbosity.Info, $"[{deferredIndexes.Count}] deferred GSI indexes exist.");

                        if (deferredIndexes.Count == 0)
                        {
                            context.WriteLine(AnsibleVerbosity.Important, $"All GSI indexes have already been built.");
                            context.Changed = false;
                            return;
                        }

                        // Build the indexes (unless we're in CHECK-MODE).

                        var sbIndexList = new StringBuilder();

                        foreach (var deferredIndex in deferredIndexes)
                        {
                            sbIndexList.AppendWithSeparator($"{CbHelper.LiteralName(deferredIndex.Name)}", ", ");
                        }

                        if (context.CheckMode)
                        {
                            context.WriteLine(AnsibleVerbosity.Important, $"These GSI indexes will be built when CHECK-MODE is disabled: {sbIndexList}.");
                            context.Changed = false;
                            return;
                        }

                        var buildCommand = $"BUILD INDEX ON {CbHelper.LiteralName(bucket.Name)} ({sbIndexList})";

                        context.WriteLine(AnsibleVerbosity.Trace, $"BUILD COMMAND: {buildCommand}");
                        context.WriteLine(AnsibleVerbosity.Info, $"Building indexes: {sbIndexList}");
                        await bucket.QuerySafeAsync <dynamic>(buildCommand);
                        context.WriteLine(AnsibleVerbosity.Info, $"Build command submitted.");
                        context.Changed = true;

                        // The Couchbase BUILD INDEX command doesn't wait for the index
                        // building to complete so, we'll just spin until all of the
                        // indexes we're building are online.

                        if (wait)
                        {
                            context.WriteLine(AnsibleVerbosity.Info, $"Waiting for the indexes to be built.");

                            foreach (var deferredIndex in deferredIndexes)
                            {
                                await bucket.WaitForIndexAsync(deferredIndex.Name, "online");
                            }

                            context.WriteLine(AnsibleVerbosity.Info, $"Completed building [{deferredIndexes.Count}] indexes.");
                        }
                        break;

                    default:

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

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

            // Obtain common arguments.

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

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

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

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

            state = state.ToLowerInvariant();

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

            context.Arguments.TryGetValue <string>("text", out var configText);
            context.Arguments.TryGetValue <string>("bytes", out var configBytes);

            if (context.HasErrors)
            {
                return;
            }

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

            context.WriteLine(AnsibleVerbosity.Trace, $"Inspecting [{configName}] config.");

            var manager = hive.GetReachableManager();
            var exists  = hive.Docker.Config.Exists(configName);
            var bytes   = (byte[])null;

            if (exists)
            {
                context.WriteLine(AnsibleVerbosity.Trace, $"{configName}] config exists.");
            }
            else
            {
                context.WriteLine(AnsibleVerbosity.Trace, $"[{configName}] config does not exist.");
            }

            switch (state)
            {
            case "absent":

                if (exists)
                {
                    context.Changed = !context.CheckMode;

                    if (context.CheckMode)
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"Config [{configName}] will be removed when CHECK-MODE is disabled.");
                    }
                    else
                    {
                        context.Changed = true;
                        context.WriteLine(AnsibleVerbosity.Trace, $"Removing config [{configName}].");

                        hive.Docker.Config.Remove(configName);
                    }
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"Config [{configName}] does not exist.");
                }
                break;

            case "present":

                if (configText == null && configBytes == null)
                {
                    context.WriteErrorLine("One of the [text] or [bytes] module parameters is required.");
                    return;
                }
                else if (configText != null && configBytes != null)
                {
                    context.WriteErrorLine("Only one of [text] or [bytes] can be specified.");
                    return;
                }

                if (configBytes != null)
                {
                    try
                    {
                        bytes = Convert.FromBase64String(configBytes);
                    }
                    catch
                    {
                        context.WriteErrorLine("[bytes] is not a valid base-64 encoded value.");
                        return;
                    }
                }

                if (exists)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"Config [{configName}] already exists.");
                }
                else
                {
                    if (context.CheckMode)
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"Config [{configName}] will be created when CHECK-MODE is disabled.");
                    }
                    else
                    {
                        context.Changed = true;
                        context.WriteLine(AnsibleVerbosity.Trace, $"Creating config [{configName}].");

                        if (bytes != null)
                        {
                            hive.Docker.Config.Set(configName, bytes);
                        }
                        else
                        {
                            hive.Docker.Config.Set(configName, configText);
                        }
                    }
                }
                break;

            default:

                throw new ArgumentException($"[state={state}] is not one of the valid choices: [present] or [absent].");
            }
        }
Esempio n. 12
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].");
            }
        }
Esempio n. 13
0
        /// <summary>
        /// Implements the built-in <b>neon_dashboard</b> module.
        /// </summary>
        /// <param name="context">The module context.</param>
        public void Run(ModuleContext context)
        {
            var hive   = HiveHelper.Hive;
            var consul = HiveHelper.Consul;

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

            // Obtain common arguments.

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

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

            if (!HiveDefinition.IsValidName(name))
            {
                throw new ArgumentException($"[{name}] is not a valid dashboard name.");
            }

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

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

            state = state.ToLowerInvariant();

            if (context.HasErrors)
            {
                return;
            }

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

            switch (state)
            {
            case "absent":

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

                if (hive.Dashboard.Get(name) != null)
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"Dashboard [{name}] already exists.");

                    if (context.CheckMode)
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"Dashboard [{name}] will be deleted when CHECK-MODE is disabled.");
                    }
                    else
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"Deleting dashboard [{name}].");
                        hive.Dashboard.Remove(name);
                        context.WriteLine(AnsibleVerbosity.Trace, $"Dashboard [{name}] deleted.");
                        context.Changed = true;
                    }
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"Dashboard [{name}] does not exist.");
                }
                break;

            case "present":

                // Parse the PRESENT arguments.

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

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

                if (!Uri.TryCreate(url, UriKind.Absolute, out var urlParsed))
                {
                    throw new ArgumentException($"[url={url}] is not valid.");
                }

                url = urlParsed.ToString();

                context.Arguments.TryGetValue <string>("title", out var title);
                context.Arguments.TryGetValue <string>("folder", out var folder);
                context.Arguments.TryGetValue <string>("description", out var description);

                if (context.HasErrors)
                {
                    return;
                }

                // Build the dashboard definition from the arguments.

                var newDashboard = new HiveDashboard()
                {
                    Name        = name,
                    Title       = title,
                    Folder      = folder,
                    Url         = url,
                    Description = description
                };

                // Validate the dashboard.

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

                var errors = newDashboard.Validate(hive.Definition);

                if (errors.Count > 0)
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"[{errors.Count}] dashboard validation errors.");

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

                    context.Failed = true;
                    return;
                }

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

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

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

                var existingDashboard = hive.Dashboard.Get(name);
                var changed           = false;

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

                    changed = !NeonHelper.JsonEquals(newDashboard, existingDashboard);

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

                if (changed)
                {
                    if (context.CheckMode)
                    {
                        context.WriteLine(AnsibleVerbosity.Info, $"Dashboard [{name}] will be updated when CHECK-MODE is disabled.");
                    }
                    else
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating dashboard.");
                        hive.Dashboard.Set(newDashboard);
                        context.WriteLine(AnsibleVerbosity.Info, $"Dashboard updated.");

                        context.Changed = true;
                    }

                    context.CheckMode = !context.CheckMode;
                }

                break;

            default:

                throw new ArgumentException($"[state={state}] is not one of the valid choices: [present] or [absent].");
            }
        }