Skip to content

Instantly share code, notes, and snippets.

@jollyroger
Last active August 29, 2015 14:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jollyroger/0315ac0293f4e264c42c to your computer and use it in GitHub Desktop.
Save jollyroger/0315ac0293f4e264c42c to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*-
'''
Management of Bitbucket deploy keys
===================================
.. code-block:: yaml
production_server_deploy_key:
bitbucket_deploy_key.present:
- user: username
- repo: my_awesome_app
- admin_user: admin
- admin_pass: secret
- name: app-deploy-production-key
- pubkey_file: /path/to/id_rsa.pub
'''
# Import python libs
import os
import requests
from Crypto.PublicKey import RSA
# Import salt libs
import salt.utils
def __virtual__():
'''
Only load if the mysql module is in __salt__
'''
return 'bitbucket_deployment_key'
class APIError(Exception):
def __init__(self, status_code, text, more=None):
self.status_code = status_code
self.message = text
if more:
self.more = more
return
def __str__(self):
return "HTTP status code {0}: {1}. Details: {2}".format(
self.status_code, self.message, self.more)
class BitbucketConnection:
def __init__(self, user, repo, admin_user, password):
self.user = user
self.repo = repo
self.admin_user = admin_user if admin_user else user
self.password = password
return
def url(self):
return 'https://bitbucket.org/api/1.0/repositories/{0}/{1}/deploy-keys'.format(
self.user, self.repo)
def keys(self):
reply = requests.get(self.url(), auth=(self.admin_user, self.password))
if reply.status_code == 200:
return reply.json()
else:
raise APIError(reply.status_code, reply.reason)
def add(self, key, label=None):
data = {'key': key}
if label:
data['label'] = label
reply = requests.post(self.url(), auth=(self.admin_user,
self.password), data=data)
if reply.status_code == 200:
return reply.json()
else:
raise APIError(reply.status_code, reply.reason, reply.content)
def get(self, key):
key = key.strip()
for _key in self.keys():
if _key['key'] == key:
return _key
else:
raise KeyError("Public key not found")
def delete(self, pk):
url2 = self.url() + "/" + str(pk)
reply = requests.delete(url2, auth=(self.admin_user, self.password))
if reply.status_code != 204:
raise APIError(reply.status_code, reply.reason, reply.content)
def present(name,
user=None,
repo=None,
admin_user=None,
password=None,
pubkey_file=None):
'''
Ensure that the named deployment key is present in repository's key list.
Repository is identified by ``user`` and ``repo`` options. ``admin_user``
should be able to access admin page of the repository. If no ``admin_user``
option is set, ``user`` option is silently used.
name
The name of the deployment key in the repo list. Corresponds to the
``label`` option in Bitbucket API
user
Bitbucket user who is owner of the repository.
repo
Repository that belongs to mentioned user.
admin_user
Bitbucket account name which is capable to manage deployment keys.
password
The password to use for above user.
pubkey_file
Path to the public key.
'''
ret = {'name': name,
'changes': {},
'result': True,
'comment': 'Key {0} is already present for repository {1}/{2}'.format(
name, user, repo)}
if not user:
ret['comment'] = 'User should be specified'
ret['result'] = False
return ret
elif not repo:
ret['comment'] = 'Repository name should be specified'
ret['result'] = False
return ret
elif not password:
ret['comment'] = 'User password should be specified'
ret['result'] = False
return ret
elif not pubkey_file:
ret['comment'] = 'Password to the public key file should be specified'
ret['result'] = False
return ret
if not admin_user:
admin_user = user
max_pubkey_size = 655536L
# read the contents of the public key
try:
statinfo = os.stat(pubkey_file)
if statinfo.st_size > max_pubkey_size:
ret['comment'] = 'Something\'s wrong with the pubkey_file: file ' \
'size too large'
ret['result'] = False
return ret
pubkey_raw = open(pubkey_file, 'r').read()
pubkey = RSA.importKey(pubkey_raw)
except IOError, err:
ret['comment'] = 'Error reading pubkey_file {0}: {1}'.format(
pubkey_file, err)
ret['result'] = False
return ret
except ValueError, err:
ret['comment'] = 'pubkey_file does not contain public key: {0}'.format(
err)
ret['result'] = False
return ret
conn = BitbucketConnection(user, repo, admin_user, password)
try:
key = conn.get(pubkey_raw)
ret['comment'] = 'Key is already present for repository {0}/{1} ' \
'with name {2}'.format(user, repo, key['label'])
except APIError, err:
if err.status_code != 401:
raise
ret['comment'] = 'Cannot access {0}/{1} deployment keys: invalid ' \
'credentials for user {2}'.format(user, repo, admin_user)
ret['result'] = False
return ret
except KeyError:
if __opts__['test'] == True:
ret['comment'] = 'New deployment key will be added to ' \
'{0}/{1} repository'.format(user, repo)
ret['changes'] = {
'old': '',
'new': 'New deploy key will be added: {0}\n{1}'.format(name,
pubkey_raw)}
ret['result'] = None
return ret
try:
result = conn.add(pubkey_raw, name)
except APIError, err:
ret['comment'] = 'Error adding deployment key {0}: {1}'.format(
name, err)
ret['result'] = False
return ret
ret['comment'] = 'Key {0} has been added to {1}/{2} repository'.format(
result['label'], user, repo)
return ret
{% import "company/macros.jinja" as global %}
{% import "company/buildbot/macros.jinja" as cfg %}
{% set ssh_dir = "%s/.ssh"|format(cfg.home) %}
{% set ssh_authorized_keys = "%s/authorized_keys"|format(ssh_dir) %}
{% set ssh_private_key = "%s/id_rsa"|format(ssh_dir) %}
{% set ssh_public_key = "%s.pub"|format(ssh_private_key) %}
service_ssh_key:
ssh_known_hosts.present:
- name: bitbucket.org
- user: {{ cfg.user }}
- fingerprint: 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40
{{ ssh_dir }}:
file.directory:
- user: {{ cfg.user }}
- group: {{ cfg.group }}
- mode: 0700
- clean: False
# Generate ssh deploy key on the server
generate_deploy_private_key:
cmd.run:
- name: "ssh-keygen -q -N '' -f {{ ssh_private_key }}"
- user: {{ cfg.user }}
- group: {{ cfg.group }}
- creates: {{ ssh_private_key }}
{{ ssh_private_key }}:
file.managed:
- user: {{ cfg.user }}
- group: {{ cfg.group }}
- mode: 0600
- require:
- cmd: generate_deploy_private_key
- require_in:
- file: {{ ssh_dir }}
generate_deploy_public_key:
cmd.run:
- name: "ssh-keygen -y -f {{ ssh_private_key }} > {{ ssh_public_key }}"
- user: {{ cfg.user }}
- group: {{ cfg.group }}
- unless: "test {{ ssh_public_key }} -nt {{ ssh_private_key }}"
- creates: {{ ssh_public_key }}
- require:
- file: {{ ssh_private_key }}
{{ ssh_public_key }}:
file.managed:
- user: {{ cfg.user }}
- group: {{ cfg.group }}
- mode: 0644
- require:
- cmd: generate_deploy_public_key
- require_in:
- file: {{ ssh_dir }}
populate_buildmaster_deploy_key:
bitbucket_deployment_key.present:
- name: {{ grains.host }}-{{ cfg.host }}
- user: {{ cfg.bitbucket_user }}
- repo: {{ cfg.bitbucket_repo }}
- admin_user: {{ cfg.bitbucket_admin_user }}
- password: {{ cfg.bitbucket_password }}
- pubkey_file: {{ ssh_public_key }}
- require:
- cmd: generate_deploy_public_key
- file: {{ ssh_public_key }}
{% set name = "myproject") %}
{% set domains = ["buildbot.mycompany.com"]) %}
{% set user = "buildbot" %}
{% set group = user %}
{% set home = "/var/lib/%s"|format(user) %}
{% set buildbot_dir = "%s/masters/%s"|format(home, name) %}
{% set bitbucket_user = salt['pillar.get']('bitbucket:user', 'mycompany') %}
{% set bitbucket_admin_user = salt['pillar.get']('bitbucket:admin_user', bitbucket_user) %}
{% set bitbucket_password = salt['pillar.get']('bitbucket:admin_pass', 'secret') %}
{% set bitbucket_repo = 'buildbotcfg' %}
{% set htpasswd_auth = salt['pillar.get']('nginx:control_vhost:auth_file', '') %}
# These are taken from buildbot repo and not updated automatically
{% set buildbot_pb_port = 9989 %}
{% set buildbot_http_port = 8011 %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment