Nexus2 and Gerrit project builder 57/4057/5
authorAndrew Grimberg <agrimberg@linuxfoundation.org>
Mon, 13 Feb 2017 14:17:03 +0000 (06:17 -0800)
committerAndrew Grimberg <agrimberg@linuxfoundation.org>
Mon, 6 Mar 2017 18:57:17 +0000 (10:57 -0800)
Initial checkin of project build system for Nexus 2 and Gerrit

The script currently does the following:

* takes 2 yaml files (see examples) for configuration
  - settings.yaml == administrative / global settings
  - config.yaml == repository structure to build including nexus
    passwords
* Walks config.yaml for all repositories and creates targets,
  privileges, roles and users based upon our standard configuration

Presently does not create the Gerrit repositories

* Staging repo re-ordering hack script

NOTE: This does not work against Nexus 3 as the REST API has been
removed in Nexus 3. Presently scripting Nexus 3 requires groovy

Change-Id: Ia06444a85e167a1e5685f9e569322c9b0e0b8c97
Signed-off-by: Andrew Grimberg <agrimberg@linuxfoundation.org>
project_builder/.gitignore [new file with mode: 0644]
project_builder/config.example.yaml [new file with mode: 0644]
project_builder/nexus/__init__.py [new file with mode: 0644]
project_builder/reorder_staging_repo.py [new file with mode: 0755]
project_builder/repo_build.py [new file with mode: 0755]
project_builder/requirements.txt [new file with mode: 0644]
project_builder/settings.example.yaml [new file with mode: 0644]

diff --git a/project_builder/.gitignore b/project_builder/.gitignore
new file mode 100644 (file)
index 0000000..2c6c322
--- /dev/null
@@ -0,0 +1,3 @@
+# Do not store settings.yaml or config.yaml
+settings.yaml
+config.yaml
diff --git a/project_builder/config.example.yaml b/project_builder/config.example.yaml
new file mode 100644 (file)
index 0000000..7d21f3b
--- /dev/null
@@ -0,0 +1,16 @@
+---
+# vim: sw=2 ts=2 sts=2 et :
+base_groupId: 'org.example'
+repositories:
+  foo:
+    password: 'foo user pass'
+    repositories:
+      bar:
+        password: 'foo-bar user pass'
+        repositories:
+          baz:
+            password: 'foo-bar-baz user pass'
+            extra_privs:
+              # extra roles that should be assigned to the user
+              # These need to be the full display name and not the roleId
+              - 'Staging: Deployer (autorelease)'
diff --git a/project_builder/nexus/__init__.py b/project_builder/nexus/__init__.py
new file mode 100644 (file)
index 0000000..13a9337
--- /dev/null
@@ -0,0 +1,241 @@
+# -*- code: utf-8 -*-
+# vim: sw=4 ts=4 sts=4 et :
+
+#
+
+"""
+Library for working with Sonatype Nexus REST API
+"""
+
+__title__ = 'nexus'
+__version__ = '0.1.0'
+__build__ = 0x000101
+__author__ = 'Andrew Grimberg'
+__license__ = 'Apache 2.0'
+__copyright__ = 'Copyright 2017 Andrew Grimberg'
+
+import requests
+import json
+from requests.auth import HTTPBasicAuth
+
+class Nexus:
+    def __init__(self, baseurl=None, username=None, password=None):
+        self.baseurl = baseurl
+
+        if username and password:
+            self.add_credentials(username, password)
+        else:
+            self.auth = None
+
+        self.headers = {
+            'Accept': 'application/json',
+            'Content-Type': 'application/json',
+        }
+
+    def add_credentials(self, username, password):
+        """
+        Create an authentication object to be used.
+        """
+        self.auth = HTTPBasicAuth(username, password)
+
+    def add_baseurl(self, url):
+        """
+        Set the base URL for nexus
+        """
+        self.baseurl = url
+
+    def get_target(self, name):
+        """
+        Get the ID of a given target name
+        """
+        url = '/'.join([self.baseurl, 'service/local/repo_targets'])
+        targets = requests.get(url, auth=self.auth, headers=self.headers).json()
+
+        for priv in targets['data']:
+            if priv['name'] == name:
+                return priv['id']
+        raise LookupError("No target found named '%s'" % (name))
+
+    def create_target(self, name, patterns):
+        """
+        Create a target with the given patterns
+        """
+        url = '/'.join([self.baseurl, 'service/local/repo_targets'])
+
+        target = {
+            'data': {
+                'contentClass': 'any',
+                'patterns': patterns,
+                'name': name,
+            }
+        }
+
+        json_data = json.dumps(target, encoding='latin-1')
+
+        r = requests.post(url, auth=self.auth, headers=self.headers, data=json_data)
+
+        return r.json()['data']['id']
+
+    def get_priv(self, name, priv):
+        """
+        Get the ID for the privilege with the given name
+        """
+        url = '/'.join([self.baseurl, 'service/local/privileges'])
+
+        search_name = '%s - (%s)' % (name, priv)
+        privileges = requests.get(url, auth=self.auth, headers=self.headers).json()
+
+        for priv in privileges['data']:
+            if priv['name'] == search_name:
+                return priv['id']
+
+        raise LookupError("No privilege found named '%s'" % name)
+
+    def create_priv(self, name, target_id, priv):
+        """
+        Create a given privilege
+
+        Privilege must be one of the following:
+
+        create
+        read
+        delete
+        update
+        """
+        url = '/'.join([self.baseurl,'service/local/privileges_target'])
+
+        privileges = {
+            'data': {
+                'name': name,
+                'description': name,
+                'method': [
+                        priv,
+                    ],
+                'repositoryGroupId': '',
+                'repositoryId': '',
+                'repositoryTargetId': target_id,
+                'type': 'target',
+            }
+        }
+
+        json_data = json.dumps(privileges, encoding='latin-1')
+        privileges = requests.post(url, auth=self.auth, headers=self.headers, data=json_data).json()
+
+        return privileges['data'][0]['id']
+
+    def get_role(self, name):
+        """
+        Get the id of a role with a given name
+        """
+        url = '/'.join([self.baseurl, 'service/local/roles'])
+        roles = requests.get(url, auth=self.auth, headers=self.headers).json()
+
+        for role in roles['data']:
+            if role['name'] == name:
+                return role['id']
+
+        raise LookupError("No role with name '%s'" % (name))
+
+    def create_role(self, name, privs):
+        """
+        Create a role with the given privileges
+        """
+        url = '/'.join([self.baseurl, 'service/local/roles'])
+
+        role = {
+            'data': {
+                'id': name,
+                'name': name,
+                'description': name,
+                'privileges': privs,
+                'roles': [
+                        'repository-any-read',
+                    ],
+                'sessionTimeout': 60,
+            }
+        }
+
+        json_data = json.dumps(role, encoding='latin-1')
+
+        r = requests.post(url, auth=self.auth, headers=self.headers, data=json_data)
+
+        return r.json()['data']['id']
+
+    def get_user(self, user_id):
+        """
+        Determine if a user with a given userId exists
+        """
+        url = '/'.join([self.baseurl, 'service/local/users'])
+        users = requests.get(url, auth=self.auth, headers=self.headers).json()
+
+        for user in users['data']:
+            if user['userId'] == user_id:
+                return
+
+        raise LookupError("No user with id '%s'" % (user_id))
+
+    def create_user(self, name, domain, role_id, password, extra_roles=[]):
+        """
+        Create a Deployment user with a specific role_id and potentially extra roles
+
+        User is created with the nx-deployment role attached
+        """
+        url = '/'.join([self.baseurl, 'service/local/users'])
+
+        user = {
+            'data': {
+                'userId': name,
+                'email': '%s-deploy@%s' % (name, domain),
+                'firstName': name,
+                'lastName': 'Deployment',
+                'roles': [
+                        role_id,
+                        'nx-deployment',
+                    ],
+                'password': password,
+                'status': 'active',
+            }
+        }
+
+        for role in extra_roles:
+            user['data']['roles'].append(self.get_role(role))
+
+        json_data = json.dumps(user, encoding='latin-1')
+
+        r = requests.post(url, auth=self.auth, headers=self.headers, data=json_data)
+
+    def get_repo_group(self, name):
+        """
+        Get the repository ID for a repo group that has a specific name
+        """
+        url = '/'.join([self.baseurl, 'service/local/repo_groups'])
+
+        repos = requests.get(url, auth=self.auth, headers=self.headers).json()
+
+        for repo in repos['data']:
+            if repo['name'] == name:
+                return repo['id']
+
+        raise LookupError("No repository group named '%s'" % (name))
+
+    def get_repo_group_details(self, repoId):
+        """
+        Get the current configuration of a given repo group with a specific ID
+        """
+        url = '/'.join([self.baseurl, 'service/local/repo_groups', repoId])
+
+        return requests.get(url, auth=self.auth, headers=self.headers).json()['data']
+
+    def update_repo_group_details(self, repoId, data):
+        """
+        Update the given repo group with new configuration
+        """
+        url = '/'.join([self.baseurl, 'service/local/repo_groups', repoId])
+
+        repo = {
+            'data': data
+        }
+
+        json_data = json.dumps(repo, encoding='latin-1')
+
+        r = requests.put(url, auth=self.auth, headers=self.headers, data=json_data)
diff --git a/project_builder/reorder_staging_repo.py b/project_builder/reorder_staging_repo.py
new file mode 100755 (executable)
index 0000000..866896a
--- /dev/null
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+# -*- code: utf-8 -*-
+# vim: sw=4 ts=4 sts=4 et :
+
+#
+# NOTE: This is a hack for forcing the 'Staging Repositories' repo group
+# to be in the correct reverse sorted order. There is a problem with
+# Nexus where it is not doing this like it should be
+#
+
+import argparse
+import sys
+import nexus
+import yaml
+from operator import itemgetter
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-s', '--settings', type=str,
+    help='security and settings yaml file')
+args = parser.parse_args()
+
+if not args.settings:
+    sys.exit('Settings file is required')
+
+
+# open our settings file
+f = open(args.settings, 'r')
+settings = yaml.load(f)
+f.close()
+
+for setting in ['nexus', 'user', 'password']:
+    if not setting in settings:
+        sys.exit('{} needs to be defined'.format(setting))
+
+n = nexus.Nexus(settings['nexus'], settings['user'], settings['password'])
+
+try:
+    repo_id = n.get_repo_group('Staging Repositories')
+except LookupError as e:
+    sys.exit("Staging repository 'Staging Repositories' cannot be found")
+
+repo_details = n.get_repo_group_details(repo_id)
+
+sorted_repos = sorted(repo_details['repositories'], key=lambda k: k['id'], reverse=True)
+
+for repos in sorted_repos:
+    del repos['resourceURI']
+    del repos['name']
+
+repo_update = repo_details
+repo_update['repositories'] = sorted_repos
+del repo_update['contentResourceURI']
+del repo_update['repoType']
+
+n.update_repo_group_details(repo_id, repo_update)
diff --git a/project_builder/repo_build.py b/project_builder/repo_build.py
new file mode 100755 (executable)
index 0000000..f2f1d93
--- /dev/null
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+# -*- code: utf-8 -*-
+# vim: sw=4 ts=4 sts=4 et :
+
+import argparse
+import sys
+import nexus
+import yaml
+
+parser = argparse.ArgumentParser()
+parser.add_argument('-s', '--settings', type=str,
+    help='security and settings yaml file')
+parser.add_argument('-c', '--config', type=str,
+    help='configuration to be created')
+args = parser.parse_args()
+
+if not args.settings:
+    sys.exit('Settings file is required')
+
+if not args.config:
+    sys.exit('Config file is required')
+
+
+# open our settings file
+f = open(args.settings, 'r')
+settings = yaml.load(f)
+f.close()
+
+for setting in ['nexus', 'user', 'password', 'email_domain']:
+    if not setting in settings:
+        sys.exit('{} needs to be defined'.format(setting))
+
+# open our config file
+f = open(args.config, 'r')
+config = yaml.load(f)
+f.close()
+
+n = nexus.Nexus(settings['nexus'], settings['user'], settings['password'])
+
+def create_nexus_perms(name, targets, email, password, extra_privs=[]):
+    # Create target
+    try:
+        target_id = n.get_target(name)
+    except LookupError as e:
+        target_id = n.create_target(name, targets)
+
+    # Create privileges
+    privs_set = [
+            'create',
+            'delete',
+            'read',
+            'update',
+        ]
+
+    privs = {}
+    for priv in privs_set:
+        try:
+            privs[priv] = n.get_priv(name, priv)
+        except LookupError as e:
+            privs[priv] = n.create_priv(name, target_id, priv)
+
+    # Create Role
+    try:
+        role_id = n.get_role(name)
+    except LookupError as e:
+        role_id = n.create_role(name, privs)
+
+    # Create user
+    try:
+        n.get_user(name)
+    except LookupError as e:
+        n.create_user(name, email, role_id, password, extra_privs)
+
+def do_build_repo(repo, repoId, config, base_groupId):
+    print('Building for %s.%s' % (base_groupId, repo))
+    groupId = '%s.%s' % (base_groupId, repo)
+    target = '^/%s/.*' % groupId.replace('.', '[/\.]')
+    if 'extra_privs' in config:
+        extra_privs = config['extra_privs']
+    else:
+        extra_privs = []
+    create_nexus_perms(repoId, [target], settings['email_domain'],
+        config['password'], extra_privs)
+    if 'repositories' in config:
+        for sub_repo in config['repositories']:
+            sub_repo_id = '%s-%s' % (repoId, sub_repo)
+            do_build_repo(sub_repo, sub_repo_id, config['repositories'][sub_repo],
+                groupId)
+
+for repo in config['repositories']:
+    do_build_repo(repo, repo, config['repositories'][repo], config['base_groupId'])
diff --git a/project_builder/requirements.txt b/project_builder/requirements.txt
new file mode 100644 (file)
index 0000000..a82933a
--- /dev/null
@@ -0,0 +1,12 @@
+appdirs==1.4.0
+ecdsa==0.11
+httplib2==0.10.3
+packaging==16.8
+paramiko==1.16.0
+pbr==1.10.0
+pycrypto==2.6.1
+pygerrit==1.0.0
+pyparsing==2.1.10
+PyYAML==3.12
+requests==2.9.1
+six==1.10.0
diff --git a/project_builder/settings.example.yaml b/project_builder/settings.example.yaml
new file mode 100644 (file)
index 0000000..5266cf9
--- /dev/null
@@ -0,0 +1,13 @@
+---
+# vim: sw=2 ts=2 sts=3 et :
+
+# Nexus 2 system to work against. This should be the full path to the base web
+# interface including any URL context.
+nexus: 'http://nexus2.example.com'
+
+# Administrative nexus user account credentials
+user: 'admin'
+password: 'admin123'
+
+# email domain that nexus user emails should be generated with
+email_domain: 'example.com'