Add support to configure SCM Import for projects

Change-Id: Ic95dd313542d13381c879b11bae4c79498300f55
diff --git a/_modules/rundeck.py b/_modules/rundeck.py
index 9d53361..f47c837 100644
--- a/_modules/rundeck.py
+++ b/_modules/rundeck.py
@@ -8,6 +8,8 @@
 LOG = logging.getLogger(__name__)
 
 
+# Project
+
 def get_project(name):
     session, make_url = get_session()
     resp = session.get(make_url("/api/18/project/{}".format(name)))
@@ -39,25 +41,6 @@
     LOG.debug("create_project: %s", name)
 
 
-def create_project_config(name, params, config=None):
-    config = dict(config) if config else {}
-    if params['description']:
-       config['project.description'] = params['description']
-    else:
-       config.pop('project.description', None)
-    config.update({
-        'resources.source.1.config.file':
-                "/var/rundeck/projects/{}/etc/resources.yaml".format(name),
-        'resources.source.1.config.format': 'resourceyaml',
-        'resources.source.1.config.generateFileAutomatically': 'true',
-        'resources.source.1.config.includeServerNode': 'false',
-        'resources.source.1.config.requireFileExists': 'false',
-        'project.ssh-keypath': '/var/rundeck/.ssh/id_rsa',
-        'resources.source.1.type': 'file',
-    })
-    return config
-
-
 def update_project_config(name, project, config):
     session, make_url = get_session()
     resp = session.put(
@@ -80,6 +63,251 @@
             .format(name, make_url.base_url, status_code))
 
 
+# SCM
+
+def get_plugin(project_name, integration):
+    session, make_url = get_session()
+    resp = session.get(make_url("/api/18/project/{}/scm/{}/config"
+                                .format(project_name, integration)))
+    if resp.status_code == 200:
+        return True, resp.json()
+    elif resp.status_code == 404:
+        return True, None
+    return False, (
+        "Could not get config for the {} plugin of the {} project"
+        .format(integration, project_name))
+
+
+def get_plugin_status(project_name, integration):
+    def get_plugin(plugins, plugin_type):
+        for plugin in plugins:
+            if plugin['type'] == plugin_type:
+                return plugin
+        raise salt.exceptions.SaltInvocationError(
+            "Could not find status for the {}/{} plugin of the {} project"
+            .format(integration, plugin_type, project_name))
+
+    session, make_url = get_session()
+    resp = session.get(make_url("/api/18/project/{}/scm/{}/plugins"
+                                .format(project_name, integration)))
+    if resp.status_code == 200:
+        plugin_type = "git-{}".format(integration)
+        status = get_plugin(resp.json()['plugins'], plugin_type)
+        return True, status
+    return False, (
+        "Could not get status for the {} plugin of the {} project"
+        .format(integration, project_name))
+
+
+def get_plugin_state(project_name, integration):
+    session, make_url = get_session()
+    resp = session.get(make_url("/api/18/project/{}/scm/{}/status"
+                                .format(project_name, integration)))
+    if resp.status_code == 200:
+        return True, resp.json()
+    return False, (
+        "Could not get state for the {} plugin of the {} project"
+        .format(integration, project_name))
+
+
+def disable_plugin(project_name, integration):
+    session, make_url = get_session()
+    resp = session.post(make_url(
+        "/api/15/project/{}/scm/{}/plugin/git-{}/disable"
+        .format(project_name, integration, integration)))
+    if resp.status_code == 200:
+        msg = resp.json()
+        return True, msg['message']
+    return False, (
+        "Could not disable the {} plugin for the {} project: {}"
+        .format(integration, project_name, resp.status_code))
+
+
+def enable_plugin(project_name, integration):
+    session, make_url = get_session()
+    resp = session.post(make_url(
+        "/api/15/project/{}/scm/{}/plugin/git-{}/enable"
+        .format(project_name, integration, integration)))
+    if resp.status_code == 200:
+        msg = resp.json()
+        return True, msg['message']
+    return False, (
+        "Could not enable the {} plugin for the {} project: {}"
+        .format(integration, project_name, resp.status_code))
+
+
+# SCM Import
+
+def setup_scm_import(project_name, params):
+    session, make_url = get_session()
+    config = create_scm_import_config(project_name, params)
+    resp = session.post(
+        make_url("/api/15/project/{}/scm/import/plugin/git-import/setup"
+                 .format(project_name)),
+        json={
+            'config': config,
+        },
+        allow_redirects=False,
+    )
+    if resp.status_code == 200:
+        return True, resp.json()
+    return False, (
+        "Could not configure SCM Import for the {} project: {}/{}"
+        .format(project_name, resp.status_code, resp.text))
+
+
+def update_scm_import_config(project_name, plugin, config):
+    session, make_url = get_session()
+    resp = session.post(
+        make_url("/api/15/project/{}/scm/import/plugin/git-import/setup"
+                 .format(project_name)),
+        json={
+            'config': config,
+        },
+        allow_redirects=False,
+    )
+    if resp.status_code == 200:
+        return True, resp.json()
+    return False, (
+        "Could not update SCM Import for the {} project: {}/{}"
+        .format(project_name, resp.status_code, resp.text))
+
+
+def perform_scm_import_tracking(project_name, plugin, params):
+    format = plugin['config']['format']
+    file_pattern = params.get('file_pattern')
+    if not file_pattern:
+        file_pattern = DEFAULT_FILE_PATTERNS[format]
+
+    session, make_url = get_session()
+    resp = session.post(
+        make_url("/api/15/project/{}/scm/import/action/initialize-tracking"
+                 .format(project_name)),
+        json={
+            'input': {
+                'filePattern': file_pattern,
+                'useFilePattern': 'true',
+            },
+            'jobs': [],
+            'items': [],
+            'deleted': [],
+        },
+        allow_redirects=False,
+    )
+    if resp.status_code == 200:
+        return True, resp.json()
+    return False, (
+        "Could not update SCM Import for the {} project: {}/{}"
+        .format(project_name, resp.status_code, resp.text))
+
+DEFAULT_FILE_PATTERNS = {
+    'yaml': r'.*\.yaml',
+    'xml': r'.*\.xml',
+}
+
+
+def perform_scm_import_pull(project_name, plugin, params):
+    session, make_url = get_session()
+    resp = session.post(
+        make_url("/api/15/project/{}/scm/import/action/remote-pull"
+                 .format(project_name)),
+        json={
+            'input': {},
+            'jobs': [],
+            'items': [],
+            'deleted': [],
+        },
+        allow_redirects=False,
+    )
+    if resp.status_code == 200:
+        return True, resp.json()
+    return False, (
+        "Could not pull remote changes for the {} project: {}/{}"
+        .format(project_name, resp.status_code, resp.text))
+
+
+def perform_scm_import(project_name, plugin, params):
+    session, make_url = get_session()
+    ok, inputs = get_plugin_action_inputs(
+        project_name, 'import', 'import-all')
+    if not ok:
+        return False, inputs
+    items = list(item['itemId'] for item in inputs['importItems'])
+    resp = session.post(
+        make_url("/api/15/project/{}/scm/import/action/import-all"
+                 .format(project_name)),
+        json={
+            'input': {},
+            'jobs': [],
+            'items': items,
+            'deleted': [],
+        },
+        allow_redirects=False,
+    )
+    if resp.status_code == 200:
+        return True, resp.json()
+    return False, (
+        "Could not import jobs for the {} project: {}/{}"
+        .format(project_name, resp.status_code, resp.text))
+
+
+# Utils
+
+def create_project_config(project_name, params, config=None):
+    config = dict(config) if config else {}
+    if params['description']:
+       config['project.description'] = params['description']
+    else:
+       config.pop('project.description', None)
+    config.update({
+        'resources.source.1.config.file':
+            "/var/rundeck/projects/{}/etc/resources.yaml".format(project_name),
+        'resources.source.1.config.format': 'resourceyaml',
+        'resources.source.1.config.generateFileAutomatically': 'true',
+        'resources.source.1.config.includeServerNode': 'false',
+        'resources.source.1.config.requireFileExists': 'false',
+        'project.ssh-keypath': '/var/rundeck/.ssh/id_rsa',
+        'resources.source.1.type': 'file',
+    })
+    return config
+
+
+def create_scm_import_config(project_name, params, config=None):
+    config = dict(config) if config else {}
+
+    format = params.get('format', 'yaml')
+    if format not in DEFAULT_FILE_PATTERNS:
+        supported_formats = DEFAULT_FILE_PATTERNS.keys()
+        raise salt.exceptions.SaltInvocationError(
+            "Unsupported format {} for the {} SCM import module, should be {}"
+            .format(format, project_name, ','.join(supported_formats)))
+
+    config.update({
+        'dir': "/var/rundeck/projects/{}/scm".format(project_name),
+        'url': params['address'],
+        'branch': params.get('branch', 'master'),
+        'fetchAutomatically': 'true',
+        'format': format,
+        'pathTemplate': params.get(
+            'path_template', '${job.group}${job.name}.${config.format}'),
+        'importUuidBehavior': params.get('import_uuid_behavior', 'remove'),
+        'strictHostKeyChecking': 'yes',
+    })
+    return config
+
+
+def get_plugin_action_inputs(project_name, integration, action):
+    session, make_url = get_session()
+    resp = session.get(
+        make_url("/api/18/project/cicd/scm/import/action/import-all/input"))
+    if resp.status_code == 200:
+        return True, resp.json()
+    return False, (
+        "Could not get inputs for the {} action for the {} project: {}/{}"
+        .format(action, project_name, resp.status_code, resp.text))
+
+
+
 def get_session():
     def make_url(url):
         return urljoin(make_url.base_url, url)
diff --git a/_states/rundeck_project.py b/_states/rundeck_project.py
index 5919297..29dc76a 100644
--- a/_states/rundeck_project.py
+++ b/_states/rundeck_project.py
@@ -36,13 +36,13 @@
             LOG.warning("{}: {}".format(project["config"], config))
             __salt__['rundeck.update_project_config'](name, project, config)
             ret['comment'] = "Project {} was updated.".format(name)
-            ret['changes'][name] = "UPDATED"
+            ret['changes'][name] = 'UPDATED'
         else:
             ret['comment'] = "Project {} is already up to date.".format(name)
     else:
         __salt__['rundeck.create_project'](name, params)
         ret['comment'] = "Project {} was created.".format(name)
-        ret['changes'][name] = "CREATED"
+        ret['changes'][name] = 'CREATED'
     ret['result'] = True
     return ret
 
diff --git a/_states/rundeck_scm.py b/_states/rundeck_scm.py
new file mode 100644
index 0000000..afe34f9
--- /dev/null
+++ b/_states/rundeck_scm.py
@@ -0,0 +1,166 @@
+import logging
+
+LOG = logging.getLogger(__name__)
+
+
+def __virtual__():
+    if 'rundeck.get_project' not in __salt__:
+        return (
+            False,
+            'The rundeck_scm state module cannot be loaded: rundeck is '
+            'unavailable',
+        )
+    return True
+
+
+def present_import(name, project_name, **params):
+    result = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+        'pchanges': {},
+    }
+    if __opts__['test'] == True:
+        result['comment'] = 'There is nothing to change in the test mode.'
+        result['result'] = None
+    ok, plugin = __salt__['rundeck.get_plugin'](project_name, 'import')
+    if ok:
+        if plugin:
+            config = __salt__['rundeck.create_scm_import_config'](
+                project_name, params, config=plugin['config'])
+            LOG.debug("SCM Import for the %s project: %s/%s",
+                      project_name, plugin["config"], config)
+            if plugin['config'] != config:
+                ok, plugin = __salt__['rundeck.update_scm_import_config'](
+                    project_name, plugin, config)
+                result['comment'] = (
+                    "SCM Import plugin for the {} project was updated."
+                    .format(project_name))
+                result['changes'][name] = 'UPDATED'
+            else:
+                result['comment'] = (
+                    "SCM Import plugin for the {} project is already up to "
+                    "date.".format(project_name))
+            result['result'] = True
+        else:
+            ok, plugin = __salt__['rundeck.setup_scm_import'](
+                project_name, params)
+            if ok:
+                result['changes'][name] = 'CREATED'
+                result['comment'] = (
+                    "SCM Import was configured for the {} project."
+                    .format(project_name))
+                result['result'] = True
+            else:
+                result['comment'] = plugin
+    else:
+        result['comment'] = plugin
+    return result
+
+
+def sync_import(name, project_name, **params):
+    result = {
+        'name': name,
+        'changes': {},
+        'result': True,
+        'comment': '',
+        'pchanges': {},
+    }
+
+    if __opts__['test'] == True:
+        result['comment'] = 'There is nothing to change in the test mode.'
+        result['result'] = None
+        return result
+
+    ok, plugin = __salt__['rundeck.get_plugin'](project_name, 'import')
+    if not ok:
+        result['comment'] = plugin
+        return result
+
+    ok, state = __salt__['rundeck.get_plugin_state'](project_name, 'import')
+    if not ok:
+        result['comment'] = state
+        return result
+
+    history = []
+
+    for action_name, action in [
+            ('initialize-tracking', 'rundeck.perform_scm_import_tracking'),
+            ('remote-pull', 'rundeck.perform_scm_import_pull'),
+            ('import-all', 'rundeck.perform_scm_import'),
+            ]:
+        if action_name in state['actions']:
+            ok, msg = __salt__[action](
+                project_name, plugin, params)
+            if not ok:
+                result['comment'] = msg
+                result['result'] = False
+                return result
+            else:
+                history.append(msg['message'])
+
+            ok, state = __salt__['rundeck.get_plugin_state'](
+                project_name, 'import')
+            if not ok:
+                result['comment'] = state
+                result['result'] = False
+                return result
+
+    if history:
+        result['changes'][name] = '\n'.join(history)
+    return result
+
+
+def disabled_import(name, project_name):
+    result = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+        'pchanges': {},
+    }
+    if __opts__['test'] == True:
+        result['comment'] = 'There is nothing to change in the test mode.'
+        result['result'] = None
+    ok, status = __salt__['rundeck.get_plugin_status'](project_name, 'import')
+    if ok:
+        if status['enabled']:
+            ok, msg = __salt__['rundeck.disable_plugin'](project_name, 'import')
+            result['comment'] = msg
+            if ok:
+                result['changes'][name] = 'DISABLED'
+                result['result'] = True
+        else:
+            result['result'] = True
+    else:
+        result['comment'] = status
+    return result
+
+
+def enabled_import(name, project_name):
+    result = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+        'pchanges': {},
+    }
+    if __opts__['test'] == True:
+        result['comment'] = 'There is nothing to change in the test mode.'
+        result['result'] = None
+    ok, status = __salt__['rundeck.get_plugin_status'](project_name, 'import')
+    if ok:
+        if status['configured'] and not status['enabled']:
+            ok, msg = __salt__['rundeck.enable_plugin'](project_name, 'import')
+            result['comment'] = msg
+            if ok:
+                result['changes'][name] = 'ENABLED'
+                result['result'] = True
+        elif not status['configured']:
+            result['comment'] = "Could not enable not configured SCM plugin."
+        else:
+            result['result'] = True
+    else:
+        result['comment'] = status
+    return result
diff --git a/rundeck/client/project.sls b/rundeck/client/project.sls
index 2dd212a..a143d92 100644
--- a/rundeck/client/project.sls
+++ b/rundeck/client/project.sls
@@ -5,12 +5,12 @@
 
 {%- set project_name = project.name|default(name) %}
 
-rundeck_{{ project_name }}_project:
+rundeck-{{ project_name }}-project:
   rundeck_project.present:
     - name: {{ project_name }}
-    - description: {{ project.description|default("") }}
+    - description: {{ project.description|default('') }}
 
-rundeck_{{ project_name }}_resources:
+rundeck-{{ project_name }}-resources:
   file.managed:
     - name: {{ server.root_dir }}/rundeck/projects/{{ project_name }}/etc/resources.yaml
     - source: salt://rundeck/files/resources.yaml
@@ -21,6 +21,53 @@
     - context:
         project_name: {{ project_name }}
     - require:
-      - rundeck_project: rundeck_{{ project_name }}_project
+      - rundeck_project: rundeck-{{ project_name }}-project
+
+{%- set plugin = project.plugin|default({}) %}
+
+{%- if plugin.import is defined %}
+
+{%- set _import = plugin.import %}
+
+rundeck-{{ project_name }}-scm-import:
+  rundeck_scm.present_import:
+    - name: git-import
+    - address: {{ _import.address }}
+    - project_name: {{ project_name }}
+{%- if _import.format is defined %}
+    - format: {{ _import.format }}
+{%- endif %}
+{%- if _import.branch is defined %}
+    - branch: {{ _import.branch }}
+{%- endif %}
+{%- if _import.import_uuid_behavior is defined %}
+    - import_uuid_behavior: {{ _import.import_uuid_behavior }}
+{%- endif %}
+{%- if _import.path_template is defined %}
+    - path_template: {{ _import.path_template }}
+{%- endif %}
+    - require:
+      - rundeck_project: rundeck-{{ project_name }}-project
+
+rundeck-{{ project_name }}-scm-import-enable:
+  rundeck_scm.enabled_import:
+    - name: git-import
+    - project_name: {{ project_name }}
+    - require:
+      - rundeck_scm: rundeck-{{ project_name }}-scm-import
+
+rundeck-{{ project_name }}-scm-import-sync:
+  rundeck_scm.sync_import:
+    - name: git-import
+    - project_name: {{ project_name }}
+{%- if _import.file_pattern is defined %}
+    - file_pattern: {{ _import.file_pattern }}
+{%- endif %}
+    - require:
+      - rundeck_scm: rundeck-{{ project_name }}-scm-import
+    - watch:
+      - rundeck_scm: rundeck-{{ project_name }}-scm-import-enable
+
+{%- endif %}
 
 {%- endfor %}