Bladeren bron

Initial revision.

Andrew Klopper 8 jaren geleden
commit
ae5c0f4d8e
8 gewijzigde bestanden met toevoegingen van 340 en 0 verwijderingen
  1. 2 0
      .gitignore
  2. 6 0
      INSTALL
  3. 10 0
      bootstrap_new_linode.sh
  4. 45 0
      build_backend_private_ips_jinja.py
  5. 67 0
      deploy_disk_image.py
  6. 6 0
      requirements.in
  7. 15 0
      requirements.txt
  8. 189 0
      utils.py

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
1
+*.pyc
2
+config.py

+ 6 - 0
INSTALL

@@ -0,0 +1,6 @@
1
+Run 'pip install dnspython' manually first if you get any errors while trying
2
+to do a 'pip install -r ...', 'pip-compile' or 'pip-sync'.
3
+
4
+To fix pkg_resources errors under Ubuntu, run:
5
+
6
+	apt-get install --reinstall python-pkg-resources python-setuptools

+ 10 - 0
bootstrap_new_linode.sh

@@ -0,0 +1,10 @@
1
+#!/bin/sh
2
+
3
+if [ $# -ne 1 ]; then
4
+	echo "Usage: $0 TARGET"
5
+	exit 1
6
+fi
7
+TARGET=$1
8
+
9
+salt $TARGET saltutil.sync_all
10
+salt $TARGET state.apply bootstrap

+ 45 - 0
build_backend_private_ips_jinja.py

@@ -0,0 +1,45 @@
1
+#!/data/virtualenvs/linode/bin/python2
2
+
3
+import argparse
4
+import sys
5
+
6
+import utils
7
+import config
8
+
9
+parser = argparse.ArgumentParser()
10
+
11
+parser.add_argument('datacenter')
12
+
13
+args = parser.parse_args()
14
+
15
+api = utils.get_api(config.linode_api_key)
16
+
17
+dcs = utils.get_datacenter_dict(api)
18
+if not args.datacenter in dcs:
19
+    raise RuntimeError('Invalid datacenter')
20
+dc = dcs[args.datacenter]
21
+
22
+private_ips = []
23
+linodes = api.linode_list()
24
+for linode in linodes:
25
+    if linode['DATACENTERID'] != dc['DATACENTERID']:
26
+        sys.stderr.write("INFO: Skipping %s from datacenter %s\n" % (linode['LABEL'], linode['DATACENTERID']))
27
+        continue
28
+    ips = api.linode_ip_list(LinodeID=linode['LINODEID'])
29
+    found = False
30
+    for ip in ips:
31
+        if not ip['ISPUBLIC']:
32
+            private_ips.append(ip['IPADDRESS'])
33
+            found = True
34
+    if not found:
35
+        sys.stderr.write("WARNING: %s does not have a private IP address\n" % linode['LABEL'])
36
+
37
+print "# ********************"
38
+print "# DO NOT EDIT MANUALLY"
39
+print "# ********************"
40
+print "# Generated by: build_backend_private_ips_jinja.py"
41
+print ""
42
+print "{% set addresses = ["
43
+for ip in sorted(private_ips):
44
+    print "  '%s'," % ip
45
+print "] %}"

+ 67 - 0
deploy_disk_image.py

@@ -0,0 +1,67 @@
1
+#!/data/virtualenvs/linode/bin/python2
2
+
3
+from __future__ import print_function
4
+import argparse
5
+import sys
6
+
7
+import utils
8
+import config
9
+
10
+parser = argparse.ArgumentParser()
11
+
12
+linode_group = parser.add_mutually_exclusive_group(required=True)
13
+linode_group.add_argument('--linode-id', type=int, dest='linode_id')
14
+linode_group.add_argument('--linode-label', dest='linode_label')
15
+
16
+parser.add_argument('--host-name', dest='host_name')
17
+parser.add_argument('--disk-size-mb', dest='disk_size_mb')
18
+parser.add_argument('--swap-size-mb', dest='swap_size_mb', required=True)
19
+parser.add_argument('--data-disk-size-mb', dest='data_disk_size_mb', required=True)
20
+
21
+args = parser.parse_args()
22
+
23
+api = utils.get_api(config.linode_api_key)
24
+
25
+if args.linode_id is not None:
26
+    linode = utils.get_linode_by_id(api, args.linode_id)
27
+else:
28
+    linode = utils.get_linode_by_label(api, args.linode_label)
29
+
30
+linode_id = linode['LINODEID']
31
+host_name = linode['LABEL'] if args.host_name is None else args.host_name
32
+disk_size_mb = config.disk_size_mb if args.disk_size_mb is None else args.disk_size_mb
33
+
34
+ips = api.linode_ip_list(LinodeID=linode_id)
35
+for ip in ips:
36
+    if not ip['ISPUBLIC']:
37
+        break
38
+else:
39
+    print('Adding private IP address to Linode')
40
+    api.linode_ip_addprivate(LinodeID=linode_id)
41
+
42
+swap_disk = utils.get_swap_disk(api, linode_id)
43
+if swap_disk is None:
44
+    swap_disk_id = utils.create_swap_disk(api, linode_id, args.swap_size_mb)
45
+else:
46
+    swap_disk_id = swap_disk['DISKID']
47
+    print('Reusing existing swap disk', file=sys.stderr)
48
+
49
+data_disk = utils.get_data_disk(api, linode_id)
50
+if data_disk is None:
51
+    data_disk_id = utils.create_data_disk(api, linode_id, args.data_disk_size_mb)
52
+else:
53
+    data_disk_id = data_disk['DISKID']
54
+    print('Reusing existing data disk', file=sys.stderr)
55
+
56
+utils.deploy_linode_from_stackscript(
57
+    api=api,
58
+    linode_id=linode_id,
59
+    configuration_label=config.configuration_label,
60
+    stackscript_label=config.stackscript_label,
61
+    stackscript_args=config.get_stackscript_args(host_name),
62
+    distribution_label=config.distribution_label,
63
+    disk_label=config.disk_label,
64
+    disk_size_mb=disk_size_mb,
65
+    root_password=config.root_password,
66
+    extra_disk_ids=[swap_disk_id, data_disk_id]
67
+)

+ 6 - 0
requirements.in

@@ -0,0 +1,6 @@
1
+pip-tools
2
+linode-python
3
+dnspython
4
+easyzone
5
+pytz
6
+pycurl

+ 15 - 0
requirements.txt

@@ -0,0 +1,15 @@
1
+#
2
+# This file is autogenerated by pip-compile
3
+# To update, run:
4
+#
5
+#    pip-compile --output-file requirements.txt requirements.in
6
+#
7
+click==6.7                # via pip-tools
8
+dnspython==1.15.0
9
+easyzone==1.2.2
10
+first==2.0.1              # via pip-tools
11
+linode-python==1.1.1
12
+pip-tools==1.9.0
13
+pycurl==7.43.0
14
+pytz==2017.2
15
+six==1.10.0               # via pip-tools

+ 189 - 0
utils.py

@@ -0,0 +1,189 @@
1
+import json
2
+import os
3
+import re
4
+from datetime import datetime
5
+from pytz import utc
6
+from linode.api import Api
7
+
8
+_datacenter_dict = None
9
+_pricing_plan_dict = None
10
+_distribution_dict = None
11
+_stackscript_dict = None
12
+_linode_dict = None
13
+_kernel_dict = None
14
+
15
+def get_api(apiKey):
16
+    return Api(key=apiKey)
17
+
18
+def get_datacenter_dict(api, reload=False):
19
+    global _datacenter_dict
20
+    if reload or (_datacenter_dict is None):
21
+        _datacenter_dict = {dc['ABBR']: dc for dc in api.avail_datacenters()}
22
+    return _datacenter_dict
23
+
24
+def get_pricing_plan_dict(api, reload=False):
25
+    global _pricing_plan_dict
26
+    if reload or (_pricing_plan_dict is None):
27
+        _pricing_plan_dict = {plan['LABEL']: plan for plan in api.avail_linodeplans()}
28
+    return _pricing_plan_dict
29
+
30
+def get_distribution_dict(api, reload=False):
31
+    global _distribution_dict
32
+    if reload or (_distribution_dict is None):
33
+       _distribution_dict = {dist['LABEL']: dist for dist in api.avail_distributions()}
34
+    return _distribution_dict
35
+
36
+def get_stackscript_dict(api, reload=False):
37
+    global _stackscript_dict
38
+    if reload or (_stackscript_dict is None):
39
+        _stackscript_dict = {script['LABEL']: script for script in api.stackscript_list()}
40
+    return _stackscript_dict
41
+
42
+def get_kernel_dict(api, reload=False):
43
+    global _kernel_dict
44
+    if reload or (_kernel_dict is None):
45
+        _kernel_dict = {kernel['LABEL']: kernel for kernel in api.avail_kernels()}
46
+    return _kernel_dict
47
+
48
+def get_latest_kernel(api, is_64_bit=True):
49
+    kernels = get_kernel_dict(api)
50
+    prefix = 'latest 64 bit' if is_64_bit else 'latest 32 bit'
51
+    for label, kernel in kernels.items():
52
+        if label.lower().startswith(prefix):
53
+            return kernel
54
+    raise RuntimeError('Latest %s bit kernel not found' % ('64' if is_64_bit else '32',))
55
+
56
+def get_linode_dict(api, reload=False):
57
+    global _linode_dict
58
+    if reload or (_linode_dict is None):
59
+        _linode_dict = {linode['LABEL']: linode for linode in api.linode_list()}
60
+    return _linode_dict
61
+
62
+def get_linode_by_label(api, linode_label, reload=False):
63
+    linodes = get_linode_dict(api, reload)
64
+    if linode_label in linodes:
65
+        return linodes[linode_label]
66
+    else:
67
+        raise ValueError('Invalid Linode label: ' + linode_label)
68
+
69
+def get_linode_by_id(api, linode_id, reload=False):
70
+    linodes = get_linode_dict(api, reload)
71
+    for linode in linodes.values():
72
+        if linode['LINODEID'] == linode_id:
73
+            return linode
74
+    raise ValueError('Invalid Linode ID: ' + linode_id)
75
+
76
+def get_swap_disk(api, linode_id):
77
+    for disk in api.linode_disk_list(LinodeID=linode_id):
78
+        if disk['TYPE'] == 'swap':
79
+            return disk
80
+    return None
81
+
82
+def get_data_disk(api, linode_id):
83
+    for disk in api.linode_disk_list(LinodeID=linode_id):
84
+        if disk['LABEL'].lower() == 'data':
85
+            return disk
86
+    return None
87
+
88
+def create_data_disk(api, linode_id, data_disk_size_mb):
89
+    disk = api.linode_disk_create(LinodeID=linode_id, Size=data_disk_size_mb, Label="Data", Type="ext4")
90
+    return disk['DiskID']
91
+
92
+def create_swap_disk(api, linode_id, swap_disk_size_mb):
93
+    disk = api.linode_disk_create(LinodeID=linode_id, Size=swap_disk_size_mb, Label="Swap", Type="swap")
94
+    return disk['DiskID']
95
+
96
+def create_linode(api, datacenter_abbr, pricing_plan_label):
97
+    dcs = get_datacenter_dict(api)
98
+    if datacenter_abbr not in dcs:
99
+        raise ValueError('Invalid datacenter: ' + datacenter_abbr)
100
+
101
+    plans = get_pricing_plan_dict(api)
102
+    if pricing_plan_label not in plans:
103
+        raise ValueError('Invalid pricing plan: ' + pricing_plan_label)
104
+
105
+    dc_id = dcs[datacenter_abbr]['DATACENTERID']
106
+    plan_id = plans[pricing_plan_label]['PLANID']
107
+
108
+    return api.linode_create(DatacenterID=dc_id, PlanID=plan_id)
109
+
110
+def deploy_linode_from_stackscript(api, linode_id, configuration_label, stackscript_label, stackscript_args, distribution_label, disk_label, disk_size_mb, root_password, kernel_id=None, extra_disk_ids=[]):
111
+    scripts = get_stackscript_dict(api)
112
+    script = scripts.get(stackscript_label)
113
+    if script is None:
114
+        raise ValueError('Invalid stackscript: ' + stackscript_label)
115
+
116
+    dists = get_distribution_dict(api)
117
+    dist = dists.get(distribution_label)
118
+    if dist is None:
119
+        raise ValueError('Invalid distribution: ' + distribution_label)
120
+
121
+    if not str(dist['DISTRIBUTIONID']) in str(script['DISTRIBUTIONIDLIST']).split(','):
122
+        raise ValueError('The specified StackScript does not support the specified distribution')
123
+
124
+    if kernel_id is None:
125
+        kernel = get_latest_kernel(api, dist['IS64BIT'] != 0)
126
+        kernel_id = kernel['KERNELID']
127
+
128
+    disk = api.linode_disk_createfromstackscript(
129
+        LinodeID=linode_id,
130
+        StackScriptID=script['STACKSCRIPTID'],
131
+        StackScriptUDFResponses=json.dumps(stackscript_args),
132
+        DistributionID=dist['DISTRIBUTIONID'],
133
+        Label=disk_label,
134
+        Size=disk_size_mb,
135
+        rootPass=root_password
136
+    )
137
+    disk_list = ','.join(str(x) for x in [disk['DiskID']] + list(extra_disk_ids))
138
+
139
+    config = api.linode_config_create(
140
+        LinodeID=linode_id,
141
+        KernelID=kernel_id,
142
+        Label=configuration_label,
143
+        DiskList=disk_list,
144
+        helper_distro=True,
145
+        helper_disableUpdateDB=True,
146
+        helper_depmod=True,
147
+        devtmpfs_automount=True,
148
+        helper_network=True
149
+    )
150
+
151
+    return config
152
+
153
+def parse_asn1_utctime(value):
154
+    if value == '':
155
+        return None
156
+    else:
157
+        matches = re.match(r'^(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)Z$', value)
158
+        if matches:
159
+            year = int(matches.group(1))
160
+            year = year + (2000 if year <= 50 else 1900)
161
+            return datetime(year, int(matches.group(2)), int(matches.group(3)), int(matches.group(4)), int(matches.group(5)), int(matches.group(6)), tzinfo=utc)
162
+        else:
163
+            raise ValueError("Invalid ASN1 UTCTime value: {}".format(value))
164
+
165
+def load_openssl_ca_index(ca_root_dir):
166
+    ret = {}
167
+    with open(os.path.join(ca_root_dir, 'index.txt'), 'r') as f:
168
+        # Later instances of the same certificate name will replace earlier
169
+        # ones.
170
+        for line in f:
171
+            fields = line.rstrip("\r\n").split("\t")
172
+
173
+            # As per OpenSSL's apps/apps.h
174
+            ret[fields[5]] = {
175
+                'type': fields[0],
176
+                'exp_date': parse_asn1_utctime(fields[1]),
177
+                'rev_date': parse_asn1_utctime(fields[2]),
178
+                'serial': fields[3],
179
+                'file': fields[4],
180
+                'name': fields[5],
181
+            }
182
+    return ret
183
+
184
+def find_cert_with_common_name(ca_index, common_name):
185
+    suffix = '/CN=' + common_name
186
+    for key, value in ca_index.items():
187
+        if key.endswith(suffix):
188
+            return value
189
+    return None