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