Merge "Rework cell functions in novav21 state"
diff --git a/_states/novav21.py b/_states/novav21.py
index b9f9dd0..ffda7d1 100644
--- a/_states/novav21.py
+++ b/_states/novav21.py
@@ -11,11 +11,13 @@
 #    under the License.
 
 import logging
-import six
-from six.moves import zip_longest
 import time
+
 import salt
 from salt.exceptions import CommandExecutionError
+import six
+from six.moves import urllib
+from six.moves import zip_longest
 
 LOG = logging.getLogger(__name__)
 
@@ -282,78 +284,174 @@
     return _non_existent(name, 'Keypair')
 
 
-def cell_present(name='cell1', transport_url='none:///', db_engine='mysql',
-                 db_name='nova_upgrade', db_user='nova', db_password=None,
-                 db_address='0.0.0.0'):
+def _urlencode(dictionary):
+    return '&'.join([
+        '='.join((urllib.parse.quote(key), urllib.parse.quote(str(value))))
+        for key, value in dictionary.items()])
+
+
+def _cmd_raising_helper(command_string, test=False, runas='nova'):
+    if test:
+        LOG.info('This is a test run of the following command: "%s"' %
+                 command_string)
+        return ''
+    result = __salt__['cmd.run_all'](command_string, python_shell=True,
+                                     runas=runas)
+    if result.get('retcode', 0) != 0:
+        raise CommandExecutionError(
+            "Command '%(cmd)s' returned error code %(code)s." %
+            {'cmd': command_string, 'code': result.get('retcode')})
+    return result['stdout']
+
+
+def cell_present(
+        name, db_name=None, db_user=None, db_password=None, db_address=None,
+        messaging_hosts=None, messaging_user=None, messaging_password=None,
+        messaging_virtual_host=None, db_engine='mysql',
+        messaging_engine='rabbit', messaging_query_params=None,
+        db_query_params=None, runas='nova', test=False):
     """Ensure nova cell is present
 
     For newly created cells this state also runs discover_hosts and
-    map_instances."""
+    map_instances.
+
+    :param name: name of the cell to be present.
+    :param db_engine: cell database engine.
+    :param db_name: name of the cell database.
+    :param db_user: username for the cell database.
+    :param db_password: password for the cell database.
+    :param db_address: cell database host. If not provided, other db parameters
+        passed to this function are ignored, create/update cell commands will
+        be using connection strings from the config files.
+    :param messaging_engine: cell messaging engine.
+    :param messaging_hosts: a list of dictionaries of messaging hosts of the
+        cell, containing host and optional port keys. port key defaults
+        to 5672. If not provided, other messaging parameters passed to this
+        function are ignored, create/update cell commands will be using
+        connection strings from the config files.
+    :param messaging_user: username for cell messaging hosts.
+    :param messaging_password: password for cell messaging hosts.
+    :param messaging_virtual_host: cell messaging vhost.
+    :param messaging_query_params: dictionary of query params to append to
+        transport URL.
+    :param db_query_params: dictionary of query params to append to database
+        connection URL. charset=utf8 will always be present in query params.
+    :param runas: username to run the shell commands under.
+    :param test: if this is a test run, actual commands changing state won't
+        be run. False by default.
+    """
     cell_info = __salt__['cmd.shell'](
-        "nova-manage cell_v2 list_cells --verbose | "
-        "awk '/%s/ {print $4,$6,$8}'" % name).split()
-    db_connection = (
-        '%(db_engine)s+pymysql://%(db_user)s:%(db_password)s@'
-        '%(db_address)s/%(db_name)s?charset=utf8' % {
-            'db_engine': db_engine, 'db_user': db_user,
-            'db_password': db_password, 'db_address': db_address,
-            'db_name': db_name})
-    args = {'transport_url': transport_url, 'db_connection': db_connection}
+        "nova-manage cell_v2 list_cells --verbose 2>/dev/null | "
+        "awk '/%s/ {print $4,$6,$8}'" % name, runas=runas).split()
+    if messaging_hosts:
+        transport_url = '%s://' % messaging_engine
+        for h in messaging_hosts:
+            if 'host' not in h:
+                ret = _create_failed(name, 'Nova cell')
+                ret['comment'] = (
+                    'messaging_hosts parameter of cell_present call should be '
+                    'a list of dicts containing host and optionally port keys')
+                return ret
+            transport_url = (
+                '%(transport_url)s%(user)s:%(password)s@%(host)s:%(port)s,' %
+                {'transport_url': transport_url, 'user': messaging_user,
+                 'password': messaging_password, 'host': h['host'],
+                 'port': h.get('port', 5672)})
+        transport_url = '%(transport_url)s/%(messaging_virtual_host)s' % {
+            'transport_url': transport_url.rstrip(','),
+            'messaging_virtual_host': messaging_virtual_host}
+        if messaging_query_params:
+            transport_url = '%(transport_url)s?%(query)s' % {
+                'transport_url': transport_url,
+                'query': _urlencode(messaging_query_params)}
+    else:
+        transport_url = None
+    if db_address:
+        db_connection = (
+            '%(db_engine)s+pymysql://%(db_user)s:%(db_password)s@'
+            '%(db_address)s/%(db_name)s' % {
+                'db_engine': db_engine, 'db_user': db_user,
+                'db_password': db_password, 'db_address': db_address,
+                'db_name': db_name})
+        if not db_query_params:
+            db_query_params = {}
+        db_query_params['charset'] = 'utf8'
+        db_connection = '%(db_connection)s?%(query)s' % {
+            'db_connection': db_connection,
+            'query': _urlencode(db_query_params)}
+    else:
+        db_connection = None
+    changes = {}
     # There should be at least 1 component printed to cell_info
     if len(cell_info) >= 1:
         cell_info = dict(zip_longest(
             ('cell_uuid', 'existing_transport_url', 'existing_db_connection'),
             cell_info))
-        cell_uuid, existing_transport_url, existing_db_connection = cell_info
-        command_string = ''
-        if existing_transport_url != transport_url:
-            command_string = (
-                '%s --transport-url %%(transport_url)s' % command_string)
-        if existing_db_connection != db_connection:
-            command_string = (
-                '%s --database_connection %%(db_connection)s' % command_string)
-        if not command_string:
+        command_string = (
+            '--transport-url \'%(transport_url)s\' '
+            if transport_url else '' +
+            '--database_connection \'%(db_connection)s\''
+            if db_connection else '')
+        if cell_info['existing_transport_url'] != transport_url:
+            changes['transport_url'] = transport_url
+        if cell_info['existing_db_connection'] != db_connection:
+            changes['db_connection'] = db_connection
+        if not changes:
             return _no_change(name, 'Nova cell')
         try:
-            __salt__['cmd.shell'](
+            _cmd_raising_helper(
                 ('nova-manage cell_v2 update_cell --cell_uuid %s %s' % (
-                    cell_uuid, command_string)) % args)
+                    cell_info['cell_uuid'], command_string)) % {
+                    'transport_url': transport_url,
+                    'db_connection': db_connection}, test=test, runas=runas)
             LOG.warning("Updating the transport_url or database_connection "
                         "fields on a running system will NOT result in all "
                         "nodes immediately using the new values. Use caution "
-                        "when changing these values.")
-            ret = _updated(name, 'Nova cell', args)
+                        "when changing these values. You need to restart all "
+                        "nova services on all controllers after this action.")
+            ret = _updated(name, 'Nova cell', changes)
         except Exception as e:
             ret = _update_failed(name, 'Nova cell')
             ret['comment'] += '\nException: %s' % e
         return ret
-    args.update(name=name)
+    changes = {'transport_url': transport_url, 'db_connection': db_connection,
+               'name': name}
     try:
-        cell_uuid = __salt__['cmd.shell'](
-            'nova-manage cell_v2 create_cell --name %(name)s '
-            '--transport-url %(transport_url)s '
-            '--database_connection %(db_connection)s --verbose' % args)
-        __salt__['cmd.shell']('nova-manage cell_v2 discover_hosts '
-                              '--cell_uuid %s --verbose' % cell_uuid)
-        __salt__['cmd.shell']('nova-manage cell_v2 map_instances '
-                              '--cell_uuid %s' % cell_uuid)
-        ret = _created(name, 'Nova cell', args)
+        _cmd_raising_helper((
+            'nova-manage cell_v2 create_cell --name %(name)s ' +
+            ('--transport-url \'%(transport_url)s\' ' if transport_url else '')
+            + ('--database_connection \'%(db_connection)s\' '
+            if db_connection else '') + '--verbose ') % changes,
+            test=test, runas=runas)
+        ret = _created(name, 'Nova cell', changes)
     except Exception as e:
         ret = _create_failed(name, 'Nova cell')
         ret['comment'] += '\nException: %s' % e
     return ret
 
 
-def cell_absent(name, force=False):
-    """Ensure cell is absent"""
+def cell_absent(name, force=False, runas='nova', test=False):
+    """Ensure cell is absent
+
+    :param name: name of the cell to delete.
+    :param force: force cell deletion even if it contains host mappings
+        (host entries will be removed as well).
+    :param runas: username to run the shell commands under.
+    :param test: if this is a test run, actual commands changing state won't
+        be run. False by default.
+    """
     cell_uuid = __salt__['cmd.shell'](
-        "nova-manage cell_v2 list_cells | awk '/%s/ {print $4}'" % name)
+        "nova-manage cell_v2 list_cells 2>/dev/null | awk '/%s/ {print $4}'" %
+        name, runas=runas)
     if not cell_uuid:
         return _non_existent(name, 'Nova cell')
     try:
-        __salt__['cmd.shell'](
+        # Note that if the cell contains any hosts, you need to pass force
+        # parameter for the deletion to succeed. Cell that has any instance
+        # mappings can not be deleted even with force.
+        _cmd_raising_helper(
             'nova-manage cell_v2 delete_cell --cell_uuid %s %s' % (
-                cell_uuid, '--force' if force else ''))
+                cell_uuid, '--force' if force else ''), test=test, runas=runas)
         ret = _deleted(name, 'Nova cell')
     except Exception as e:
         ret = _delete_failed(name, 'Nova cell')
@@ -361,6 +459,38 @@
     return ret
 
 
+def instances_mapped_to_cell(name, timeout=60, runas='nova'):
+    """Ensure that all instances in the cell are mapped
+
+    :param name: cell name.
+    :param timeout: amount of time in seconds mapping process should finish in.
+    :param runas: username to run the shell commands under.
+    """
+    cell_uuid = __salt__['cmd.shell'](
+        "nova-manage cell_v2 list_cells 2>/dev/null | "
+        "awk '/%s/ {print $4}'" % name, runas=runas)
+    result = {'name': name, 'changes': {}, 'result': False}
+    if not cell_uuid:
+        result['comment'] = (
+            'Failed to map all instances in cell {0}, it does not exist'
+            .format(name))
+        return result
+    start_time = time.time()
+    while True:
+        rc = __salt__['cmd.retcode']('nova-manage cell_v2 map_instances '
+                                     '--cell_uuid %s' % cell_uuid, runas=runas)
+        if rc == 0 or time.time() - start_time > timeout:
+            break
+    if rc != 0:
+        result['comment'] = (
+            'Failed to map all instances in cell {0} in {1} seconds'
+            .format(name, timeout))
+        return result
+    result['comment'] = 'All instances mapped in cell {0}'.format(name)
+    result['result'] = True
+    return result
+
+
 def _db_version_update(db, version, human_readable_resource_name):
     existing_version = __salt__['cmd.shell'](
         'nova-manage %s version 2>/dev/null' % db)