예제 #1
0
        /// <summary>
        /// Installs hypervisor guest integration services.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseInstallGuestIntegrationServices(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            var hostingEnvironment = controller.Get <HostingEnvironment>(KubeSetupProperty.HostingEnvironment);

            // This currently applies only to on-premise hypervisors.

            if (!KubeHelper.IsOnPremiseHypervisorEnvironment(hostingEnvironment))
            {
                return;
            }

            InvokeIdempotent("base/guest-integration",
                             () =>
            {
                controller.LogProgress(this, verb: "setup", message: "guest integration services");

                var guestServicesScript =
                    $@"#!/bin/bash
set -euo pipefail

cat <<EOF >> /etc/initramfs-tools/modules
hv_vmbus
hv_storvsc
hv_blkvsc
hv_netvsc
EOF

{KubeNodeFolder.Bin}/safe-apt-get install -yq linux-virtual linux-cloud-tools-virtual linux-tools-virtual
update-initramfs -u
";
                SudoCommand(CommandBundle.FromScript(guestServicesScript), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #2
0
        /// <summary>
        /// Disables the Linux memory swap file.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseDisableSwap(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/swap-disable",
                             () =>
            {
                controller.LogProgress(this, verb: "disable", message: "swap");

                // Disable SWAP by editing [/etc/fstab] to remove the [/swap.img...] line.

                var sbFsTab = new StringBuilder();

                using (var reader = new StringReader(DownloadText("/etc/fstab")))
                {
                    foreach (var line in reader.Lines())
                    {
                        if (!line.Contains("/swap.img"))
                        {
                            sbFsTab.AppendLine(line);
                        }
                    }
                }

                UploadText("/etc/fstab", sbFsTab, permissions: "644", owner: "root:root");
            });
        }
예제 #3
0
        /// <summary>
        /// Disables <b>cloud-init</b>.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseDisableCloudInit(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            var hostingEnvironment = controller.Get <HostingEnvironment>(KubeSetupProperty.HostingEnvironment);

            // Do this only for non-cloud environments.

            if (KubeHelper.IsCloudEnvironment(hostingEnvironment))
            {
                return;
            }

            InvokeIdempotent("base/cloud-init",
                             () =>
            {
                controller.LogProgress(this, verb: "disable", message: "cloud-init");

                var disableCloudInitScript =
                    $@"
set -euo pipefail
mkdir -p /etc/cloud
touch /etc/cloud/cloud-init.disabled
";
                SudoCommand(CommandBundle.FromScript(disableCloudInitScript), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #4
0
        /// <summary>
        /// Removes unneeded packages.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseRemovePackages(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/remove-packages",
                             () =>
            {
                controller.LogProgress(this, verb: "remove", message: "unneeded packages");

                // Purge unneeded packages.

                var removePackagesScript =
                    $@"
set -euo pipefail

{KubeNodeFolder.Bin}/safe-apt-get purge -y \
    git git-man \
    iso-codes \
    locales \
    manpages man-db \
    python3-twisted \
    snapd \
    vim vim-runtime vim-tiny

{KubeNodeFolder.Bin}/safe-apt-get autoremove -y
";
                SudoCommand(CommandBundle.FromScript(removePackagesScript), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #5
0
        /// <summary>
        /// Performs low-level initialization of a cluster.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        /// <param name="upgradeLinux">Optionally upgrade the node's Linux distribution (defaults to <c>false</c>).</param>
        /// <param name="patchLinux">Optionally apply any available Linux security patches (defaults to <c>true</c>).</param>
        public void BaseInitialize(ISetupController controller, bool upgradeLinux = false, bool patchLinux = true)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            var hostingEnvironment = controller.Get <HostingEnvironment>(KubeSetupProperty.HostingEnvironment);

            // Wait for boot/connect.

            controller.LogProgress(this, verb: "login", message: $"[{KubeConst.SysAdminUser}]");

            WaitForBoot();
            VerifyNodeOS(controller);
            BaseDisableSwap(controller);
            BaseInstallToolScripts(controller);
            BaseConfigureDebianFrontend(controller);
            UpdateRootCertificates();
            BaseInstallPackages(controller);
            BaseConfigureApt(controller);
            BaseConfigureBashEnvironment(controller);
            BaseConfigureDnsIPv4Preference(controller);
            BaseRemoveSnap(controller);
            BaseRemovePackages(controller);

            if (patchLinux)
            {
                BasePatchLinux(controller);
            }

            BaseCreateKubeFolders(controller);

            if (upgradeLinux)
            {
                BaseUpgradeLinuxDistribution(controller);
            }
        }
예제 #6
0
        /// <summary>
        /// <para>
        /// Installs the tool scripts, making them executable.
        /// </para>
        /// <note>
        /// Any <b>".sh"</b> file extensions will be removed for ease-of-use.
        /// </note>
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseInstallToolScripts(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/tool-scripts",
                             () =>
            {
                controller.LogProgress(this, verb: "setup", message: "tools (base)");

                // Upload any tool scripts to the neonKUBE bin folder, stripping
                // the [*.sh] file type (if present) and then setting execute
                // permissions.

                var scriptsFolder = KubeHelper.Resources.GetDirectory("/Tools");        // $hack(jefflill): https://github.com/nforgeio/neonKUBE/issues/1121

                foreach (var file in scriptsFolder.GetFiles())
                {
                    var targetName = file.Name;

                    if (Path.GetExtension(targetName) == ".sh")
                    {
                        targetName = Path.GetFileNameWithoutExtension(targetName);
                    }

                    using (var toolStream = file.OpenStream())
                    {
                        UploadText(LinuxPath.Combine(KubeNodeFolder.Bin, targetName), toolStream, permissions: "744");
                    }
                }
            });
        }
예제 #7
0
        /// <summary>
        /// Upgrades the Linux distribution on the node.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseUpgradeLinuxDistribution(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/upgrade-linux",
                             () =>
            {
                controller.LogProgress(this, verb: "upgrade", message: "linux distribution");
                UpgradeLinuxDistribution();
            });
        }
예제 #8
0
        /// <summary>
        /// Updates Linux by applying just the outstanding security updates.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BasePatchLinux(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/patch-linux",
                             () =>
            {
                controller.LogProgress(this, verb: "patch", message: "linux");
                UpdateLinux();
            });
        }
예제 #9
0
        /// <summary>
        /// Returns the <see cref="IKubernetes"/> client persisted in the controller passed.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        /// <returns>The <see cref="Kubernetes"/> client.</returns>
        /// <exception cref="InvalidOperationException">
        /// Thrown when there is no persisted Kubernetes client, indicating that <see cref="ConnectCluster(ISetupController)"/>
        /// has not been called yet.
        /// </exception>
        public static IKubernetes GetK8sClient(ISetupController controller)
        {
            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));

            try
            {
                return(controller.Get <IKubernetes>(KubeSetupProperty.K8sClient));
            }
            catch (Exception e)
            {
                throw new InvalidOperationException($"Cannot retrieve the Kubernetes client because the cluster hasn't been connected via [{nameof(ConnectCluster)}()].", e);
            }
        }
예제 #10
0
        /// <summary>
        /// Customizes the OpenSSH configuration on a
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseConfigureOpenSsh(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/openssh",
                             () =>
            {
                // Upload the OpenSSH server configuration and restart OpenSSH.

                controller.LogProgress(this, verb: "configure", message: "openssh");

                UploadText("/etc/ssh/sshd_config", KubeHelper.OpenSshConfig);
                SudoCommand("systemctl restart sshd", RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #11
0
        /// <summary>
        /// Installs the Helm charts as a single ZIP archive written to the
        /// neonKUBE node's Helm folder.
        /// </summary>
        /// <param name="node">The node instance.</param>
        /// <param name="controller">The setup controller.</param>
        public static void NodeInstallHelmArchive(this ILinuxSshProxy node, ISetupController controller)
        {
            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));

            using (var ms = new MemoryStream())
            {
                controller.LogProgress(node, verb: "setup", message: "helm charts (zip)");

                var helmFolder = KubeSetup.Resources.GetDirectory("/Helm");    // $hack(jefflill): https://github.com/nforgeio/neonKUBE/issues/1121

                helmFolder.Zip(ms, searchOptions: SearchOption.AllDirectories, zipOptions: StaticZipOptions.LinuxLineEndings);

                ms.Seek(0, SeekOrigin.Begin);
                node.Upload(LinuxPath.Combine(KubeNodeFolder.Helm, "charts.zip"), ms, permissions: "660");
            }
        }
예제 #12
0
        /// <summary>
        /// Configures the Debian frontend terminal to non-interactive.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseConfigureBashEnvironment(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/bash-environment",
                             () =>
            {
                controller.LogProgress(this, verb: "configure", message: "environment variables");

                var script =
                    @"
set -euo pipefail
echo '. /etc/environment' > /etc/profile.d/env.sh
";
                SudoCommand(CommandBundle.FromScript(script), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #13
0
        /// <summary>
        /// Configures the Debian frontend terminal to non-interactive.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseConfigureDebianFrontend(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/debian-frontend",
                             () =>
            {
                controller.LogProgress(this, verb: "configure", message: "tty");

                // We need to append [DEBIAN_FRONTEND] to the [/etc/environment] file but
                // we haven't installed [zip/unzip] yet so we can't use a command bundle.
                // We'll just use [tee] in this case.

                SudoCommand("echo DEBIAN_FRONTEND=noninteractive | tee -a /etc/environment", RunOptions.Defaults | RunOptions.FaultOnError);
                SudoCommand("echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections", RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #14
0
        /// <summary>
        /// Ubuntu defaults DNS to prefer IPv6 lookups over IPv4 which can cause
        /// performance problems.  This method reconfigures DNS to favor IPv4.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseConfigureDnsIPv4Preference(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/dns-ipv4",
                             () =>
            {
                controller.LogProgress(this, verb: "configure", message: "dns ipv4 preference");

                var script =
                    @"
set -euo pipefail

#------------------------------------------------------------------------------
# We need to modify how [getaddressinfo] handles DNS lookups 
# so that IPv4 lookups are preferred over IPv6.  Ubuntu prefers
# IPv6 lookups by default.  This can cause performance problems 
# because in most situations right now, the server would be doing
# 2 DNS queries, one for AAAA (IPv6) which will nearly always 
# fail (at least until IPv6 is more prevalent) and then querying
# for the for A (IPv4) record.
#
# This can also cause issues when the server is behind a NAT.
# I ran into a situation where [apt-get update] started failing
# because one of the archives had an IPv6 address in addition to
# an IPv4.  Here's a note about this issue:
#
#       http://ubuntuforums.org/showthread.php?t=2282646
#
# We're going to uncomment the line below in [gai.conf] and
# change it to the following line to prefer IPv4.
#
#       #precedence ::ffff:0:0/96  10
#       precedence ::ffff:0:0/96  100
#
# Note that this does not completely prevent the resolver from
# returning IPv6 addresses.  You'll need to prvent this on an
# application by application basis, like using the [curl -4] option.

sed -i 's!^#precedence ::ffff:0:0/96  10$!precedence ::ffff:0:0/96  100!g' /etc/gai.conf
";
                SudoCommand(CommandBundle.FromScript(script), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #15
0
        /// <summary>
        /// Updates the node hostname and related configuration.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        private void UpdateHostname(ISetupController controller)
        {
            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));

            controller.LogProgress(this, verb: "configure", message: "hostname");

            // Update the hostname.

            SudoCommand($"hostnamectl set-hostname {Name}");

            // Update the [/etc/hosts] file to resolve the new hostname.

            // $hack(jefflill):
            //
            // We need to obtain the private address of the node when [TMetadata]
            // is a [NodeDefinition] since the [Address] for cloud cluster nodes
            // will reference the external load balancer IP.
            //
            // We'll use [Address] when the [TMetadata != NodeDefinition].

            var nodeAddress    = Address.ToString();
            var nodeDefinition = Metadata as NodeDefinition;

            if (nodeDefinition != null)
            {
                nodeAddress = nodeDefinition.Address;
            }

            var separator = new string(' ', Math.Max(16 - nodeAddress.Length, 1));
            var sbHosts   = new StringBuilder();

            sbHosts.Append(
                $@"
127.0.0.1	    localhost
127.0.0.1       kubernetes-control-plane neon-desktop
{nodeAddress}{separator}{Name} {KubeConst.LocalClusterRegistry}
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters
");
            UploadText("/etc/hosts", sbHosts, tabStop: 4, outputEncoding: Encoding.UTF8);
        }
예제 #16
0
        /// <summary>
        /// Waits for all of the dependency tasks to complete.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        /// <returns>Thre tracking <see cref="Task"/>.</returns>
        /// <exception cref="InvalidOperationException">Thrown if <see cref="WaitAsync"/> has already been called.</exception>
        public async Task WaitAsync(ISetupController controller)
        {
            await SyncContext.Clear;

            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));

            if (waitAsyncCalled)
            {
                throw new NotSupportedException($"Cannot call [{nameof(Add)}()] after [{nameof(WaitAsync)}()] has been called.");
            }

            foreach (var pendingTask in pendingTasks)
            {
                if (pendingTask.Node != null)
                {
                    controller.LogProgress(pendingTask.Node, verb: pendingTask.Verb, message: pendingTask.Message);
                }
                else
                {
                    controller.LogProgress(verb: pendingTask.Verb, message: pendingTask.Message);
                }

                if (controller.IsCancelPending)
                {
                    return;
                }

                await pendingTask.Task;

                if (pendingTask.Node != null)
                {
                    controller.LogProgress(pendingTask.Node, verb: pendingTask.Verb, message: pendingTask.Message);
                }
                else
                {
                    controller.LogProgress(verb: pendingTask.Verb, message: pendingTask.Message);
                }
            }

            IsComplete = true;
        }
예제 #17
0
        /// <summary>
        /// Removes any installed snaps as well as the entire snap infrastructure.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseRemoveSnap(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/remove-snap",
                             () =>
            {
                controller.LogProgress(this, verb: "remove", message: "snap");

                var script =
                    @"
set -euo pipefail

safe-apt-get purge snapd -yq

rm -rf ~/snap
rm -rf /var/cache/snapd
rm -rf /snap
";
                SudoCommand(CommandBundle.FromScript(script), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #18
0
        /// <summary>
        /// Create the node folders required by neoneKUBE.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseCreateKubeFolders(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/folders",
                             () =>
            {
                controller.LogProgress(this, verb: "create", message: "node folders");

                var folderScript =
                    $@"
set -euo pipefail

mkdir -p {KubeNodeFolder.Bin}
chmod 750 {KubeNodeFolder.Bin}

mkdir -p {KubeNodeFolder.Config}
chmod 750 {KubeNodeFolder.Config}

mkdir -p {KubeNodeFolder.Setup}
chmod 750 {KubeNodeFolder.Setup}

mkdir -p {KubeNodeFolder.Helm}
chmod 750 {KubeNodeFolder.Helm}

mkdir -p {KubeNodeFolder.State}
chmod 750 {KubeNodeFolder.State}

mkdir -p {KubeNodeFolder.State}/setup
chmod 750 {KubeNodeFolder.State}/setup

mkdir -p {KubeNodeFolder.NeonRun}
chmod 740 {KubeNodeFolder.NeonRun}
";
                SudoCommand(CommandBundle.FromScript(folderScript), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #19
0
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        internal SetupClusterStatus(ISetupController controller)
        {
            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));

            this.isClone      = false;
            this.controller   = controller;
            this.cluster      = controller.Get <ClusterProxy>(KubeSetupProperty.ClusterProxy);
            this.GlobalStatus = controller.GlobalStatus;
            this.globalStatus = this.GlobalStatus;

            // Initialize the cluster node/host status instances.

            this.Nodes = new List <SetupNodeStatus>();

            foreach (var node in cluster.Nodes)
            {
                Nodes.Add(new SetupNodeStatus(node, node.NodeDefinition));
            }

            this.Hosts = new List <SetupNodeStatus>();

            foreach (var host in cluster.Hosts)
            {
                Hosts.Add(new SetupNodeStatus(host, new object()));
            }

            // Initialize the setup steps.

            this.Steps = new List <SetupStepStatus>();

            foreach (var step in controller.GetStepStatus().Where(step => !step.IsQuiet))
            {
                Steps.Add(step);
            }

            this.CurrentStep = Steps.SingleOrDefault(step => step.Number == controller.CurrentStepNumber);
        }
예제 #20
0
        /// <summary>
        /// Disables DHCP.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseDisableDhcp(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            var hostingEnvironment = controller.Get <HostingEnvironment>(KubeSetupProperty.HostingEnvironment);

            InvokeIdempotent("base/dhcp",
                             () =>
            {
                controller.LogProgress(this, verb: "disable", message: "dhcp");

                var initNetPlanScript =
                    $@"
set -euo pipefail

rm -rf /etc/netplan/*

cat <<EOF > /etc/netplan/no-dhcp.yaml
# This file is used to disable the network when a new VM is created 
# from a template is booted.  The [neon-init] service handles network
# provisioning in conjunction with the cluster prepare step.
#
# Cluster prepare inserts a virtual DVD disc with a script that
# handles the network configuration which [neon-init] will
# execute.

network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: no
EOF
";
                SudoCommand(CommandBundle.FromScript(initNetPlanScript), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #21
0
        /// <summary>
        /// Performs common node configuration.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        /// <param name="clusterManifest">The cluster manifest.</param>
        public void SetupNode(ISetupController controller, ClusterManifest clusterManifest)
        {
            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));
            Covenant.Requires <ArgumentNullException>(clusterManifest != null, nameof(clusterManifest));

            var nodeDefinition    = NeonHelper.CastTo <NodeDefinition>(Metadata);
            var clusterDefinition = Cluster.Definition;
            var hostingManager    = controller.Get <IHostingManager>(KubeSetupProperty.HostingManager);

            InvokeIdempotent("setup/node",
                             () =>
            {
                PrepareNode(controller);
                ConfigureEnvironmentVariables(controller);
                SetupPackageProxy(controller);
                UpdateHostname(controller);
                NodeInitialize(controller);
                NodeInstallCriO(controller, clusterManifest);
                NodeInstallIPVS(controller);
                NodeInstallPodman(controller);
                NodeInstallKubernetes(controller);
                SetupKublet(controller);
            });
        }
예제 #22
0
        /// <summary>
        /// Gets a list of taints that are currently applied to all nodes matching the given node label/value pair.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        /// <param name="labelKey">The target nodes label key.</param>
        /// <param name="labelValue">The target nodes label value.</param>
        /// <returns>The taint list.</returns>
        public static async Task <List <V1Taint> > GetTaintsAsync(ISetupController controller, string labelKey, string labelValue)
        {
            await SyncContext.Clear;

            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));

            var taints = new List <V1Taint>();

            foreach (var node in (await GetK8sClient(controller).ListNodeAsync()).Items.Where(node => node.Metadata.Labels.Any(label => label.Key == labelKey && label.Value == labelValue)))
            {
                if (node.Spec.Taints?.Count() > 0)
                {
                    foreach (var taint in node.Spec.Taints)
                    {
                        if (!taints.Any(t => t.Key == taint.Key && t.Effect == taint.Effect && t.Value == taint.Value))
                        {
                            taints.Add(taint);
                        }
                    }
                }
            }

            return(taints);
        }
예제 #23
0
        /// <summary>
        /// Configures NTP and also installs some tool scripts for managing this.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void SetupConfigureNtp(ISetupController controller)
        {
            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));

            InvokeIdempotent("setup/ntp",
                             () =>
            {
                controller.LogProgress(this, verb: "configure", message: "ntp");

                var clusterDefinition = this.Cluster.Definition;
                var nodeDefinition    = this.NodeDefinition;

                var script =
                    $@"
mkdir -p /var/log/ntp

cat <<EOF > /etc/ntp.conf
# For more information about this file, see the man pages
# ntp.conf(5), ntp_acc(5), ntp_auth(5), ntp_clock(5), ntp_misc(5), ntp_mon(5).

# Specifies that NTP will implicitly trust the time source, 
# even if it is way off from the current system time.  This
# can happen when:
#
#       1. The BIOS time is set incorrectly
#       2. The machine is a VM that woke up from a long sleep
#       3. The time source is messed up
#       4. Somebody is screwing with us by sending bad time responses
#
# Normally, NTP is configured to panic when it sees a difference
# of 1000 seconds or more between the source and the system time
# and NTP will log an error and terminate.
#
# NTP is configured to start with the [-g] option, which will 
# ignore 1000+ second differences when the service starts.  This
# handles issues #1 and #2 above.
#
# The setting below disables the 1000 second check and also allows
# NTP to modify the system clock in steps of 1 second (normally,
# this is much smaller).

# \$todo(jefflill):
#
# This was the default setting when NTP was installed but I'm not 
# entirely sure that this is what we'd want for production.  The
# [man ntp.conf] page specifically recommends not messing with
# these defaults.

tinker panic 0 dispersion 1.000

# Specifies the file used to track the frequency offset of The
# local clock oscillator.

driftfile /var/lib/ntp/ntp.drift

# Configure a log file.

logfile /var/log/ntp/ntp.log
 
# Permit time synchronization with our time source, but do not
# permit the source to query or modify the service on this system.

restrict default kod nomodify notrap nopeer noquery
restrict -6 default kod nomodify notrap nopeer noquery

# Permit all access over the loopback interface.  This could
# be tightened as well, but to do so would effect some of
# the administrative functions.

restrict 127.0.0.1
restrict -6 ::1

# Hosts on local networks are less restricted.

restrict 10.0.0.0 mask 255.0.0.0 nomodify notrap
restrict 169.254.0.0 mask 255.255.0.0 nomodify notrap
restrict 172.16.0.0 mask 255.255.0.0 nomodify notrap
restrict 192.168.0.0 mask 255.255.0.0 nomodify notrap

# Use local system time when the time sources are not reachable.

server  127.127.1.0 # local clock
fudge   127.127.1.0 stratum 10

# Specify the time sources.

EOF

# Append the time sources to the configuration file.
#
# We're going to prefer the first source.  This will generally
# be the first control-plane node.  The nice thing about this
# the worker nodes will be using the same time source on
# the local network, so the clocks should be very close to
# being closely synchronized.

sources=({GetNtpSources()})

echo ""*** TIME SOURCES = ${{sources}}"" 1>&2

preferred_set=false

for source in ""${{sources[@]}}"";
do
    if ! ${{preferred_set}} ; then
        echo ""server $source burst iburst prefer"" >> /etc/ntp.conf
        preferred_set=true
    else
        echo ""server $source burst iburst"" >> /etc/ntp.conf
    fi
done

# Generate the [{KubeNodeFolder.Bin}/update-time] script.

cat <<EOF > {KubeNodeFolder.Bin}/update-time
#!/bin/bash
#------------------------------------------------------------------------------
# This script stops the NTP time service and forces an immediate update.  This 
# is called from the NTP init.d script (as modified below) to ensure that we 
# obtain the current time on boot (even if the system clock is way out of sync).
# This may also be invoked manually.
#
# Usage:
#
#       update-time [--norestart]
#
#       --norestart - Don't restart NTP

. $<load-cluster-conf-quiet>

restart=true

for arg in \$@
do
    if [ ""\${{arg}}"" = ""--norestart"" ] ; then
        restart=false
    fi
done

if \${{restart}} ; then
    service ntp stop
fi

ntpdate ${{sources[@]}}

if \${{restart}} ; then
    service ntp start
fi
EOF

chmod 700 {KubeNodeFolder.Bin}/update-time

# Edit the NTP [/etc/init.d/ntp] script to initialize the hardware clock and
# call [update-time] before starting NTP.

cat <<""EOF"" > /etc/init.d/ntp
#!/bin/sh

### BEGIN INIT INFO
# Provides:        ntp
# Required-Start:  $network $remote_fs $syslog
# Required-Stop:   $network $remote_fs $syslog
# Default-Start:   2 3 4 5
# Default-Stop:    1
# Short-Description: Start NTP daemon
### END INIT INFO

PATH=/sbin:/bin:/usr/sbin:/usr/bin

. /lib/lsb/init-functions

DAEMON=/usr/sbin/ntpd
PIDFILE=/var/run/ntpd.pid

test -x $DAEMON || exit 5

if [ -r /etc/default/ntp ]; then
    . /etc/default/ntp
fi

if [ -e /var/lib/ntp/ntp.conf.dhcp ]; then
    NTPD_OPTS=""$NTPD_OPTS -c /var/lib/ntp/ntp.conf.dhcp""
fi

LOCKFILE=/var/lock/ntpdate

lock_ntpdate() {{
    if [ -x /usr/bin/lockfile-create ]; then
        lockfile-create $LOCKFILE
        lockfile-touch $LOCKFILE &
        LOCKTOUCHPID=""$!""
    fi
}}

unlock_ntpdate() {{
    if [ -x /usr/bin/lockfile-create ] ; then
        kill $LOCKTOUCHPID
        lockfile-remove $LOCKFILE
    fi
}}

RUNASUSER=ntp
UGID=$(getent passwd $RUNASUSER | cut -f 3,4 -d:) || true
if test ""$(uname -s)"" = ""Linux""; then
        NTPD_OPTS=""$NTPD_OPTS -u $UGID""
fi

case $1 in
    start)
        log_daemon_msg ""Starting NTP server"" ""ntpd""
        if [ -z ""$UGID"" ]; then
            log_failure_msg ""user \""$RUNASUSER\"" does not exist""
            exit 1
        fi

        #------------------------------
        # This is the modification.

		# This bit of voodoo disables Hyper-V time synchronization with The
		# host server.  We don't want this because the cluster is doing its
		# own time management and time sync will fight us.  This is described
		# here:
		#
		# https://social.msdn.microsoft.com/Forums/en-US/8c0a1026-0b02-405a-848e-628e68229eaf/i-have-a-lot-of-time-has-been-changed-in-the-journal-of-my-linux-boxes?forum=WAVirtualMachinesforWindows

		log_daemon_msg ""Start: Disabling Hyper-V time synchronization"" ""ntpd""
		echo 2dd1ce17-079e-403c-b352-a1921ee207ee > /sys/bus/vmbus/drivers/hv_util/unbind
		log_daemon_msg ""Finished: Disabling Hyper-V time synchronization"" ""ntpd""
        
        log_daemon_msg ""Start: Updating current time"" ""ntpd""
        {KubeNodeFolder.Bin}/update-time --norestart
        log_daemon_msg ""Finished: Updating current time"" ""ntpd""

        #------------------------------

        lock_ntpdate
        start-stop-daemon --start --quiet --oknodo --pidfile $PIDFILE --startas $DAEMON -- -p $PIDFILE $NTPD_OPTS
        status=$?
        unlock_ntpdate
        log_end_msg $status
        ;;
    stop)
        log_daemon_msg ""Stopping NTP server"" ""ntpd""
        start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE
        log_end_msg $?
        rm -f $PIDFILE
        ;;
    restart|force-reload)
        $0 stop && sleep 2 && $0 start
        ;;
    try-restart)
        if $0 status >/dev/null; then
            $0 restart
        else
            exit 0
        fi
        ;;
    reload)
        exit 3
        ;;
    status)
        status_of_proc $DAEMON ""NTP server""
        ;;
    *)
        echo ""Usage: $0 {{start|stop|restart|try-restart|force-reload|status}}""
        exit 2
        ;;
esac
EOF

# Restart NTP to ensure that we've picked up the current time.

service ntp restart
";
                SudoCommand(CommandBundle.FromScript(script), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #24
0
        /// <summary>
        /// Installs the <b>neon-init</b> service which is a poor man's cloud-init like
        /// service we use to configure the network and credentials for VMs hosted in non-cloud
        /// hypervisors.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        /// <remarks>
        /// <para>
        /// Install and configure the [neon-init] service.  This is a simple script
        /// that is configured to run as a oneshot systemd service before networking is
        /// started.  This is currently used to configure the node's static IP address
        /// configuration on first boot, so we don't need to rely on DHCP (which may not
        /// be available in some environments).
        /// </para>
        /// <para>
        /// [neon-init] is intended to run the first time a node is booted after
        /// being created from a template.  It checks to see if a special ISO with a
        /// configuration script named [neon-init.sh] is inserted into the VMs DVD
        /// drive and when present, the script will be executed and the [/etc/neon-init/ready]
        /// file will be created to indicate that the service no longer needs to do this for
        /// subsequent reboots.
        /// </para>
        /// <note>
        /// The script won't create the [/etc/neon-init] when the script ISO doesn't exist
        /// for debugging purposes.
        /// </note>
        /// </remarks>
        public void BaseInstallNeonInit(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/neon-init",
                             () =>
            {
                controller.LogProgress(this, verb: "setup", message: "neon-init.service");

                var neonNodePrepScript =
                    $@"# Ensure that the neon binary folder exists.

mkdir -p {KubeNodeFolder.Bin}

# Create the systemd unit file.

cat <<EOF > /etc/systemd/system/neon-init.service

[Unit]
Description=neonKUBE one-time node preparation service 
After=systemd-networkd.service

[Service]
Type=oneshot
ExecStart={KubeNodeFolder.Bin}/neon-init
RemainAfterExit=false
StandardOutput=journal+console

[Install]
WantedBy=multi-user.target
EOF

# Create the service script.

cat <<EOF > {KubeNodeFolder.Bin}/neon-init
#!/bin/bash
#------------------------------------------------------------------------------
# FILE:	        neon-init
# CONTRIBUTOR:  Jeff Lill
# COPYRIGHT:	Copyright (c) 2005-2022 by neonFORGE LLC.  All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the ""License"");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an ""AS IS"" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This script is run early during node boot before the netork is configured
# as a poor man's way for neonKUBE cluster setup to configure the network
# without requiring DHCP.  Here's how this works:
#
#       1. neonKUBE cluster setup creates a node VM from a template.
#
#       2. Setup creates a temporary ISO (DVD) image with a script named 
#          [neon-init.sh] on it and uploads this to the Hyper-V
#          or XenServer host machine.
#
#       3. Setup inserts the VFD into the VM's DVD drive and starts the VM.
#
#       4. The VM boots, eventually running this script (via the
#          [neon-init] service).
#
#       5. This script checks whether a DVD is present, mounts
#          it and checks it for the [neon-init.sh] script.
#
#       6. If the DVD and script file are present, this service will
#          execute the script via Bash, peforming any required custom setup.
#          Then this script creates the [/etc/neon-init] file which 
#          prevents the service from doing anything during subsequent node 
#          reboots.
#
#       7. The service just exits if the DVD and/or script file are 
#          not present.  This shouldn't happen in production but is useful
#          for script debugging.
#
# NOTE: Ubuntu 22.04 seems to have removed the [/dev/dvd] device but 
#       [/dev/cdrom] works, su we're swiching to that.

# Run the prep script only once.

if [ -f /etc/neon-init/ready ] ; then
    echo ""INFO: Machine is already ready.""
    exit 0
fi

# Create a mount point for the DVD.

if ! mkdir -p /media/neon-init ; then
    echo ""ERROR: Cannot create DVD mount point.""
    exit 1
fi

# Wait up to 120 seconds for for the DVD to be discovered.  It can
# take some time for this to happen.

mounted=$false

for i in {{1..24}}; do

    # Sleep for 5 seconds.  We're doing this first to give Linux
    # a chance to discover the DVD and then this will act as a
    # retry interval.  24 iterations at 5 seconds each is 120 seconds.

    sleep 5

    # Try mounting the DVD.

    if mount /dev/cdrom /media/neon-init ; then
        
        mounted=$true
        break
    fi

    echo ""WARNING: No DVD is present (yet).""

done

if ! $mounted; then
    echo ""WARNING: No DVD is present: exiting""
    exit 1
fi

# Check for the [neon-init.sh] script and execute it.

if [ ! -f /media/neon-init/neon-init.sh ] ; then
    echo ""WARNING: No [neon-init.sh] script is present on the DVD: exiting""
    rm -rf /media/neon-init
    exit 0
fi

# The script file is present so execute it.  Note that we're
# passing the path where the DVD is mounted as a parameter.

echo ""INFO: Running [neon-init.sh]""
bash /media/neon-init/neon-init.sh /media/neon-init

# Unmount the DVD and cleanup.

echo ""INFO: Cleanup""
umount /media/neon-init
rm -rf /media/neon-init

# Disable [neon-init] so it does nothing the next time it's launched.

mkdir -p /etc/neon-init
touch /etc/neon-init/ready
EOF

chmod 744 {KubeNodeFolder.Bin}/neon-init

# Configure [neon-init] to start at boot.

systemctl enable neon-init
systemctl daemon-reload

# ---------------------------------------------------
";
                SudoCommand(CommandBundle.FromScript(neonNodePrepScript), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #25
0
        /// <summary>
        /// Configures the APT package manager.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        /// <param name="packageManagerRetries">Optionally specifies the packager manager retries (defaults to <b>5</b>).</param>
        /// <param name="allowPackageManagerIPv6">Optionally prevent the package manager from using IPv6 (defaults to <c>false</c>.</param>
        public void BaseConfigureApt(ISetupController controller, int packageManagerRetries = 5, bool allowPackageManagerIPv6 = false)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/apt",
                             () =>
            {
                controller.LogProgress(this, verb: "configure", message: "package manager");

                if (!allowPackageManagerIPv6)
                {
                    // Restrict the [apt] package manager to using IPv4 to communicate
                    // with the package mirrors, since IPv6 doesn't work sometimes.

                    UploadText("/etc/apt/apt.conf.d/99-force-ipv4-transport", "Acquire::ForceIPv4 \"true\";");
                    SudoCommand("chmod 644 /etc/apt/apt.conf.d/99-force-ipv4-transport", RunOptions.Defaults | RunOptions.FaultOnError);
                }

                // Configure [apt] to retry.

                UploadText("/etc/apt/apt.conf.d/99-retries", $"APT::Acquire::Retries \"{packageManagerRetries}\";");
                SudoCommand("chmod 644 /etc/apt/apt.conf.d/99-retries", RunOptions.Defaults | RunOptions.FaultOnError);

                // We're going to disable apt updating services so we can control when this happens.

                var disableAptServices =
                    @"
set -euo pipefail

#------------------------------------------------------------------------------
# Disable the [apt-timer] and [apt-daily] services.  We're doing this 
# for two reasons:
#
#   1. These services interfere with with [apt-get] usage during
#      cluster setup and is also likely to interfere with end-user
#      configuration activities as well.
#
#   2. Automatic updates for production and even test clusters is
#      just not a great idea.  You just don't want a random update
#      applied in the middle of the night which might cause trouble.
#
# We're going to implement our own cluster updating machanism
# that will be smart enough to update the nodes such that the
# impact on cluster workloads will be limited.

systemctl stop apt-daily.timer
systemctl mask apt-daily.timer

systemctl stop apt-daily.service
systemctl mask apt-daily.service

# It may be possible for the auto updater to already be running so we'll
# wait here for it to release any lock files it holds.

while fuser /var/lib/dpkg/lock >/dev/null 2>&1; do
    sleep 1
done

#------------------------------------------------------------------------------
# Update the APT config to disable updates there as well.
cat > /etc/apt/apt.conf.d/20auto-upgrades <<EOF
APT::Periodic::Update-Package-Lists ""0"";
APT::Periodic::Download-Upgradeable-Packages ""0"";
APT::Periodic::AutocleanInterval ""0"";
APT::Periodic::Unattended-Upgrade ""1"";
EOF
";
                SudoCommand(CommandBundle.FromScript(disableAptServices), RunOptions.Defaults | RunOptions.FaultOnError);
            });
        }
예제 #26
0
        /// <summary>
        /// Configures the <b>kublet</b> service.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        /// <remarks>
        /// <note>
        /// Kubelet is installed in <see cref="NodeSshProxy{TMetadata}.NodeInstallKubernetes"/> when configuring
        /// the node image and is then configured for the cluster here.
        /// </note>
        /// </remarks>
        public void SetupKublet(ISetupController controller)
        {
            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));

            InvokeIdempotent("setup/kublet",
                             () =>
            {
                controller.LogProgress(this, verb: "setup", message: "kublet");

                // Configure the image GC thesholds.  We'll stick with the defaults of 80/85% of the
                // OS disk for most nodes and customize this at 93/95% for clusters with OS disks
                // less than 64GB.  We want higher thesholds for smaller disks to leave more space
                // for user images and local volumes, especially for the neonDESKTOP built-in cluster.
                //
                // We're going to use this command to retrieve the node's disk information:
                //
                //       fdisk --list -o Device,Size,Type | grep ^/dev
                //
                // which will produce output like:
                //
                //      /dev/sda1     1M BIOS boot
                //      /dev/sda2   128G Linux filesystem
                //
                // We're going to look for the line with "Linux filesystem" and and then extract and
                // parse the size in the second column to decide which GC thresholds to use.
                //
                // $note(jefflill):
                //
                // This assumes that there's only one Linux filesystem for each cluster node which is
                // currently the case for all neonKUBE clusters.  The cStor disks are managed by OpenEBS
                // and will not be reported as a file system.  I'll add an assert to verify this to
                // make this easier diagnose in the future if we decide to allow multiple file systems.
                //
                // $todo(jefflill):
                //
                // We're hardcoding this now based on the current node disk size but eventually it
                // might make sense to add settings to the cluster definition so user can override
                // this, perhaps customizing specfic nodes.

                var imageLowGcThreshold  = 80;
                var imageHighGcThreshold = 85;
                var diskSize             = 0L;
                var result = SudoCommand(CommandBundle.FromScript("fdisk --list -o Device,Size,Type | grep ^/dev")).EnsureSuccess();

                using (var reader = new StringReader(result.OutputText))
                {
                    var filesystemCount = 0;

                    foreach (var line in reader.Lines())
                    {
                        if (!line.EndsWith("Linux filesystem") && !line.EndsWith("Linux"))
                        {
                            continue;
                        }

                        filesystemCount++;

                        var fields    = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
                        var sizeField = fields[1];
                        var sizeUnit  = sizeField.Last();
                        var rawSize   = decimal.Parse(sizeField.Substring(0, sizeField.Length - 1));

                        switch (sizeUnit)
                        {
                        case 'G':

                            diskSize = (long)(rawSize * ByteUnits.GibiBytes);
                            break;

                        case 'T':

                            diskSize = (long)(rawSize * ByteUnits.TebiBytes);
                            break;

                        default:

                            Covenant.Assert(false, $"Expecting partition size unit to be [G] or [T], not [{sizeUnit}].");
                            break;
                        }
                    }

                    Covenant.Assert(filesystemCount == 1, $"Expected exactly [1] Linux file system but are seeing [{filesystemCount}].");
                }

                if (diskSize < 64 * ByteUnits.GibiBytes)
                {
                    imageLowGcThreshold  = 93;
                    imageHighGcThreshold = 95;
                }

                var script =
                    $@"
set -euo pipefail

echo KUBELET_EXTRA_ARGS=--feature-gates=\""AllAlpha=false\"" --cgroup-driver=systemd --container-runtime-endpoint='unix:///var/run/crio/crio.sock' --runtime-request-timeout=5m --resolv-conf=/run/systemd/resolve/resolv.conf --image-gc-low-threshold={imageLowGcThreshold} --image-gc-high-threshold={imageHighGcThreshold} > /etc/default/kubelet
systemctl daemon-reload
systemctl restart kubelet
systemctl enable kubelet
";
                SudoCommand(CommandBundle.FromScript(script), RunOptions.FaultOnError);
            });
        }
예제 #27
0
        /// <summary>
        /// Installs a prepositioned Helm chart from a control-plane node.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        /// <param name="chartName">
        /// <para>
        /// The name of the Helm chart.
        /// </para>
        /// <note>
        /// Helm does not allow dashes <b>(-)</b> in chart names but to avoid problems
        /// with copy/pasting, we will automatically convert any dashes to underscores
        /// before installing the chart.  This is also nice because this means that the
        /// chart name passed can be the same as the release name in the calling code.
        /// </note>
        /// </param>
        /// <param name="releaseName">Optionally specifies the component release name.</param>
        /// <param name="namespace">Optionally specifies the namespace where Kubernetes namespace where the Helm chart should be installed. This defaults to <b>default</b></param>
        /// <param name="prioritySpec">
        /// <para>
        /// Optionally specifies the Helm variable and priority class for any pods deployed by the chart.
        /// This needs to be specified as: <b>PRIORITYCLASSNAME</b> or <b>VALUENAME=PRIORITYCLASSNAME</b>,
        /// where <b>VALUENAME</b> optionally specifies the name of the Helm value and <b>PRIORITYCLASSNAME</b>
        /// is one of the priority class names defined by <see cref="PriorityClass"/>.
        /// </para>
        /// <note>
        /// The priority class will saved as the <b>priorityClassName</b> Helm value when no value
        /// name is specified.
        /// </note>
        /// </param>
        /// <param name="values">Optionally specifies Helm chart values.</param>
        /// <param name="progressMessage">Optionally specifies progress message.  This defaults to <paramref name="releaseName"/>.</param>
        /// <returns>The tracking <see cref="Task"/>.</returns>
        /// <exception cref="KeyNotFoundException">Thrown if the priority class specified by <paramref name="prioritySpec"/> is not defined by <see cref="PriorityClass"/>.</exception>
        /// <remarks>
        /// neonKUBE images prepositions the Helm chart files embedded as resources in the <b>Resources/Helm</b>
        /// project folder to cluster node images as the <b>/lib/neonkube/helm/charts.zip</b> archive.  This
        /// method unzips that file to the same folder (if it hasn't been unzipped already) and then installs
        /// the helm chart (if it hasn't already been installed).
        /// </remarks>
        public async Task InstallHelmChartAsync(
            ISetupController controller,
            string chartName,
            string releaseName  = null,
            string @namespace   = "default",
            string prioritySpec = null,
            Dictionary <string, object> values = null,
            string progressMessage             = null)
        {
            await SyncContext.Clear;

            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));
            Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(chartName), nameof(chartName));

            chartName = chartName.Replace('-', '_');

            if (string.IsNullOrEmpty(releaseName))
            {
                releaseName = chartName.Replace("_", "-");
            }

            // Extract the Helm chart value name and priority class name from [priorityClass]
            // when passed.

            string priorityClassVariable = null;
            string priorityClassName     = null;

            if (!string.IsNullOrEmpty(prioritySpec))
            {
                var equalPos = prioritySpec.IndexOf('=');

                if (equalPos == -1)
                {
                    priorityClassVariable = "priorityClassName";
                    priorityClassName     = prioritySpec;
                }
                else
                {
                    priorityClassVariable = prioritySpec.Substring(0, equalPos).Trim();
                    priorityClassName     = prioritySpec.Substring(equalPos + 1).Trim();

                    if (string.IsNullOrEmpty(priorityClassVariable) || string.IsNullOrEmpty(priorityClassName))
                    {
                        throw new FormatException($"[{prioritySpec}] is not valid.  This must be formatted like: NAME=PRIORITYCLASSNAME");
                    }
                }

                PriorityClass.EnsureKnown(priorityClassName);
            }

            // Unzip the Helm chart archive if we haven't done so already.

            InvokeIdempotent("setup/helm-unzip",
                             () =>
            {
                controller.LogProgress(this, verb: "unzip", message: "helm charts");

                var zipPath = LinuxPath.Combine(KubeNodeFolder.Helm, "charts.zip");

                SudoCommand($"unzip -o {zipPath} -d {KubeNodeFolder.Helm} || true");
                SudoCommand($"rm -f {zipPath}");
            });

            // Install the chart when we haven't already done so.

            InvokeIdempotent($"setup/helm-install-{releaseName}",
                             () =>
            {
                controller.LogProgress(this, verb: "install", message: progressMessage ?? releaseName);

                var valueOverrides = new StringBuilder();

                if (!string.IsNullOrEmpty(priorityClassVariable))
                {
                    valueOverrides.AppendWithSeparator($"--set {priorityClassVariable}={priorityClassName}");
                }

                if (values != null)
                {
                    foreach (var value in values)
                    {
                        if (value.Value == null)
                        {
                            valueOverrides.AppendWithSeparator($"--set {value.Key}=null");
                            continue;
                        }

                        var valueType = value.Value.GetType();

                        switch (value.Value)
                        {
                        case string s:
                            valueOverrides.AppendWithSeparator($"--set-string {value.Key}=\"{value.Value}\"");
                            break;

                        case Boolean b:
                            valueOverrides.AppendWithSeparator($"--set {value.Key}=\"{value.Value.ToString().ToLower()}\"");
                            break;

                        default:
                            valueOverrides.AppendWithSeparator($"--set {value.Key}={value.Value}");
                            break;
                        }
                    }
                }

                var helmChartScript = new StringBuilder();

                helmChartScript.AppendLineLinux(
                    $@"
set -euo pipefail

cd {KubeNodeFolder.Helm}
");

                if (controller.Get <bool>(KubeSetupProperty.MaintainerMode))
                {
                    helmChartScript.AppendLineLinux(
                        $@"
if `helm list --namespace {@namespace} | awk '{{print $1}}' | grep -q ""^{releaseName}$""`; then
    helm uninstall {releaseName} --namespace {@namespace}
fi
");
                }

                helmChartScript.AppendLineLinux(
                    $@"
helm install {releaseName} --namespace {@namespace} -f {chartName}/values.yaml {valueOverrides} ./{chartName}

START=`date +%s`
DEPLOY_END=$((START+15))

set +e

until [ `helm status {releaseName} --namespace {@namespace} | grep ""STATUS: deployed"" | wc -l` -eq 1  ];
do
  if [ $((`date +%s`)) -gt $DEPLOY_END ]; then
    helm uninstall {releaseName} --namespace {@namespace} || true
    exit 1
  fi
   sleep 1
done
");

                var scriptString = helmChartScript.ToString();
                SudoCommand(CommandBundle.FromScript(helmChartScript), RunOptions.FaultOnError).EnsureSuccess();
            });

            await Task.CompletedTask;
        }
예제 #28
0
        /// <summary>
        /// <para>
        /// Connects to a Kubernetes cluster if it already exists.  This sets the <see cref="KubeSetupProperty.K8sClient"/>
        /// property in the setup controller state when Kubernetes is running and a connection has not already
        /// been established.
        /// </para>
        /// <note>
        /// The <see cref="KubeSetupProperty.K8sClient"/> will not be set when Kubernetes has not been started, so
        /// <see cref="ObjectDictionary.Get{TValue}(string)"/> calls for this property will fail when the
        /// cluster has not been connected yet, which will be useful for debugging setup steps that require
        /// a connection but this hasn't happened yet.
        /// </note>
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public static void ConnectCluster(ISetupController controller)
        {
            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));

            if (controller.ContainsKey(KubeSetupProperty.K8sClient))
            {
                return;     // Already connected
            }

            var cluster    = controller.Get <ClusterProxy>(KubeSetupProperty.ClusterProxy);
            var configFile = GetCurrentKubeConfigPath();

            if (!string.IsNullOrEmpty(configFile) && File.Exists(configFile))
            {
                // We're using a generated wrapper class to handle transient retries rather than
                // modifying the built-in base retry policy.  We're really just trying to handle
                // the transients that happen during setup when the API server is unavailable for
                // some reaon (like it's being restarted).

                var k8s = new KubernetesWithRetry(KubernetesClientConfiguration.BuildConfigFromConfigFile(configFile, currentContext: cluster.KubeContext.Name));

                k8s.RetryPolicy =
                    new ExponentialRetryPolicy(
                        transientDetector:
                        exception =>
                {
                    var exceptionType = exception.GetType();

                    // Exceptions like this happen when a API server connection can't be established
                    // because the server isn't running or ready.

                    if (exceptionType == typeof(HttpRequestException) && exception.InnerException != null && exception.InnerException.GetType() == typeof(SocketException))
                    {
                        return(true);
                    }

                    var httpOperationException = exception as HttpOperationException;

                    if (httpOperationException != null)
                    {
                        var statusCode = httpOperationException.Response.StatusCode;

                        switch (statusCode)
                        {
                        case HttpStatusCode.GatewayTimeout:
                        case HttpStatusCode.InternalServerError:
                        case HttpStatusCode.RequestTimeout:
                        case HttpStatusCode.ServiceUnavailable:
                        case (HttpStatusCode)423:                   // Locked
                        case (HttpStatusCode)429:                   // Too many requests

                            return(true);
                        }
                    }

                    // This might be another variant of the check just above.  This looks like an SSL negotiation problem.

                    if (exceptionType == typeof(HttpRequestException) && exception.InnerException != null && exception.InnerException.GetType() == typeof(IOException))
                    {
                        return(true);
                    }

                    return(false);
                },
                        maxAttempts:          int.MaxValue,
                        initialRetryInterval: TimeSpan.FromSeconds(1),
                        maxRetryInterval:     TimeSpan.FromSeconds(5),
                        timeout:              TimeSpan.FromMinutes(5));

                controller.Add(KubeSetupProperty.K8sClient, k8s);
            }
        }
예제 #29
0
        /// <summary>
        /// Installs the required <b>base image</b> packages.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void BaseInstallPackages(ISetupController controller)
        {
            Covenant.Requires <ArgumentException>(controller != null, nameof(controller));

            InvokeIdempotent("base/base-packages",
                             () =>
            {
                controller.LogProgress(this, verb: "setup", message: "base packages");

                // Install the packages.  Note that we haven't added our tool folder to the PATH
                // yet, so we'll use the fully qualified path to [safe-apt-get].

                SudoCommand($"{KubeNodeFolder.Bin}/safe-apt-get update", RunOptions.Defaults | RunOptions.FaultOnError);
                SudoCommand($"{KubeNodeFolder.Bin}/safe-apt-get install -yq apt-cacher-ng ntp secure-delete sysstat zip", RunOptions.Defaults | RunOptions.FaultOnError);

                // $note(jefflill):
                //
                // I've seen some situations after a reboot where the machine complains about
                // running out of entropy.  Apparently, modern CPUs have an instruction that
                // returns cryptographically random data, but these CPUs weren't available
                // until 2015 so our old HP SL 385 G10 XenServer machines won't support this.
                //
                // An reasonable alternative is [haveged]:
                //
                //       https://wiki.archlinux.org/index.php/Haveged
                //       https://www.digitalocean.com/community/tutorials/how-to-setup-additional-entropy-for-cloud-servers-using-haveged
                //
                // This article warns about using this though:
                //
                //       https://lwn.net/Articles/525459/
                //
                // The basic problem is that headless servers generally have very poor entropy
                // sources because there's no mouse, keyboard, or active video card.  Outside
                // of the new CPU instruction, the only sources are the HDD and network drivers.
                // [haveged] works by timing running code at very high resolution and hoping to
                // see execution time jitter and then use that as an entropy source.

                // $note(jefflill):
                //
                // The official [haveged] releases before [1.9.8-4ubuntu3] have this bug that
                // prevents [haveged] from running in a container.  We're also seeing (transient?)
                // problems when installing this package on WSL2.
                //
                //      https://bugs.launchpad.net/ubuntu/+source/haveged/+bug/1894877
                //      https://launchpad.net/ubuntu/+source/haveged/1.9.8-4ubuntu3
                //
                // It appears that WSL2 preloads entropy from the Windows host when the distro
                // boots, so we should be OK without [haveged] in this case.
                //
                //      https://github.com/Microsoft/WSL/issues/4416
                //      https://github.com/microsoft/WSL/issues/1789
                //
                // It looks like Linux kernels beginning with v5.6 integrate the HAVEGED algorithm
                // directly, so we don't need to install the [haveged] service in this case:
                //
                //      https://github.com/jirka-h/haveged/blob/master/README.md

                if (this.KernelVersion < new Version(5, 6, 0))
                {
                    SudoCommand($"{KubeNodeFolder.Bin}/safe-apt-get install -yq haveged", RunOptions.Defaults | RunOptions.FaultOnError);
                }
            });
        }
예제 #30
0
        /// <summary>
        /// Configures cluster package manager caching.
        /// </summary>
        /// <param name="controller">The setup controller.</param>
        public void SetupPackageProxy(ISetupController controller)
        {
            Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller));

            var nodeDefinition    = NeonHelper.CastTo <NodeDefinition>(Metadata);
            var clusterDefinition = Cluster.Definition;
            var hostingManager    = controller.Get <IHostingManager>(KubeSetupProperty.HostingManager);

            InvokeIdempotent("setup/package-caching",
                             () =>
            {
                controller.LogProgress(this, verb: "configure", message: "apt package proxy");

                // Configure the [apt-cacher-ng] pckage proxy service on control-plane nodes.

                if (NodeDefinition.Role == NodeRole.ControlPlane)
                {
                    var proxyServiceScript =
                        $@"
	set -eou pipefail	# Enable full failure detection

	{KubeNodeFolder.Bin}/safe-apt-get update
	{KubeNodeFolder.Bin}/safe-apt-get install -yq apt-cacher-ng

	# Configure the cache to pass-thru SSL requests
	# and then restart.

	echo ""PassThroughPattern:^.*:443$"" >> /etc/apt-cacher-ng/acng.conf
	systemctl restart apt-cacher-ng

	set -eo pipefail	# Revert back to partial failure detection

	# Give the proxy service a chance to start.

	sleep 5
";
                    SudoCommand(CommandBundle.FromScript(proxyServiceScript), RunOptions.FaultOnError);
                }

                var sbPackageProxies = new StringBuilder();

                if (clusterDefinition.PackageProxy != null)
                {
                    foreach (var proxyEndpoint in clusterDefinition.PackageProxy.Split(' ', StringSplitOptions.RemoveEmptyEntries))
                    {
                        sbPackageProxies.AppendWithSeparator(proxyEndpoint);
                    }
                }

                // Configure the package manager to use the first control-plane as the proxy by default,
                // failing over to the other control-plane nodes (in order) when necessary.

                var proxySelectorScript =
                    $@"
# Configure APT proxy selection.

echo {sbPackageProxies} > {KubeNodeFolder.Config}/package-proxy

cat <<EOF > /usr/local/bin/get-package-proxy
#!/bin/bash
#------------------------------------------------------------------------------
# FILE:        get-package-proxy
# CONTRIBUTOR: Generated by [neon-cli] during cluster setup.
#
# This script determine which (if any) configured APT proxy caches are running
# and returns its endpoint or ""DIRECT"" if none of the proxies are available and 
# the distribution's mirror should be accessed directly.  This uses the
# [{KubeNodeFolder.Config}/package-proxy] file to obtain the list of proxies.
#
# This is called when the following is specified in the APT configuration,
# as we do further below:
#
#		Acquire::http::Proxy-Auto-Detect ""/usr/local/bin/get-package-proxy"";
#
# See this link for more information:
#
#		https://trent.utfs.org/wiki/Apt-get#Failover_Proxy
NEON_PACKAGE_PROXY=$(cat {KubeNodeFolder.Config}/package-proxy)
if [ ""\${{NEON_PACKAGE_PROXY}}"" == """" ] ; then
    echo DIRECT
    exit 0
fi
for proxy in ${{NEON_PACKAGE_PROXY}}; do
	if nc -w1 -z \${{proxy/:/ }}; then
		echo http://\${{proxy}}/
		exit 0
	fi
done
echo DIRECT
exit 0
EOF

chmod 775 /usr/local/bin/get-package-proxy

cat <<EOF > /etc/apt/apt.conf
//-----------------------------------------------------------------------------
// FILE:        /etc/apt/apt.conf
// CONTRIBUTOR: Generated by during neonKUBE cluster setup.
//
// This file configures APT on the local machine to proxy requests through the
// [apt-cacher-ng] instance(s) at the configured.  This uses the [/usr/local/bin/get-package-proxy] 
// script to select a working PROXY if there are more than one, or to go directly to the package
// mirror if none of the proxies are available.
//
// Presumably, this cache is running on the local network which can dramatically
// reduce external network traffic to the APT mirrors and improve cluster setup 
// and update performance.

Acquire::http::Proxy-Auto-Detect ""/usr/local/bin/get-package-proxy"";
EOF
";
                SudoCommand(CommandBundle.FromScript(proxySelectorScript), RunOptions.FaultOnError);
            });
        }