Refactor configuration

  - split client SLS to config and repo
  - replace modules artifactory_config and artifactory_repo by the single
    module named `artifactory`

Change-Id: I0fa2dadc2443dbbd40bb10f3745dc4330f0c4578
diff --git a/_modules/artifactory.py b/_modules/artifactory.py
new file mode 100644
index 0000000..6987ada
--- /dev/null
+++ b/_modules/artifactory.py
@@ -0,0 +1,251 @@
+# -*- coding: utf-8 -*-
+'''
+Module for configuring Artifactory.
+===================================
+'''
+
+import json
+import logging
+import requests
+
+from collections import OrderedDict
+
+from lxml import etree
+from lxml import objectify
+
+
+log = logging.getLogger(__name__)
+
+
+def _api_call(endpoint, data=None, headers=None, method='GET',
+              **connection_args):
+
+    log.debug('Got connection args: {}'.format(connection_args))
+
+    # Set default values if empty
+    if 'proto' not in connection_args:
+        connection_args['proto'] = 'http'
+    if 'host' not in connection_args:
+        connection_args['host'] = 'localhost'
+    if 'port' not in connection_args:
+        connection_args['port'] = 80
+
+    base_url = connection_args.get(
+        'url',
+        '{proto}://{host}:{port}/artifactory'.format(**connection_args)
+    )
+    api_url = base_url + '/api'
+
+    username = connection_args.get('user', 'admin')
+    password = connection_args.get('password', 'password')
+    ssl_verify = connection_args.get('ssl_verify', True)
+
+    # Prepare session object
+    api_connection = requests.Session()
+    api_connection.auth = (username, password)
+    api_connection.verify = ssl_verify
+
+    # Override default method if data given
+    if(data and method == 'GET'):
+        method = 'POST'
+
+    endpoint_url = api_url + endpoint
+    log.debug('Doing {0} request to {1}'.format(method, endpoint_url))
+
+    # API call request
+    resp = api_connection.request(
+        method=method,
+        url=endpoint_url,
+        data=data,
+        headers=headers
+    )
+
+    if resp.status_code == requests.codes.ok:
+        return True, resp.text
+    else:
+        errors = json.loads(resp.text).get('errors')
+        if errors:
+            for error in errors:
+                log.error('%(status)s:%(message)s' % error)
+        else:
+            log.error('%(status)s:%(message)s' % json.loads(resp.text))
+        return False, json.loads(resp.text)
+
+
+def get_license(**kwargs):
+    endpoint = '/system/license'
+
+    return _api_call(endpoint, **kwargs)
+
+
+def add_license(license_key, **kwargs):
+    endpoint = '/system/license'
+
+    change_data = {
+        'licenseKey': license_key,
+    }
+
+    return _api_call(
+        endpoint=endpoint,
+        data=json.dumps(change_data),
+        headers={'Content-Type': 'application/json'},
+        **kwargs
+    )
+
+def get_config(**kwargs):
+    endpoint = '/system/configuration'
+
+    return _api_call(endpoint, **kwargs)
+
+
+def set_config(config_data, **kwargs):
+    endpoint = '/system/configuration'
+
+    return _api_call(
+        endpoint=endpoint,
+        data=config_data,
+        headers={'Content-Type': 'application/xml'},
+        **kwargs
+    )
+
+
+def get_ldap_config(name, **kwargs):
+
+    result, config_data = get_config(**kwargs)
+    config = objectify.fromstring(config_data.encode('ascii'))
+
+    # Find existing LDAP settings with specified key ...
+    ldap_config = None
+    for ldap_setting_iter in config.security.ldapSettings.getchildren():
+        if ldap_setting_iter.key.text == name:
+            ldap_config = ldap_setting_iter
+            break
+
+    # ... and create new one if not exists
+    if ldap_config is None:
+        ldap_config = objectify.SubElement(
+            config.security.ldapSettings, 'ldapSetting')
+        objectify.SubElement(ldap_config, 'key')._setText(name)
+
+    return result, etree.tostring(ldap_config)
+
+def set_ldap_config(name, uri, base=None, enabled=True, dn_pattern=None,
+                    manager_dn=None, manager_pass=None, search_subtree=True,
+                    search_filter='(&(objectClass=inetOrgPerson)(uid={0}))',
+                    attr_mail='mail', create_users=True, safe_search=True,
+                    **kwargs):
+
+    result, config_data = get_config(**kwargs)
+    config = objectify.fromstring(config_data.encode('ascii'))
+
+    # NOTE! Elements must ber sorted in exact order!
+    key_map = OrderedDict([
+        ('enabled', 'enabled'),
+        ('ldapUrl', 'uri'),
+        ('userDnPattern', 'dn_pattern'),
+        ('search', ''),
+        ('autoCreateUser', 'create_users'),
+        ('emailAttribute', 'attr_mail'),
+        ('ldapPoisoningProtection', 'safe_search'),
+    ])
+
+    key_map_search = OrderedDict([
+        ('searchFilter', 'search_filter'),
+        ('searchBase', 'base'),
+        ('searchSubTree', 'search_subtree'),
+        ('managerDn', 'manager_dn'),
+        ('managerPassword', 'manager_pass'),
+    ])
+
+    # Find existing LDAP settings with specified key ...
+    ldap_config = None
+    for ldap_setting_iter in config.security.ldapSettings.getchildren():
+        if ldap_setting_iter.key.text == name:
+            ldap_config = ldap_setting_iter
+            search_config = ldap_config.search
+            break
+
+    # ... and create new one if not exists
+    if ldap_config is None:
+        ldap_config = objectify.SubElement(
+            config.security.ldapSettings, 'ldapSetting')
+        objectify.SubElement(ldap_config, 'key')._setText(name)
+
+    # LDAP options
+    for xml_key, var_name in key_map.iteritems():
+
+        # Search subtree must follow element order
+        if xml_key == 'search' and not hasattr(ldap_config, 'search'):
+            search_config = objectify.SubElement(ldap_config, 'search')
+            break
+
+        if var_name in locals():
+            # Replace None with empty strings
+            var_value = locals()[var_name] or ''
+            if isinstance(var_value, bool):
+                # Boolean values should be lowercased
+                xml_text = str(var_value).lower()
+            else:
+                xml_text = str(var_value)
+
+            if hasattr(ldap_config, xml_key):
+                ldap_config[xml_key]._setText(xml_text)
+            else:
+                objectify.SubElement(ldap_config, xml_key)._setText(
+                    xml_text)
+
+    # Search options (same code as above but using search_config)
+    for xml_key, var_name in key_map_search.iteritems():
+        if var_name in locals():
+            # Replace None with empty strings
+            var_value = locals()[var_name] or ''
+            if isinstance(var_value, bool):
+                # Boolean values should be lowercased
+                xml_text = str(var_value).lower()
+            else:
+                xml_text = str(var_value)
+
+            if hasattr(search_config, xml_key):
+                search_config[xml_key]._setText(xml_text)
+            else:
+                objectify.SubElement(search_config, xml_key)._setText(
+                    xml_text)
+
+    change_data = etree.tostring(config)
+
+    return set_config(change_data, **kwargs)
+
+
+def list_repos(**kwargs):
+    endpoint = '/repositories'
+
+    return _api_call(endpoint, **kwargs)
+
+
+def get_repo(name, **kwargs):
+    result, repo_list = list_repos(**kwargs)
+    if name in [r['key'] for r in json.loads(repo_list)]:
+        endpoint = '/repositories/' + name
+        return _api_call(endpoint, **kwargs)
+    else:
+        return True, {}
+
+
+def set_repo(name, repo_config, **kwargs):
+    log.debug('Got repo parameters: {}'.format(repo_config))
+
+    result, repo_list = list_repos(**kwargs)
+    if name in [r['key'] for r in json.loads(repo_list)]:
+        method = 'POST'
+    else:
+        method = 'PUT'
+
+    endpoint = '/repositories/' + name
+
+    return _api_call(
+        endpoint=endpoint,
+        method=method,
+        data=json.dumps(repo_config),
+        headers={'Content-Type': 'application/json'},
+        **kwargs
+    )
diff --git a/_modules/artifactory_config.py b/_modules/artifactory_config.py
deleted file mode 100644
index 5fa4e66..0000000
--- a/_modules/artifactory_config.py
+++ /dev/null
@@ -1,168 +0,0 @@
-# -*- coding: utf-8 -*-
-'''
-Module for configuring Artifactory.
-change admin password
-add license key
-confingure ldap
-'''
-
-# Import python libs
-from __future__ import absolute_import
-import os
-import base64
-import logging
-
-# Import Salt libs
-import salt.utils
-import salt.ext.six.moves.http_client  # pylint: disable=import-error,redefined-builtin,no-name-in-module
-from salt.ext.six.moves import urllib  # pylint: disable=no-name-in-module
-from salt.ext.six.moves.urllib.error import HTTPError, URLError  # pylint: disable=no-name-in-module
-
-import json
-import requests
-
-log = logging.getLogger(__name__)
-
-__virtualname__ = 'artifactory_config'
-
-
-def __virtual__():
-
-    return True
-
-
-
-#    "repoLayoutRef" : "maven-2-default",
-
-class Artifactoryconfig:
-
-    def __init__(self, config={}):
-
-        self.files = []
-        self.def_password = 'password'
-        self.def_user= 'admin'
-
-
-        client_config = {
-            'artifactory_url': 'http://your-instance/artifactory/api',
-            'username': 'your-user',
-            'password': 'password',
-            'license_key' :'key',
-            'artifactory_ldap_url': 'http://localhost',
-#            'headers': {'Content-type': 'application/json'},
-            'ssl_verify': True
-        }
-
-        client_config.update(config)
-
-        # Set instance variables for every value in party_config
-        for k, v in client_config.items():
-            setattr(self, '%s' % (k,), v)
-
-    def change_admin_password(self,  **connection_args):
-        """
-        Usage: POST /api/security/users/authorization/changePassword  -H "Content-type: application/json" -d ' { "userName" : "{user}", "oldPassword" : "{old password}", "newPassword1" : "{new password}", "newPassword2" : "{verify new password}" }
-        :param connection_args:
-        :return: 0 if ok
-        """
-
-        url = self.artifactory_url + '/security/users/authorization/changePassword'
-        log.error(str(url))
-        data_pass={ "userName": self.def_user, "oldPassword": self.def_password, "newPassword2": self.password,"newPassword1": self.password}
-        auth = (self.username, self.def_password)
-
-        r = requests.post(url, auth=auth, json=data_pass)
-        log.error(str(r.text))
-        return r
-
-    def add_license_key(self, **connection_args):
-        """
-        Usage: POST /api/system/license
-        :param connection_args:
-        :return: 0 if ok
-        """
-        url = self.artifactory_url +'/system/license'
-        log.error(str(url))
-        auth = (self.username, self.password)
-        key = {"licenseKey": self.license_key}
-        log.error(str(key))
-        r = requests.post(url, auth=auth, json=key)
-        log.error(str(r.text))
-        return r
-
-    def configure_ldap(self, **connection_args):
-        url = self.artifactory_url + '/system/configuration'
-        auth = (self.username, self.password)
-        # r = requests.post(url,auth=auth,json=data)
-
-        r = requests.get(url, auth=auth)
-        # print json.dumps(r.text)
-        #log.error(str(r.text))
-
-        ldap_url = self.artifactory_ldap_url
-        searchFilter = "uid={0}"
-        xmlTemplate = """
-                    <ldapSetting>
-                        <key>ldap</key>
-                        <enabled>true</enabled>
-                        <ldapUrl>%(url)s</ldapUrl>
-                        <search>
-                            <searchFilter>%(sf)s</searchFilter>
-                            <searchSubTree>true</searchSubTree>
-                        </search>
-                        <autoCreateUser>true</autoCreateUser>
-                        <emailAttribute>mail</emailAttribute>
-                        <ldapPoisoningProtection>true</ldapPoisoningProtection>
-                    </ldapSetting>
-                    """
-        xml_date = {'url': ldap_url, 'sf': searchFilter}
-        a = xmlTemplate % xml_date
-#        log.error(str( r.text))
-        z = str(r.text).split('<ldapSettings/>')
-        out = str(z[0] + '<ldapSettings>' + a + '</ldapSettings>' + z[1])
-
-        # out=str(r.text).split('<ldapSettings/>')[0]+'<ldapSettings/>'+str(r.text).split('<ldapSettings/>')[1]
-        headers = {'Content-Type': 'application/xml'}
-
-        r = requests.post(url, auth=auth, data=out, headers=headers)
-        log.error(str(r.text))
-        return r
-
-def _client(**connection_args):
-    '''
-    Set up artifactory credentials
-
-    '''
-
-    prefix = "artifactory"
-
-    # look in connection_args first, then default to config file
-    def get(key, default=None):
-        return connection_args.get('connection_' + key,
-            __salt__['config.get'](prefix, {})).get(key, default)
-
-    client_config = {
-      'artifactory_url': '%s://%s:%s/artifactory/api' % (get('proto', 'http'), get('host', 'localhost'), get('port', '8080')),
-      'ssl_verify': get('ssl_verify', True),'license_key':get('license_key','key'),'artifactory_ldap_url':get('ldap_server','url'),
-      'ldap_searchFilter':get('ldap_searchFilter','uid={0}'),'ldap_account_base':get('ldap_account_base','accaunt')
-    }
-
-    user = get('user', False)
-    password = get('password', False)
-    if user and password:
-      client_config['username'] = user
-      client_config['password'] = password
-
-    artifactory_config = Artifactoryconfig(client_config)
-
-    return artifactory_config
-
-
-
-def artifactory_init(**connection_args):
-
-    artifactory = _client(**connection_args)
-    artifactory.change_admin_password()
-    artifactory.add_license_key()
-    artifactory.configure_ldap()
-
diff --git a/_modules/artifactory_repo.py b/_modules/artifactory_repo.py
deleted file mode 100644
index a97d215..0000000
--- a/_modules/artifactory_repo.py
+++ /dev/null
@@ -1,330 +0,0 @@
-# -*- coding: utf-8 -*-
-'''
-Module for fetching artifacts from Artifactory
-'''
-
-# Import python libs
-from __future__ import absolute_import
-import os
-import base64
-import logging
-
-# Import Salt libs
-import salt.utils
-import salt.ext.six.moves.http_client  # pylint: disable=import-error,redefined-builtin,no-name-in-module
-from salt.ext.six.moves import urllib  # pylint: disable=no-name-in-module
-from salt.ext.six.moves.urllib.error import HTTPError, URLError  # pylint: disable=no-name-in-module
-
-import json
-import requests
-
-log = logging.getLogger(__name__)
-
-__virtualname__ = 'artifactory_repo'
-
-
-def __virtual__():
-
-    return True
-
-
-repo_config = {
-    "key": "local-repo1",
-    "rclass" : "local",
-    "packageType": "generic",
-    "description": "The local repository public description",
-}
-
-#    "repoLayoutRef" : "maven-2-default",
-
-class ArtifactoryClient:
-
-    def __init__(self, config={}):
-
-        self.files = []
-
-        client_config = {
-            'artifactory_url': 'http://your-instance/artifactory/api',
-            'search_prop': 'search/prop',
-            'search_name': 'search/artifact',
-            'search_repos': 'repositories',
-            'username': 'your-user',
-            'password': 'password',
-            'headers': {'Content-type': 'application/json'},
-            'ssl_verify': True
-        }
-
-        client_config.update(config)
-
-        # Set instance variables for every value in party_config
-        for k, v in client_config.items():
-            setattr(self, '%s' % (k,), v)
-
-    def create_repository(self, name, config, **connection_args):
-        repositories = []
-
-        query = "%s/%s/%s" % (self.artifactory_url, self.search_repos, name)
-        auth = (self.username, self.password)
-
-        r = requests.put(query, auth=auth, json=config, verify=self.ssl_verify)
-        print(r.content)
-
-        raw_response = self.query_artifactory(query)
-        if raw_response is None:
-            return []
-        response = json.loads(raw_response.text)
-        for line in response:
-            for item in line:
-                repositories.append(line)
-
-        if repositories:
-            return repositories
-
-        return []
-
-
-    def get_repositories(self, repo_type=None, **connection_args):
-        repositories = []
-
-        if repo_type is None:
-            query = "%s/%s" % (self.artifactory_url, self.search_repos)
-        else:
-            query = "%s/%s?type=%s" % (self.artifactory_url,
-                                       self.search_repos, repo_type)
-
-        raw_response = self.query_artifactory(query)
-        if raw_response is None:
-            return []
-        response = json.loads(raw_response.text)
-        for line in response:
-            for item in line:
-                repositories.append(line)
-
-        if repositories:
-            return repositories
-
-        return []
-
-
-    def query_artifactory(self, query, query_type='get'):
-        """
-        Send request to Artifactory API endpoint.
-        @param: query - Required. The URL (including endpoint) to send to the Artifactory API
-        @param: query_type - Optional. CRUD method. Defaults to 'get'.
-        """
-
-        auth = (self.username, self.password)
-        query_type = query_type.lower()
-
-        if query_type == "get":
-            response = requests.get(query, auth=auth, headers=self.headers, verify=self.ssl_verify)
-        elif query_type == "put":
-            response = requests.put(query, data=query.split('?', 1)[1], auth=auth, headers=self.headers, verify=self.ssl_verify)
-        if query_type == "post":
-            pass
-
-        if not response.ok:
-            return None
-
-        return response
-
-    def query_file_info(self, filename):
-        """
-        Send request to Artifactory API endpoint for file details.
-        @param: filename - Required. The shortname of the artifact
-        """
-        query = "%s/storage/%s" % (self.artifactory_url, filename)
-
-        raw_response = self.query_artifactory(query)
-        if raw_response is None:
-            return raw_response
-        response = json.loads(raw_response.text)
-
-        return response
-
-    def find_by_properties(self, properties):
-        """
-        Look up an artifact, or artifacts, in Artifactory by using artifact properties.
-        @param: properties - List of properties to use as search criteria.
-        """
-        query = "%s/%s?%s" % (self.artifactory_url,
-                              self.search_prop, urlencode(properties))
-        raw_response = self.query_artifactory(query)
-        if raw_response is None:
-            return raw_response
-
-        response = json.loads(raw_response.text)
-
-        for item in response['results']:
-            for k, v in item.items():
-                setattr(self, '%s' % (k,), v)
-
-        if not response['results']:
-            return None
-
-        artifact_list = []
-        for u in response['results']:
-            artifact_list.append(os.path.basename(u['uri']))
-
-        self.files = artifact_list
-        setattr(self, 'count', len(artifact_list))
-
-        return "OK"
-
-    def find(self, filename):
-        """
-        Look up an artifact, or artifacts, in Artifactory by
-        its filename.
-        @param: filename - Filename of the artifact to search.
-        """
-        query = "%s/%s?name=%s" % (self.artifactory_url,
-                                   self.search_name, filename)
-        raw_response = self.query_artifactory(query)
-        if raw_response is None:
-            return raw_response
-        response = json.loads(raw_response.text)
-        if len(response['results']) < 1:
-            return None
-
-        setattr(self, 'name', filename)
-        setattr(self, 'url', json.dumps(response))
-
-        return "OK"
-
-    def get_properties(self, filename, properties=None):
-        """
-        Get an artifact's properties, as defined in the Properties tab in
-        Artifactory.
-        @param: filename - Filename of artifact of which to get properties.
-        @param: properties - Optional. List of properties to help filter results.
-        """
-        if properties:
-            query = "%s?properties=%s" % (filename, ",".join(properties))
-        else:
-            query = "%s?properties" % filename
-
-        raw_response = self.query_artifactory(query)
-        if raw_response is None:
-            return raw_response
-        response = json.loads(raw_response.text)
-        for key, value in response.items():
-            setattr(self, '%s' % (key,), value)
-
-        return "OK"
-
-
-def _client(**connection_args):
-    '''
-    Set up artifactory credentials
-
-    '''
-
-    prefix = "artifactory"
-
-    # look in connection_args first, then default to config file
-    def get(key, default=None):
-        return connection_args.get('connection_' + key,
-            __salt__['config.get'](prefix, {})).get(key, default)
-
-    client_config = {
-      'artifactory_url': '%s://%s:%s/artifactory/api' % (get('proto', 'http'), get('host', 'localhost'), get('port', '8080')),
-      'ssl_verify': get('ssl_verify', True)
-    }
-
-    user = get('user', False)
-    password = get('password', False)
-    if user and password:
-      client_config['username'] = user
-      client_config['password'] = password
-
-    artifactory_client = ArtifactoryClient(client_config)
-
-    return artifactory_client
-
-
-def repo_list(repo_type=None, **connection_args):
-    '''
-    Return a list of available repositories
-
-    CLI Example:
-
-    .. code-block:: bash
-
-        salt '*' artifactory_repo.repo_list
-        salt '*' artifactory_repo.repo_list REMOTE
-        salt '*' artifactory_repo.repo_list LOCAL
-    '''
-    ret = {}
-
-    artifactory = _client(**connection_args)
-    repos = artifactory.get_repositories(repo_type)
-
-    for repo in repos:
-        if 'key' in repo:
-            ret[repo.get('key')] = repo
-    return ret
-
-
-def repo_get(name, **connection_args):
-    '''
-    Return a list of available repositories
-
-    CLI Example:
-
-    .. code-block:: bash
-
-        salt '*' artifactory_repo.repo_get reponame
-    '''
-
-    ret = {}
-
-    repos = repo_list(None, **connection_args)
-    if not name in repos:
-        return {'Error': "Error retrieving repository {0}".format(name)}
-    ret[name] = repos[name]
-    return ret
-
-
-def repo_create(name, repo_type="local", package="generic", url=None, **connection_args):
-    '''
-    Create a artifactory repository
-
-    :param name: new repo name
-    :param repo_type: new repo type
-    :param package: new repo package type
-        "gradle" | "ivy" | "sbt" | "nuget" | "gems" | "npm" | "bower" |
-        "debian" | "pypi" | "docker" | "vagrant" | "gitlfs" | "yum" |
-        "generic"
-
-
-    CLI Examples:
-
-    .. code-block:: bash
-
-        salt '*' artifactory_repo.repo_create projectname remote generic
-
-    '''
-    ret = {}
-
-    if url in connection_args and url == None:
-        url = connection_args['url']
-
-    repo = repo_get(name, **connection_args)
-
-    if repo and not "Error" in repo:
-        log.debug("Repository {0} exists".format(name))
-        return repo
-
-    repo_config = {
-        "key": name,
-        "rclass" : repo_type,
-        "packageType": package,
-        "description": "The local repository public description",
-    }
-
-    if repo_type == "remote":
-        repo_config['url'] = url
-
-    artifactory = _client(**connection_args)
-    artifactory.create_repository(name, repo_config)
-    return repo_get(name, **connection_args)