Add ability to configure secret keys

Change-Id: I119e00a4b7feaec9d93401ef9e825ee46d304be4
diff --git a/README.rst b/README.rst
index d473b3d..904604b 100644
--- a/README.rst
+++ b/README.rst
@@ -84,6 +84,40 @@
 Configure Client
 ~~~~~~~~~~~~~~~~
 
+Configure Secret Keys
+^^^^^^^^^^^^^^^^^^^^^
+
+It is possible to configure secret items in Key Storage in Rundeck:
+
+.. code-block:: yaml
+
+     rundeck:
+       client:
+         enabled: true
+         secret:
+           openstack/username:
+             type: password
+             content: admin
+           openstack/password:
+             type: password
+             content: password
+           openstack/keypair/private:
+             type: private
+             content: <private>
+           openstack/keypair/public:
+             type: public
+             content: <public>
+
+It is possible to disable keys to be sure that they are not present in Rundeck:
+
+.. code-block:: yaml
+
+    rundeck:
+       client:
+         secret:
+           openstack/username:
+             enabled: false
+
 Configure Projects
 ^^^^^^^^^^^^^^^^^^
 
diff --git a/_modules/rundeck.py b/_modules/rundeck.py
index f47c837..5d9c7a3 100644
--- a/_modules/rundeck.py
+++ b/_modules/rundeck.py
@@ -251,6 +251,58 @@
         .format(project_name, resp.status_code, resp.text))
 
 
+# Key Store
+
+def get_secret_metadata(path):
+    session, make_url = get_session()
+    resp = session.get(
+        make_url("/api/11/storage/keys/{}".format(path)),
+        allow_redirects=False,
+    )
+    if resp.status_code == 200:
+        return True, resp.json()
+    elif resp.status_code == 404:
+        return True, None
+    return False, (
+        "Could not retrieve metadata for the {} secret key: {}/{}"
+        .format(path, resp.status_code, resp.text))
+
+
+def upload_secret(path, type, content, update=False):
+    session, make_url = get_session()
+    session.headers['Content-Type'] = SECRET_CONTENT_TYPE[type]
+    method = session.put if update else session.post
+    resp = method(
+        make_url("/api/11/storage/keys/{}".format(path)),
+        data=content,
+        allow_redirects=False,
+    )
+    if resp.status_code in (200, 201):
+        return True, resp.json()
+    return False, (
+        "Could not create or update the {} secret key with the type {}: {}/{}"
+        .format(path, type, resp.status_code, resp.text))
+
+SECRET_CONTENT_TYPE = {
+    "private": "application/octet-stream",
+    "public": "application/pgp-keys",
+    "password": "application/x-rundeck-data-password",
+}
+
+
+def delete_secret(path):
+    session, make_url = get_session()
+    resp = session.delete(
+        make_url("/api/11/storage/keys/{}".format(path)),
+        allow_redirects=False,
+    )
+    if resp.status_code == 204:
+        return True, None
+    return False, (
+        "Could not delete the {} secret key: {}/{}"
+        .format(path, resp.status_code, resp.text))
+
+
 # Utils
 
 def create_project_config(project_name, params, config=None):
diff --git a/_states/rundeck_secret.py b/_states/rundeck_secret.py
new file mode 100644
index 0000000..c162a75
--- /dev/null
+++ b/_states/rundeck_secret.py
@@ -0,0 +1,72 @@
+import logging
+
+LOG = logging.getLogger(__name__)
+
+
+def __virtual__():
+    if 'rundeck.get_project' not in __salt__:
+        return (
+            False,
+            'The rundeck_project state module cannot be loaded: rundeck is '
+            'unavailable',
+        )
+    return True
+
+
+def present(name, type, content):
+    result = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+        'pchanges': {},
+    }
+    if __opts__['test'] == True:
+        result['comment'] = 'Nothing to create in the test mode.'
+        result['result'] = None
+        return result
+    ok, secret = __salt__['rundeck.get_secret_metadata'](name)
+    if ok:
+        do_update = secret is not None
+        ok, msg = __salt__['rundeck.upload_secret'](name, type, content,
+                                                    update=do_update)
+        if ok:
+            result['changes'][name] = 'UPLOADED'
+            result['comment'] = (
+                "The {} secret key with the {} type was successfully uploaded."
+                .format(name, type))
+            result['result'] = True
+        else:
+            result['comment'] = msg
+    else:
+        result['comment'] = secret
+    return result
+
+
+def absent(name):
+    result = {
+        'name': name,
+        'changes': {},
+        'result': False,
+        'comment': '',
+        'pchanges': {},
+    }
+    if __opts__['test'] == True:
+        result['comment'] = 'Nothing to remove in the test mode.'
+        result['result'] = None
+        return result
+    ok, secret = __salt__['rundeck.get_secret_metadata'](name)
+    if ok:
+        if not secret:
+            result['result'] = True
+            return result
+        ok, msg = __salt__['rundeck.delete_secret'](name)
+        if ok:
+            result['changes'][name] = 'DELETED'
+            result['comment'] = "Secret key {} was removed.".format(name)
+            result['result'] = True
+        else:
+            result['comment'] = msg
+    else:
+        result['comment'] = secret
+    return result
diff --git a/rundeck/client/init.sls b/rundeck/client/init.sls
index 866b983..54b4f3b 100644
--- a/rundeck/client/init.sls
+++ b/rundeck/client/init.sls
@@ -5,6 +5,9 @@
 {%- if client.project is defined %}
   - rundeck.client.project
 {%- endif %}
+{%- if client.secret is defined %}
+  - rundeck.client.secret
+{%- endif %}
 {%- endif %}
 
 {%- if grains.get('noservices', False) %}
diff --git a/rundeck/client/project.sls b/rundeck/client/project.sls
index 63b4467..fd97e1c 100644
--- a/rundeck/client/project.sls
+++ b/rundeck/client/project.sls
@@ -1,4 +1,4 @@
-{% from "rundeck/map.jinja" import server with context %}
+{%- from "rundeck/map.jinja" import server with context %}
 {%- from "rundeck/map.jinja" import client with context %}
 
 {%- for name, project in client.project.items() %}
diff --git a/rundeck/client/secret.sls b/rundeck/client/secret.sls
new file mode 100644
index 0000000..7625bfb
--- /dev/null
+++ b/rundeck/client/secret.sls
@@ -0,0 +1,29 @@
+{%- from "rundeck/map.jinja" import client with context %}
+
+{%- for name, secret in client.get('secret', {}).items() %}
+
+{%- set path = secret.path|default(name) %}
+
+{%- if secret.enabled|default(True) %}
+
+rundeck-key-{{ path|replace('/', '-') }}-create:
+  rundeck_secret.present:
+    - name: {{ path }}
+    - type: {{ secret['type'] }}
+    - content: {{ secret['content'] }}
+    {%- if grains.get('noservices', False) %}
+    - onlyif: 'false'
+    {%- endif %}
+
+{%- else %}
+
+rundeck-key-{{ path|replace('/', '-') }}-delete:
+  rundeck_secret.absent:
+    - name: {{ path }}
+    {%- if grains.get('noservices', False) %}
+    - onlyif: 'false'
+    {%- endif %}
+
+{%- endif %}
+
+{%- endfor %}
diff --git a/tests/pillar/client.sls b/tests/pillar/client.sls
index 31babdb..f15bc39 100644
--- a/tests/pillar/client.sls
+++ b/tests/pillar/client.sls
@@ -19,3 +19,20 @@
           import:
             address: http://gerrit.cluster.local/jobs/rundeck-jobs.git
             branch: master
+    secret:
+      openstack/auth_url:
+        type: password
+        content: http://openstack.cluster.local/identity/v3/auth/tokens
+      openstack/username:
+        type: password
+        content: admin
+      openstack/password:
+        type: password
+        content: password
+      openstack/project_name:
+        type: password
+        content: admin
+      openstack/keypair:
+        enabled: false
+      ssh/runbook:
+        enabled: false