Add basic client for Rundeck

Change-Id: I4b1a8deedb858ec664d4f888ef78458bd19f102f
diff --git a/_modules/rundeck.py b/_modules/rundeck.py
new file mode 100644
index 0000000..9d53361
--- /dev/null
+++ b/_modules/rundeck.py
@@ -0,0 +1,117 @@
+import logging
+
+import salt.exceptions
+
+import requests
+from requests.compat import urljoin
+
+LOG = logging.getLogger(__name__)
+
+
+def get_project(name):
+    session, make_url = get_session()
+    resp = session.get(make_url("/api/18/project/{}".format(name)))
+    status_code = resp.status_code
+    if status_code == 200:
+        return resp.json()
+    elif status_code == 404:
+        return None
+    raise salt.exceptions.SaltInvocationError(
+        "Could not retrieve information about project {} from Rundeck {}: {}"
+        .format(name, make_url.base_url, status_code))
+
+
+def create_project(name, params):
+    session, make_url = get_session()
+    config = create_project_config(name, params)
+    LOG.debug("create_project: %s", name)
+    LOG.warning("create_project.config: %s/%s", name, config)
+    resp = session.post(
+        make_url("/api/18/projects"),
+        json={
+            'name': name,
+            'config': config,
+        },
+        allow_redirects=False,
+    )
+    if resp.status_code == 201:
+        return resp.json()
+    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(
+        make_url("/api/18/project/{}/config".format(name)),
+        json=config,
+        allow_redirects=False,
+    )
+    if resp.status_code == 201:
+        return resp.json()
+    LOG.debug("update_project: %s", name)
+
+
+def delete_project(name):
+    session, make_url = get_session()
+    resp = session.delete(make_url("/api/18/project/{}".format(name)))
+    status_code = resp.status_code
+    if status_code != 204:
+        raise salt.exceptions.SaltInvocationError(
+            "Could not remove project {} from Rundeck {}: {}"
+            .format(name, make_url.base_url, status_code))
+
+
+def get_session():
+    def make_url(url):
+        return urljoin(make_url.base_url, url)
+
+    rundeck_url = __salt__['config.get']('rundeck.url')
+    make_url.base_url = rundeck_url
+
+    api_token = __salt__['config.get']('rundeck.api_token')
+    username = __salt__['config.get']('rundeck.username')
+    password = __salt__['config.get']('rundeck.password')
+
+    session = requests.Session()
+
+    if api_token:
+        session.headers.update({
+            'Content-Type': 'application/json',
+            'X-Rundeck-Auth-Token': api_token,
+        })
+    else:
+        resp = session.post(make_url('/j_security_check'),
+            data={
+                'j_username': username,
+                'j_password': password,
+            },
+        )
+        if (resp.status_code != 200 or
+                '/user/error' in resp.url or
+                '/user/login' in resp.url):
+            raise salt.exceptions.SaltInvocationError(
+                "Username/passowrd authorization failed in Rundeck {} for "
+                "user {}".format(rundeck_url, username))
+    session.params.update({
+        'format': 'json',
+    })
+    return session, make_url
diff --git a/_states/rundeck_project.py b/_states/rundeck_project.py
new file mode 100644
index 0000000..2e877a6
--- /dev/null
+++ b/_states/rundeck_project.py
@@ -0,0 +1,58 @@
+import logging
+
+LOG = logging.getLogger(__name__)
+
+
+def present(name, description=''):
+    ret = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+        'pchanges': {},
+    }
+    if __opts__['test'] == True:
+        ret['comment'] = 'Nothing to change in the test mode.'
+        ret['result'] = None
+        return ret
+    params = {
+        "description": description,
+    }
+    project = __salt__['rundeck.get_project'](name)
+    if project:
+        config = __salt__['rundeck.create_project_config'](
+            name, params, config=project["config"])
+        if project["config"] != config:
+            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"
+        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['result'] = True
+    return ret
+
+
+def absent(name):
+    ret = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+        'pchanges': {},
+    }
+    if __opts__['test'] == True:
+        ret['comment'] = 'Nothing to remove in the test mode.'
+        ret['result'] = None
+        return ret
+    project = __salt__['rundeck.get_project'](name)
+    if project:
+        __salt__['rundeck.delete_project'](name)
+        ret['changes'][name] = 'DELETED'
+        ret['comment'] = "Project {} was removed.".format(name)
+    ret['result'] = True
+    return ret
diff --git a/debian/control b/debian/control
index 5cc8cd3..5ac5f59 100644
--- a/debian/control
+++ b/debian/control
@@ -10,6 +10,6 @@
 
 Package: salt-formula-rundeck
 Architecture: all
-Depends: ${misc:Depends}, salt-master, reclass
+Depends: ${misc:Depends}, salt-master, reclass, python-requests
 Description: rundeck salt formula
  Install and configure Rundeck.
diff --git a/debian/install b/debian/install
index 796f558..c3dc990 100644
--- a/debian/install
+++ b/debian/install
@@ -1,3 +1,4 @@
 rundeck/*           /usr/share/salt-formulas/env/rundeck/
 metadata/service/*  /usr/share/salt-formulas/reclass/service/rundeck/
 _states/*           /usr/share/salt-formulas/env/_states/
+_modules/*           /usr/share/salt-formulas/env/_modules/
diff --git a/metadata/service/client/init.yml b/metadata/service/client/init.yml
new file mode 100644
index 0000000..deefeb0
--- /dev/null
+++ b/metadata/service/client/init.yml
@@ -0,0 +1,6 @@
+applications:
+  - rundeck
+parameters:
+  rundeck:
+    client:
+      enabled: true
diff --git a/metadata/service/common.yml b/metadata/service/common.yml
new file mode 100644
index 0000000..233451a
--- /dev/null
+++ b/metadata/service/common.yml
@@ -0,0 +1,6 @@
+parameters:
+  _param:
+    rundeck_runbook_user: runbook
+    rundeck_admin_username: admin
+    rundeck_admin_password: password
+    rundeck_admin_token: password
diff --git a/metadata/service/server/single.yml b/metadata/service/server/single.yml
index 0524e89..d4c767c 100644
--- a/metadata/service/server/single.yml
+++ b/metadata/service/server/single.yml
@@ -1,12 +1,9 @@
 applications:
   - rundeck
 classes:
+  - service.rundeck.common
   - service.rundeck.support
 parameters:
-  _param:
-    rundeck_admin_username: admin
-    rundeck_admin_password: password
-    rundeck_admin_token: password
   rundeck:
     server:
       enabled: true
diff --git a/rundeck/client/init.sls b/rundeck/client/init.sls
new file mode 100644
index 0000000..e0c7eff
--- /dev/null
+++ b/rundeck/client/init.sls
@@ -0,0 +1,13 @@
+{%- from "rundeck/map.jinja" import client with context %}
+
+{%- if client.enabled|default(False) %}
+include:
+{%- if client.project is defined %}
+  - rundeck.client.project
+{%- endif %}
+{%- endif %}
+
+/etc/salt/minion.d/_rundeck.conf:
+  file.managed:
+  - source: salt://rundeck/files/_rundeck.conf
+  - template: jinja
diff --git a/rundeck/client/project.sls b/rundeck/client/project.sls
new file mode 100644
index 0000000..2dd212a
--- /dev/null
+++ b/rundeck/client/project.sls
@@ -0,0 +1,26 @@
+{% from "rundeck/map.jinja" import server with context %}
+{%- from "rundeck/map.jinja" import client with context %}
+
+{%- for name, project in client.project.items() %}
+
+{%- set project_name = project.name|default(name) %}
+
+rundeck_{{ project_name }}_project:
+  rundeck_project.present:
+    - name: {{ project_name }}
+    - description: {{ project.description|default("") }}
+
+rundeck_{{ project_name }}_resources:
+  file.managed:
+    - name: {{ server.root_dir }}/rundeck/projects/{{ project_name }}/etc/resources.yaml
+    - source: salt://rundeck/files/resources.yaml
+    - template: jinja
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
+    - mode: 640
+    - context:
+        project_name: {{ project_name }}
+    - require:
+      - rundeck_project: rundeck_{{ project_name }}_project
+
+{%- endfor %}
diff --git a/rundeck/files/_rundeck.conf b/rundeck/files/_rundeck.conf
new file mode 100644
index 0000000..7ab8449
--- /dev/null
+++ b/rundeck/files/_rundeck.conf
@@ -0,0 +1,10 @@
+{%- from "rundeck/map.jinja" import client with context %}
+{%- from "rundeck/map.jinja" import make_url with context %}
+{%- set creds = client.server.credentials %}
+rundeck.url: "{{ make_url(client.server.endpoint) }}"
+{%- if creds.api_token is defined %}
+rundeck.api_token: "{{ creds.api_token }}"
+{%- else %}
+rundeck.username: "{{ creds.username }}"
+rundeck.password: "{{ creds.password }}"
+{%- endif %}
diff --git a/rundeck/files/framework.properties b/rundeck/files/framework.properties
index 566de8e..0f30e08 100644
--- a/rundeck/files/framework.properties
+++ b/rundeck/files/framework.properties
@@ -6,8 +6,8 @@
 framework.server.username={{ admin.name }}
 framework.server.password={{ admin.password }}
 
-framework.server.hostname={{ server.api.hostname }}
-framework.server.name={{ server.api.hostname }}
+framework.server.hostname={{ server.api.host }}
+framework.server.name={{ server.api.host }}
 framework.server.port={{ server.api.port }}
 
 {%- set server_url = make_url(server.api) %}
@@ -16,7 +16,7 @@
 framework.server.url={{ server_url }}
 
 framework.ssh.user={{ server.ssh.user }}
-framework.ssh.keypath=/var/lib/rundeck/.ssh/id_rsa
+framework.ssh.keypath=/var/rundeck/.ssh/id_rsa
 framework.ssh.timeout={{ server.ssh.timeout }}
 
 rdeck.base=/var/lib/rundeck
diff --git a/rundeck/files/private_key b/rundeck/files/private_key
new file mode 100644
index 0000000..f53aacb
--- /dev/null
+++ b/rundeck/files/private_key
@@ -0,0 +1 @@
+{%- from "rundeck/map.jinja" import server with context -%}{{ server.ssh.private_key }}
diff --git a/rundeck/files/resources.yaml b/rundeck/files/resources.yaml
new file mode 100644
index 0000000..3436ce0
--- /dev/null
+++ b/rundeck/files/resources.yaml
@@ -0,0 +1,11 @@
+{%- from "rundeck/map.jinja" import client with context %}
+
+{%- for node in client.project.get(project_name, {}).get("node", {}).values() %}
+
+{{ node.nodename }}:
+  hostname: {{ node.hostname }}
+  nodename: {{ node.nodename }}
+  username: {{ node.username }}
+  tags: {{ node.get("tags", [])|yaml }}
+
+{%- endfor %}
diff --git a/rundeck/map.jinja b/rundeck/map.jinja
index 143d47a..bae03e9 100644
--- a/rundeck/map.jinja
+++ b/rundeck/map.jinja
@@ -2,7 +2,6 @@
   'Debian': {
     'home_dir': '/var/lib/rundeck',
     'root_dir': '/srv/rundeck',
-    'secure': False,
     'user': {
       'name': 'rundeck',
       'group': 'rundeck',
@@ -20,7 +19,9 @@
   },
 }, merge=salt['pillar.get']('rundeck:server')) %}
 
+{%- set client = salt['pillar.get']('rundeck:client') %}
+
 {% macro make_url(endpoint) -%}
 {%- if endpoint.get('https', False) -%}https://{%- else -%}http://{%- endif -%}
-{{ endpoint.hostname }}:{{ endpoint.port }}
+{{ endpoint.host }}:{{ endpoint.port }}
 {%- endmacro %}
diff --git a/rundeck/server/init.sls b/rundeck/server/init.sls
index db89cd5..6ca6120 100644
--- a/rundeck/server/init.sls
+++ b/rundeck/server/init.sls
@@ -1,4 +1,4 @@
-{% from "rundeck/map.jinja" import server with context %}
+{%- from "rundeck/map.jinja" import server with context %}
 {%- if server.enabled|default(False) %}
 
 rundeck_group:
@@ -13,7 +13,7 @@
   user.present:
     - name: {{ server.user.name }}
     - home: {{ server.home_dir }}
-    - shell: /bin/bash
+    - shell: /bin/false
     {%- if server.user.uid is defined %}
     - uid: {{ server.user.uid }}
     {%- endif %}
@@ -22,15 +22,15 @@
     {%- endif %}
     - system: True
     - groups:
-      - rundeck
+      - {{ server.user.group }}
     - require:
       - group: rundeck_group
 
 rundeck_home_dir:
   file.directory:
     - name: {{ server.home_dir }}
-    - user: rundeck
-    - group: rundeck
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
     - mode: 755
     - require:
       - user: rundeck_user
@@ -38,17 +38,31 @@
 rundeck_root_dir:
   file.directory:
     - name: {{ server.root_dir }}
-    - user: rundeck
-    - group: rundeck
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
     - mode: 755
     - require:
       - user: rundeck_user
 
+rundeck_lib_dirs:
+  file.directory:
+    - names:
+      - {{ server.root_dir }}/log
+      - {{ server.root_dir }}/logs
+      - {{ server.root_dir }}/plugins
+      - {{ server.root_dir }}/rundeck
+      - {{ server.root_dir }}/storage
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
+    - mode: 755
+    - require:
+      - file: rundeck_root_dir
+
 rundeck_etc_dir:
   file.directory:
     - name: {{ server.root_dir }}/etc
-    - user: rundeck
-    - group: rundeck
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
     - mode: 755
     - require:
       - user: rundeck_user
@@ -58,8 +72,8 @@
     - name: {{ server.root_dir }}/etc/framework.properties
     - source: salt://rundeck/files/framework.properties
     - template: jinja
-    - user: rundeck
-    - group: rundeck
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
     - mode: 640
     - require:
       - file: rundeck_etc_dir
@@ -69,8 +83,8 @@
     - name: {{ server.root_dir }}/etc/tokens.properties
     - source: salt://rundeck/files/tokens.properties
     - template: jinja
-    - user: rundeck
-    - group: rundeck
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
     - mode: 640
     - require:
       - file: rundeck_etc_dir
@@ -80,8 +94,8 @@
     - name: {{ server.root_dir }}/etc/realm.properties
     - source: salt://rundeck/files/realm.properties
     - template: jinja
-    - user: rundeck
-    - group: rundeck
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
     - mode: 640
     - require:
       - file: rundeck_etc_dir
@@ -91,10 +105,30 @@
     - name: {{ server.root_dir }}/etc/rundeck-config.properties
     - source: salt://rundeck/files/rundeck-config.properties
     - template: jinja
-    - user: rundeck
-    - group: rundeck
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
     - mode: 640
     - require:
       - file: rundeck_etc_dir
 
+rundeck_ssh_dir:
+  file.directory:
+    - name: {{ server.root_dir }}/rundeck/.ssh
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
+    - mode: 700
+    - require:
+      - file: rundeck_root_dir
+
+rundeck_ssh_private_key:
+  file.managed:
+    - name: {{ server.root_dir }}/rundeck/.ssh/id_rsa
+    - source: salt://rundeck/files/private_key
+    - template: jinja
+    - user: {{ server.user.name }}
+    - group: {{ server.user.group }}
+    - mode: 600
+    - require:
+      - file: rundeck_ssh_dir
+
 {%- endif %}