#50202 Ticket 50197 - Container init tools
Closed 3 years ago by spichugi. Opened 5 years ago by firstyear.
firstyear/389-ds-base 50197-container-integration  into  master

file modified
+1
@@ -812,6 +812,7 @@ 

  %{_mandir}/man8/dsctl.8.gz

  %{_sbindir}/dsidm

  %{_mandir}/man8/dsidm.8.gz

+ %{_sbindir}/dscontainer

  

  %files -n cockpit-389-ds -f cockpit.list

  %{_datarootdir}/metainfo/389-console/org.cockpit-project.389-console.metainfo.xml

@@ -0,0 +1,244 @@ 

+ #!/usr/bin/python3

+ 

+ # --- BEGIN COPYRIGHT BLOCK ---

+ # Copyright (C) 2019 William Brown <william@blackhats.net.au>

+ # All rights reserved.

+ #

+ # License: GPL (version 3 or any later version).

+ # See LICENSE for details.

+ # --- END COPYRIGHT BLOCK ---

+ 

+ # Why does this exist, and what does it do?

+ ###########################################

+ #

+ # This entry point exists because it's hard to make 389 really "stateless"

+ # in the way a container environment expects, and especially with systems

+ # like kubernetes with volume setup etc.

+ #

+ # This script will detect if an instance exists in the volume locations

+ # and if one does not (new, or ephemeral) we create a container-optimised

+ # instance of 389-ds.

+ #

+ # If an instance *does* exist, we will start it up, and let it run. Simple

+ # as that!

+ #

+ 

+ import grp

+ import pwd

+ import atexit

+ import os

+ import signal

+ import sys

+ import subprocess

+ import argparse, argcomplete

+ from argparse import RawTextHelpFormatter

+ 

+ 

+ from lib389 import DirSrv

+ from lib389.cli_base import setup_script_logger

+ from lib389.instance.setup import SetupDs

+ from lib389.instance.options import General2Base, Slapd2Base

+ from lib389.passwd import password_generate

+ from lib389.paths import Paths

+ 

+ # We setup the logger in verbose mode to make sure debug info

+ # is always available!

+ log = setup_script_logger("container-init", True)

+ 

+ def begin_magic():

+     log.info("The 389 Directory Server Container Bootstrap")

+     # Leave this comment here: UofA let me take this code with me provided

+     # I gave attribution. -- wibrown

+     log.info("Inspired by works of: ITS, The University of Adelaide")

+ 

+     # Setup our ds_paths ...

+     # Notice we pre-populate the instance id, which allows the start up to work correctly

+     # to find the correct configuration path?

+     #

+     # We wouldn't need this *except* for testing containers that build to /opt/dirsrv

+     paths = Paths(serverid='localhost')

+ 

+     # Make sure that /data/config, /data/ssca and /data/config exist, because

+     # k8s may not template them out.

+     #

+     # Big note for those at home: This means you need your dockerfile to run

+     # something like:

+     # EXPOSE 3389 3636

+     # RUN mkdir -p /data/config && \

+     #     mkdir -p /data/ssca && \

+     #     ln -s /data/config /etc/dirsrv/slapd-localhost && \

+     #     ln -s /data/ssca /etc/dirsrv/ssca && \

+     # # Temporal volumes for each instance

+     # VOLUME /data

+     #

+     # When I said this was a container tool, I really really meant it!

+     #

+     # Q: "William, why do you symlink in these locations?"

+     # A: Docker lets you mount in volumes. The *simpler* we can make this for a user

+     # the absolute beter. This means any downstream container can simply use:

+     # docker run -v 389_data:/data ... 389-ds:latest

+     # If we were to use the "normal paths", we would require MORE volume mounts, with

+     # cryptic paths and complexity. Not friendly at all.

+     #

+     # Q: "William, why not change the paths in the config?"

+     # A: Despite the fact that ds alleges support for moving content and paths, this

+     # is not possible for the /etc/dirsrv content unless at COMPILE time. Additionally

+     # some parts of the code base make assumptions. Instead of fighting legacy, we want

+     # results now! So we mask our limitations with symlinks.

+     #

+     for d in [

+         '/data/config',

+         '/data/ssca',

+         '/data/db',

+         '/data/bak',

+         '/data/ldif',

+         '/data/run',

+         '/data/run/lock',

+         '/data/logs'

+     ]:

+         if not os.path.exists(d):

+             os.makedirs(d, mode=0o770)

+ 

+     # Do we have correct permissions to our volumes? With the power of thoughts and

+     # prayers, we continue blindy and ... well hope.

+ 

+     # Do we have an instance? We can only tell by the /data/config/container.inf

+     # marker file

+     if not os.path.exists('/data/config/container.inf'):

+         # Nope? Make one ...

+         log.info("Initialising 389-ds-container due to empty volume ...")

+         rpw = password_generate()

+ 

+         g2b = General2Base(log)

+         s2b = Slapd2Base(log)

+         # Fill in container defaults?

+ 

+         g2b.set('strict_host_checking', False)

+         g2b.set('selinux', False)

+         g2b.set('systemd', False)

+         g2b.set('start', False)

+ 

+         s2b.set('instance_name', 'localhost')

+ 

+         # We use our user/group from the current user, begause in envs like kubernetes

+         # it WILL NOT be dirsrv

+         user_name = pwd.getpwuid(os.getuid())[0]

+         group_name = grp.getgrgid(os.getgid())[0]

+ 

+         s2b.set('user', user_name)

+         s2b.set('group', group_name)

+         s2b.set('root_password', rpw)

+         s2b.set('port', 3389)

+         s2b.set('secure_port', 3636)

+ 

+         s2b.set('local_state_dir', '/data')

+         s2b.set('inst_dir', '/data')

+         s2b.set('db_dir', '/data/db')

+         # Why is this bak? Some dsctl commands use INST_DIR/bak, not "backup_dir"

+         # due to some legacy handling of paths in lib389's population of instances.

+         s2b.set('backup_dir', '/data/bak')

+         s2b.set('ldif_dir', '/data/ldif')

+         s2b.set('run_dir', '/data/run')

+         s2b.set('lock_dir', '/data/run/lock')

+         s2b.set('ldapi', '/data/run/slapd.socket')

+ 

+         s2b.set('log_dir', '/data/logs')

+         s2b.set('access_log', '/data/logs/access')

+         s2b.set('error_log', '/data/logs/error')

+         s2b.set('audit_log', '/data/logs/audit')

+ 

+         # Now collect and submit for creation.

+         sds = SetupDs(verbose=True, dryrun=False, log=log, containerised=True)

+ 

+         if not sds.create_from_args(g2b.collect(), s2b.collect()):

+             log.error("Failed to create instance")

+             sys.exit(1)

+ 

+         log.info("IMPORTANT: Set cn=Directory Manager password to \"%s\"" % rpw)

+ 

+     # Create the marker to say we exist. This is also a good writable permissions

+     # test for the volume.

+     with open('/data/config/container.inf', 'w'):

+         pass

+ 

+     # TODO: All of this is contingent on the server starting *and*

+     # ldapi working ... Perhaps these are better inside ns-slapd core

+     # and we just proxy/filter the env through?

+     # TODO: Should we reset cn=Directory Manager from env?

+     # TODO: Should we set replica id from env?

+     # TODO: Should we set replication agreements from env?

+     # TODO: Should we allow re-indexing at startup from env?

+ 

+     # Yep! Run it ...

+     # Now unlike a normal lib389 start, we use subprocess and don't fork!

+     # TODO: Should we pass in a loglevel from env?

+     log.info("Starting 389-ds-container ...")

+ 

+     global ds_proc

+     ds_proc = subprocess.Popen([

+         "%s/ns-slapd" % paths.sbin_dir,

+         "-D", paths.config_dir,

+         # See /ldap/servers/slapd/slap.h SLAPD_DEFAULT_ERRORLOG_LEVEL

+         "-d", "266354688",

+         ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

+ 

+     # To make sure we really do shutdown, we actually re-block on the proc

+     # again here to be sure it's done.

+     def kill_ds():

+         if ds_proc is None:

+             pass

+         else:

+             try:

+                 os.kill(ds_proc.pid, signal.SIGTERM)

+             except ProcessLookupError:

+                 # It's already gone ...

+                 pass

+         log.info("STOPPING: Shutting down 389-ds-container ...")

+         ds_proc.wait()

+ 

+     atexit.register(kill_ds)

+ 

+     # Now wait ...

+     try:

+         ds_proc.wait()

+     except KeyboardInterrupt:

+         pass

+     # THE LETTER OF THE DAY IS C AND THE NUMBER IS 10

+ 

+ if __name__ == '__main__':

+     parser = argparse.ArgumentParser(allow_abbrev=True, description="""

+ dscontainer - this is a container entry point that will run a stateless

+ instance of 389-ds. You should not use this unless you are developing or

+ building a container image of some nature. As a result, this tool is

+ *extremely* opinionated, and you will need your container build file to

+ have certain settings to work correctly.

+ 

+ \tEXPOSE 3389 3636

+ \tRUN mkdir -p /data/config && \\

+ \t    mkdir -p /data/ssca && \\

+ \t    ln -s /data/config /etc/dirsrv/slapd-localhost && \\

+ \t    ln -s /data/ssca /etc/dirsrv/ssca && \\

+ \tVOLUME /data

+ 

+ This is an example of the minimal required configuration. The 389

+ instance will be created with ports 3389 and 3636. *All* of the data will

+ be installed under /data. This means that to "reset" an instance you only

+ need to remove the content of /data. In the case there is no instance

+ one will be created.

+ 

+ No backends or suffixes are created by default, as we can not assume your

+ domain component. The cn=Directory Manager password is randomised on

+ install, and can be viewed in the setup log, or can be accessed via ldapi

+ - the ldapi socket is placed into /data so you can access it from the

+ container host.

+     """, formatter_class=RawTextHelpFormatter)

+     parser.add_argument('-r', '--runit',

+                         help="Actually run the instance! You understand what that means ...",

+                         action='store_true', default=False, dest='runit')

+     argcomplete.autocomplete(parser)

+ 

+     args = parser.parse_args()

+ 

+     if args.runit:

+         begin_magic()

+ 

file modified
-4
@@ -28,16 +28,12 @@ 

  fromfile_parser.add_argument('file', help="Inf file to use with prepared answers. You can generate an example of this with 'dscreate create-template'")

  fromfile_parser.add_argument('-n', '--dryrun', help="Validate system and configurations only. Do not alter the system.",

                               action='store_true', default=False)

- fromfile_parser.add_argument('-c', '--containerized', help="Indicate to the installer that this is running in a container. Used to disable systemd native components, even if they are installed.",

-                              action='store_true', default=False)

  fromfile_parser.set_defaults(func=cli_instance.instance_create)

  

  interactive_parser = subparsers.add_parser('interactive', help="Start interactive installer for Directory Server installation")

  interactive_parser.set_defaults(func=cli_instance.instance_create_interactive)

  

  template_parser = subparsers.add_parser('create-template', help="Display an example inf answer file, or provide a file name to write it to disk.")

- template_parser.add_argument('-c', '--containerized', help="Indicate to the installer that this is running in a container. Used to disable systemd native components, even if they are installed.",

-                              action='store_true', default=False)

  template_parser.add_argument('template_file', nargs="?", default=None, help="Write example template to this file")

  template_parser.set_defaults(func=cli_instance.instance_example)

  

file modified
+5 -1
@@ -14,6 +14,7 @@ 

  import logging

  import sys

  import signal

+ import os

  from lib389.utils import get_instance_list

  from lib389.cli_base import _get_arg

  from lib389 import DirSrv
@@ -44,7 +45,10 @@ 

      )

  

  subparsers = parser.add_subparsers(help="action")

- cli_instance.create_parser(subparsers)

+ # We can only use the instance tools like start/stop etc in a non-container

+ # environment. If we are in a container, we only allow the tasks.

+ if not os.path.exists('/data/config/container.inf'):

+     cli_instance.create_parser(subparsers)

  cli_dbtasks.create_parser(subparsers)

  

  argcomplete.autocomplete(parser)

@@ -63,10 +63,7 @@ 

  

  

  def instance_create(inst, log, args):

-     if args.containerized:

-         log.debug("Containerized features requested.")

- 

-     sd = SetupDs(args.verbose, args.dryrun, log, args.containerized)

+     sd = SetupDs(args.verbose, args.dryrun, log)

      if sd.create_from_inf(args.file):

          # print("Successfully created instance")

          return True
@@ -76,9 +73,6 @@ 

  

  

  def instance_example(inst, log, args):

-     if args.containerized:

-         log.debug("Containerized features requested.")

- 

      header = """

  ; 

  ; This is a version 2 ds setup inf file.
@@ -97,7 +91,7 @@ 

  """

  

      g2b = General2Base(log)

-     s2b = Slapd2Base(log, args.containerized)

+     s2b = Slapd2Base(log)

      b2b = Backend2Base(log, "backend-userroot")

  

      if args.template_file:

@@ -116,7 +116,7 @@ 

  

          self._options['strict_host_checking'] = True

          self._type['strict_host_checking'] = bool

-         self._helptext['strict_host_checking'] = "Sets whether the server verifies the forward and reverse record set in the \"full_machine_name\" parameter. When installing this instance with GSSAPI authentication behind a load balancer, set this parameter to \"false\"."

+         self._helptext['strict_host_checking'] = "Sets whether the server verifies the forward and reverse record set in the \"full_machine_name\" parameter. When installing this instance with GSSAPI authentication behind a load balancer, set this parameter to \"false\". Container installs imply \"false\"."

  

          self._options['selinux'] = True

          self._type['selinux'] = bool
@@ -126,6 +126,11 @@ 

          self._type['systemd'] = bool

          self._helptext['systemd'] = "Enables systemd platform features. If set to \"True\", dscreate auto-detects whether systemd is installed. Set this only to \"False\" in a development environment."

  

+ 

+         self._options['start'] = True

+         self._type['start'] = bool

+         self._helptext['start'] = "Starts the instance after the install completes. If false, the instance is created but started."

+ 

          self._options['defaults'] = INSTALL_LATEST_CONFIG

          self._type['defaults'] = str

          self._helptext['defaults'] = "Directory Server enables administrators to use the default values for cn=config entries from a specific version. If you set this parameter to \"{LATEST}\", which is the default, the instance always uses the default values of the latest version. For example, to configure that the instance uses default values from version 1.3.5, set this parameter to \"001003005\". The format of this value is XXXYYYZZZ, where X is the major version, Y the minor version, and Z the patch level. Note that each part of the value uses 3 digits and must be filled with leading zeros if necessary.".format(LATEST=INSTALL_LATEST_CONFIG)
@@ -137,7 +142,7 @@ 

  

  

  class Slapd2Base(Options2):

-     def __init__(self, log, container=False):

+     def __init__(self, log):

          super(Slapd2Base, self).__init__(log)

          self._section = 'slapd'

  
@@ -169,17 +174,11 @@ 

          self._type['prefix'] = str

          self._helptext['prefix'] = "Sets the file system prefix for all other directories. You can refer to this value in other fields using the {prefix} variable or the $PREFIX environment variable. Only set this parameter in a development environment."

  

-         if container:

-             self._options['port'] = 3389

-         else:

-             self._options['port'] = 389

+         self._options['port'] = 389

          self._type['port'] = int

          self._helptext['port'] = "Sets the TCP port the instance uses for LDAP connections."

  

-         if container:

-             self._options['secure_port'] = 3636

-         else:

-             self._options['secure_port'] = 636

+         self._options['secure_port'] = 636

          self._type['secure_port'] = int

          self._helptext['secure_port'] = "Sets the TCP port the instance uses for TLS-secured LDAP connections (LDAPS)."

  

@@ -649,6 +649,7 @@ 

          self.log.debug("FINISH: Completed installation for %s", slapd['instance_name'])

          if not self.verbose:

              self.log.info("Completed installation for %s", slapd['instance_name'])

+         return True

  

      def _install_ds(self, general, slapd, backends):

          """
@@ -761,7 +762,7 @@ 

          os.chmod(dstfile, 0o440)

  

          # If we are on the correct platform settings, systemd

-         if general['systemd'] and not self.containerised:

+         if general['systemd']:

              # Should create the symlink we need, but without starting it.

              subprocess.check_call(["systemctl",

                                     "enable",
@@ -829,12 +830,12 @@ 

              csr = tlsdb.create_rsa_key_and_csr()

              (ca, crt) = ssca.rsa_ca_sign_csr(csr)

              tlsdb.import_rsa_crt(ca, crt)

-             if not self.containerised and general['selinux']:

+             if general['selinux']:

                  # Set selinux port label

                  selinux_label_port(slapd['secure_port'])

  

          # Do selinux fixups

-         if not self.containerised and general['selinux']:

+         if general['selinux']:

              selinux_paths = ('backup_dir', 'cert_dir', 'config_dir', 'db_dir', 'ldif_dir',

                               'lock_dir', 'log_dir', 'run_dir', 'schema_dir', 'tmp_dir')

              for path in selinux_paths:
@@ -861,10 +862,7 @@ 

          # tests with standalone.enable_tls if we do not. It's only when security; on

          # that we actually start listening on it.

          if not slapd['secure_port']:

-             if self.containerised:

-                 slapd['secure_port'] = "3636"

-             else:

-                 slapd['secure_port'] = "636"

+             slapd['secure_port'] = "636"

          ds_instance.config.set('nsslapd-secureport', '%s' % slapd['secure_port'])

          if slapd['self_sign_cert']:

              ds_instance.config.set('nsslapd-security', 'on')
@@ -917,12 +915,13 @@ 

          else:

              self.log.debug("Skipping default SASL maps - no backend found!")

  

+         # Change the root password finally

+         ds_instance.config.set('nsslapd-rootpw', slapd['root_password'])

+ 

          # Complete.

-         if self.containerised:

-             # In a container build we need to stop DirSrv at the end

-             ds_instance.stop()

-         else:

-             # If we are not a container, change the root password finally

-             ds_instance.config.set('nsslapd-rootpw', slapd['root_password'])

+         if general['start']:

              # Restart for changes to take effect - this could be removed later

              ds_instance.restart(post_open=False)

+         else:

+             # Just stop the instance now.

+             ds_instance.stop()

@@ -120,6 +120,19 @@ 

          except FileExistsError:

              pass

  

+         # Write a README to let people know what this is

+         readme_file = '%s/%s' % (self._certdb, 'README.txt')

+         if not os.path.exists(readme_file):

+             with open(readme_file, 'w') as f:

+                 f.write("""

+ SSCA - Simple Self-Signed Certificate Authority

+ 

+ This is part of the 389 Directory Server project's lib389 toolkit. It

+ creates a simple, standalone certificate authority for testing and

+ development purposes. It's suitable for evaluation and testing purposes

+ only.

+                 """)

+ 

          # In the future we may add the needed option to avoid writing the pin

          # files.

          # Write the pin.txt, and the pwdfile.txt

file modified
+8 -3
@@ -44,7 +44,10 @@ 

  def password_generate(length=64):

      """Generate a complex password with at least

      one upper case letter, a lower case letter, a digit

-     and a special character

+     and a special character. The special characters are limited

+     to a set that can be highlighted with double-click to allow

+     easier copy-paste to a password-manager. Most password strength

+     comes from length anyway, so this is why we use a long length (64)

  

      :param length: a password length

      :type length: int
@@ -56,13 +59,15 @@ 

      # The number of possible values for a byte is 256 which is a multiple of 64

      # Maybe it is an overkill for our case but it can come handy one day

      # (especially consider the fact we can use it for CLI tools)

-     chars = string.ascii_letters + string.digits + '*&'

+     chars = string.ascii_letters + string.digits + '-.'

  

      # Get the minimal requirements

+     # Don't use characters that prevent easy highlight for copy paste ...

+     # It's the little details that make us great

      pw = [random.choice(string.ascii_lowercase),

            random.choice(string.ascii_uppercase),

            random.choice(string.digits),

-           '!']

+           '.']

  

      # Use the simple algorithm to generate more or less secure password

      for i in range(length - 3):

file modified
+1 -2
@@ -45,8 +45,6 @@ 

          'Development Status :: 4 - Beta',

          'Intended Audience :: Developers',

          'Operating System :: POSIX :: Linux',

-         'Programming Language :: Python :: 2',

-         'Programming Language :: Python :: 2.7',

          'Programming Language :: Python :: 3',

          'Programming Language :: Python :: 3.4',

          'Programming Language :: Python :: 3.5',
@@ -65,6 +63,7 @@ 

              'cli/dsconf',

              'cli/dscreate',

              'cli/dsidm',

+             'cli/dscontainer',

              ]),

          ('/usr/share/man/man8', [

              'man/dsctl.8',

Bug Description: It's important that 389 Directory Server
has a functional, correct, and high quality container integration
system. After years of work on the server core and lib389, this is
nearly possible.

Importantly, containers have certain requirements we must understand.
All state must be in external-filesystem volumes. We can not assume
that we have an instance installed, so must create one on launch.
If one exists, we need to expose it. We don't have the ability to
ask questions, so we need to use environment, or work with no
input at all. We can't make assumptions about backends. Finally,
we need to assume that we could be a new version of the server -
we don't know about anything else.

Fix Description: This adds a dscontainer wrapper tool that is
intended for operation inside of containers. It handles and binds
many of the existing parts of lib389 for container support. I have
cleaned up past container support realising how it was done wasn't
as elegant as this.

The dscontainer tool is intended to be the entry point from a
dockerfile, IE the CMD directive.

There are still some avenues to explore. For example, we could
attempt to override the storage paths for logs and db rather than
relying on dockerfile system links. (this may break apparmor though).

https://pagure.io/389-ds-base/issue/50197

Author: William Brown william@blackhats.net.au

Review by: ???

rebased onto 33ddf56f8505c4462da8fd2f8520211b92f73852

5 years ago

@spichugi or @mreynolds did you want to take a look at this since you are both involved in cli-tool business now :)

It would be nice to add '--help', '-h' flags, becuase it is not user friendly now... Maybe you can add the usage example (docker file) to the '-h'?

I was thinking about this. This isn't really a tool meant for general use, it's meant for docker container builders so I was wondering about adding -h but also a "--ireallyknowwhatiamdoing" or similar.

2 new commits added

  • Ticket 50197 - Container integration part 2
  • Ticket 50197 - Container init tools
5 years ago

Okay, I've added a --help, and a -r flag fro "really runit", so you can't run this by accident.

@spichugi Can you check this again please :)

Build failed...

Checking for unpackaged file(s): /usr/lib/rpm/check-files /builddir/build/BUILDROOT/389-ds-base-1.4.1.1-20190215git4eb6905f4.fc29.x86_64
BUILDSTDERR: error: Installed (but unpackaged) file(s) found:
BUILDSTDERR:    /usr/sbin/dscontainer
RPM build errors:
BUILDSTDERR:     Installed (but unpackaged) file(s) found:
BUILDSTDERR:    /usr/sbin/dscontainer
Child return code was: 1
EXCEPTION: [Error()]

2 new commits added

  • Ticket 50197 - Container integration part 2
  • Ticket 50197 - Container init tools
5 years ago

I think we really should have a design doc for how we want to approach containers first. I understand you, William, have it thought through but we all need to have a better picture of where we're heading. This PR is full of TODO comments, "magic", and not very uniform code - there should be an amount of tiding up. Indeed we need this feature, however, conducted in more transparent way.

@mhonek I can write up a design doc, but this is actually the last piece needed to have containers working at this point. It's pretty late in the process (mainly because no one else has been interested in the process or those changes ....)

https://www.port389.org/docs/389ds/design/docker.html it's here, but the wiki is broken and refuses to render that page. Maybe it's a markdown error? Ruby is broken on SUSE atm so I can't build myself.

You can see the source at https://www.port389.org/docs/389ds/design/docker.md though if that helps,

https://www.port389.org/docs/389ds/design/docker.html it's here, but the wiki is broken and refuses to render that page. Maybe it's a markdown error?

I did a fresh commit to the wiki and now the doc is present.

@mhonek The doc has been up are there any comments?

@mhonek @mreynolds @spichugi Any reviews on this please? I would really like to get it commited so that I can progress on developing a docker image thanks.

Thanks for the write up, and this LGTM

Go for it. And sorry for the delay.

rebased onto fb5ae2c

5 years ago

Pull-Request has been merged by firstyear

5 years ago

389-ds-base is moving from Pagure to Github. This means that new issues and pull requests
will be accepted only in 389-ds-base's github repository.

This pull request has been cloned to Github as issue and is available here:
- https://github.com/389ds/389-ds-base/issues/3261

If you want to continue to work on the PR, please navigate to the github issue,
download the patch from the attachments and file a new pull request.

Thank you for understanding. We apologize for all inconvenience.

Pull-Request has been closed by spichugi

3 years ago