From 0312050421c3174c9641b4355b48ab78849de6f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Harald=20Jens=C3=A5s?= <hjensas@redhat.com>
Date: Wed, 31 Oct 2018 16:09:45 +0100
Subject: [PATCH] Move UndercloudPostDeployment to python

Configuring Nova (quota, flavors) and Mistral (workbooks,
workflows, etc.) is a lot faster if we do it in python.

Initial undercloud install - 3.5x faster
----------------------------------------
Run deployment UndercloudPostDeployment ---- 130.50s  < Shell
Run deployment UndercloudPostDeployment ----  37.39s  < Python

Re-Running undercloud install - 10x faster
------------------------------------------
Run deployment UndercloudPostDeployment ---- 405.01s < Shell
Run deployment UndercloudPostDeployment ----  39.95s < Python

Change-Id: If7b3ad701e434ed0d606356b9bbab2716d53c5bb
---
 extraconfig/post_deploy/undercloud_post.py   | 198 +++++++++++++++++++
 extraconfig/post_deploy/undercloud_post.sh   |  85 --------
 extraconfig/post_deploy/undercloud_post.yaml |  43 +++-
 3 files changed, 239 insertions(+), 87 deletions(-)
 create mode 100644 extraconfig/post_deploy/undercloud_post.py

diff --git a/extraconfig/post_deploy/undercloud_post.py b/extraconfig/post_deploy/undercloud_post.py
new file mode 100644
index 0000000000..b1b8426864
--- /dev/null
+++ b/extraconfig/post_deploy/undercloud_post.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python
+
+import json
+import os
+import openstack
+import subprocess
+
+from keystoneauth1 import session
+from keystoneauth1 import exceptions as ks_exceptions
+import keystoneauth1.identity.generic as ks_auth
+from mistralclient.api import client as mistralclient
+from mistralclient.api import base as mistralclient_exc
+
+
+AUTH_URL = os.environ['auth_url']
+ADMIN_PASSWORD = os.environ['admin_password']
+CONF = json.loads(os.environ['config'])
+KS_AUTH = {'auth_url': AUTH_URL,
+           'project_name': 'admin',
+           'username': 'admin',
+           'password': ADMIN_PASSWORD,
+           'project_domain_name': 'Default',
+           'user_domain_name': 'Default'}
+WORKBOOK_PATH = '/usr/share/openstack-tripleo-common/workbooks'
+THT_DIR = '/usr/share/openstack-tripleo-heat-templates'
+
+
+def _run_command(args, env=None, name=None):
+    """Run the command defined by args and return its output
+
+    :param args: List of arguments for the command to be run.
+    :param env: Dict defining the environment variables. Pass None to use
+        the current environment.
+    :param name: User-friendly name for the command being run. A value of
+        None will cause args[0] to be used.
+    """
+    if name is None:
+        name = args[0]
+
+    if env is None:
+        env = os.environ
+    env = env.copy()
+
+    # When running a localized python script, we need to tell it that we're
+    # using utf-8 for stdout, otherwise it can't tell because of the pipe.
+    env['PYTHONIOENCODING'] = 'utf8'
+
+    try:
+        return subprocess.check_output(args,
+                                       stderr=subprocess.STDOUT,
+                                       env=env).decode('utf-8')
+    except subprocess.CalledProcessError as ex:
+        print('ERROR: %s failed: %s' % (name, ex.output))
+        raise
+
+
+def _configure_nova(sdk):
+    """ Disable nova quotas """
+    sdk.set_compute_quotas('admin', cores='-1', instances='-1', ram='-1')
+
+    # Configure flavors.
+    sizings = {'ram': 4096, 'vcpus': 1, 'disk': 40}
+    extra_specs = {'resources:CUSTOM_BAREMETAL': 1,
+                   'resources:VCPU': 0,
+                   'resources:MEMORY_MB': 1,
+                   'resources:DISK_GB': 0,
+                   'capabilities:boot_option': 'local'}
+    profiles = ['control', 'compute', 'ceph-storage', 'block-storage',
+                'swift-storage']
+    flavors = [flavor.name for flavor in sdk.list_flavors()]
+    if 'baremetal' not in flavors:
+        flavor = sdk.create_flavor('baremetal', **sizings)
+        sdk.set_flavor_specs(flavor.id, extra_specs)
+    for profile in profiles:
+        if profile not in flavors:
+            flavor = sdk.create_flavor(profile, **sizings)
+            extra_specs.update({'capabilities:profile': profile})
+            sdk.set_flavor_specs(flavor.id, extra_specs)
+    print('INFO: Undercloud Post - Nova configuration completed successfully.')
+
+
+def _create_default_keypair(sdk):
+    """ Set up a default keypair. """
+    ssh_dir = os.path.join(CONF['home_dir'], '.ssh')
+    public_key_file = os.path.join(ssh_dir, 'id_rsa.pub')
+    if (not [True for kp in sdk.compute.keypairs() if kp.name == 'default'] and
+            os.path.isfile(public_key_file)):
+        with open(public_key_file, 'r') as pub_key_file:
+            sdk.compute.create_keypair(name='default',
+                                       public_key=pub_key_file.read())
+
+
+def _configure_wrokbooks_and_workflows(mistral):
+    for workbook in [w for w in mistral.workbooks.list()
+                     if w.name.startswith('tripleo')]:
+        mistral.workbooks.delete(workbook.name)
+    managed_tag = 'tripleo-common-managed'
+    all_workflows = mistral.workflows.list()
+    workflows_delete = [w.name for w in all_workflows
+                        if managed_tag in w.tags]
+    # in order to delete workflows they should have no triggers associated
+    for trigger in [t for t in mistral.cron_triggers.list()
+                    if t.workflow_name in workflows_delete]:
+        mistral.cron_triggers.delete(trigger.name)
+    for workflow_name in workflows_delete:
+        mistral.workflows.delete(workflow_name)
+    for workbook in [f for f in os.listdir(WORKBOOK_PATH)
+                     if os.path.isfile(os.path.join(WORKBOOK_PATH, f))]:
+        mistral.workbooks.create(os.path.join(WORKBOOK_PATH, workbook))
+    print('INFO: Undercloud post - Mistral workbooks configured successfully.')
+
+
+def _create_logging_cron(mistral):
+    mistral.cron_triggers.create(
+        'publish-ui-logs-hourly',
+        'tripleo.plan_management.v1.publish_ui_logs_to_swift',
+        pattern='0 * * * *')
+    print('INFO: Undercloud post - Cron triggers configured successfully.')
+
+
+def _store_snmp_password_in_mistral_env(mistral):
+    """ Store the SNMP password in a mistral environment """
+    env_name = 'tripleo.undercloud-config'
+    config_data = {
+        'undercloud_ceilometer_snmpd_password':
+            CONF['snmp_readonly_user_password']
+    }
+    try:
+        mistral.environments.get(env_name).variables
+        mistral.environments.update(
+            name=env_name,
+            description='Undercloud configuration parameters',
+            variables=json.dumps(config_data, sort_keys=True))
+    except (ks_exceptions.NotFound, mistralclient_exc.APIException):
+        # The environment is not created, we need to create it
+        mistral.environments.create(
+            name=env_name,
+            description='Undercloud configuration parameters',
+            variables=json.dumps(config_data, sort_keys=True))
+    print('INFO: Undercloud post - Mistral environment configured '
+          'successfully.')
+
+
+def _prepare_ssh_environment(mistral):
+    mistral.executions.create('tripleo.validations.v1.copy_ssh_key')
+
+
+def _upload_validations_to_swift(mistral):
+    mistral.executions.create('tripleo.validations.v1.upload_validations')
+
+
+def _create_default_plan(mistral):
+    plan_exists = [True for c in sdk.list_containers() if
+                   c['name'] == 'overcloud']
+    if not plan_exists and os.path.isdir(THT_DIR):
+        mistral.executions.create(
+            'tripleo.plan_management.v1.create_deployment_plan',
+            workflow_input={'container': 'overcloud',
+                            'use_default_templates': True})
+        print('INFO: Undercloud post - Default plan overcloud created.')
+
+
+nova_api_enabled = 'true' in _run_command(
+    ['hiera', 'nova_api_enabled']).lower()
+mistral_api_enabled = 'true' in _run_command(
+    ['hiera','mistral_api_enabled']).lower()
+tripleo_validations_enabled = 'true' in _run_command(
+    ['hiera', 'tripleo_validations_enabled']).lower()
+
+if not nova_api_enabled:
+    print('WARNING: Undercloud Post - Nova API is disabled.')
+if not mistral_api_enabled:
+    print('WARNING: Undercloud Post - Mistral API is disabled.')
+if not tripleo_validations_enabled:
+    print('WARNING: Undercloud Post - Tripleo validations is disabled.')
+
+sdk = openstack.connect(**KS_AUTH)
+
+try:
+    if nova_api_enabled:
+        _configure_nova(sdk)
+        _create_default_keypair(sdk)
+    if mistral_api_enabled:
+        mistral = mistralclient.client(
+            mistral_url=sdk.workflow.get_endpoint(),
+            session=session.Session(auth=ks_auth.Password(**KS_AUTH)))
+        _configure_wrokbooks_and_workflows(mistral)
+        _create_logging_cron(mistral)
+        _store_snmp_password_in_mistral_env(mistral)
+        _create_default_plan(mistral)
+        if tripleo_validations_enabled:
+            _prepare_ssh_environment(mistral)
+            _upload_validations_to_swift(mistral)
+            print('INFO: Undercloud post - Validations execututed and '
+                  'uploaded to Swift.')
+except Exception:
+    print('ERROR: Undercloud Post - Failed.')
+    raise
diff --git a/extraconfig/post_deploy/undercloud_post.sh b/extraconfig/post_deploy/undercloud_post.sh
index 6139bd9437..f5a3954561 100755
--- a/extraconfig/post_deploy/undercloud_post.sh
+++ b/extraconfig/post_deploy/undercloud_post.sh
@@ -6,7 +6,6 @@ ln -sf /etc/puppet/hiera.yaml /etc/hiera.yaml
 HOMEDIR="$homedir"
 USERNAME=`ls -ld $HOMEDIR | awk {'print $3'}`
 GROUPNAME=`ls -ld $HOMEDIR | awk {'print $4'}`
-THT_DIR="/usr/share/openstack-tripleo-heat-templates"
 
 # WRITE OUT STACKRC
 touch $HOMEDIR/stackrc
@@ -70,87 +69,3 @@ if ! grep "$(cat $HOMEDIR/.ssh/id_rsa.pub)" $HOMEDIR/.ssh/authorized_keys; then
     cat $HOMEDIR/.ssh/id_rsa.pub >> $HOMEDIR/.ssh/authorized_keys
 fi
 chown -R "$USERNAME:$GROUPNAME" "$HOMEDIR/.ssh"
-
-if [ "$(hiera nova_api_enabled)" = "true" ]; then
-    # Disable nova quotas
-    openstack quota set --cores -1 --instances -1 --ram -1 $(openstack project show admin | awk '$2=="id" {print $4}')
-
-  # Configure flavors.
-  RESOURCES='--property resources:CUSTOM_BAREMETAL=1 --property resources:DISK_GB=0 --property resources:MEMORY_MB=0 --property resources:VCPU=0 --property capabilities:boot_option=local'
-  SIZINGS='--ram 4096 --vcpus 1 --disk 40'
-
-  if ! openstack flavor show baremetal >/dev/null 2>&1; then
-      openstack flavor create $SIZINGS $RESOURCES baremetal
-  fi
-  if ! openstack flavor show control >/dev/null 2>&1; then
-      openstack flavor create $SIZINGS $RESOURCES --property capabilities:profile=control control
-  fi
-  if ! openstack flavor show compute >/dev/null 2>&1; then
-      openstack flavor create $SIZINGS $RESOURCES --property capabilities:profile=compute compute
-  fi
-  if ! openstack flavor show ceph-storage >/dev/null 2>&1; then
-      openstack flavor create $SIZINGS $RESOURCES --property capabilities:profile=ceph-storage ceph-storage
-  fi
-  if ! openstack flavor show block-storage >/dev/null 2>&1; then
-      openstack flavor create $SIZINGS $RESOURCES --property capabilities:profile=block-storage block-storage
-  fi
-  if ! openstack flavor show swift-storage >/dev/null 2>&1; then
-    openstack flavor create $SIZINGS $RESOURCES --property capabilities:profile=swift-storage swift-storage
-  fi
-fi
-
-# Set up a default keypair.
-if [ ! -e $HOMEDIR/.ssh/id_rsa ]; then
-    sudo -E -u $USERNAME ssh-keygen -t rsa -N '' -f $HOMEDIR/.ssh/id_rsa
-fi
-
-if openstack keypair show default; then
-    echo Keypair already exists.
-else
-    echo Creating new keypair.
-    openstack keypair create --public-key $HOMEDIR/.ssh/id_rsa.pub 'default'
-fi
-
-# MISTRAL WORKFLOW CONFIGURATION
-if [ "$(hiera mistral_api_enabled)" = "true" ]; then
-    echo Configuring Mistral workbooks.
-    for workbook in $(openstack workbook list | grep tripleo | cut -f 2 -d ' '); do
-        openstack workbook delete $workbook
-    done
-    if openstack cron trigger show publish-ui-logs-hourly >/dev/null 2>&1; then
-        openstack cron trigger delete publish-ui-logs-hourly
-    fi
-
-    for workflow in $(openstack workflow list -c Name -f value --filter tags=tripleo-common-managed); do
-        openstack workflow delete $workflow
-    done
-
-    for workbook in $(ls /usr/share/openstack-tripleo-common/workbooks/*); do
-        openstack workbook create $workbook
-    done
-    openstack cron trigger create publish-ui-logs-hourly tripleo.plan_management.v1.publish_ui_logs_to_swift --pattern '0 * * * *'
-    echo Mistral workbooks configured successfully.
-
-  # Store the SNMP password in a mistral environment
-  if ! openstack workflow env show tripleo.undercloud-config >/dev/null 2>&1; then
-      TMP_MISTRAL_ENV=$(mktemp)
-      echo "{\"name\": \"tripleo.undercloud-config\", \"variables\": {\"undercloud_ceilometer_snmpd_password\": \"$snmp_readonly_user_password\"}}" > $TMP_MISTRAL_ENV
-      echo Configure Mistral environment with undercloud-config
-      openstack workflow env create $TMP_MISTRAL_ENV
-  fi
-
-  # Create the default deployment plan from /usr/share/openstack-tripleo-heat-templates
-  # but only if there is no overcloud container in swift yet.
-  if [ -d "$THT_DIR" ] && ! openstack container list -c Name -f value | grep -qe "^overcloud$"; then
-      echo Create default deployment plan
-      openstack workflow execution create tripleo.plan_management.v1.create_deployment_plan '{"container": "overcloud", "use_default_templates": true}'
-  fi
-
-  if [ "$(hiera tripleo_validations_enabled)" = "true" ]; then
-      echo Execute copy_ssh_key validations
-      openstack workflow execution create tripleo.validations.v1.copy_ssh_key
-
-      echo Upload validations to Swift
-      openstack workflow execution create tripleo.validations.v1.upload_validations
-  fi
-fi
diff --git a/extraconfig/post_deploy/undercloud_post.yaml b/extraconfig/post_deploy/undercloud_post.yaml
index 0b2bb6dd56..a736e9f1b1 100644
--- a/extraconfig/post_deploy/undercloud_post.yaml
+++ b/extraconfig/post_deploy/undercloud_post.yaml
@@ -98,7 +98,6 @@ resources:
         - name: deploy_identifier
         - name: admin_password
         - name: auth_url
-        - name: snmp_readonly_user_password
         - name: internal_tls_ca_file
       config: {get_file: ./undercloud_post.sh}
 
@@ -112,7 +111,6 @@ resources:
         ssl_certificate: {get_param: SSLCertificate}
         homedir: {get_param: UndercloudHomeDir}
         admin_password: {get_param: AdminPassword}
-        snmp_readonly_user_password: {get_param: SnmpdReadonlyUserPassword}
         internal_tls_ca_file:
           if:
           - ca_file_enabled
@@ -133,6 +131,47 @@ resources:
               port: 5000
               path: /
 
+  UndercloudPostPyConfig:
+    type: OS::Heat::SoftwareConfig
+    properties:
+      group: script
+      inputs:
+        - name: admin_password
+        - name: auth_url
+        - name: config
+      config: {get_file: ./undercloud_post.py}
+
+  UndercloudPostPyDeployment:
+    type: OS::Heat::SoftwareDeployments
+    depends_on: UndercloudPostDeployment
+    properties:
+      name: UndercloudPostPyDeployment
+      servers: {get_param: servers}
+      config: {get_resource: UndercloudPostPyConfig}
+      input_values:
+        admin_password: {get_param: AdminPassword}
+        auth_url:
+          if:
+          - tls_enabled
+          - make_url:
+              scheme: https
+              host: {get_param: [DeployedServerPortMap, 'public_virtual_ip', fixed_ips, 0, ip_address]}
+              port: 13000
+              path: /
+          - make_url:
+              scheme: http
+              host: {get_param: [DeployedServerPortMap, 'control_virtual_ip', fixed_ips, 0, ip_address]}
+              port: 5000
+              path: /
+        config:
+          str_replace:
+            template: JSON
+            params:
+              JSON:
+                home_dir: {get_param: UndercloudHomeDir}
+                snmp_readonly_user_password: {get_param: SnmpdReadonlyUserPassword}
+
+
   UndercloudCtlplaneNetworkConfig:
     type: OS::Heat::SoftwareConfig
     properties: