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/README.rst b/README.rst
index f9be099..60ac3ec 100644
--- a/README.rst
+++ b/README.rst
@@ -84,6 +84,20 @@
           repo_type: remote
           url: "http://totheremoterepo:80/"
 
+
+Repository configuration
+========================
+
+Sample pillar above shows basic repository configuration, but you can use any parameters
+described in https://www.jfrog.com/confluence/display/RTF/Repository+Configuration+JSON
+
+This module does direct map from pillar parameters to repository JSON description
+with two aliases for compatibility:
+
+  * repo_type -> rclass
+  * package_type -> packageType
+
+
 Read more
 =========
 
@@ -91,6 +105,7 @@
 * https://www.jfrog.com/confluence/display/RTF/PostgreSQL
 * https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API#ArtifactoryRESTAPI-REPOSITORIES
 * https://www.jfrog.com/confluence/display/RTF/Repository+Configuration+JSON
+
 Documentation and Bugs
 ======================
 
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)
diff --git a/_states/artifactory.py b/_states/artifactory.py
new file mode 100644
index 0000000..ac40b1a
--- /dev/null
+++ b/_states/artifactory.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+'''
+Management of artifactory configuration
+=======================================
+'''
+
+
+def __virtual__():
+    if 'artifactory.get_license' in __salt__:
+        return 'artifactory'
+    else:
+        return False, 'Execution module "artifactory" is not loaded'
+
+
+def add_license_key(name, license_key, **kwargs):
+
+    kwargs = __salt__['pillar.get']('artifactory:client:server')
+    kwargs.pop('license_key', None)
+
+    result, old_license_data = __salt__['artifactory.get_license'](**kwargs)
+
+    result, res_data = __salt__['artifactory.add_license'](
+        license_key,
+        **kwargs
+    )
+
+    # Prepare data to return
+    ret = {
+        'name': name,
+        'changes': {},
+        'result': result,
+        'comment': '',
+        'pchanges': {},
+    }
+
+    if result:
+        result, new_license_data = __salt__['artifactory.get_license'](**kwargs)
+
+        if old_license_data != new_license_data:
+            ret['changes'] = {
+                'old': json.dumps(old_license_data),
+                'new': json.dumps(new_license_data),
+            }
+        ret['comment'] = res_data
+    else:
+        ret['comment'] = res_data['message']
+        if ret['comment'] == ('License could not be installed due '
+                              'to an error: License already exists.'):
+            ret['result'] = True
+
+    return ret
+
+def configure_ldap(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 = __salt__['pillar.get']('artifactory:client:server')
+
+    result, ldap_config_old = __salt__['artifactory.get_ldap_config'](name, **kwargs)
+
+    result, res_data = __salt__['artifactory.set_ldap_config'](
+        name, uri, base, enabled, dn_pattern, manager_dn, manager_pass,
+        search_subtree, search_filter, attr_mail, create_users, safe_search,
+        **kwargs
+    )
+
+    # Prepare data to return
+    ret = {
+        'name': name,
+        'changes': {},
+        'result': result,
+        'comment': '',
+        'pchanges': {},
+    }
+
+    if result:
+        result, ldap_config_new = __salt__['artifactory.get_ldap_config'](name, **kwargs)
+        if ldap_config_old != ldap_config_new:
+            ret['changes'] = {
+                'old': ldap_config_old,
+                'new': ldap_config_new,
+            }
+        ret['comment'] = res_data
+    else:
+        ret['comment'] = res_data.get('errors')[0]['message']
+
+    return ret
+
+def configure_repo(name, **kwargs):
+
+    repo_config = kwargs
+    repo_name = repo_config['key']
+
+    rclass = repo_config.pop('repo_type', 'local')
+    if 'rclass' not in repo_config:
+        repo_config['rclass'] = rclass
+
+    packageType = repo_config.pop('package_type', 'generic')
+    if 'packageType' not in repo_config:
+        repo_config['packageType'] = packageType
+
+    kwargs = __salt__['pillar.get']('artifactory:client:server')
+
+    result, repo_config_old = __salt__['artifactory.get_repo'](repo_name, **kwargs)
+
+    # Prepare data to return
+    ret = {
+        'name': name,
+        'changes': {},
+        'result': result,
+        'comment': '',
+        'pchanges': {},
+    }
+
+    result, res_data = __salt__['artifactory.set_repo'](repo_name, repo_config, **kwargs)
+
+    if result:
+        result, repo_config_new = __salt__['artifactory.get_repo'](repo_name, **kwargs)
+        if repo_config_old != repo_config_new:
+            ret['changes'] = {
+                'old': repo_config_old,
+                'new': repo_config_new,
+            }
+        ret['comment'] = res_data
+    else:
+        ret['comment'] = res_data.get('errors')[0]['message']
+
+    return ret
diff --git a/_states/artifactory_config.py b/_states/artifactory_config.py
deleted file mode 100644
index 683ccc9..0000000
--- a/_states/artifactory_config.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-'''
-Management of artifactory configuration
-======================================
-
-:depends:   - requests Python module
-:configuration: See :py:mod:`salt.modules.artifactory` for setup instructions.
-'''
-
-def __virtual__():
-    '''
-    Only load if the artifactory module is in __salt__
-    '''
-    return True
-
-
-def artifactory_init( **kwargs):
-    
-    out = __salt__['artifactory_config.artifactory_init']( **kwargs)
-
-    return out
diff --git a/_states/artifactory_repo.py b/_states/artifactory_repo.py
deleted file mode 100644
index e6265c5..0000000
--- a/_states/artifactory_repo.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# -*- coding: utf-8 -*-
-'''
-Management of artifactory repositories
-======================================
-
-:depends:   - requests Python module
-:configuration: See :py:mod:`salt.modules.artifactory` for setup instructions.
-
-.. code-block:: yaml
-
-    local_artifactory_repo:
-      artifactory_repo.repo_present:
-      - name: remote_artifactory_repo
-      - package_type: generic
-      - repo_type: local
-    remote_artifactory_repo:
-      artifactory_repo.repo_present:
-      - name: remote_artifactory_repo
-      - repo_type: remote
-      - url: "http://totheremoterepo:80/"
-
-'''
-
-def __virtual__():
-    '''
-    Only load if the artifactory module is in __salt__
-    '''
-    return True
-
-
-def repo_present(name, repo_type, package_type, url=None, **kwargs):
-    '''
-    Ensures that the artifactory repo exists
-    
-    :param name: new repo name
-    :param description: short repo description
-    '''
-    ret = {'name': name,
-           'changes': {},
-           'result': True,
-           'comment': 'Repository "{0}" already exists'.format(name)}
-
-    # Check if repo is already present
-    repo = __salt__['artifactory_repo.repo_get'](name=name, **kwargs)
-
-    if 'Error' not in repo:
-        #update repo
-        pass
-    else:
-        # Create repo
-        __salt__['artifactory_repo.repo_create'](name, repo_type, package_type, url, **kwargs)
-        ret['comment'] = 'Repository "{0}" has been added'.format(name)
-        ret['changes']['repo'] = 'Created'
-    return ret
diff --git a/artifactory/client.sls b/artifactory/client.sls
deleted file mode 100644
index 7588d7d..0000000
--- a/artifactory/client.sls
+++ /dev/null
@@ -1,24 +0,0 @@
-{% from "artifactory/map.jinja" import client with context %}
-{%- if client.enabled %}
-
-artifactory_client_install:
-  pkg.installed:
-  - names: {{ client.pkgs }}
-
-artifactory_config:
-    artifactory_config.artifactory_init
-
-{%- for repo_name, repo in client.repo.iteritems() %}
-
-artifactory_client_repo_{{ repo_name }}:
-  artifactory_repo.repo_present:
-  - name: {{ repo_name }}
-  - repo_type: {{ repo.repo_type }}
-  - package_type: {{ repo.package_type }}
-  {%- if repo.url is defined %}
-  - url: {{ repo.url }}
-  {%- endif %}
-
-{%- endfor %}
-
-{%- endif %}
diff --git a/artifactory/client/config.sls b/artifactory/client/config.sls
new file mode 100644
index 0000000..487ba4e
--- /dev/null
+++ b/artifactory/client/config.sls
@@ -0,0 +1,30 @@
+{% from "artifactory/map.jinja" import client with context %}
+{%- if client.server.license_key is defined %}
+add_license_data:
+  artifactory.add_license_key:
+  - license_key: {{client.server.license_key}}
+{%- endif %}
+
+{%- if client.server.ldap_server is defined %}
+ldap:
+    artifactory.configure_ldap:
+      - uri: {{client.server.ldap_server}}
+      - enabled: {{client.server.ldap_server_enabled|default('true')}}
+{%- if client.server.get('ldap_dn_pattern') %}
+      - dn_pattern: {{client.server.ldap_dn_pattern}}
+{%- endif %}
+{%- if client.server.get('ldap_account_base') %}
+      - base: {{client.server.ldap_account_base}}
+{%- endif %}
+{%- if client.server.get('ldap_searchFilter') %}
+      - search_filter: {{client.server.ldap_searchFilter}}
+{%- endif %}
+      - search_subtree: {{client.server.ldap_searchSubtree|default('true')}}
+{%- if client.server.get('ldap_managerDn') %}
+      - manager_dn: {{client.server.ldap_managerDn}}
+      - manager_pass: {{client.server.ldap_managerPass}}
+{%- endif %}
+      - attr_mail: {{client.server.ldap_attr_mail|default('mail')}}
+      - create_users: {{client.server.ldap_create_users|default('true')}}
+      - safe_search: {{client.server.ldap_safe_search|default('true')}}
+{%- endif %}
diff --git a/artifactory/client/init.sls b/artifactory/client/init.sls
new file mode 100644
index 0000000..cd5864c
--- /dev/null
+++ b/artifactory/client/init.sls
@@ -0,0 +1,13 @@
+{% from "artifactory/map.jinja" import client with context %}
+{%- if client.enabled %}
+
+artifactory_client_install:
+  pkg.installed:
+  - names: {{ client.pkgs }}
+
+include:
+- artifactory.client.config
+{%- if client.repo is defined %}
+- artifactory.client.repo
+{%- endif %}
+{%- endif %}
diff --git a/artifactory/client/repo.sls b/artifactory/client/repo.sls
new file mode 100644
index 0000000..3ba728c
--- /dev/null
+++ b/artifactory/client/repo.sls
@@ -0,0 +1,11 @@
+{% from "artifactory/map.jinja" import client with context %}
+{%- for repo_name, repo in client.repo.iteritems() %}
+
+artifactory_repo_{{ repo_name }}:
+  artifactory.configure_repo:
+  - key: {{ repo_name }}
+{%- for key, value in repo.iteritems() %}
+  - {{key}}: {{value}}
+{%- endfor %}
+
+{%- endfor %}
diff --git a/artifactory/map.jinja b/artifactory/map.jinja
index 9decf29..5e2f5d5 100644
--- a/artifactory/map.jinja
+++ b/artifactory/map.jinja
@@ -25,6 +25,7 @@
 {%- load_yaml as client_defaults %}
 default:
   pkgs:
+  - python-lxml
   - python-requests
   repo: {}
 {%- endload %}